mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-10-25 20:22:22 +00:00
Cache transformed and compiled shaders on disk (#2194)
This commit is contained in:
1 parent
d876a203da
commit
37afddab5d
40 files changed
+2131
-338
No files matched your search
@@ -47,3 +47,4 @@ dist/tracy-natives/
|
||||
*.jfr
|
||||
|
||||
tracy-client/angelica/
|
||||
shaderc_failures/
|
||||
@@ -320,6 +320,7 @@ val spirvTest by tasks.registering(Test::class) {
|
||||
filter {
|
||||
includeTestsMatching("com.gtnewhorizons.angelica.glsm.shader.SpirvShaderTranslator*Test")
|
||||
includeTestsMatching("com.gtnewhorizons.angelica.glsm.ffp.FFPUniformBlockSpirvLayoutTest")
|
||||
includeTestsMatching("com.gtnewhorizons.angelica.glsm.shader.SpirvCompilerDiskCacheTest")
|
||||
}
|
||||
|
||||
// libshaderc.so / libspirv-cross.so are extracted by extractNatives3.
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.glsm;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement.Usage;
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.SpirvShaderTranslator;
|
||||
import org.antlr.v4.runtime.tree.ParseTree;
|
||||
import org.antlr.v4.runtime.tree.TerminalNode;
|
||||
@@ -139,12 +140,20 @@ public class CompatShaderTransformer {
|
||||
if (cached != null) {
|
||||
result = cached;
|
||||
} else {
|
||||
try {
|
||||
result = transformInternal(source, isFragment);
|
||||
final ShaderDiskCache.Key dk = ShaderDiskCache.isEnabled() ? diskKey(source, isFragment) : null;
|
||||
final String disk = dk != null ? ShaderDiskCache.getString(dk) : null;
|
||||
if (disk != null) {
|
||||
result = disk;
|
||||
cache.put(key, result);
|
||||
} catch (Exception e) {
|
||||
GLStateManager.LOGGER.warn("CompatShaderTransformer: AST transformation failed, falling back to version fixup only", e);
|
||||
result = fixupVersion(source);
|
||||
} else {
|
||||
try {
|
||||
result = transformInternal(source, isFragment);
|
||||
cache.put(key, result);
|
||||
if (dk != null) ShaderDiskCache.putString(dk, result);
|
||||
} catch (Exception e) {
|
||||
GLStateManager.LOGGER.warn("CompatShaderTransformer: AST transformation failed, falling back to version fixup only", e);
|
||||
result = fixupVersion(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,6 +173,14 @@ public class CompatShaderTransformer {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int minGlslVersion() {
|
||||
return RENDER_BACKEND != null ? RENDER_BACKEND.getMinGLSLVersion() : 330;
|
||||
}
|
||||
|
||||
static ShaderDiskCache.Key diskKey(String source, boolean isFragment) {
|
||||
return ShaderDiskCache.key("compat").str(source).b(isFragment).i(minGlslVersion());
|
||||
}
|
||||
|
||||
public static boolean isCoreShader(String source) {
|
||||
final Matcher vm = VERSION_PATTERN.matcher(source);
|
||||
if (vm.find()) {
|
||||
@@ -186,7 +203,7 @@ public class CompatShaderTransformer {
|
||||
declaredVersion = Integer.parseInt(versionMatcher.group(1));
|
||||
}
|
||||
|
||||
final int targetVersion = Math.max(declaredVersion, RENDER_BACKEND != null ? RENDER_BACKEND.getMinGLSLVersion() : 330);
|
||||
final int targetVersion = Math.max(declaredVersion, minGlslVersion());
|
||||
|
||||
// Pre-parse reserved word renaming - prevents ANTLR parse failures
|
||||
source = GlslTransformUtils.replaceTexture(source);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.gtnewhorizons.angelica.glsm.shader;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
|
||||
import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware;
|
||||
import org.lwjgl.Version;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
import org.lwjgl.system.SharedLibrary;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
@Lwjgl3Aware
|
||||
public final class ShaderCacheIO {
|
||||
|
||||
private ShaderCacheIO() {}
|
||||
|
||||
public static byte[] toHeap(ByteBuffer buf) {
|
||||
final byte[] heap = new byte[buf.remaining()];
|
||||
buf.duplicate().get(heap);
|
||||
return heap;
|
||||
}
|
||||
|
||||
public static ByteBuffer toNative(byte[] data) {
|
||||
final ByteBuffer buf = MemoryUtil.memAlloc(data.length);
|
||||
buf.put(data).flip();
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static <K, V> void lruPut(Object2ObjectLinkedOpenHashMap<K, V> map, K key, V value, int max) {
|
||||
synchronized (map) {
|
||||
map.putAndMoveToFirst(key, value);
|
||||
while (map.size() > max) map.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeStr(DataOutputStream out, String s) throws IOException {
|
||||
if (s == null) {
|
||||
out.writeInt(-1);
|
||||
return;
|
||||
}
|
||||
final byte[] utf8 = s.getBytes(StandardCharsets.UTF_8);
|
||||
out.writeInt(utf8.length);
|
||||
out.write(utf8);
|
||||
}
|
||||
|
||||
public static String readStr(ByteBuffer in) {
|
||||
final int n = in.getInt();
|
||||
if (n == -1) return null;
|
||||
final int pos = in.position();
|
||||
in.position(pos + n);
|
||||
return new String(in.array(), in.arrayOffset() + pos, n, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static int readCount(ByteBuffer in) {
|
||||
final int n = in.getInt();
|
||||
if (n < 0 || n > in.remaining()) throw new BufferUnderflowException();
|
||||
return n;
|
||||
}
|
||||
|
||||
public static void writeBytes(DataOutputStream out, byte[] data) throws IOException {
|
||||
out.writeInt(data.length);
|
||||
out.write(data);
|
||||
}
|
||||
|
||||
public static byte[] readBytes(ByteBuffer in) {
|
||||
final byte[] out = new byte[readCount(in)];
|
||||
in.get(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
public static void writeInts(DataOutputStream out, int[] values) throws IOException {
|
||||
out.writeInt(values.length);
|
||||
for (int v : values) out.writeInt(v);
|
||||
}
|
||||
|
||||
public static int[] readInts(ByteBuffer in) {
|
||||
final int n = readCount(in);
|
||||
final int[] out = new int[n];
|
||||
for (int i = 0; i < n; i++) out[i] = in.getInt();
|
||||
return out;
|
||||
}
|
||||
|
||||
public static void writeSortedStrs(DataOutputStream out, Set<String> values) throws IOException {
|
||||
final Set<String> sorted = values == null ? Set.of() : new TreeSet<>(values);
|
||||
out.writeInt(sorted.size());
|
||||
for (String s : sorted) writeStr(out, s);
|
||||
}
|
||||
|
||||
public static Set<String> readStrSet(ByteBuffer in) {
|
||||
final String[] out = new String[readCount(in)];
|
||||
for (int i = 0; i < out.length; i++) out[i] = readStr(in);
|
||||
return Set.copyOf(Arrays.asList(out));
|
||||
}
|
||||
|
||||
public static String libraryId(SharedLibrary lib) {
|
||||
final String path = lib.getPath();
|
||||
return Version.getVersion() + '|' + lib.getName() + '|' + (path == null ? -1 : new File(path).length());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
package com.gtnewhorizons.angelica.glsm.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.threading.AngelicaWorkers;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO.readBytes;
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO.readCount;
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO.readStr;
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO.writeBytes;
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO.writeStr;
|
||||
|
||||
public final class ShaderDiskCache {
|
||||
private static final Logger LOGGER = LogManager.getLogger("ShaderDiskCache");
|
||||
private static final int MAGIC = 0x41534331;
|
||||
private static final int HEADER_BYTES = 12;
|
||||
static final long MAX_BYTES = 256L << 20;
|
||||
private static volatile Path root;
|
||||
private static final AtomicBoolean writeWarned = new AtomicBoolean();
|
||||
private static final AtomicInteger TMP_SEQ = new AtomicInteger();
|
||||
|
||||
private ShaderDiskCache() {}
|
||||
|
||||
public static void configure(Path dir, String salt) {
|
||||
try {
|
||||
Files.createDirectories(dir);
|
||||
final Path saltFile = dir.resolve("salt");
|
||||
final String existing = Files.isRegularFile(saltFile) ? new String(Files.readAllBytes(saltFile), StandardCharsets.UTF_8) : null;
|
||||
if (!salt.equals(existing)) {
|
||||
wipe(dir);
|
||||
Files.write(saltFile, salt.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
root = dir;
|
||||
AngelicaWorkers.run(() -> enforceCap(dir, MAX_BYTES));
|
||||
LOGGER.info("Shader disk cache at {}", dir);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
root = null;
|
||||
LOGGER.warn("Shader disk cache disabled", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isEnabled() {
|
||||
return root != null;
|
||||
}
|
||||
|
||||
public static Key key(String layer) {
|
||||
return new Key(layer);
|
||||
}
|
||||
|
||||
public static byte[] get(Key key) {
|
||||
final Path r = root;
|
||||
if (r == null) return null;
|
||||
final Path file = key.path(r);
|
||||
final byte[] payload;
|
||||
try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ)) {
|
||||
payload = read(ch);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
if (payload == null) {
|
||||
try {
|
||||
Files.deleteIfExists(file);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
private static byte[] read(FileChannel ch) throws IOException {
|
||||
final long size = ch.size();
|
||||
if (size < HEADER_BYTES || size - HEADER_BYTES > Integer.MAX_VALUE) return null;
|
||||
final ByteBuffer header = ByteBuffer.allocate(HEADER_BYTES);
|
||||
readFully(ch, header);
|
||||
header.flip();
|
||||
if (header.getInt() != MAGIC) return null;
|
||||
final int length = header.getInt();
|
||||
final int crc = header.getInt();
|
||||
if (length != size - HEADER_BYTES) return null;
|
||||
final byte[] payload = new byte[length];
|
||||
readFully(ch, ByteBuffer.wrap(payload));
|
||||
final CRC32 c = new CRC32();
|
||||
c.update(payload, 0, length);
|
||||
return (int) c.getValue() == crc ? payload : null;
|
||||
}
|
||||
|
||||
private static void readFully(FileChannel ch, ByteBuffer buf) throws IOException {
|
||||
while (buf.hasRemaining()) {
|
||||
if (ch.read(buf) < 0) throw new EOFException();
|
||||
}
|
||||
}
|
||||
|
||||
public static void put(Key key, byte[] payload) {
|
||||
final Path r = root;
|
||||
if (r == null) return;
|
||||
final Path target = key.path(r);
|
||||
final Path tmp = target.resolveSibling(key.hex() + '.' + TMP_SEQ.getAndIncrement() + ".tmp");
|
||||
try {
|
||||
try {
|
||||
write(tmp, payload);
|
||||
} catch (IOException e) {
|
||||
Files.createDirectories(target.getParent());
|
||||
write(tmp, payload);
|
||||
}
|
||||
Files.move(tmp, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
try {
|
||||
Files.deleteIfExists(tmp);
|
||||
} catch (IOException ignored) {}
|
||||
if (writeWarned.compareAndSet(false, true)) LOGGER.warn("Shader disk cache write failed for {}", target, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void write(Path tmp, byte[] payload) throws IOException {
|
||||
final CRC32 c = new CRC32();
|
||||
c.update(payload, 0, payload.length);
|
||||
final ByteBuffer header = ByteBuffer.allocate(HEADER_BYTES);
|
||||
header.putInt(MAGIC).putInt(payload.length).putInt((int) c.getValue()).flip();
|
||||
final ByteBuffer[] parts = { header, ByteBuffer.wrap(payload) };
|
||||
try (FileChannel ch = FileChannel.open(tmp, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||
while (header.hasRemaining() || parts[1].hasRemaining()) ch.write(parts);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getString(Key key) {
|
||||
final byte[] payload = get(key);
|
||||
return payload == null ? null : new String(payload, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static void putString(Key key, String value) {
|
||||
put(key, value.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public static Map<String, String> getStrings(Key key) {
|
||||
final byte[] payload = get(key);
|
||||
if (payload == null) return null;
|
||||
try {
|
||||
final ByteBuffer in = ByteBuffer.wrap(payload);
|
||||
final int count = readCount(in);
|
||||
final Map<String, String> out = new LinkedHashMap<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
out.put(readStr(in), readStr(in));
|
||||
}
|
||||
return out;
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void putStrings(Key key, Map<String, String> values) {
|
||||
int size = 0;
|
||||
for (String v : values.values()) size += (v == null ? 0 : v.length()) + 64;
|
||||
final ByteArrayOutputStream bytes = new ByteArrayOutputStream(size);
|
||||
try (DataOutputStream out = new DataOutputStream(bytes)) {
|
||||
out.writeInt(values.size());
|
||||
for (Map.Entry<String, String> e : values.entrySet()) {
|
||||
writeStr(out, e.getKey());
|
||||
writeStr(out, e.getValue());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
put(key, bytes.toByteArray());
|
||||
}
|
||||
|
||||
public static Blob getBlob(Key key) {
|
||||
final byte[] payload = get(key);
|
||||
if (payload == null) return null;
|
||||
try {
|
||||
final ByteBuffer in = ByteBuffer.wrap(payload);
|
||||
final String tag = readStr(in);
|
||||
final byte[] data = readBytes(in);
|
||||
return new Blob(tag, data);
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void putBlob(Key key, String tag, byte[] data) {
|
||||
final ByteArrayOutputStream bytes = new ByteArrayOutputStream(data.length + 64);
|
||||
try (DataOutputStream out = new DataOutputStream(bytes)) {
|
||||
writeStr(out, tag);
|
||||
writeBytes(out, data);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
put(key, bytes.toByteArray());
|
||||
}
|
||||
|
||||
public record Blob(String tag, byte[] data) {}
|
||||
|
||||
static void enforceCap(Path dir, long maxBytes) {
|
||||
try {
|
||||
final long[] total = {0};
|
||||
Files.walkFileTree(dir, new SimpleFileVisitor<>() {
|
||||
@Override public FileVisitResult visitFile(Path p, BasicFileAttributes a) { total[0] += a.size(); return FileVisitResult.CONTINUE; }
|
||||
@Override public FileVisitResult visitFileFailed(Path p, IOException e) { return FileVisitResult.CONTINUE; }
|
||||
});
|
||||
if (total[0] > maxBytes) wipe(dir);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
LOGGER.warn("Shader disk cache cap check failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void wipe(Path dir) throws IOException {
|
||||
final List<Path> paths = new ArrayList<>();
|
||||
try (Stream<Path> walk = Files.walk(dir)) {
|
||||
walk.forEach(paths::add);
|
||||
}
|
||||
for (Path p : paths) {
|
||||
if (Files.isRegularFile(p) && !p.getFileName().toString().equals("salt")) Files.deleteIfExists(p);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Key {
|
||||
private final String layer;
|
||||
private final MessageDigest md;
|
||||
private String hex;
|
||||
|
||||
Key(String layer) {
|
||||
this.layer = layer;
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
str(layer);
|
||||
}
|
||||
|
||||
public Key str(String s) {
|
||||
if (s == null) return i(-1);
|
||||
final byte[] utf8 = s.getBytes(StandardCharsets.UTF_8);
|
||||
i(utf8.length);
|
||||
md.update(utf8);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Key i(int v) {
|
||||
md.update((byte) (v >>> 24));
|
||||
md.update((byte) (v >>> 16));
|
||||
md.update((byte) (v >>> 8));
|
||||
md.update((byte) v);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Key b(boolean v) {
|
||||
md.update((byte) (v ? 1 : 0));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Key bytes(byte[] v) {
|
||||
i(v.length);
|
||||
md.update(v);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Key sortedStrs(Collection<String> values) {
|
||||
final String[] sorted = values.toArray(new String[0]);
|
||||
Arrays.sort(sorted);
|
||||
i(sorted.length);
|
||||
for (String s : sorted) str(s);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String hex() {
|
||||
if (hex == null) {
|
||||
final byte[] digest = md.digest();
|
||||
final StringBuilder sb = new StringBuilder(digest.length * 2);
|
||||
for (byte d : digest) {
|
||||
sb.append(Character.forDigit((d >>> 4) & 0xF, 16)).append(Character.forDigit(d & 0xF, 16));
|
||||
}
|
||||
hex = sb.toString();
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
Path path(Path root) {
|
||||
return root.resolve(layer).resolve(hex() + ".bin");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,11 @@ public final class SpirvCompiler {
|
||||
private static final int CACHE_MAX = 256;
|
||||
private static final Object2ObjectLinkedOpenHashMap<CacheKey, byte[]> CACHE = new Object2ObjectLinkedOpenHashMap<>();
|
||||
|
||||
@Lwjgl3Aware
|
||||
private static final class Toolchain {
|
||||
static final String ID = ShaderCacheIO.libraryId(Shaderc.getLibrary());
|
||||
}
|
||||
|
||||
private record CacheKey(String source, int shaderKind, Options opts) {}
|
||||
|
||||
private SpirvCompiler() {}
|
||||
@@ -53,6 +58,10 @@ public final class SpirvCompiler {
|
||||
public record Result(@Nullable ByteBuffer spirv, @Nullable String error, @Nullable Path dumpPath) {}
|
||||
|
||||
public static Result compile(String source, int shaderKind, String debugName, Options opts) {
|
||||
return compile(source, shaderKind, debugName, opts, true);
|
||||
}
|
||||
|
||||
public static Result compile(String source, int shaderKind, String debugName, Options opts, boolean diskCache) {
|
||||
final String cleanSrc = source.indexOf('\0') >= 0 ? source.replace("\0", "") : source;
|
||||
|
||||
final CacheKey key = new CacheKey(cleanSrc, shaderKind, opts);
|
||||
@@ -60,12 +69,37 @@ public final class SpirvCompiler {
|
||||
synchronized (CACHE) {
|
||||
cached = CACHE.getAndMoveToFirst(key);
|
||||
}
|
||||
if (cached != null) {
|
||||
final ByteBuffer copy = MemoryUtil.memAlloc(cached.length);
|
||||
copy.put(cached).flip();
|
||||
return new Result(copy, null, null);
|
||||
if (cached != null) return new Result(ShaderCacheIO.toNative(cached), null, null);
|
||||
|
||||
ShaderDiskCache.Key diskKey = null;
|
||||
if (diskCache && ShaderDiskCache.isEnabled()) {
|
||||
diskKey = diskKey(cleanSrc, shaderKind, opts);
|
||||
final byte[] disk = ShaderDiskCache.get(diskKey);
|
||||
if (disk != null) {
|
||||
ShaderCacheIO.lruPut(CACHE, key, disk, CACHE_MAX);
|
||||
return new Result(ShaderCacheIO.toNative(disk), null, null);
|
||||
}
|
||||
}
|
||||
|
||||
final Result result = compileUncached(cleanSrc, shaderKind, debugName, opts);
|
||||
final ByteBuffer spirv = result.spirv();
|
||||
if (spirv != null) {
|
||||
final byte[] heap = ShaderCacheIO.toHeap(spirv);
|
||||
ShaderCacheIO.lruPut(CACHE, key, heap, CACHE_MAX);
|
||||
if (diskKey != null) ShaderDiskCache.put(diskKey, heap);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String toolchainId() {
|
||||
return Toolchain.ID;
|
||||
}
|
||||
|
||||
static ShaderDiskCache.Key diskKey(String cleanSrc, int shaderKind, Options opts) {
|
||||
return ShaderDiskCache.key("spirv").str(toolchainId()).str(cleanSrc).i(shaderKind).str(opts.toString());
|
||||
}
|
||||
|
||||
private static Result compileUncached(String cleanSrc, int shaderKind, String debugName, Options opts) {
|
||||
final long compiler = COMPILER.get();
|
||||
final long optionsHandle = Shaderc.shaderc_compile_options_initialize();
|
||||
if (optionsHandle == 0L) {
|
||||
@@ -98,12 +132,6 @@ public final class SpirvCompiler {
|
||||
final ByteBuffer copy = MemoryUtil.memAlloc(spirv.remaining());
|
||||
copy.put(spirv);
|
||||
copy.flip();
|
||||
final byte[] heap = new byte[copy.remaining()];
|
||||
copy.duplicate().get(heap);
|
||||
synchronized (CACHE) {
|
||||
CACHE.putAndMoveToFirst(key, heap);
|
||||
while (CACHE.size() > CACHE_MAX) CACHE.removeLast();
|
||||
}
|
||||
return new Result(copy, null, null);
|
||||
} finally {
|
||||
if (result != 0L) Shaderc.shaderc_result_release(result);
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package com.gtnewhorizons.angelica.glsm;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
|
||||
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@@ -872,4 +877,28 @@ class CompatShaderTransformerTest {
|
||||
assertNoLoc(c, 1, "vec4 a_y"); // must not collide with the hex-reserved slot
|
||||
assertLoc(c, 0, "vec4 a_y");
|
||||
}
|
||||
|
||||
@Test
|
||||
void diskCacheHitReplacesTransform(@TempDir Path dir) {
|
||||
final String src = """
|
||||
#version 120
|
||||
void main() {
|
||||
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
|
||||
}
|
||||
""";
|
||||
try {
|
||||
ShaderDiskCache.configure(dir, "t");
|
||||
CompatShaderTransformer.clearCache();
|
||||
final String first = CompatShaderTransformer.transform(src, false);
|
||||
assertTrue(first.contains("angelica_ProjectionMatrix"));
|
||||
CompatShaderTransformer.clearCache();
|
||||
assertEquals(first, CompatShaderTransformer.transform(src, false));
|
||||
CompatShaderTransformer.clearCache();
|
||||
ShaderDiskCache.putString(CompatShaderTransformer.diskKey(src, false), "POISON");
|
||||
assertEquals("POISON", CompatShaderTransformer.transform(src, false));
|
||||
} finally {
|
||||
CompatShaderTransformer.clearCache();
|
||||
Reflect.setStatic(ShaderDiskCache.class, "root", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.gtnewhorizons.angelica.glsm.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ShaderDiskCacheTest {
|
||||
|
||||
@TempDir
|
||||
Path dir;
|
||||
|
||||
@AfterEach
|
||||
void reset() {
|
||||
Reflect.setStatic(ShaderDiskCache.class, "root", null);
|
||||
}
|
||||
|
||||
private static long binCount(Path dir) throws IOException {
|
||||
try (Stream<Path> walk = Files.walk(dir)) {
|
||||
return walk.filter(p -> p.getFileName().toString().endsWith(".bin")).count();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundTripAndDisabled() throws IOException {
|
||||
ShaderDiskCache.put(ShaderDiskCache.key("l").str("x"), new byte[] {1});
|
||||
assertNull(ShaderDiskCache.get(ShaderDiskCache.key("l").str("x")));
|
||||
assertEquals(0, binCount(dir));
|
||||
|
||||
ShaderDiskCache.configure(dir, "a");
|
||||
ShaderDiskCache.put(ShaderDiskCache.key("l").str("x"), new byte[] {1, 2, 3});
|
||||
assertArrayEquals(new byte[] {1, 2, 3}, ShaderDiskCache.get(ShaderDiskCache.key("l").str("x")));
|
||||
assertNull(ShaderDiskCache.get(ShaderDiskCache.key("l").str("y")));
|
||||
|
||||
ShaderDiskCache.put(ShaderDiskCache.key("t").str("empty"), new byte[0]);
|
||||
assertArrayEquals(new byte[0], ShaderDiskCache.get(ShaderDiskCache.key("t").str("empty")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void saltChangeWipes() {
|
||||
ShaderDiskCache.configure(dir, "a");
|
||||
ShaderDiskCache.put(ShaderDiskCache.key("l").str("x"), new byte[] {7});
|
||||
ShaderDiskCache.configure(dir, "a");
|
||||
assertArrayEquals(new byte[] {7}, ShaderDiskCache.get(ShaderDiskCache.key("l").str("x")));
|
||||
ShaderDiskCache.configure(dir, "b");
|
||||
assertNull(ShaderDiskCache.get(ShaderDiskCache.key("l").str("x")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void corruptAndTruncatedAreMisses() throws IOException {
|
||||
ShaderDiskCache.configure(dir, "a");
|
||||
final ShaderDiskCache.Key k = ShaderDiskCache.key("l").str("x");
|
||||
ShaderDiskCache.put(k, new byte[] {1, 2, 3, 4});
|
||||
final Path file = k.path(dir);
|
||||
final byte[] raw = Files.readAllBytes(file);
|
||||
raw[raw.length - 1] ^= 1;
|
||||
Files.write(file, raw);
|
||||
assertNull(ShaderDiskCache.get(ShaderDiskCache.key("l").str("x")));
|
||||
assertFalse(Files.exists(file));
|
||||
|
||||
ShaderDiskCache.put(ShaderDiskCache.key("l").str("x"), new byte[] {1, 2, 3, 4});
|
||||
final byte[] full = Files.readAllBytes(file);
|
||||
final byte[] cut = new byte[full.length - 2];
|
||||
System.arraycopy(full, 0, cut, 0, cut.length);
|
||||
Files.write(file, cut);
|
||||
assertNull(ShaderDiskCache.get(ShaderDiskCache.key("l").str("x")));
|
||||
|
||||
final ShaderDiskCache.Key shortKey = ShaderDiskCache.key("t").str("short");
|
||||
ShaderDiskCache.put(shortKey, new byte[] {1});
|
||||
final Path shortFile = shortKey.path(dir);
|
||||
Files.write(shortFile, new byte[] {0x41, 0x53});
|
||||
assertNull(ShaderDiskCache.get(ShaderDiskCache.key("t").str("short")));
|
||||
assertFalse(Files.exists(shortFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void keysAreFramed() {
|
||||
assertNotEquals(ShaderDiskCache.key("l").str("ab").str("c").hex(), ShaderDiskCache.key("l").str("a").str("bc").hex());
|
||||
assertNotEquals(ShaderDiskCache.key("l").str(null).hex(), ShaderDiskCache.key("l").str("").hex());
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringsAndBlobs() {
|
||||
ShaderDiskCache.configure(dir, "a");
|
||||
final Map<String, String> values = new LinkedHashMap<>();
|
||||
values.put("VERTEX", null);
|
||||
values.put("FRAGMENT", "x".repeat(100000));
|
||||
ShaderDiskCache.putStrings(ShaderDiskCache.key("s").str("1"), values);
|
||||
assertEquals(values, ShaderDiskCache.getStrings(ShaderDiskCache.key("s").str("1")));
|
||||
|
||||
ShaderDiskCache.putBlob(ShaderDiskCache.key("b").str("1"), "main0", new byte[] {9, 8, 7});
|
||||
final ShaderDiskCache.Blob blob = ShaderDiskCache.getBlob(ShaderDiskCache.key("b").str("1"));
|
||||
assertNotNull(blob);
|
||||
assertEquals("main0", blob.tag());
|
||||
assertArrayEquals(new byte[] {9, 8, 7}, blob.data());
|
||||
}
|
||||
|
||||
@Test
|
||||
void overCapWipesEntriesAndKeepsSalt() throws IOException {
|
||||
ShaderDiskCache.configure(dir, "a");
|
||||
for (int i = 0; i < 4; i++) ShaderDiskCache.put(ShaderDiskCache.key("t").i(i), new byte[1000]);
|
||||
ShaderDiskCache.enforceCap(dir, 1 << 20);
|
||||
assertEquals(4, binCount(dir));
|
||||
ShaderDiskCache.enforceCap(dir, 3000);
|
||||
assertEquals(0, binCount(dir));
|
||||
assertTrue(Files.isRegularFile(dir.resolve("salt")));
|
||||
ShaderDiskCache.put(ShaderDiskCache.key("t").i(9), new byte[] {5});
|
||||
assertArrayEquals(new byte[] {5}, ShaderDiskCache.get(ShaderDiskCache.key("t").i(9)));
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.gtnewhorizons.angelica.glsm.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
import org.lwjgl.util.shaderc.Shaderc;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@Tag("lwjgl3")
|
||||
class SpirvCompilerDiskCacheTest {
|
||||
|
||||
private static final String SRC = "#version 460 core\nlayout(location = 0) in vec4 p;\nvoid main() { gl_Position = p; }\n";
|
||||
|
||||
@TempDir
|
||||
Path dir;
|
||||
|
||||
@AfterEach
|
||||
void reset() {
|
||||
Reflect.setStatic(ShaderDiskCache.class, "root", null);
|
||||
SpirvCompiler.clearCache();
|
||||
}
|
||||
|
||||
@Test
|
||||
void diskEntryServedAfterMemoryClear() {
|
||||
final SpirvCompiler.Options opts = SpirvCompiler.Options.vulkanForced460Core();
|
||||
ShaderDiskCache.configure(dir, "t");
|
||||
final SpirvCompiler.Result first = SpirvCompiler.compile(SRC, Shaderc.shaderc_vertex_shader, "disk-cache-test", opts);
|
||||
assertNotNull(first.spirv(), first.error());
|
||||
MemoryUtil.memFree(first.spirv());
|
||||
|
||||
SpirvCompiler.clearCache();
|
||||
ShaderDiskCache.put(SpirvCompiler.diskKey(SRC, Shaderc.shaderc_vertex_shader, opts), new byte[] {1, 2, 3, 4});
|
||||
final SpirvCompiler.Result second = SpirvCompiler.compile(SRC, Shaderc.shaderc_vertex_shader, "disk-cache-test", opts);
|
||||
final ByteBuffer spirv = second.spirv();
|
||||
assertNotNull(spirv);
|
||||
try {
|
||||
assertArrayEquals(new byte[] {1, 2, 3, 4}, ShaderCacheIO.toHeap(spirv));
|
||||
} finally {
|
||||
MemoryUtil.memFree(spirv);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void diskOptOutSkipsSpirvLayer() {
|
||||
ShaderDiskCache.configure(dir, "t");
|
||||
final SpirvCompiler.Result r = SpirvCompiler.compile(SRC, Shaderc.shaderc_vertex_shader, "disk-opt-out", SpirvCompiler.Options.vulkanForced460Core(), false);
|
||||
assertNotNull(r.spirv(), r.error());
|
||||
MemoryUtil.memFree(r.spirv());
|
||||
assertFalse(Files.exists(dir.resolve("spirv")));
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,18 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
|
||||
import org.antlr.v4.runtime.tree.ParseTreeWalker;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
import org.taumc.glsl.grammar.GLSLParserBaseListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.applyEdits;
|
||||
|
||||
public final class ClipZRemap {
|
||||
|
||||
private static final String INJECTION_STD = "gl_Position.z = gl_Position.z * 0.5 + gl_Position.w * 0.5;\n";
|
||||
|
||||
private ClipZRemap() {}
|
||||
|
||||
public static String injectGLToVulkanClipZ(String source) {
|
||||
final GLSLParser.Translation_unitContext root;
|
||||
try {
|
||||
root = GlslTransformUtils.parseFullQuiet(source);
|
||||
} catch (Exception e) {
|
||||
return source;
|
||||
}
|
||||
final ArrayList<Edit> edits = new ArrayList<>(1);
|
||||
collectEdits(root, edits);
|
||||
return edits.isEmpty() ? source : applyEdits(source, edits);
|
||||
}
|
||||
|
||||
public static void collectEdits(GLSLParser.Translation_unitContext root, List<Edit> edits) {
|
||||
final int[] mainCloseBraceIdx = { -1 };
|
||||
ParseTreeWalker.DEFAULT.walk(new GLSLParserBaseListener() {
|
||||
|
||||
-17
@@ -1,6 +1,5 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock.Member;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
|
||||
@@ -22,7 +21,6 @@ import java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.applyEdits;
|
||||
|
||||
public final class PerFrameBlockInjector {
|
||||
|
||||
@@ -30,21 +28,6 @@ public final class PerFrameBlockInjector {
|
||||
|
||||
private PerFrameBlockInjector() {}
|
||||
|
||||
public static String inject(String source, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
|
||||
if (normalize(perFrame) == null && normalize(perPass) == null) return source;
|
||||
|
||||
final GLSLParser.Translation_unitContext root;
|
||||
try {
|
||||
root = GlslTransformUtils.parseFullQuiet(source);
|
||||
} catch (Exception e) {
|
||||
return source;
|
||||
}
|
||||
|
||||
final List<Edit> edits = new ArrayList<>();
|
||||
collectEdits(root, perFrame, perPass, edits);
|
||||
return edits.isEmpty() ? source : applyEdits(source, edits);
|
||||
}
|
||||
|
||||
public static void collectEdits(GLSLParser.Translation_unitContext root, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass, List<Edit> out) {
|
||||
final PerFrameUniformBlock[] blocks = new PerFrameUniformBlock[ShaderManager.BLOCK_COUNT];
|
||||
blocks[ShaderManager.BLOCK_PER_FRAME] = normalize(perFrame);
|
||||
|
||||
@@ -17,7 +17,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.applyEdits;
|
||||
|
||||
public final class SamplerStripper {
|
||||
|
||||
@@ -25,20 +24,6 @@ public final class SamplerStripper {
|
||||
|
||||
private SamplerStripper() {}
|
||||
|
||||
public static String stripUnused(String source) {
|
||||
if (!source.contains("sampler")) return source;
|
||||
|
||||
final GLSLParser.Translation_unitContext root;
|
||||
try {
|
||||
root = GlslTransformUtils.parseFullQuiet(source);
|
||||
} catch (Exception e) {
|
||||
return source;
|
||||
}
|
||||
final List<Edit> edits = new ArrayList<>();
|
||||
collectEdits(root, source, edits);
|
||||
return edits.isEmpty() ? source : applyEdits(source, edits);
|
||||
}
|
||||
|
||||
public static void collectEdits(GLSLParser.Translation_unitContext root, String source, List<Edit> edits) {
|
||||
record SamplerDecl(String name, int start, int stop) {}
|
||||
final List<SamplerDecl> samplers = new ArrayList<>();
|
||||
|
||||
+162
-104
@@ -4,12 +4,16 @@ import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock.Member;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
|
||||
import com.gtnewhorizons.angelica.glsm.threading.AngelicaWorkers;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileCache;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileUtil;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.dxbc.DxbcCrossCompile;
|
||||
@@ -105,32 +109,74 @@ public final class ShaderManager {
|
||||
return id;
|
||||
}
|
||||
|
||||
private record PrewarmKey(String source, int glShaderType) {}
|
||||
private record PrewarmEntry(byte[] remappedSpirv, StageReflection reflection, GraphicsBindingMap graphicsBindingMap, Set<String> boolUniforms, String transformedSource) {}
|
||||
private record SourceKey(String source, int glShaderType, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {}
|
||||
private record PrewarmHit(ByteBuffer spirv, StageReflection reflection, GraphicsBindingMap graphicsBindingMap, Set<String> boolUniforms, String transformedSource) {}
|
||||
private static final int PREWARM_CACHE_MAX = 256;
|
||||
private static final Object2ObjectLinkedOpenHashMap<PrewarmKey, PrewarmEntry> PREWARM_CACHE = new Object2ObjectLinkedOpenHashMap<>();
|
||||
private static final Object2ObjectLinkedOpenHashMap<SourceKey, ShaderStageSerializer.Stage> PREWARM_CACHE = new Object2ObjectLinkedOpenHashMap<>();
|
||||
|
||||
private record TransformKey(String source, int glShaderType, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {}
|
||||
private record TransformEntry(String source, Set<String> boolUniforms) {}
|
||||
private static final int TRANSFORM_CACHE_MAX = 256;
|
||||
private static final Object2ObjectLinkedOpenHashMap<TransformKey, TransformEntry> TRANSFORM_CACHE = new Object2ObjectLinkedOpenHashMap<>();
|
||||
private static final Object2ObjectLinkedOpenHashMap<SourceKey, PrewarmTransformResult> TRANSFORM_CACHE = new Object2ObjectLinkedOpenHashMap<>();
|
||||
|
||||
public static void clearPrewarmCache() {
|
||||
synchronized (PREWARM_CACHE) { PREWARM_CACHE.clear(); }
|
||||
synchronized (TRANSFORM_CACHE) { TRANSFORM_CACHE.clear(); }
|
||||
}
|
||||
|
||||
static ShaderDiskCache.Key stageKey(String source, int glShaderType) {
|
||||
return ShaderDiskCache.key("sdl-stage").str(SpirvCompiler.toolchainId()).str(CrossCompileUtil.spvcId()).str(source).i(glShaderType);
|
||||
}
|
||||
|
||||
static ShaderDiskCache.Key prewarmKey(String source, int glShaderType, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
|
||||
return appendBlocks(ShaderDiskCache.key("sdl-prewarm").str(SpirvCompiler.toolchainId()).str(CrossCompileUtil.spvcId()).str(source).i(glShaderType), perFrame, perPass);
|
||||
}
|
||||
|
||||
static ShaderDiskCache.Key transformDiskKey(String source, int glShaderType, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
|
||||
return appendBlocks(ShaderDiskCache.key("sdl-transform").str(source).i(glShaderType), perFrame, perPass);
|
||||
}
|
||||
|
||||
private static ShaderDiskCache.Key appendBlocks(ShaderDiskCache.Key key, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
|
||||
appendBlock(key, perFrame);
|
||||
appendBlock(key, perPass);
|
||||
return key;
|
||||
}
|
||||
|
||||
private static void appendBlock(ShaderDiskCache.Key key, PerFrameUniformBlock block) {
|
||||
if (block == null) {
|
||||
key.i(-1);
|
||||
return;
|
||||
}
|
||||
key.i(block.members().size());
|
||||
for (Member m : block.members()) key.str(m.name()).str(m.type().name());
|
||||
}
|
||||
|
||||
private static boolean isGraphics(int glShaderType) {
|
||||
return glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER;
|
||||
}
|
||||
|
||||
private static ShaderStageSerializer.Stage loadStage(ShaderDiskCache.Key key) {
|
||||
final byte[] payload = ShaderDiskCache.get(key);
|
||||
return payload == null ? null : ShaderStageSerializer.decode(payload);
|
||||
}
|
||||
|
||||
private static PrewarmTransformResult loadTransform(ShaderDiskCache.Key key) {
|
||||
final byte[] payload = ShaderDiskCache.get(key);
|
||||
return payload == null ? null : ShaderStageSerializer.decodeTransform(payload);
|
||||
}
|
||||
|
||||
private static void warnSeparateSamplerOnHit(StageReflection reflection, int glShaderType) {
|
||||
if (!reflection.extraUniformNames().isEmpty()) {
|
||||
warnSeparateSampler(glShaderType == GL20.GL_VERTEX_SHADER ? "vertex" : "fragment");
|
||||
}
|
||||
}
|
||||
|
||||
private static PrewarmHit lookupPrewarm(String src, int glShaderType) {
|
||||
final PrewarmEntry e;
|
||||
final ShaderStageSerializer.Stage e;
|
||||
synchronized (PREWARM_CACHE) {
|
||||
e = PREWARM_CACHE.getAndMoveToFirst(new PrewarmKey(src, glShaderType));
|
||||
e = PREWARM_CACHE.getAndMoveToFirst(new SourceKey(src, glShaderType, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock));
|
||||
}
|
||||
if (e == null) return null;
|
||||
// per-hit copy: linkProgram patches the returned SPIR-V in place (applyVaryingMatch / applyAttribLocationsAndInputMask)
|
||||
final ByteBuffer copy = memAlloc(e.remappedSpirv().length);
|
||||
copy.put(e.remappedSpirv()).flip();
|
||||
return new PrewarmHit(copy, e.reflection(), e.graphicsBindingMap(), e.boolUniforms(), e.transformedSource());
|
||||
return new PrewarmHit(ShaderCacheIO.toNative(e.spirv()), e.reflection(), e.bindingMap(), e.boolUniforms(), e.source());
|
||||
}
|
||||
|
||||
public ShaderManager(Device device) {
|
||||
@@ -165,26 +211,22 @@ public final class ShaderManager {
|
||||
return;
|
||||
}
|
||||
|
||||
final TransformKey transformKey = new TransformKey(raw, obj.type, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
|
||||
TransformEntry transformed;
|
||||
final PerFrameUniformBlock perFrame = GLSMHooks.perFrameUniformBlock;
|
||||
final PerFrameUniformBlock perPass = GLSMHooks.perPassUniformBlock;
|
||||
final SourceKey transformKey = new SourceKey(raw, obj.type, perFrame, perPass);
|
||||
PrewarmTransformResult transformed;
|
||||
synchronized (TRANSFORM_CACHE) {
|
||||
transformed = TRANSFORM_CACHE.getAndMoveToFirst(transformKey);
|
||||
}
|
||||
if (transformed == null) {
|
||||
final GlslVulkanPreprocess.Result pre = GlslVulkanPreprocess.run(raw, obj.type, "shader" + shader, true);
|
||||
String src = pre != null ? pre.rewrittenSource() : raw;
|
||||
if (obj.isVertex()) {
|
||||
src = ClipZRemap.injectGLToVulkanClipZ(src);
|
||||
}
|
||||
src = SamplerStripper.stripUnused(src);
|
||||
if (obj.type == GL20.GL_VERTEX_SHADER || obj.type == GL20.GL_FRAGMENT_SHADER) {
|
||||
src = PerFrameBlockInjector.inject(src, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
|
||||
}
|
||||
transformed = new TransformEntry(src, pre != null ? pre.boolUniforms() : Set.of());
|
||||
synchronized (TRANSFORM_CACHE) {
|
||||
TRANSFORM_CACHE.putAndMoveToFirst(transformKey, transformed);
|
||||
while (TRANSFORM_CACHE.size() > TRANSFORM_CACHE_MAX) TRANSFORM_CACHE.removeLast();
|
||||
final ShaderDiskCache.Key diskKey = ShaderDiskCache.isEnabled() ? transformDiskKey(raw, obj.type, perFrame, perPass) : null;
|
||||
if (diskKey != null) transformed = loadTransform(diskKey);
|
||||
if (transformed == null) {
|
||||
transformed = transformOrNull(raw, obj.type, "shader" + shader, perFrame, perPass);
|
||||
if (transformed == null) transformed = new PrewarmTransformResult(raw, Set.of());
|
||||
else if (diskKey != null) ShaderDiskCache.put(diskKey, ShaderStageSerializer.encodeTransform(transformed.source(), transformed.boolUniforms()));
|
||||
}
|
||||
ShaderCacheIO.lruPut(TRANSFORM_CACHE, transformKey, transformed, TRANSFORM_CACHE_MAX);
|
||||
}
|
||||
obj.boolUniforms = transformed.boolUniforms();
|
||||
obj.source = transformed.source();
|
||||
@@ -192,13 +234,29 @@ public final class ShaderManager {
|
||||
final int shaderKind = shaderKindFor(obj);
|
||||
final int glType = obj.type;
|
||||
final String finalSrc = obj.source;
|
||||
obj.spirvFuture = AngelicaWorkers.submit(() -> {
|
||||
final SpirvCompiler.Result r = SpirvCompiler.compile(finalSrc, shaderKind, "shader" + shader, SpirvCompiler.Options.vulkanForced460Core());
|
||||
if (r.spirv() != null && (glType == GL20.GL_VERTEX_SHADER || glType == GL20.GL_FRAGMENT_SHADER)) {
|
||||
return new AsyncCompile(r, remapSpirvForSDLGPU(r.spirv(), glType), reflectStage(r.spirv(), glType == GL20.GL_VERTEX_SHADER));
|
||||
obj.spirvFuture = AngelicaWorkers.submit(() -> compileStageAsync(finalSrc, shaderKind, glType, shader));
|
||||
}
|
||||
|
||||
private static AsyncCompile compileStageAsync(String src, int shaderKind, int glType, int shader) {
|
||||
final boolean graphics = isGraphics(glType);
|
||||
final ShaderDiskCache.Key diskKey = graphics && ShaderDiskCache.isEnabled() ? stageKey(src, glType) : null;
|
||||
if (diskKey != null) {
|
||||
final ShaderStageSerializer.Stage d = loadStage(diskKey);
|
||||
if (d != null) {
|
||||
warnSeparateSamplerOnHit(d.reflection(), glType);
|
||||
return new AsyncCompile(new SpirvCompiler.Result(ShaderCacheIO.toNative(d.spirv()), null, null), d.bindingMap(), d.reflection());
|
||||
}
|
||||
return new AsyncCompile(r, null, null);
|
||||
});
|
||||
}
|
||||
final SpirvCompiler.Result r = SpirvCompiler.compile(src, shaderKind, "shader" + shader, SpirvCompiler.Options.vulkanForced460Core(), diskKey == null);
|
||||
if (r.spirv() != null && graphics) {
|
||||
final GraphicsBindingMap map = remapSpirvForSDLGPU(r.spirv(), glType);
|
||||
final StageReflection reflection = reflectStage(r.spirv(), glType == GL20.GL_VERTEX_SHADER);
|
||||
if (diskKey != null && reflection != StageReflection.EMPTY) {
|
||||
ShaderDiskCache.put(diskKey, ShaderStageSerializer.encode(ShaderCacheIO.toHeap(r.spirv()), reflection, map, null, null));
|
||||
}
|
||||
return new AsyncCompile(r, map, reflection);
|
||||
}
|
||||
return new AsyncCompile(r, null, null);
|
||||
}
|
||||
|
||||
private static int shaderKindFor(ShaderObject obj) {
|
||||
@@ -214,22 +272,24 @@ public final class ShaderManager {
|
||||
|
||||
public record PrewarmTransformResult(String source, Set<String> boolUniforms) {}
|
||||
|
||||
public static String applyPrewarmTransforms(String transformedSource, int glShaderType) {
|
||||
return applyPrewarmTransformsFull(transformedSource, glShaderType).source();
|
||||
public static PrewarmTransformResult applyPrewarmTransformsFull(String transformedSource, int glShaderType, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
|
||||
final PrewarmTransformResult r = transformOrNull(transformedSource, glShaderType, "prewarm", perFrame, perPass);
|
||||
return r != null ? r : new PrewarmTransformResult(transformedSource, Set.of());
|
||||
}
|
||||
|
||||
public static PrewarmTransformResult applyPrewarmTransformsFull(String transformedSource, int glShaderType) {
|
||||
private static PrewarmTransformResult transformOrNull(String transformedSource, int glShaderType, String debugName, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
|
||||
final GLSLParser.Translation_unitContext root;
|
||||
try {
|
||||
root = GlslTransformUtils.parseFullQuiet(transformedSource);
|
||||
} catch (Exception e) {
|
||||
return new PrewarmTransformResult(transformedSource, Set.of());
|
||||
LOG.warn("glsl-transformation-lib parse failed for '{}': {}", debugName, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
final List<Edit> edits = new ArrayList<>();
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER) {
|
||||
PerFrameBlockInjector.collectEdits(root, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock, edits);
|
||||
if (isGraphics(glShaderType)) {
|
||||
PerFrameBlockInjector.collectEdits(root, perFrame, perPass, edits);
|
||||
}
|
||||
final GlslVulkanPreprocess.Metadata meta = GlslVulkanPreprocess.collectEdits(transformedSource, root, glShaderType, "prewarm", true, edits);
|
||||
final GlslVulkanPreprocess.Metadata meta = GlslVulkanPreprocess.collectEdits(transformedSource, root, glShaderType, debugName, true, edits);
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER) {
|
||||
ClipZRemap.collectEdits(root, edits);
|
||||
}
|
||||
@@ -238,12 +298,12 @@ public final class ShaderManager {
|
||||
return new PrewarmTransformResult(s, meta.boolUniforms());
|
||||
}
|
||||
|
||||
public static PrewarmTransformResult applyPrewarmTransformsFull(String finalSource, GLSLParser.Translation_unitContext bodyTree, int headerLen, int glShaderType) {
|
||||
public static PrewarmTransformResult applyPrewarmTransformsFull(String finalSource, GLSLParser.Translation_unitContext bodyTree, int headerLen, int glShaderType, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
|
||||
final String header = finalSource.substring(0, headerLen);
|
||||
final String body = finalSource.substring(headerLen);
|
||||
final List<Edit> edits = new ArrayList<>();
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER) {
|
||||
PerFrameBlockInjector.collectEdits(bodyTree, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock, edits);
|
||||
if (isGraphics(glShaderType)) {
|
||||
PerFrameBlockInjector.collectEdits(bodyTree, perFrame, perPass, edits);
|
||||
}
|
||||
final GlslVulkanPreprocess.Metadata meta = GlslVulkanPreprocess.collectEdits(null, bodyTree, glShaderType, "prewarm", true, edits);
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER) {
|
||||
@@ -271,31 +331,39 @@ public final class ShaderManager {
|
||||
return;
|
||||
}
|
||||
|
||||
final PrewarmKey key = new PrewarmKey(transformedSource, glShaderType);
|
||||
final PerFrameUniformBlock perFrame = GLSMHooks.perFrameUniformBlock;
|
||||
final PerFrameUniformBlock perPass = GLSMHooks.perPassUniformBlock;
|
||||
final SourceKey key = new SourceKey(transformedSource, glShaderType, perFrame, perPass);
|
||||
synchronized (PREWARM_CACHE) {
|
||||
if (PREWARM_CACHE.containsKey(key)) return;
|
||||
}
|
||||
|
||||
final PrewarmTransformResult pre = bodyTree != null
|
||||
? applyPrewarmTransformsFull(transformedSource, bodyTree, headerLen, glShaderType)
|
||||
: applyPrewarmTransformsFull(transformedSource, glShaderType);
|
||||
final boolean graphics = isGraphics(glShaderType);
|
||||
final ShaderDiskCache.Key diskKey = ShaderDiskCache.isEnabled() ? prewarmKey(transformedSource, glShaderType, perFrame, perPass) : null;
|
||||
if (diskKey != null) {
|
||||
final ShaderStageSerializer.Stage d = loadStage(diskKey);
|
||||
if (d != null) {
|
||||
if (graphics) warnSeparateSamplerOnHit(d.reflection(), glShaderType);
|
||||
ShaderCacheIO.lruPut(PREWARM_CACHE, key, graphics ? d : new ShaderStageSerializer.Stage(d.spirv(), StageReflection.EMPTY, GraphicsBindingMap.EMPTY, d.boolUniforms(), d.source()), PREWARM_CACHE_MAX);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final SpirvCompiler.Result r = SpirvCompiler.compile(pre.source(), shaderKindFor(glShaderType), "prewarm", SpirvCompiler.Options.vulkanForced460Core());
|
||||
final PrewarmTransformResult pre = bodyTree != null
|
||||
? applyPrewarmTransformsFull(transformedSource, bodyTree, headerLen, glShaderType, perFrame, perPass)
|
||||
: applyPrewarmTransformsFull(transformedSource, glShaderType, perFrame, perPass);
|
||||
|
||||
final SpirvCompiler.Result r = SpirvCompiler.compile(pre.source(), shaderKindFor(glShaderType), "prewarm", SpirvCompiler.Options.vulkanForced460Core(), diskKey == null);
|
||||
if (r.spirv() == null) return;
|
||||
|
||||
try {
|
||||
GraphicsBindingMap graphicsBindingMap = GraphicsBindingMap.EMPTY;
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER) {
|
||||
graphicsBindingMap = remapSpirvForSDLGPU(r.spirv(), glShaderType);
|
||||
}
|
||||
final StageReflection reflection = (glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER)
|
||||
? reflectStage(r.spirv(), glShaderType == GL20.GL_VERTEX_SHADER) : StageReflection.EMPTY;
|
||||
final GraphicsBindingMap graphicsBindingMap = graphics ? remapSpirvForSDLGPU(r.spirv(), glShaderType) : GraphicsBindingMap.EMPTY;
|
||||
final StageReflection reflection = graphics ? reflectStage(r.spirv(), glShaderType == GL20.GL_VERTEX_SHADER) : StageReflection.EMPTY;
|
||||
|
||||
final byte[] heap = new byte[r.spirv().remaining()];
|
||||
r.spirv().duplicate().get(heap);
|
||||
synchronized (PREWARM_CACHE) {
|
||||
PREWARM_CACHE.putAndMoveToFirst(key, new PrewarmEntry(heap, reflection, graphicsBindingMap, pre.boolUniforms(), pre.source()));
|
||||
while (PREWARM_CACHE.size() > PREWARM_CACHE_MAX) PREWARM_CACHE.removeLast();
|
||||
final byte[] heap = ShaderCacheIO.toHeap(r.spirv());
|
||||
ShaderCacheIO.lruPut(PREWARM_CACHE, key, new ShaderStageSerializer.Stage(heap, reflection, graphicsBindingMap, pre.boolUniforms(), pre.source()), PREWARM_CACHE_MAX);
|
||||
if (diskKey != null && (!graphics || reflection != StageReflection.EMPTY)) {
|
||||
ShaderDiskCache.put(diskKey, ShaderStageSerializer.encode(heap, reflection, graphicsBindingMap, pre.boolUniforms(), pre.source()));
|
||||
}
|
||||
} finally {
|
||||
memFree(r.spirv());
|
||||
@@ -327,7 +395,7 @@ public final class ShaderManager {
|
||||
}
|
||||
if (obj.spirv != null && obj.spirv != r.spirv()) memFree(obj.spirv);
|
||||
obj.spirv = r.spirv();
|
||||
if (!alreadyRemapped && (obj.type == GL20.GL_VERTEX_SHADER || obj.type == GL20.GL_FRAGMENT_SHADER)) {
|
||||
if (!alreadyRemapped && isGraphics(obj.type)) {
|
||||
obj.graphicsBindingMap = remapSpirvForSDLGPU(obj.spirv, obj.type);
|
||||
obj.reflection = reflectStage(obj.spirv, obj.isVertex());
|
||||
}
|
||||
@@ -1033,11 +1101,11 @@ public final class ShaderManager {
|
||||
return new CrossCompiled(SDL_GPU_SHADERFORMAT_SPIRV, spirv, "main", false);
|
||||
}
|
||||
if (device.supportsMsl()) {
|
||||
final MslCrossCompile.Output out = MslCrossCompile.compile(spirv, glShaderType);
|
||||
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, glShaderType);
|
||||
return new CrossCompiled(SDL_GPU_SHADERFORMAT_MSL, out.code(), out.entrypoint(), true);
|
||||
}
|
||||
if (device.supportsDxbc()) {
|
||||
final DxbcCrossCompile.Output out = DxbcCrossCompile.compile(spirv, glShaderType);
|
||||
final CrossCompileCache.Output out = DxbcCrossCompile.compile(spirv, glShaderType);
|
||||
return new CrossCompiled(SDL_GPU_SHADERFORMAT_DXBC, out.code(), out.entrypoint(), true);
|
||||
}
|
||||
if (device.supportsDxil()) {
|
||||
@@ -1097,7 +1165,11 @@ public final class ShaderManager {
|
||||
}
|
||||
|
||||
private static void warnOnSeparateSamplers(long resources, MemoryStack stack, String stage) {
|
||||
if (CrossCompileUtil.countResources(resources, Spvc.SPVC_RESOURCE_TYPE_SEPARATE_SAMPLERS, stack) > 0 && separateSamplerWarned.compareAndSet(false, true)) {
|
||||
if (CrossCompileUtil.countResources(resources, Spvc.SPVC_RESOURCE_TYPE_SEPARATE_SAMPLERS, stack) > 0) warnSeparateSampler(stage);
|
||||
}
|
||||
|
||||
private static void warnSeparateSampler(String stage) {
|
||||
if (separateSamplerWarned.compareAndSet(false, true)) {
|
||||
LOG.error("{} shader declares a separate sampler; SDL_GPU cannot bind one and its descriptor will be left unremapped", stage);
|
||||
}
|
||||
}
|
||||
@@ -1561,27 +1633,8 @@ public final class ShaderManager {
|
||||
}
|
||||
final int memberCount = Spvc.spvc_type_get_num_member_types(Spvc.spvc_compiler_get_type_handle(compiler, ubo.type_id()));
|
||||
for (int m = 0; m < memberCount; m++) {
|
||||
final String memberName = Spvc.spvc_compiler_get_member_name(compiler, ubo.base_type_id(), m);
|
||||
if (memberName == null || memberName.isEmpty()) continue;
|
||||
int memberOffset = 0;
|
||||
if (Spvc.spvc_compiler_type_struct_member_offset(compiler, baseTypeHandle, m, pOffset) == Spvc.SPVC_SUCCESS) {
|
||||
memberOffset = pOffset.get(0);
|
||||
}
|
||||
int memberSize = 0;
|
||||
if (Spvc.spvc_compiler_get_declared_struct_member_size(compiler, baseTypeHandle, m, pSize) == Spvc.SPVC_SUCCESS) {
|
||||
memberSize = (int) pSize.get(0);
|
||||
}
|
||||
int memberArrayStride = 0;
|
||||
if (Spvc.spvc_compiler_type_struct_member_array_stride(compiler, baseTypeHandle, m, pStride) == Spvc.SPVC_SUCCESS) {
|
||||
memberArrayStride = pStride.get(0);
|
||||
}
|
||||
final int memberTypeId = Spvc.spvc_type_get_member_type(baseTypeHandle, m);
|
||||
final long memberTypeHandle = Spvc.spvc_compiler_get_type_handle(compiler, memberTypeId);
|
||||
final int memberVectorSize = Spvc.spvc_type_get_vector_size(memberTypeHandle);
|
||||
final int memberColumns = Spvc.spvc_type_get_columns(memberTypeHandle);
|
||||
final int memberBaseType = Spvc.spvc_type_get_basetype(memberTypeHandle);
|
||||
final int memberArrayLen = (memberArrayStride > 0) ? Math.max(1, memberSize / memberArrayStride) : 1;
|
||||
uboMembers.add(new UboMember(memberName, memberOffset, memberSize, memberArrayStride, memberVectorSize, memberColumns, memberBaseType, memberArrayLen));
|
||||
final UboMember member = reflectMember(compiler, ubo.base_type_id(), baseTypeHandle, m, pOffset, pStride, pSize);
|
||||
if (member != null) uboMembers.add(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1722,30 +1775,35 @@ public final class ShaderManager {
|
||||
private static void collectBlockMembers(long compiler, SpvcReflectedResource block, long baseTypeHandle, IntBuffer pOffset, IntBuffer pStride, PointerBuffer pSize, List<UboMember> out) {
|
||||
final int memberCount = Spvc.spvc_type_get_num_member_types(Spvc.spvc_compiler_get_type_handle(compiler, block.type_id()));
|
||||
for (int m = 0; m < memberCount; m++) {
|
||||
final String memberName = Spvc.spvc_compiler_get_member_name(compiler, block.base_type_id(), m);
|
||||
if (memberName == null || memberName.isEmpty()) continue;
|
||||
int memberOffset = 0;
|
||||
if (Spvc.spvc_compiler_type_struct_member_offset(compiler, baseTypeHandle, m, pOffset) == Spvc.SPVC_SUCCESS) {
|
||||
memberOffset = pOffset.get(0);
|
||||
}
|
||||
int memberSize = 0;
|
||||
if (Spvc.spvc_compiler_get_declared_struct_member_size(compiler, baseTypeHandle, m, pSize) == Spvc.SPVC_SUCCESS) {
|
||||
memberSize = (int) pSize.get(0);
|
||||
}
|
||||
int memberArrayStride = 0;
|
||||
if (Spvc.spvc_compiler_type_struct_member_array_stride(compiler, baseTypeHandle, m, pStride) == Spvc.SPVC_SUCCESS) {
|
||||
memberArrayStride = pStride.get(0);
|
||||
}
|
||||
final int memberTypeId = Spvc.spvc_type_get_member_type(baseTypeHandle, m);
|
||||
final long memberTypeHandle = Spvc.spvc_compiler_get_type_handle(compiler, memberTypeId);
|
||||
out.add(new UboMember(memberName, memberOffset, memberSize, memberArrayStride,
|
||||
Spvc.spvc_type_get_vector_size(memberTypeHandle),
|
||||
Spvc.spvc_type_get_columns(memberTypeHandle),
|
||||
Spvc.spvc_type_get_basetype(memberTypeHandle),
|
||||
(memberArrayStride > 0) ? Math.max(1, memberSize / memberArrayStride) : 1));
|
||||
final UboMember member = reflectMember(compiler, block.base_type_id(), baseTypeHandle, m, pOffset, pStride, pSize);
|
||||
if (member != null) out.add(member);
|
||||
}
|
||||
}
|
||||
|
||||
private static UboMember reflectMember(long compiler, int baseTypeId, long baseTypeHandle, int m, IntBuffer pOffset, IntBuffer pStride, PointerBuffer pSize) {
|
||||
final String memberName = Spvc.spvc_compiler_get_member_name(compiler, baseTypeId, m);
|
||||
if (memberName == null || memberName.isEmpty()) return null;
|
||||
int memberOffset = 0;
|
||||
if (Spvc.spvc_compiler_type_struct_member_offset(compiler, baseTypeHandle, m, pOffset) == Spvc.SPVC_SUCCESS) {
|
||||
memberOffset = pOffset.get(0);
|
||||
}
|
||||
int memberSize = 0;
|
||||
if (Spvc.spvc_compiler_get_declared_struct_member_size(compiler, baseTypeHandle, m, pSize) == Spvc.SPVC_SUCCESS) {
|
||||
memberSize = (int) pSize.get(0);
|
||||
}
|
||||
final long memberTypeHandle = Spvc.spvc_compiler_get_type_handle(compiler, Spvc.spvc_type_get_member_type(baseTypeHandle, m));
|
||||
int memberArrayStride = 0;
|
||||
if (Spvc.spvc_type_get_num_array_dimensions(memberTypeHandle) > 0
|
||||
&& Spvc.spvc_compiler_type_struct_member_array_stride(compiler, baseTypeHandle, m, pStride) == Spvc.SPVC_SUCCESS) {
|
||||
memberArrayStride = pStride.get(0);
|
||||
}
|
||||
return new UboMember(memberName, memberOffset, memberSize, memberArrayStride,
|
||||
Spvc.spvc_type_get_vector_size(memberTypeHandle),
|
||||
Spvc.spvc_type_get_columns(memberTypeHandle),
|
||||
Spvc.spvc_type_get_basetype(memberTypeHandle),
|
||||
(memberArrayStride > 0) ? Math.max(1, memberSize / memberArrayStride) : 1);
|
||||
}
|
||||
|
||||
private static int countResourceTypeStatic(long resources, int resourceType, MemoryStack stack) {
|
||||
final PointerBuffer pList = stack.pointers(0);
|
||||
final PointerBuffer pCount = stack.pointers(0);
|
||||
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager.BlockReflection;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager.FsInput;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager.GraphicsBindingMap;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager.PrewarmTransformResult;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager.ResourceCounts;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager.StageReflection;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager.UboMember;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager.VsInput;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager.VsOutput;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO.readBytes;
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO.readCount;
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO.readInts;
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO.readStr;
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO.readStrSet;
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO.writeBytes;
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO.writeInts;
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO.writeSortedStrs;
|
||||
import static com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO.writeStr;
|
||||
|
||||
final class ShaderStageSerializer {
|
||||
|
||||
record Stage(byte[] spirv, StageReflection reflection, GraphicsBindingMap bindingMap, Set<String> boolUniforms, String source) {}
|
||||
|
||||
private interface Reader<T> {
|
||||
T read(ByteBuffer in);
|
||||
}
|
||||
|
||||
private interface Writer<T> {
|
||||
void write(DataOutputStream out, T value) throws IOException;
|
||||
}
|
||||
|
||||
private ShaderStageSerializer() {}
|
||||
|
||||
static byte[] encode(byte[] spirv, StageReflection reflection, GraphicsBindingMap bindingMap, Set<String> boolUniforms, String source) {
|
||||
final ByteArrayOutputStream bytes = new ByteArrayOutputStream(spirv.length + (source == null ? 0 : source.length()) + 4096);
|
||||
try (DataOutputStream out = new DataOutputStream(bytes)) {
|
||||
writeBytes(out, spirv);
|
||||
writeReflection(out, reflection);
|
||||
writeInts(out, bindingMap.roStorageTextureGlSlots());
|
||||
writeInts(out, bindingMap.rwStorageTextureGlSlots());
|
||||
writeInts(out, bindingMap.roSsboGlSlots());
|
||||
writeSortedStrs(out, boolUniforms);
|
||||
writeStr(out, source);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
|
||||
static Stage decode(byte[] data) {
|
||||
try {
|
||||
final ByteBuffer in = ByteBuffer.wrap(data);
|
||||
final byte[] spirv = readBytes(in);
|
||||
final StageReflection reflection = readReflection(in);
|
||||
final GraphicsBindingMap bindingMap = new GraphicsBindingMap(readInts(in), readInts(in), readInts(in));
|
||||
final Set<String> bools = readStrSet(in);
|
||||
final String source = readStr(in);
|
||||
return new Stage(spirv, reflection, bindingMap, bools, source);
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static byte[] encodeTransform(String source, Set<String> boolUniforms) {
|
||||
final ByteArrayOutputStream bytes = new ByteArrayOutputStream(source.length() + 256);
|
||||
try (DataOutputStream out = new DataOutputStream(bytes)) {
|
||||
writeStr(out, source);
|
||||
writeSortedStrs(out, boolUniforms);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
|
||||
static PrewarmTransformResult decodeTransform(byte[] data) {
|
||||
try {
|
||||
final ByteBuffer in = ByteBuffer.wrap(data);
|
||||
final String source = readStr(in);
|
||||
final Set<String> bools = readStrSet(in);
|
||||
return new PrewarmTransformResult(source, bools);
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeReflection(DataOutputStream out, StageReflection r) throws IOException {
|
||||
out.writeInt(r.counts().numSamplers());
|
||||
out.writeInt(r.counts().numUBOs());
|
||||
out.writeInt(r.counts().numStorageBuffers());
|
||||
out.writeInt(r.counts().numStorageTextures());
|
||||
writeStrList(out, r.samplerNames());
|
||||
writeStrList(out, r.extraUniformNames());
|
||||
writeStrList(out, r.storageImageNames());
|
||||
out.writeInt(r.uboSize());
|
||||
writeList(out, r.uboMembers(), ShaderStageSerializer::writeMember);
|
||||
writeList(out, r.vsInputs(), (o, v) -> {
|
||||
writeStr(o, v.name());
|
||||
o.writeInt(v.binaryOffset());
|
||||
o.writeInt(v.originalLocation());
|
||||
o.writeInt(v.vecSize());
|
||||
o.writeInt(v.baseType());
|
||||
});
|
||||
writeList(out, r.vsOutputs(), (o, v) -> {
|
||||
writeStr(o, v.name());
|
||||
o.writeInt(v.originalLocation());
|
||||
});
|
||||
writeList(out, r.fsInputs(), (o, v) -> {
|
||||
writeStr(o, v.name());
|
||||
o.writeInt(v.binaryOffset());
|
||||
o.writeInt(v.originalLocation());
|
||||
});
|
||||
out.writeInt(r.maxOutputLocation());
|
||||
out.writeInt(r.numReadonlyStorageBuffers());
|
||||
out.writeInt(r.numReadwriteStorageBuffers());
|
||||
out.writeInt(r.numReadonlyStorageTextures());
|
||||
out.writeInt(r.numReadwriteStorageTextures());
|
||||
out.writeInt(r.blocks().length);
|
||||
for (BlockReflection b : r.blocks()) {
|
||||
out.writeInt(b.size());
|
||||
out.writeInt(b.binding());
|
||||
writeList(out, b.members(), ShaderStageSerializer::writeMember);
|
||||
out.writeBoolean(b.readOnly());
|
||||
}
|
||||
}
|
||||
|
||||
private static StageReflection readReflection(ByteBuffer in) {
|
||||
final ResourceCounts counts = new ResourceCounts(in.getInt(), in.getInt(), in.getInt(), in.getInt());
|
||||
final List<String> samplerNames = readStrList(in);
|
||||
final List<String> extraNames = readStrList(in);
|
||||
final List<String> storageImageNames = readStrList(in);
|
||||
final int uboSize = in.getInt();
|
||||
final List<UboMember> uboMembers = readList(in, ShaderStageSerializer::readMember);
|
||||
final List<VsInput> vsInputs = readList(in, i -> new VsInput(readStr(i), i.getInt(), i.getInt(), i.getInt(), i.getInt()));
|
||||
final List<VsOutput> vsOutputs = readList(in, i -> new VsOutput(readStr(i), i.getInt()));
|
||||
final List<FsInput> fsInputs = readList(in, i -> new FsInput(readStr(i), i.getInt(), i.getInt()));
|
||||
final int maxOutputLocation = in.getInt();
|
||||
final int roBuf = in.getInt();
|
||||
final int rwBuf = in.getInt();
|
||||
final int roTex = in.getInt();
|
||||
final int rwTex = in.getInt();
|
||||
final int blockCount = readCount(in);
|
||||
final BlockReflection[] blocks = new BlockReflection[blockCount];
|
||||
for (int b = 0; b < blockCount; b++) {
|
||||
blocks[b] = new BlockReflection(in.getInt(), in.getInt(), readList(in, ShaderStageSerializer::readMember), in.get() != 0);
|
||||
}
|
||||
return new StageReflection(counts, samplerNames, extraNames, storageImageNames, uboSize, uboMembers, vsInputs, vsOutputs, fsInputs,
|
||||
maxOutputLocation, roBuf, rwBuf, roTex, rwTex, blocks);
|
||||
}
|
||||
|
||||
private static void writeMember(DataOutputStream out, UboMember m) throws IOException {
|
||||
writeStr(out, m.name());
|
||||
out.writeInt(m.offset());
|
||||
out.writeInt(m.size());
|
||||
out.writeInt(m.arrayStride());
|
||||
out.writeInt(m.vectorSize());
|
||||
out.writeInt(m.columns());
|
||||
out.writeInt(m.baseType());
|
||||
out.writeInt(m.arrayLen());
|
||||
}
|
||||
|
||||
private static UboMember readMember(ByteBuffer in) {
|
||||
return new UboMember(readStr(in), in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.getInt());
|
||||
}
|
||||
|
||||
private static <T> void writeList(DataOutputStream out, List<T> list, Writer<T> writer) throws IOException {
|
||||
out.writeInt(list.size());
|
||||
for (T t : list) writer.write(out, t);
|
||||
}
|
||||
|
||||
private static <T> List<T> readList(ByteBuffer in, Reader<T> reader) {
|
||||
final int n = readCount(in);
|
||||
final List<T> out = new ArrayList<>(n);
|
||||
for (int i = 0; i < n; i++) out.add(reader.read(in));
|
||||
return out;
|
||||
}
|
||||
|
||||
private static void writeStrList(DataOutputStream out, List<String> list) throws IOException {
|
||||
writeList(out, list, ShaderCacheIO::writeStr);
|
||||
}
|
||||
|
||||
private static List<String> readStrList(ByteBuffer in) {
|
||||
return readList(in, ShaderCacheIO::readStr);
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader.cross;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class CrossCompileCache {
|
||||
|
||||
public record Output(ByteBuffer code, String entrypoint) {}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Uncached {
|
||||
Output compile(ByteBuffer spirv, int glShaderType);
|
||||
}
|
||||
|
||||
private static final int CACHE_MAX = 256;
|
||||
|
||||
private static final class CacheKey {
|
||||
private final byte[] spirvBytes;
|
||||
private final int glShaderType;
|
||||
private final int hash;
|
||||
|
||||
CacheKey(byte[] b, int t) {
|
||||
this.spirvBytes = b;
|
||||
this.glShaderType = t;
|
||||
this.hash = Arrays.hashCode(b) * 31 + t;
|
||||
}
|
||||
|
||||
@Override public int hashCode() { return hash; }
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
return o instanceof CacheKey k && k.glShaderType == glShaderType && k.hash == hash && Arrays.equals(k.spirvBytes, spirvBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private record CacheValue(byte[] codeBytes, String entrypoint) {}
|
||||
|
||||
private final Object2ObjectLinkedOpenHashMap<CacheKey, CacheValue> cache = new Object2ObjectLinkedOpenHashMap<>();
|
||||
private final String layer;
|
||||
private final Supplier<String> toolchainId;
|
||||
private final Uncached uncached;
|
||||
|
||||
public CrossCompileCache(String layer, Supplier<String> toolchainId, Uncached uncached) {
|
||||
this.layer = layer;
|
||||
this.toolchainId = toolchainId;
|
||||
this.uncached = uncached;
|
||||
}
|
||||
|
||||
public Output compile(ByteBuffer spirv, int glShaderType) {
|
||||
final byte[] spirvHeap = ShaderCacheIO.toHeap(spirv);
|
||||
final CacheKey key = new CacheKey(spirvHeap, glShaderType);
|
||||
final CacheValue cached;
|
||||
synchronized (cache) {
|
||||
cached = cache.getAndMoveToFirst(key);
|
||||
}
|
||||
if (cached != null) return new Output(ShaderCacheIO.toNative(cached.codeBytes), cached.entrypoint);
|
||||
|
||||
final ShaderDiskCache.Key diskKey = ShaderDiskCache.isEnabled() ? diskKey(spirvHeap, glShaderType) : null;
|
||||
if (diskKey != null) {
|
||||
final ShaderDiskCache.Blob blob = ShaderDiskCache.getBlob(diskKey);
|
||||
if (blob != null) {
|
||||
ShaderCacheIO.lruPut(cache, key, new CacheValue(blob.data(), blob.tag()), CACHE_MAX);
|
||||
return new Output(ShaderCacheIO.toNative(blob.data()), blob.tag());
|
||||
}
|
||||
}
|
||||
|
||||
final Output out = uncached.compile(spirv, glShaderType);
|
||||
final byte[] codeHeap = ShaderCacheIO.toHeap(out.code());
|
||||
ShaderCacheIO.lruPut(cache, key, new CacheValue(codeHeap, out.entrypoint()), CACHE_MAX);
|
||||
if (diskKey != null) ShaderDiskCache.putBlob(diskKey, out.entrypoint(), codeHeap);
|
||||
return out;
|
||||
}
|
||||
|
||||
public ShaderDiskCache.Key diskKey(byte[] spirvHeap, int glShaderType) {
|
||||
return ShaderDiskCache.key(layer).str(toolchainId.get()).bytes(spirvHeap).i(glShaderType);
|
||||
}
|
||||
}
|
||||
+9
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.sdlgpu.shader.cross;
|
||||
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.lwjgl.PointerBuffer;
|
||||
@@ -27,8 +28,16 @@ public final class CrossCompileUtil {
|
||||
|
||||
public static final AtomicInteger SHADER_DUMP_COUNTER = new AtomicInteger();
|
||||
|
||||
private static final class SpvcId {
|
||||
static final String VALUE = ShaderCacheIO.libraryId(Spvc.getLibrary());
|
||||
}
|
||||
|
||||
private CrossCompileUtil() {}
|
||||
|
||||
public static String spvcId() {
|
||||
return SpvcId.VALUE;
|
||||
}
|
||||
|
||||
public static String stageSuffix(int glShaderType) {
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER) return "vert";
|
||||
if (glShaderType == GL20.GL_FRAGMENT_SHADER) return "frag";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader.dxbc;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO;
|
||||
import org.lwjgl.PointerBuffer;
|
||||
import org.lwjgl.system.JNI;
|
||||
import org.lwjgl.system.Library;
|
||||
@@ -34,6 +35,8 @@ public final class D3DCompiler {
|
||||
|
||||
private static final SharedLibrary D3DCOMPILER = Library.loadNative(D3DCompiler.class, "com.gtnewhorizons.angelica.sdlgpu", null, "d3dcompiler_47", "d3dcompiler");
|
||||
|
||||
public static final String LIBRARY_ID = ShaderCacheIO.libraryId(D3DCOMPILER);
|
||||
|
||||
private static final long D3D_COMPILE_ADDR = apiGetFunctionAddress(D3DCOMPILER, "D3DCompile");
|
||||
|
||||
public static final int D3DCOMPILE_ENABLE_STRICTNESS = 0x800;
|
||||
|
||||
+13
-53
@@ -2,8 +2,9 @@ package com.gtnewhorizons.angelica.sdlgpu.shader.dxbc;
|
||||
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileCache;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileCache.Output;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileUtil;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.lwjgl.PointerBuffer;
|
||||
@@ -17,11 +18,9 @@ import org.lwjgl.util.spvc.SpvcReflectedResource;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.lwjgl.system.MemoryStack.stackPush;
|
||||
import static org.lwjgl.system.MemoryUtil.memAddress;
|
||||
import static org.lwjgl.system.MemoryUtil.memAlloc;
|
||||
import static org.lwjgl.system.MemoryUtil.memByteBufferNT1;
|
||||
import static org.lwjgl.system.MemoryUtil.memUTF8;
|
||||
|
||||
@@ -30,56 +29,23 @@ public final class DxbcCrossCompile {
|
||||
private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU");
|
||||
private static final String BACKEND = "DXBC";
|
||||
|
||||
public record Output(ByteBuffer code, String entrypoint) {}
|
||||
|
||||
private static final int CACHE_MAX = 256;
|
||||
private static final Object2ObjectLinkedOpenHashMap<CacheKey, CacheValue> CACHE = new Object2ObjectLinkedOpenHashMap<>();
|
||||
|
||||
private static final class CacheKey {
|
||||
public final byte[] spirvBytes;
|
||||
public final int glShaderType;
|
||||
public final int hash;
|
||||
public CacheKey(byte[] b, int t) {
|
||||
this.spirvBytes = b;
|
||||
this.glShaderType = t;
|
||||
this.hash = Arrays.hashCode(b) * 31 + t;
|
||||
}
|
||||
@Override public int hashCode() { return hash; }
|
||||
@Override public boolean equals(Object o) {
|
||||
if (!(o instanceof CacheKey k)) return false;
|
||||
return k.glShaderType == glShaderType && k.hash == hash && Arrays.equals(k.spirvBytes, spirvBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private record CacheValue(byte[] codeBytes, String entrypoint) {}
|
||||
|
||||
public static void clearCache() {
|
||||
synchronized (CACHE) { CACHE.clear(); }
|
||||
}
|
||||
private static final CrossCompileCache CACHE = new CrossCompileCache("dxbc", () -> CrossCompileUtil.spvcId() + '|' + D3DCompiler.LIBRARY_ID, DxbcCrossCompile::crossCompileUncached);
|
||||
|
||||
private DxbcCrossCompile() {}
|
||||
|
||||
public static Output compile(ByteBuffer spirv, int glShaderType) {
|
||||
final boolean isCompute = (glShaderType == GL43.GL_COMPUTE_SHADER);
|
||||
final String target;
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER) target = "vs_5_1";
|
||||
else if (glShaderType == GL20.GL_FRAGMENT_SHADER) target = "ps_5_1";
|
||||
else if (isCompute) target = "cs_5_1";
|
||||
else throw new UnsupportedOperationException("Unsupported shader type for DXBC cross-compile: 0x" + Integer.toHexString(glShaderType));
|
||||
return CACHE.compile(spirv, glShaderType);
|
||||
}
|
||||
|
||||
final byte[] spirvHeap = new byte[spirv.remaining()];
|
||||
spirv.duplicate().get(spirvHeap);
|
||||
final CacheKey key = new CacheKey(spirvHeap, glShaderType);
|
||||
final CacheValue cached;
|
||||
synchronized (CACHE) {
|
||||
cached = CACHE.getAndMoveToFirst(key);
|
||||
}
|
||||
if (cached != null) {
|
||||
final ByteBuffer copy = memAlloc(cached.codeBytes.length);
|
||||
copy.put(cached.codeBytes).flip();
|
||||
return new Output(copy, cached.entrypoint);
|
||||
}
|
||||
private static String targetFor(int glShaderType) {
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER) return "vs_5_1";
|
||||
if (glShaderType == GL20.GL_FRAGMENT_SHADER) return "ps_5_1";
|
||||
if (glShaderType == GL43.GL_COMPUTE_SHADER) return "cs_5_1";
|
||||
throw new UnsupportedOperationException("Unsupported shader type for DXBC cross-compile: 0x" + Integer.toHexString(glShaderType));
|
||||
}
|
||||
|
||||
private static Output crossCompileUncached(ByteBuffer spirv, int glShaderType) {
|
||||
final String target = targetFor(glShaderType);
|
||||
final int dumpId = SystemProperties.dumpShaders() ? CrossCompileUtil.SHADER_DUMP_COUNTER.getAndIncrement() : -1;
|
||||
if (dumpId >= 0) CrossCompileUtil.dumpSpirv(spirv, dumpId, glShaderType);
|
||||
|
||||
@@ -90,12 +56,6 @@ public final class DxbcCrossCompile {
|
||||
try {
|
||||
final ByteBuffer dxbc = D3DCompiler.compile(memAddress(hlslNative), hlslNative.remaining(), "main", target, D3DCompiler.D3DCOMPILE_OPTIMIZATION_LEVEL3 | D3DCompiler.D3DCOMPILE_ENABLE_STRICTNESS, 0);
|
||||
if (dumpId >= 0) CrossCompileUtil.dumpBytes(dxbc, dumpId, glShaderType, "dxbc");
|
||||
final byte[] dxbcHeap = new byte[dxbc.remaining()];
|
||||
dxbc.duplicate().get(dxbcHeap);
|
||||
synchronized (CACHE) {
|
||||
CACHE.putAndMoveToFirst(key, new CacheValue(dxbcHeap, "main"));
|
||||
while (CACHE.size() > CACHE_MAX) CACHE.removeLast();
|
||||
}
|
||||
return new Output(dxbc, "main");
|
||||
} finally {
|
||||
MemoryUtil.memFree(hlslNative);
|
||||
|
||||
+13
-57
@@ -1,8 +1,9 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader.msl;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileCache;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileCache.Output;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileUtil;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.lwjgl.PointerBuffer;
|
||||
@@ -16,10 +17,8 @@ import org.lwjgl.util.spvc.SpvcReflectedResource;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.lwjgl.system.MemoryStack.stackPush;
|
||||
import static org.lwjgl.system.MemoryUtil.memAlloc;
|
||||
import static org.lwjgl.system.MemoryUtil.memUTF8;
|
||||
|
||||
public final class MslCrossCompile {
|
||||
@@ -27,32 +26,7 @@ public final class MslCrossCompile {
|
||||
private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU");
|
||||
private static final String BACKEND = "MSL";
|
||||
|
||||
public record Output(ByteBuffer code, String entrypoint) {}
|
||||
|
||||
private static final int CACHE_MAX = 256;
|
||||
private static final Object2ObjectLinkedOpenHashMap<CacheKey, CacheValue> CACHE = new Object2ObjectLinkedOpenHashMap<>();
|
||||
|
||||
private static final class CacheKey {
|
||||
public final byte[] spirvBytes;
|
||||
public final int glShaderType;
|
||||
public final int hash;
|
||||
public CacheKey(byte[] b, int t) {
|
||||
this.spirvBytes = b;
|
||||
this.glShaderType = t;
|
||||
this.hash = Arrays.hashCode(b) * 31 + t;
|
||||
}
|
||||
@Override public int hashCode() { return hash; }
|
||||
@Override public boolean equals(Object o) {
|
||||
if (!(o instanceof CacheKey k)) return false;
|
||||
return k.glShaderType == glShaderType && k.hash == hash && Arrays.equals(k.spirvBytes, spirvBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private record CacheValue(byte[] codeBytes, String entrypoint) {}
|
||||
|
||||
public static void clearCache() {
|
||||
synchronized (CACHE) { CACHE.clear(); }
|
||||
}
|
||||
private static final CrossCompileCache CACHE = new CrossCompileCache("msl", CrossCompileUtil::spvcId, MslCrossCompile::crossCompileUncached);
|
||||
|
||||
@FunctionalInterface
|
||||
private interface BufferSlotFn { public int slot(int set, int binding); }
|
||||
@@ -63,30 +37,18 @@ public final class MslCrossCompile {
|
||||
private MslCrossCompile() {}
|
||||
|
||||
public static Output compile(ByteBuffer spirv, int glShaderType) {
|
||||
final int executionModel;
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER) {
|
||||
executionModel = Spv.SpvExecutionModelVertex;
|
||||
} else if (glShaderType == GL20.GL_FRAGMENT_SHADER) {
|
||||
executionModel = Spv.SpvExecutionModelFragment;
|
||||
} else if (glShaderType == GL43.GL_COMPUTE_SHADER) {
|
||||
executionModel = Spv.SpvExecutionModelGLCompute;
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unsupported shader type for MSL cross-compile: 0x" + Integer.toHexString(glShaderType));
|
||||
}
|
||||
return CACHE.compile(spirv, glShaderType);
|
||||
}
|
||||
|
||||
final byte[] spirvHeap = new byte[spirv.remaining()];
|
||||
spirv.duplicate().get(spirvHeap);
|
||||
final CacheKey key = new CacheKey(spirvHeap, glShaderType);
|
||||
final CacheValue cached;
|
||||
synchronized (CACHE) {
|
||||
cached = CACHE.getAndMoveToFirst(key);
|
||||
}
|
||||
if (cached != null) {
|
||||
final ByteBuffer copy = memAlloc(cached.codeBytes.length);
|
||||
copy.put(cached.codeBytes).flip();
|
||||
return new Output(copy, cached.entrypoint);
|
||||
}
|
||||
private static int executionModelFor(int glShaderType) {
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER) return Spv.SpvExecutionModelVertex;
|
||||
if (glShaderType == GL20.GL_FRAGMENT_SHADER) return Spv.SpvExecutionModelFragment;
|
||||
if (glShaderType == GL43.GL_COMPUTE_SHADER) return Spv.SpvExecutionModelGLCompute;
|
||||
throw new UnsupportedOperationException("Unsupported shader type for MSL cross-compile: 0x" + Integer.toHexString(glShaderType));
|
||||
}
|
||||
|
||||
private static Output crossCompileUncached(ByteBuffer spirv, int glShaderType) {
|
||||
final int executionModel = executionModelFor(glShaderType);
|
||||
final int dumpId = SystemProperties.dumpShaders() ? CrossCompileUtil.SHADER_DUMP_COUNTER.getAndIncrement() : -1;
|
||||
if (dumpId >= 0) CrossCompileUtil.dumpSpirv(spirv, dumpId, glShaderType);
|
||||
|
||||
@@ -142,12 +104,6 @@ public final class MslCrossCompile {
|
||||
final ByteBuffer code = memUTF8(mslSrc, true);
|
||||
final String rawEntry = Spvc.spvc_compiler_get_cleansed_entry_point_name(compiler, "main", executionModel);
|
||||
final String entry = rawEntry != null ? rawEntry : "main0";
|
||||
final byte[] codeHeap = new byte[code.remaining()];
|
||||
code.duplicate().get(codeHeap);
|
||||
synchronized (CACHE) {
|
||||
CACHE.putAndMoveToFirst(key, new CacheValue(codeHeap, entry));
|
||||
while (CACHE.size() > CACHE_MAX) CACHE.removeLast();
|
||||
}
|
||||
return new Output(code, entry);
|
||||
} finally {
|
||||
Spvc.spvc_context_destroy(ctx);
|
||||
|
||||
+2
-2
@@ -24,8 +24,8 @@ class FontFilterVertexSamplerTest {
|
||||
private static String runSdlPipeline(String raw) {
|
||||
final GlslVulkanPreprocess.Result pre = GlslVulkanPreprocess.run(raw, GL20.GL_VERTEX_SHADER, "fontFilter.vsh", true);
|
||||
String src = pre != null ? pre.rewrittenSource() : raw;
|
||||
src = ClipZRemap.injectGLToVulkanClipZ(src);
|
||||
src = SamplerStripper.stripUnused(src);
|
||||
src = ShaderTransformChain.clipZ(src);
|
||||
src = ShaderTransformChain.stripUnused(src);
|
||||
return src;
|
||||
}
|
||||
|
||||
|
||||
+18
-18
@@ -27,7 +27,7 @@ class PerFrameBlockInjectorTest {
|
||||
|
||||
@Test
|
||||
void dropsLooseDeclarationsAndInjectsTheBlock() {
|
||||
final String out = PerFrameBlockInjector.inject("""
|
||||
final String out = ShaderTransformChain.inject("""
|
||||
#version 330 core
|
||||
uniform vec3 cameraPosition;
|
||||
uniform float rainStrength;
|
||||
@@ -43,7 +43,7 @@ class PerFrameBlockInjectorTest {
|
||||
|
||||
@Test
|
||||
void declaresEveryMemberEvenWhenUnused() {
|
||||
final String out = PerFrameBlockInjector.inject("""
|
||||
final String out = ShaderTransformChain.inject("""
|
||||
#version 330 core
|
||||
uniform float rainStrength;
|
||||
out vec4 fragColor;
|
||||
@@ -57,7 +57,7 @@ class PerFrameBlockInjectorTest {
|
||||
|
||||
@Test
|
||||
void blockGoesAfterTheVersionDirective() {
|
||||
final String out = PerFrameBlockInjector.inject("""
|
||||
final String out = ShaderTransformChain.inject("""
|
||||
#version 330 core
|
||||
uniform float rainStrength;
|
||||
void main() {}
|
||||
@@ -67,7 +67,7 @@ class PerFrameBlockInjectorTest {
|
||||
|
||||
@Test
|
||||
void leavesMixedDeclarationAlone() {
|
||||
final String out = PerFrameBlockInjector.inject("""
|
||||
final String out = ShaderTransformChain.inject("""
|
||||
#version 330 core
|
||||
uniform float rainStrength, packOwnedValue;
|
||||
void main() {}
|
||||
@@ -78,19 +78,19 @@ class PerFrameBlockInjectorTest {
|
||||
@Test
|
||||
void emptyMemberListIsANoOp() {
|
||||
final String src = "#version 330 core\nvoid main() {}\n";
|
||||
assertEquals(src, PerFrameBlockInjector.inject(src, new PerFrameUniformBlock(List.of()), null));
|
||||
assertEquals(src, ShaderTransformChain.inject(src, new PerFrameUniformBlock(List.of()), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noBlocksReturnsTheSourceInstance() {
|
||||
final String src = "#version 330 core\nuniform float frameTimeCounter;\nvoid main() {}\n";
|
||||
assertSame(src, PerFrameBlockInjector.inject(src, null, null));
|
||||
assertSame(src, PerFrameBlockInjector.inject(src, new PerFrameUniformBlock(List.of()), new PerFrameUniformBlock(List.of())));
|
||||
assertSame(src, ShaderTransformChain.inject(src, null, null));
|
||||
assertSame(src, ShaderTransformChain.inject(src, new PerFrameUniformBlock(List.of()), new PerFrameUniformBlock(List.of())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void memberShadowedByAPackDeclarationIsRenamedNotRedeclared() {
|
||||
final String out = PerFrameBlockInjector.inject("""
|
||||
final String out = ShaderTransformChain.inject("""
|
||||
#version 330 core
|
||||
const float rainStrength = 3.14159265359;
|
||||
out vec4 fragColor;
|
||||
@@ -108,7 +108,7 @@ class PerFrameBlockInjectorTest {
|
||||
|
||||
@Test
|
||||
void functionLocalRedeclarationDoesNotShadowTheMember() {
|
||||
final String out = PerFrameBlockInjector.inject("""
|
||||
final String out = ShaderTransformChain.inject("""
|
||||
#version 330 core
|
||||
uniform vec3 cameraPosition;
|
||||
vec3 ambient = pow(cameraPosition, vec3(0.75));
|
||||
@@ -131,12 +131,12 @@ class PerFrameBlockInjectorTest {
|
||||
|
||||
@Test
|
||||
void shadowingDoesNotChangeAnyOffset() {
|
||||
final String plain = PerFrameBlockInjector.inject("""
|
||||
final String plain = ShaderTransformChain.inject("""
|
||||
#version 330 core
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(cameraPosition, rainStrength); }
|
||||
""", BLOCK, null);
|
||||
final String shadowed = PerFrameBlockInjector.inject("""
|
||||
final String shadowed = ShaderTransformChain.inject("""
|
||||
#version 330 core
|
||||
const float rainStrength = 1.0;
|
||||
out vec4 fragColor;
|
||||
@@ -152,7 +152,7 @@ class PerFrameBlockInjectorTest {
|
||||
new Member("cameraPosition", UniformType.VEC3),
|
||||
new Member("isRightHanded", UniformType.INT)));
|
||||
|
||||
final String out = PerFrameBlockInjector.inject("""
|
||||
final String out = ShaderTransformChain.inject("""
|
||||
#version 330 core
|
||||
uniform bool isRightHanded;
|
||||
out vec4 fragColor;
|
||||
@@ -170,7 +170,7 @@ class PerFrameBlockInjectorTest {
|
||||
|
||||
@Test
|
||||
void uniformDeclaredWithTheMatchingTypeIsStillReplaced() {
|
||||
final String out = PerFrameBlockInjector.inject("""
|
||||
final String out = ShaderTransformChain.inject("""
|
||||
#version 330 core
|
||||
uniform float rainStrength;
|
||||
out vec4 fragColor;
|
||||
@@ -188,12 +188,12 @@ class PerFrameBlockInjectorTest {
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = packOwnedColor; }
|
||||
""";
|
||||
assertEquals(src, PerFrameBlockInjector.inject(src, BLOCK, null));
|
||||
assertEquals(src, ShaderTransformChain.inject(src, BLOCK, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneReferenceStillDeclaresEveryMember() {
|
||||
final String out = PerFrameBlockInjector.inject("""
|
||||
final String out = ShaderTransformChain.inject("""
|
||||
#version 330 core
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(rainStrength); }
|
||||
@@ -209,7 +209,7 @@ class PerFrameBlockInjectorTest {
|
||||
|
||||
@Test
|
||||
void bothBlocksInjectInOneParse() {
|
||||
final String out = PerFrameBlockInjector.inject("""
|
||||
final String out = ShaderTransformChain.inject("""
|
||||
#version 330 core
|
||||
uniform vec3 cameraPosition;
|
||||
uniform mat4 gbufferModelView;
|
||||
@@ -226,7 +226,7 @@ class PerFrameBlockInjectorTest {
|
||||
|
||||
@Test
|
||||
void unreferencedBlockIsNotInjected() {
|
||||
final String out = PerFrameBlockInjector.inject("""
|
||||
final String out = ShaderTransformChain.inject("""
|
||||
#version 330 core
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(cameraPosition, 1.0); }
|
||||
@@ -253,7 +253,7 @@ class PerFrameBlockInjectorTest {
|
||||
|
||||
@Test
|
||||
void injectedSourceCompilesAndReflects() {
|
||||
final String out = PerFrameBlockInjector.inject("""
|
||||
final String out = ShaderTransformChain.inject("""
|
||||
#version 330 core
|
||||
uniform vec3 cameraPosition;
|
||||
uniform float rainStrength;
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ class PerFrameBlockLocationRoutingTest {
|
||||
""";
|
||||
|
||||
private static ShaderManager.ProgramObject linkedProgram() {
|
||||
final String injected = PerFrameBlockInjector.inject(SOURCE, PER_FRAME, PER_PASS);
|
||||
final String injected = ShaderTransformChain.inject(SOURCE, PER_FRAME, PER_PASS);
|
||||
final SpirvCompiler.Result r = SpirvCompiler.compile(injected, Shaderc.shaderc_fragment_shader, "routing.frag", SpirvCompiler.Options.vulkanForced460Core());
|
||||
assertNotNull(r.spirv(), () -> "compile failed: " + r.error());
|
||||
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ class PerFrameBlockReadOnlyTest {
|
||||
|
||||
@Test
|
||||
void injectedBlocksAreDecoratedNonWritable() {
|
||||
final String injected = PerFrameBlockInjector.inject(DUAL_SOURCE, BLOCK, PER_PASS);
|
||||
final String injected = ShaderTransformChain.inject(DUAL_SOURCE, BLOCK, PER_PASS);
|
||||
final SpirvCompiler.Result r = SpirvCompiler.compile(injected, Shaderc.shaderc_fragment_shader, "readonly.frag", SpirvCompiler.Options.vulkanForced460Core());
|
||||
assertNotNull(r.spirv(), () -> "compile failed: " + r.error());
|
||||
|
||||
|
||||
+25
-2
@@ -27,7 +27,7 @@ class PrewarmSingleParseEquivalenceTest {
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final String legacyOut = ShaderTransformChain.run(source, glShaderType);
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final String parseOnceOut = ShaderManager.applyPrewarmTransforms(source, glShaderType);
|
||||
final String parseOnceOut = ShaderManager.applyPrewarmTransformsFull(source, glShaderType, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock).source();
|
||||
assertEquals(legacyOut, parseOnceOut, "single-parse vs chained pipeline diverged for " + label);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class PrewarmSingleParseEquivalenceTest {
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final String legacyOut = ShaderTransformChain.run(source, glShaderType);
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final String parseOnceOut = ShaderManager.applyPrewarmTransforms(source, glShaderType);
|
||||
final String parseOnceOut = ShaderManager.applyPrewarmTransformsFull(source, glShaderType, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock).source();
|
||||
assertEquals(legacyOut.replaceAll("\\s+", " "), parseOnceOut.replaceAll("\\s+", " "), "single-parse vs chained pipeline diverged beyond whitespace for " + label);
|
||||
}
|
||||
|
||||
@@ -141,6 +141,29 @@ class PrewarmSingleParseEquivalenceTest {
|
||||
assertEquivalent("geometry-passthrough", src, GL32.GL_GEOMETRY_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readOnlyImage_rewrittenToTextureWithSamplerlessExtension() {
|
||||
final String src = "#version 460 core\n"
|
||||
+ "layout(binding = 2, rgba8) uniform readonly image2D u_Img;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() {\n"
|
||||
+ " fragColor = imageLoad(u_Img, ivec2(1, 2)) + vec4(imageSize(u_Img), 0.0, 1.0);\n"
|
||||
+ "}\n";
|
||||
assertEquivalent("readonly-image", src, GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void samplerlessTexture_extensionInserted() {
|
||||
final String src = "#version 460 core\n"
|
||||
+ "uniform texture2D u_Tex;\n"
|
||||
+ "uniform sampler u_Smp;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() {\n"
|
||||
+ " fragColor = texelFetch(u_Tex, ivec2(0), 0) + texture(sampler2D(u_Tex, u_Smp), vec2(0.5));\n"
|
||||
+ "}\n";
|
||||
assertEquivalent("samplerless-texture", src, GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unparseable_fallsBackToInputUnchanged() {
|
||||
final String src = "this is not valid GLSL @@@@\n";
|
||||
|
||||
+2
-2
@@ -34,9 +34,9 @@ class PrewarmTreeHandoffEquivalenceTest {
|
||||
final String finalSource = HEADER + printedBody;
|
||||
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final ShaderManager.PrewarmTransformResult reparse = ShaderManager.applyPrewarmTransformsFull(finalSource, glShaderType);
|
||||
final ShaderManager.PrewarmTransformResult reparse = ShaderManager.applyPrewarmTransformsFull(finalSource, glShaderType, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final ShaderManager.PrewarmTransformResult treeFed = ShaderManager.applyPrewarmTransformsFull(finalSource, tree, HEADER.length(), glShaderType);
|
||||
final ShaderManager.PrewarmTransformResult treeFed = ShaderManager.applyPrewarmTransformsFull(finalSource, tree, HEADER.length(), glShaderType, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
|
||||
|
||||
assertEquals(reparse.source(), treeFed.source(), "tree handoff diverged from re-parse for " + label);
|
||||
assertEquals(reparse.boolUniforms(), treeFed.boolUniforms(), "bool uniforms diverged for " + label);
|
||||
|
||||
+8
-8
@@ -18,7 +18,7 @@ class SdlSamplerStripperPreprocessorTest {
|
||||
+ " float a = textureLod(u_S, vec2(0), 0.0).r;\n"
|
||||
+ "#endif\n"
|
||||
+ "}\n";
|
||||
final String out = SamplerStripper.stripUnused(src);
|
||||
final String out = ShaderTransformChain.stripUnused(src);
|
||||
assertTrue(out.contains("uniform sampler2D u_S"), "ref inside #if must keep the decl; output:\n" + out);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class SdlSamplerStripperPreprocessorTest {
|
||||
+ " float a = t * 2.0;\n"
|
||||
+ "#endif\n"
|
||||
+ "}\n";
|
||||
final String out = SamplerStripper.stripUnused(src);
|
||||
final String out = ShaderTransformChain.stripUnused(src);
|
||||
assertTrue(out.contains("uniform sampler2D u_S"),
|
||||
"unconditional ref must keep the decl; output:\n" + out);
|
||||
}
|
||||
@@ -50,7 +50,7 @@ class SdlSamplerStripperPreprocessorTest {
|
||||
+ " float b = textureLod(u_S, vec2(0), 0.0).r;\n"
|
||||
+ "#endif\n"
|
||||
+ "}\n";
|
||||
final String out = SamplerStripper.stripUnused(src);
|
||||
final String out = ShaderTransformChain.stripUnused(src);
|
||||
assertTrue(out.contains("uniform sampler2D u_S"), "refs in both branches must keep the decl; output:\n" + out);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ class SdlSamplerStripperPreprocessorTest {
|
||||
+ " fragColor = texture(u_BlockTex, v_TexCoord);\n"
|
||||
+ "#endif\n"
|
||||
+ "}\n";
|
||||
final String out = SamplerStripper.stripUnused(src);
|
||||
final String out = ShaderTransformChain.stripUnused(src);
|
||||
assertTrue(out.contains("uniform sampler2D u_BlockTex"), "terrain sampler must survive; output:\n" + out);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ class SdlSamplerStripperPreprocessorTest {
|
||||
+ " float a = 1.0;\n"
|
||||
+ "#endif\n"
|
||||
+ "}\n";
|
||||
final String out = SamplerStripper.stripUnused(src);
|
||||
final String out = ShaderTransformChain.stripUnused(src);
|
||||
assertFalse(out.contains("uniform sampler2D iris_centerDepthSmooth"), "an unreferenced sampler must still be stripped; output:\n" + out);
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ class SdlSamplerStripperPreprocessorTest {
|
||||
+ " float a = 1.0;\n"
|
||||
+ "#endif\n"
|
||||
+ "}\n";
|
||||
final String out = SamplerStripper.stripUnused(src);
|
||||
final String out = ShaderTransformChain.stripUnused(src);
|
||||
assertFalse(out.contains("uniform sampler2D u_S"), "a comment mention must not keep the decl; output:\n" + out);
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ class SdlSamplerStripperPreprocessorTest {
|
||||
+ "void main() {\n"
|
||||
+ " gl_Position = vec4(v_Position, 1.0);\n"
|
||||
+ "}\n";
|
||||
assertSame(src, SamplerStripper.stripUnused(src));
|
||||
assertSame(src, ShaderTransformChain.stripUnused(src));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -123,7 +123,7 @@ class SdlSamplerStripperPreprocessorTest {
|
||||
+ "void main() {\n"
|
||||
+ " float a = SAMPLE_IT();\n"
|
||||
+ "}\n";
|
||||
final String out = SamplerStripper.stripUnused(src);
|
||||
final String out = ShaderTransformChain.stripUnused(src);
|
||||
assertTrue(out.contains("uniform sampler2D u_S"), "a macro-body ref must keep the decl; output:\n" + out);
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -66,8 +66,8 @@ class ShaderSourceTransformCacheTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatedSourceReusesTheChainedTransform() {
|
||||
final String expected = ShaderTransformChain.run(FRAG, GL20.GL_FRAGMENT_SHADER);
|
||||
void repeatedSourceReusesTheTransform() {
|
||||
final String expected = ShaderManager.applyPrewarmTransformsFull(FRAG, GL20.GL_FRAGMENT_SHADER, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock).source();
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
|
||||
final ShaderManager sm = new ShaderManager(null);
|
||||
@@ -81,8 +81,8 @@ class ShaderSourceTransformCacheTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void vertexSourceMatchesTheChainAndCarriesClipZ() {
|
||||
final String expected = ShaderTransformChain.run(VERT, GL20.GL_VERTEX_SHADER);
|
||||
void vertexSourceMatchesTheSingleParseAndCarriesClipZ() {
|
||||
final String expected = ShaderManager.applyPrewarmTransformsFull(VERT, GL20.GL_VERTEX_SHADER, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock).source();
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
|
||||
final ShaderManager sm = new ShaderManager(null);
|
||||
@@ -104,7 +104,7 @@ class ShaderSourceTransformCacheTest {
|
||||
|
||||
GLSMHooks.perFrameUniformBlock = null;
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final String expected = ShaderTransformChain.run(PFB_FRAG, GL20.GL_FRAGMENT_SHADER);
|
||||
final String expected = ShaderManager.applyPrewarmTransformsFull(PFB_FRAG, GL20.GL_FRAGMENT_SHADER, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock).source();
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
|
||||
final int withoutBlock = sm.createShader(GL20.GL_FRAGMENT_SHADER);
|
||||
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock.Member;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.UniformType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.util.shaderc.Shaderc;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.lwjgl.system.MemoryUtil.memFree;
|
||||
|
||||
class ShaderStageSerializerTest {
|
||||
|
||||
private static final PerFrameUniformBlock BLOCK = new PerFrameUniformBlock(List.of(
|
||||
new Member("cameraPosition", UniformType.VEC3),
|
||||
new Member("rainStrength", UniformType.FLOAT)));
|
||||
|
||||
private static final String VERTEX = """
|
||||
#version 460 core
|
||||
layout(location = 0) in vec3 a_Position;
|
||||
layout(location = 1) in vec2 a_TexCoord;
|
||||
layout(location = 0) out vec2 v_TexCoord;
|
||||
layout(location = 1) out vec3 v_Color;
|
||||
uniform vec3 cameraPosition;
|
||||
uniform float rainStrength;
|
||||
layout(std140, binding = 0) uniform Scene { mat4 u_Mvp; vec4 u_Tints[3]; float u_Scale; } scene;
|
||||
void main() {
|
||||
v_TexCoord = a_TexCoord;
|
||||
v_Color = cameraPosition * rainStrength * scene.u_Tints[1].xyz * scene.u_Scale;
|
||||
gl_Position = scene.u_Mvp * vec4(a_Position, 1.0);
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String FRAGMENT = """
|
||||
#version 460 core
|
||||
layout(binding = 0) uniform sampler2D u_TexA;
|
||||
layout(binding = 1) uniform sampler2D u_TexB;
|
||||
layout(binding = 2, rgba8) readonly uniform image2D u_Img;
|
||||
layout(binding = 3) uniform sampler u_Smp;
|
||||
layout(binding = 4) uniform texture2D u_Tex;
|
||||
layout(location = 0) in vec2 v_TexCoord;
|
||||
layout(location = 1) in vec3 v_Color;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
layout(location = 1) out vec4 fragColor1;
|
||||
void main() {
|
||||
fragColor = texture(u_TexA, v_TexCoord) + texture(u_TexB, v_TexCoord) + imageLoad(u_Img, ivec2(0)) + texture(sampler2D(u_Tex, u_Smp), v_TexCoord);
|
||||
fragColor1 = vec4(v_Color, 1.0);
|
||||
}
|
||||
""";
|
||||
|
||||
private record Staged(byte[] spirv, ShaderManager.StageReflection reflection, ShaderManager.GraphicsBindingMap map) {}
|
||||
|
||||
private static Staged stage(String source, int shaderKind, int glType) {
|
||||
final SpirvCompiler.Result r = SpirvCompiler.compile(source, shaderKind, "codec", SpirvCompiler.Options.vulkanForced460Core());
|
||||
assertNotNull(r.spirv(), () -> "shaderc failed: " + r.error());
|
||||
final ByteBuffer spirv = r.spirv();
|
||||
try {
|
||||
final ShaderManager.GraphicsBindingMap map = ShaderManager.remapSpirvForSDLGPU(spirv, glType);
|
||||
final ShaderManager.StageReflection reflection = ShaderManager.reflectStage(spirv, glType == GL20.GL_VERTEX_SHADER);
|
||||
return new Staged(ShaderCacheIO.toHeap(spirv), reflection, map);
|
||||
} finally {
|
||||
memFree(spirv);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertReflectionEquals(ShaderManager.StageReflection e, ShaderManager.StageReflection a) {
|
||||
assertNotNull(a);
|
||||
assertEquals(e.counts(), a.counts());
|
||||
assertEquals(e.samplerNames(), a.samplerNames());
|
||||
assertEquals(e.extraUniformNames(), a.extraUniformNames());
|
||||
assertEquals(e.storageImageNames(), a.storageImageNames());
|
||||
assertEquals(e.uboSize(), a.uboSize());
|
||||
assertEquals(e.uboMembers(), a.uboMembers());
|
||||
assertEquals(e.vsInputs(), a.vsInputs());
|
||||
assertEquals(e.vsOutputs(), a.vsOutputs());
|
||||
assertEquals(e.fsInputs(), a.fsInputs());
|
||||
assertEquals(e.maxOutputLocation(), a.maxOutputLocation());
|
||||
assertEquals(e.numReadonlyStorageBuffers(), a.numReadonlyStorageBuffers());
|
||||
assertEquals(e.numReadwriteStorageBuffers(), a.numReadwriteStorageBuffers());
|
||||
assertEquals(e.numReadonlyStorageTextures(), a.numReadonlyStorageTextures());
|
||||
assertEquals(e.numReadwriteStorageTextures(), a.numReadwriteStorageTextures());
|
||||
assertEquals(e.blocks().length, a.blocks().length);
|
||||
for (int b = 0; b < e.blocks().length; b++) assertEquals(e.blocks()[b], a.blocks()[b], "block " + b);
|
||||
}
|
||||
|
||||
private static void assertBindingMapEquals(ShaderManager.GraphicsBindingMap e, ShaderManager.GraphicsBindingMap a) {
|
||||
assertNotNull(a);
|
||||
assertArrayEquals(e.roStorageTextureGlSlots(), a.roStorageTextureGlSlots());
|
||||
assertArrayEquals(e.rwStorageTextureGlSlots(), a.rwStorageTextureGlSlots());
|
||||
assertArrayEquals(e.roSsboGlSlots(), a.roSsboGlSlots());
|
||||
}
|
||||
|
||||
private static byte[] encodedVertex() {
|
||||
final String src = ShaderTransformChain.inject(VERTEX, BLOCK, null);
|
||||
final Staged s = stage(src, Shaderc.shaderc_vertex_shader, GL20.GL_VERTEX_SHADER);
|
||||
return ShaderStageSerializer.encode(s.spirv(), s.reflection(), s.map(), Set.of("b"), src);
|
||||
}
|
||||
|
||||
@Test
|
||||
void vertexStageRoundTrips() {
|
||||
final String src = ShaderTransformChain.inject(VERTEX, BLOCK, null);
|
||||
final Staged s = stage(src, Shaderc.shaderc_vertex_shader, GL20.GL_VERTEX_SHADER);
|
||||
assertFalse(s.reflection().vsInputs().isEmpty());
|
||||
assertFalse(s.reflection().vsOutputs().isEmpty());
|
||||
assertTrue(s.reflection().uboMembers().stream().anyMatch(m -> m.arrayLen() > 1), "needs an array member");
|
||||
assertFalse(s.reflection().blocks()[ShaderManager.BLOCK_PER_FRAME].members().isEmpty(), "needs the per-frame block");
|
||||
|
||||
final byte[] enc = ShaderStageSerializer.encode(s.spirv(), s.reflection(), s.map(), Set.of("u_flag", "u_other"), src);
|
||||
final ShaderStageSerializer.Stage d = ShaderStageSerializer.decode(enc);
|
||||
assertNotNull(d);
|
||||
assertArrayEquals(s.spirv(), d.spirv());
|
||||
assertReflectionEquals(s.reflection(), d.reflection());
|
||||
assertBindingMapEquals(s.map(), d.bindingMap());
|
||||
assertEquals(Set.of("u_flag", "u_other"), d.boolUniforms());
|
||||
assertEquals(src, d.source());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fragmentStageRoundTrips() {
|
||||
final Staged s = stage(FRAGMENT, Shaderc.shaderc_fragment_shader, GL20.GL_FRAGMENT_SHADER);
|
||||
assertEquals(List.of("u_TexA", "u_TexB"), s.reflection().samplerNames());
|
||||
assertFalse(s.reflection().storageImageNames().isEmpty(), "needs a storage image");
|
||||
assertTrue(s.reflection().extraUniformNames().contains("u_Smp"), "needs a separate sampler");
|
||||
assertFalse(s.reflection().fsInputs().isEmpty());
|
||||
assertEquals(1, s.reflection().maxOutputLocation());
|
||||
|
||||
final ShaderStageSerializer.Stage d = ShaderStageSerializer.decode(ShaderStageSerializer.encode(s.spirv(), s.reflection(), s.map(), Set.of(), FRAGMENT));
|
||||
assertNotNull(d);
|
||||
assertArrayEquals(s.spirv(), d.spirv());
|
||||
assertReflectionEquals(s.reflection(), d.reflection());
|
||||
assertBindingMapEquals(s.map(), d.bindingMap());
|
||||
assertTrue(d.boolUniforms().isEmpty());
|
||||
assertEquals(FRAGMENT, d.source());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullableFieldsSurvive() {
|
||||
final byte[] spirv = {1, 2, 3, 4};
|
||||
final ShaderStageSerializer.Stage d = ShaderStageSerializer.decode(ShaderStageSerializer.encode(spirv, ShaderManager.StageReflection.EMPTY, ShaderManager.GraphicsBindingMap.EMPTY, null, null));
|
||||
assertNotNull(d);
|
||||
assertArrayEquals(spirv, d.spirv());
|
||||
assertReflectionEquals(ShaderManager.StageReflection.EMPTY, d.reflection());
|
||||
assertBindingMapEquals(ShaderManager.GraphicsBindingMap.EMPTY, d.bindingMap());
|
||||
assertNull(d.source());
|
||||
assertTrue(d.boolUniforms().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void boolUniformOrderDoesNotChangeTheBytes() {
|
||||
final byte[] spirv = {1, 2, 3, 4};
|
||||
final Set<String> forward = new LinkedHashSet<>(List.of("a", "b", "c", "d"));
|
||||
final Set<String> backward = new LinkedHashSet<>(List.of("d", "c", "b", "a"));
|
||||
final byte[] forwardBytes = ShaderStageSerializer.encode(spirv, ShaderManager.StageReflection.EMPTY, ShaderManager.GraphicsBindingMap.EMPTY, forward, "s");
|
||||
final byte[] backwardBytes = ShaderStageSerializer.encode(spirv, ShaderManager.StageReflection.EMPTY, ShaderManager.GraphicsBindingMap.EMPTY, backward, "s");
|
||||
assertArrayEquals(forwardBytes, backwardBytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
void transformRoundTripsAndSortsBools() {
|
||||
final Set<String> forward = new LinkedHashSet<>(List.of("a", "b", "c"));
|
||||
final Set<String> backward = new LinkedHashSet<>(List.of("c", "b", "a"));
|
||||
final byte[] enc = ShaderStageSerializer.encodeTransform(FRAGMENT, forward);
|
||||
assertArrayEquals(enc, ShaderStageSerializer.encodeTransform(FRAGMENT, backward));
|
||||
final ShaderManager.PrewarmTransformResult d = ShaderStageSerializer.decodeTransform(enc);
|
||||
assertNotNull(d);
|
||||
assertEquals(FRAGMENT, d.source());
|
||||
assertEquals(Set.of("a", "b", "c"), d.boolUniforms());
|
||||
assertTrue(ShaderStageSerializer.decodeTransform(ShaderStageSerializer.encodeTransform("", null)).boolUniforms().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void truncatedPayloadsAreRejected() {
|
||||
final byte[] enc = encodedVertex();
|
||||
for (int len = 0; len < enc.length; len++) {
|
||||
assertNull(ShaderStageSerializer.decode(Arrays.copyOf(enc, len)), "prefix of " + len + " bytes decoded");
|
||||
}
|
||||
final byte[] transform = ShaderStageSerializer.encodeTransform(FRAGMENT, Set.of("a", "b"));
|
||||
for (int len = 0; len < transform.length; len++) {
|
||||
assertNull(ShaderStageSerializer.decodeTransform(Arrays.copyOf(transform, len)), "prefix of " + len + " bytes decoded");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void hugeLengthPrefixIsRejectedWithoutAllocating() {
|
||||
final byte[] enc = encodedVertex();
|
||||
final byte[] bad = enc.clone();
|
||||
bad[0] = 0x7f;
|
||||
bad[1] = (byte) 0xff;
|
||||
bad[2] = (byte) 0xff;
|
||||
bad[3] = (byte) 0xff;
|
||||
assertNull(ShaderStageSerializer.decode(bad));
|
||||
}
|
||||
}
|
||||
+36
-3
@@ -1,8 +1,16 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
final class ShaderTransformChain {
|
||||
|
||||
@@ -12,12 +20,37 @@ final class ShaderTransformChain {
|
||||
final GlslVulkanPreprocess.Result pre = GlslVulkanPreprocess.run(source, glShaderType, "test", true);
|
||||
String src = pre != null ? pre.rewrittenSource() : source;
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER) {
|
||||
src = ClipZRemap.injectGLToVulkanClipZ(src);
|
||||
src = clipZ(src);
|
||||
}
|
||||
src = SamplerStripper.stripUnused(src);
|
||||
src = stripUnused(src);
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER) {
|
||||
src = PerFrameBlockInjector.inject(src, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
|
||||
src = inject(src, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
static String clipZ(String source) {
|
||||
return edit(source, ClipZRemap::collectEdits);
|
||||
}
|
||||
|
||||
static String stripUnused(String source) {
|
||||
if (!source.contains("sampler")) return source;
|
||||
return edit(source, (root, edits) -> SamplerStripper.collectEdits(root, source, edits));
|
||||
}
|
||||
|
||||
static String inject(String source, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
|
||||
return edit(source, (root, edits) -> PerFrameBlockInjector.collectEdits(root, perFrame, perPass, edits));
|
||||
}
|
||||
|
||||
private static String edit(String source, BiConsumer<GLSLParser.Translation_unitContext, List<Edit>> collect) {
|
||||
final GLSLParser.Translation_unitContext root;
|
||||
try {
|
||||
root = GlslTransformUtils.parseFullQuiet(source);
|
||||
} catch (Exception e) {
|
||||
return source;
|
||||
}
|
||||
final List<Edit> edits = new ArrayList<>();
|
||||
collect.accept(root, edits);
|
||||
return edits.isEmpty() ? source : GlslVulkanPreprocess.applyEdits(source, edits);
|
||||
}
|
||||
}
|
||||
+324
@@ -0,0 +1,324 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock.Member;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
|
||||
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.UniformType;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.opengl.GL43;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class StageDiskCacheTest {
|
||||
|
||||
private static final String RAW = """
|
||||
#version 460 core
|
||||
in vec3 a_PosId;
|
||||
void main() {
|
||||
gl_Position = vec4(a_PosId, 1.0);
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String PFB_FRAG = """
|
||||
#version 460 core
|
||||
uniform float frameTimeCounter;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = vec4(frameTimeCounter);
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String BROKEN = """
|
||||
#version 460 core
|
||||
void main() { undeclared_var = 1.0; }
|
||||
""";
|
||||
|
||||
private static final byte[] POISON_SPIRV = {1, 2, 3, 4, 5, 6, 7, 8};
|
||||
|
||||
private static final PerFrameUniformBlock BLOCK_A = new PerFrameUniformBlock(List.of(new Member("frameTimeCounter", UniformType.FLOAT)));
|
||||
|
||||
private static final PerFrameUniformBlock BLOCK_B = new PerFrameUniformBlock(List.of(
|
||||
new Member("frameTimeCounter", UniformType.FLOAT),
|
||||
new Member("rainStrength", UniformType.FLOAT)));
|
||||
|
||||
@BeforeEach
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
Reflect.setStatic(ShaderDiskCache.class, "root", null);
|
||||
ShaderManager.clearPrewarmCache();
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
GLSMHooks.perFrameUniformBlock = null;
|
||||
GLSMHooks.perPassUniformBlock = null;
|
||||
}
|
||||
|
||||
private static byte[] poison(String marker) {
|
||||
final ShaderManager.StageReflection reflection = new ShaderManager.StageReflection(
|
||||
ShaderManager.ResourceCounts.EMPTY, List.of(marker), List.of(), List.of(), 0, List.of(), List.of(), List.of(), List.of(),
|
||||
-1, 0, 0, 0, 0, ShaderManager.BlockReflection.emptyBlocks());
|
||||
return ShaderStageSerializer.encode(POISON_SPIRV, reflection, ShaderManager.GraphicsBindingMap.EMPTY, Set.of("poison_bool"), "poison_source");
|
||||
}
|
||||
|
||||
private static ShaderManager.ShaderObject object(ShaderManager sm, int id) {
|
||||
final Map<Integer, ShaderManager.ShaderObject> objects = Reflect.get(sm, "shaderObjects");
|
||||
return objects.get(id);
|
||||
}
|
||||
|
||||
@Test
|
||||
void prewarmHitReturnsTheStoredEntry(@TempDir Path dir) {
|
||||
ShaderDiskCache.configure(dir, "t");
|
||||
ShaderManager.prewarmSpirv(RAW, GL20.GL_VERTEX_SHADER);
|
||||
final ShaderDiskCache.Key key = ShaderManager.prewarmKey(RAW, GL20.GL_VERTEX_SHADER, null, null);
|
||||
assertNotNull(ShaderDiskCache.get(key), "a successful prewarm must be stored");
|
||||
|
||||
ShaderManager.clearPrewarmCache();
|
||||
ShaderDiskCache.put(key, poison("poison_sampler"));
|
||||
ShaderManager.prewarmSpirv(RAW, GL20.GL_VERTEX_SHADER);
|
||||
|
||||
final ShaderManager sm = new ShaderManager(null);
|
||||
final int shader = sm.createShader(GL20.GL_VERTEX_SHADER);
|
||||
sm.shaderSource(shader, RAW);
|
||||
assertEquals("poison_source", sm.getShaderSource(shader));
|
||||
sm.compileShader(shader);
|
||||
|
||||
final ShaderManager.ShaderObject obj = object(sm, shader);
|
||||
assertTrue(obj.compiled);
|
||||
assertEquals(List.of("poison_sampler"), obj.reflection.samplerNames());
|
||||
assertEquals(Set.of("poison_bool"), obj.boolUniforms);
|
||||
assertArrayEquals(POISON_SPIRV, ShaderCacheIO.toHeap(obj.spirv));
|
||||
sm.deleteShader(shader);
|
||||
}
|
||||
|
||||
@Test
|
||||
void prewarmHitDoesNotShareTheSpirvBufferBetweenShaders(@TempDir Path dir) {
|
||||
ShaderDiskCache.configure(dir, "t");
|
||||
ShaderManager.prewarmSpirv(RAW, GL20.GL_VERTEX_SHADER);
|
||||
ShaderManager.clearPrewarmCache();
|
||||
ShaderManager.prewarmSpirv(RAW, GL20.GL_VERTEX_SHADER);
|
||||
|
||||
final ShaderManager sm = new ShaderManager(null);
|
||||
final int a = sm.createShader(GL20.GL_VERTEX_SHADER);
|
||||
final int b = sm.createShader(GL20.GL_VERTEX_SHADER);
|
||||
sm.shaderSource(a, RAW);
|
||||
sm.shaderSource(b, RAW);
|
||||
sm.compileShader(a);
|
||||
sm.compileShader(b);
|
||||
|
||||
final ShaderManager.ShaderObject objA = object(sm, a);
|
||||
final ShaderManager.ShaderObject objB = object(sm, b);
|
||||
assertTrue(objA.compiled && objB.compiled);
|
||||
assertTrue(objA.spirv != objB.spirv);
|
||||
assertArrayEquals(ShaderCacheIO.toHeap(objA.spirv), ShaderCacheIO.toHeap(objB.spirv));
|
||||
sm.deleteShader(a);
|
||||
sm.deleteShader(b);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shaderSourceFallbackHitReturnsTheStoredEntry(@TempDir Path dir) {
|
||||
ShaderDiskCache.configure(dir, "t");
|
||||
final ShaderManager first = new ShaderManager(null);
|
||||
final int a = first.createShader(GL20.GL_VERTEX_SHADER);
|
||||
first.shaderSource(a, RAW);
|
||||
first.compileShader(a);
|
||||
assertTrue(object(first, a).compiled);
|
||||
final String transformed = first.getShaderSource(a);
|
||||
final ShaderDiskCache.Key key = ShaderManager.stageKey(transformed, GL20.GL_VERTEX_SHADER);
|
||||
assertNotNull(ShaderDiskCache.get(key), "a successful stage compile must be stored");
|
||||
assertFalse(Files.exists(dir.resolve("spirv")), "the stage entry already holds the SPIR-V");
|
||||
first.deleteShader(a);
|
||||
|
||||
ShaderManager.clearPrewarmCache();
|
||||
ShaderDiskCache.put(key, poison("poison_sampler"));
|
||||
|
||||
final ShaderManager sm = new ShaderManager(null);
|
||||
final int b = sm.createShader(GL20.GL_VERTEX_SHADER);
|
||||
sm.shaderSource(b, RAW);
|
||||
assertEquals(transformed, sm.getShaderSource(b), "the transformed source must be available without compiling");
|
||||
sm.compileShader(b);
|
||||
|
||||
final ShaderManager.ShaderObject obj = object(sm, b);
|
||||
assertTrue(obj.compiled);
|
||||
assertEquals(List.of("poison_sampler"), obj.reflection.samplerNames());
|
||||
assertArrayEquals(POISON_SPIRV, ShaderCacheIO.toHeap(obj.spirv));
|
||||
sm.deleteShader(b);
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedCompileStoresNothing(@TempDir Path dir) {
|
||||
ShaderDiskCache.configure(dir, "t");
|
||||
ShaderManager.prewarmSpirv(BROKEN, GL20.GL_VERTEX_SHADER);
|
||||
assertNull(ShaderDiskCache.get(ShaderManager.prewarmKey(BROKEN, GL20.GL_VERTEX_SHADER, null, null)));
|
||||
|
||||
final ShaderManager sm = new ShaderManager(null);
|
||||
final int shader = sm.createShader(GL20.GL_VERTEX_SHADER);
|
||||
sm.shaderSource(shader, BROKEN);
|
||||
sm.compileShader(shader);
|
||||
assertFalse(object(sm, shader).compiled);
|
||||
assertNull(ShaderDiskCache.get(ShaderManager.stageKey(sm.getShaderSource(shader), GL20.GL_VERTEX_SHADER)));
|
||||
sm.deleteShader(shader);
|
||||
}
|
||||
|
||||
@Test
|
||||
void corruptEntryFallsBackToCompilingAndIsReplaced(@TempDir Path dir) {
|
||||
ShaderDiskCache.configure(dir, "t");
|
||||
final ShaderDiskCache.Key key = ShaderManager.prewarmKey(RAW, GL20.GL_VERTEX_SHADER, null, null);
|
||||
ShaderDiskCache.put(key, new byte[] {9, 9, 9});
|
||||
ShaderManager.prewarmSpirv(RAW, GL20.GL_VERTEX_SHADER);
|
||||
assertNotNull(ShaderStageSerializer.decode(ShaderDiskCache.get(key)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void computePrewarmRoundTripsThroughTheDisk(@TempDir Path dir) {
|
||||
ShaderDiskCache.configure(dir, "t");
|
||||
final String compute = """
|
||||
#version 460 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer B { uint data[]; } b;
|
||||
void main() { b.data[0] = 1u; }
|
||||
""";
|
||||
ShaderManager.prewarmSpirv(compute, GL43.GL_COMPUTE_SHADER);
|
||||
final ShaderDiskCache.Key key = ShaderManager.prewarmKey(compute, GL43.GL_COMPUTE_SHADER, null, null);
|
||||
assertNotNull(ShaderDiskCache.get(key));
|
||||
|
||||
ShaderManager.clearPrewarmCache();
|
||||
ShaderManager.prewarmSpirv(compute, GL43.GL_COMPUTE_SHADER);
|
||||
final ShaderManager sm = new ShaderManager(null);
|
||||
final int shader = sm.createShader(GL43.GL_COMPUTE_SHADER);
|
||||
sm.shaderSource(shader, compute);
|
||||
sm.compileShader(shader);
|
||||
final ShaderManager.ShaderObject obj = object(sm, shader);
|
||||
assertTrue(obj.compiled);
|
||||
assertTrue(obj.reflection == ShaderManager.StageReflection.EMPTY, "a compute hit must keep the shared EMPTY reflection so link reflects the remapped copy");
|
||||
sm.deleteShader(shader);
|
||||
}
|
||||
|
||||
@Test
|
||||
void prewarmEntryIsNotReusedAcrossPerFrameBlocks() {
|
||||
GLSMHooks.perFrameUniformBlock = BLOCK_A;
|
||||
ShaderManager.prewarmSpirv(PFB_FRAG, GL20.GL_FRAGMENT_SHADER);
|
||||
|
||||
GLSMHooks.perFrameUniformBlock = BLOCK_B;
|
||||
final ShaderManager sm = new ShaderManager(null);
|
||||
final int viaTransform = sm.createShader(GL20.GL_FRAGMENT_SHADER);
|
||||
sm.shaderSource(viaTransform, PFB_FRAG);
|
||||
assertTrue(sm.getShaderSource(viaTransform).contains("rainStrength"), "the first prewarm's block must not leak into a different block");
|
||||
|
||||
ShaderManager.prewarmSpirv(PFB_FRAG, GL20.GL_FRAGMENT_SHADER);
|
||||
final int viaPrewarm = sm.createShader(GL20.GL_FRAGMENT_SHADER);
|
||||
sm.shaderSource(viaPrewarm, PFB_FRAG);
|
||||
assertTrue(sm.getShaderSource(viaPrewarm).contains("rainStrength"));
|
||||
|
||||
GLSMHooks.perFrameUniformBlock = BLOCK_A;
|
||||
final int back = sm.createShader(GL20.GL_FRAGMENT_SHADER);
|
||||
sm.shaderSource(back, PFB_FRAG);
|
||||
assertFalse(sm.getShaderSource(back).contains("rainStrength"));
|
||||
sm.shutdown();
|
||||
}
|
||||
|
||||
private static byte[] poisonTransform() {
|
||||
return ShaderStageSerializer.encodeTransform("poison_transform", Set.of("poison_bool"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void transformDiskHitReturnsTheStoredEntry(@TempDir Path dir) {
|
||||
ShaderDiskCache.configure(dir, "t");
|
||||
final ShaderManager first = new ShaderManager(null);
|
||||
final int a = first.createShader(GL20.GL_VERTEX_SHADER);
|
||||
first.shaderSource(a, RAW);
|
||||
final String transformed = first.getShaderSource(a);
|
||||
final ShaderDiskCache.Key key = ShaderManager.transformDiskKey(RAW, GL20.GL_VERTEX_SHADER, null, null);
|
||||
final ShaderManager.PrewarmTransformResult stored = ShaderStageSerializer.decodeTransform(ShaderDiskCache.get(key));
|
||||
assertNotNull(stored, "a successful transform must be stored");
|
||||
assertEquals(transformed, stored.source());
|
||||
first.compileShader(a);
|
||||
first.deleteShader(a);
|
||||
|
||||
ShaderManager.clearPrewarmCache();
|
||||
ShaderDiskCache.put(key, poisonTransform());
|
||||
|
||||
final ShaderManager sm = new ShaderManager(null);
|
||||
final int b = sm.createShader(GL20.GL_VERTEX_SHADER);
|
||||
sm.shaderSource(b, RAW);
|
||||
assertEquals("poison_transform", sm.getShaderSource(b));
|
||||
assertEquals(Set.of("poison_bool"), object(sm, b).boolUniforms);
|
||||
sm.compileShader(b);
|
||||
sm.deleteShader(b);
|
||||
}
|
||||
|
||||
@Test
|
||||
void corruptTransformEntryFallsBackToTransformingAndIsReplaced(@TempDir Path dir) {
|
||||
ShaderDiskCache.configure(dir, "t");
|
||||
final ShaderDiskCache.Key key = ShaderManager.transformDiskKey(RAW, GL20.GL_VERTEX_SHADER, null, null);
|
||||
ShaderDiskCache.put(key, new byte[] {9, 9, 9});
|
||||
|
||||
final ShaderManager sm = new ShaderManager(null);
|
||||
final int shader = sm.createShader(GL20.GL_VERTEX_SHADER);
|
||||
sm.shaderSource(shader, RAW);
|
||||
assertEquals(ShaderManager.applyPrewarmTransformsFull(RAW, GL20.GL_VERTEX_SHADER, null, null).source(), sm.getShaderSource(shader));
|
||||
assertNotNull(ShaderStageSerializer.decodeTransform(ShaderDiskCache.get(key)));
|
||||
sm.compileShader(shader);
|
||||
sm.deleteShader(shader);
|
||||
}
|
||||
|
||||
@Test
|
||||
void transformKeyChangesWithEveryInput() {
|
||||
final String base = ShaderManager.transformDiskKey(RAW, GL20.GL_VERTEX_SHADER, BLOCK_A, null).hex();
|
||||
|
||||
assertEquals(base, ShaderManager.transformDiskKey(RAW, GL20.GL_VERTEX_SHADER, new PerFrameUniformBlock(List.of(new Member("frameTimeCounter", UniformType.FLOAT))), null).hex());
|
||||
assertNotEquals(base, ShaderManager.transformDiskKey(RAW + " ", GL20.GL_VERTEX_SHADER, BLOCK_A, null).hex(), "source");
|
||||
assertNotEquals(base, ShaderManager.transformDiskKey(RAW, GL20.GL_FRAGMENT_SHADER, BLOCK_A, null).hex(), "type");
|
||||
assertNotEquals(base, ShaderManager.transformDiskKey(RAW, GL20.GL_VERTEX_SHADER, BLOCK_B, null).hex(), "per-frame block");
|
||||
assertNotEquals(base, ShaderManager.transformDiskKey(RAW, GL20.GL_VERTEX_SHADER, null, null).hex(), "no per-frame block");
|
||||
assertNotEquals(base, ShaderManager.transformDiskKey(RAW, GL20.GL_VERTEX_SHADER, BLOCK_A, BLOCK_B).hex(), "per-pass block");
|
||||
assertNotEquals(base, ShaderManager.transformDiskKey(RAW, GL20.GL_VERTEX_SHADER, null, BLOCK_A).hex(), "slots are not interchangeable");
|
||||
assertNotEquals(base, ShaderManager.prewarmKey(RAW, GL20.GL_VERTEX_SHADER, null, null).hex(), "layer and toolchain ids");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stageKeyIgnoresUniformBlocks() {
|
||||
GLSMHooks.perFrameUniformBlock = BLOCK_A;
|
||||
final String base = ShaderManager.stageKey(RAW, GL20.GL_VERTEX_SHADER).hex();
|
||||
GLSMHooks.perFrameUniformBlock = BLOCK_B;
|
||||
GLSMHooks.perPassUniformBlock = BLOCK_A;
|
||||
assertEquals(base, ShaderManager.stageKey(RAW, GL20.GL_VERTEX_SHADER).hex());
|
||||
assertNotEquals(base, ShaderManager.stageKey(RAW + " ", GL20.GL_VERTEX_SHADER).hex());
|
||||
assertNotEquals(base, ShaderManager.stageKey(RAW, GL20.GL_FRAGMENT_SHADER).hex());
|
||||
}
|
||||
|
||||
@Test
|
||||
void prewarmKeyChangesWithEveryInput() {
|
||||
final String base = ShaderManager.prewarmKey(RAW, GL20.GL_VERTEX_SHADER, BLOCK_A, null).hex();
|
||||
|
||||
assertEquals(base, ShaderManager.prewarmKey(RAW, GL20.GL_VERTEX_SHADER, BLOCK_A, null).hex());
|
||||
assertEquals(base, ShaderManager.prewarmKey(RAW, GL20.GL_VERTEX_SHADER, new PerFrameUniformBlock(List.of(new Member("frameTimeCounter", UniformType.FLOAT))), null).hex(), "equal blocks built separately must agree");
|
||||
|
||||
assertNotEquals(base, ShaderManager.stageKey(RAW, GL20.GL_VERTEX_SHADER).hex());
|
||||
assertNotEquals(base, ShaderManager.prewarmKey(RAW + " ", GL20.GL_VERTEX_SHADER, BLOCK_A, null).hex());
|
||||
assertNotEquals(base, ShaderManager.prewarmKey(RAW, GL20.GL_FRAGMENT_SHADER, BLOCK_A, null).hex());
|
||||
|
||||
assertNotEquals(base, ShaderManager.prewarmKey(RAW, GL20.GL_VERTEX_SHADER, BLOCK_B, null).hex(), "an added per-frame member");
|
||||
assertNotEquals(base, ShaderManager.prewarmKey(RAW, GL20.GL_VERTEX_SHADER, new PerFrameUniformBlock(List.of(new Member("frameTimeCounter", UniformType.INT))), null).hex(), "a changed member type");
|
||||
assertNotEquals(base, ShaderManager.prewarmKey(RAW, GL20.GL_VERTEX_SHADER, new PerFrameUniformBlock(List.of(new Member("rainStrength", UniformType.FLOAT))), null).hex(), "a renamed member");
|
||||
assertNotEquals(base, ShaderManager.prewarmKey(RAW, GL20.GL_VERTEX_SHADER, null, null).hex(), "no per-frame block");
|
||||
assertNotEquals(base, ShaderManager.prewarmKey(RAW, GL20.GL_VERTEX_SHADER, BLOCK_A, BLOCK_B).hex(), "a per-pass block");
|
||||
}
|
||||
}
|
||||
+43
-4
@@ -1,17 +1,26 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader.dxbc;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
|
||||
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.SpirvTestShaders;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileCache;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.opengl.GL43;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
import org.lwjgl.util.shaderc.Shaderc;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@@ -21,6 +30,13 @@ import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
@EnabledOnOs(OS.WINDOWS)
|
||||
class DxbcCompileTest {
|
||||
private static CrossCompileCache cache() { return Reflect.getStatic(DxbcCrossCompile.class, "CACHE"); }
|
||||
|
||||
@AfterEach
|
||||
void resetDiskCache() {
|
||||
Reflect.setStatic(ShaderDiskCache.class, "root", null);
|
||||
Reflect.<Map<?, ?>>get(cache(), "cache").clear();
|
||||
}
|
||||
|
||||
private static final byte DXBC_MAGIC_D = 'D';
|
||||
private static final byte DXBC_MAGIC_X = 'X';
|
||||
@@ -34,7 +50,7 @@ class DxbcCompileTest {
|
||||
final ByteBuffer spirv = compile(SpirvTestShaders.VERTEX_GLSL, Shaderc.shaderc_vertex_shader);
|
||||
try {
|
||||
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_VERTEX_SHADER);
|
||||
final DxbcCrossCompile.Output out = DxbcCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
|
||||
final CrossCompileCache.Output out = DxbcCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
|
||||
try {
|
||||
assertNotNull(out.code());
|
||||
assertTrue(out.code().remaining() >= 4, "DXBC output too short");
|
||||
@@ -53,7 +69,7 @@ class DxbcCompileTest {
|
||||
final ByteBuffer spirv = compile(SpirvTestShaders.FRAGMENT_GLSL, Shaderc.shaderc_fragment_shader);
|
||||
try {
|
||||
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_FRAGMENT_SHADER);
|
||||
final DxbcCrossCompile.Output out = DxbcCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER);
|
||||
final CrossCompileCache.Output out = DxbcCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER);
|
||||
try {
|
||||
assertNotNull(out.code());
|
||||
assertTrue(out.code().remaining() >= 4, "DXBC output too short");
|
||||
@@ -73,7 +89,7 @@ class DxbcCompileTest {
|
||||
final ByteBuffer spirv = compile(SpirvTestShaders.HIZ_INIT_GLSL, Shaderc.shaderc_compute_shader);
|
||||
try {
|
||||
ShaderManager.remapSpirvForComputeSDLGPU(spirv);
|
||||
final DxbcCrossCompile.Output out = DxbcCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
|
||||
final CrossCompileCache.Output out = DxbcCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
|
||||
try {
|
||||
assertNotNull(out.code());
|
||||
assertTrue(out.code().remaining() >= 4, "DXBC compute output too short");
|
||||
@@ -105,7 +121,7 @@ class DxbcCompileTest {
|
||||
final ByteBuffer spirv = compile(SpirvTestShaders.VERTEX_GLSL, Shaderc.shaderc_vertex_shader);
|
||||
try {
|
||||
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_VERTEX_SHADER);
|
||||
final DxbcCrossCompile.Output out = DxbcCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
|
||||
final CrossCompileCache.Output out = DxbcCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
|
||||
MemoryUtil.memFree(out.code());
|
||||
} finally {
|
||||
MemoryUtil.memFree(spirv);
|
||||
@@ -113,6 +129,29 @@ class DxbcCompileTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void diskCacheHitReturnsStoredBlob(@TempDir Path dir) {
|
||||
ShaderDiskCache.configure(dir, "t");
|
||||
Reflect.<Map<?, ?>>get(cache(), "cache").clear();
|
||||
final ByteBuffer spirv = compile(SpirvTestShaders.VERTEX_GLSL, Shaderc.shaderc_vertex_shader);
|
||||
try {
|
||||
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_VERTEX_SHADER);
|
||||
final byte[] heap = ShaderCacheIO.toHeap(spirv);
|
||||
MemoryUtil.memFree(DxbcCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER).code());
|
||||
Reflect.<Map<?, ?>>get(cache(), "cache").clear();
|
||||
ShaderDiskCache.putBlob(cache().diskKey(heap, GL20.GL_VERTEX_SHADER), "poison_entry", "poison".getBytes(StandardCharsets.UTF_8));
|
||||
final CrossCompileCache.Output out = DxbcCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
|
||||
try {
|
||||
assertEquals("poison_entry", out.entrypoint());
|
||||
assertEquals("poison", new String(ShaderCacheIO.toHeap(out.code()), StandardCharsets.UTF_8));
|
||||
} finally {
|
||||
MemoryUtil.memFree(out.code());
|
||||
}
|
||||
} finally {
|
||||
MemoryUtil.memFree(spirv);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertDxbcMagic(ByteBuffer dxbc) {
|
||||
final ByteBuffer view = dxbc.duplicate();
|
||||
assertEquals(DXBC_MAGIC_D, view.get(0), "DXBC magic byte 0");
|
||||
|
||||
+3
-2
@@ -4,6 +4,7 @@ import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
|
||||
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileCache;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
@@ -56,7 +57,7 @@ class FragmentStorageTextureSlotTest {
|
||||
final ByteBuffer spirv = r.spirv();
|
||||
try {
|
||||
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_FRAGMENT_SHADER);
|
||||
final MslCrossCompile.Output out = MslCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER);
|
||||
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER);
|
||||
try {
|
||||
final String msl = StandardCharsets.UTF_8.decode(out.code().duplicate()).toString();
|
||||
final boolean[] seen = new boolean[samplerCount];
|
||||
@@ -135,7 +136,7 @@ class FragmentStorageTextureSlotTest {
|
||||
final ByteBuffer spirv = Reflect.get(obj, "spirv");
|
||||
final ShaderManager.StageReflection refl = Reflect.get(obj, "reflection");
|
||||
|
||||
final MslCrossCompile.Output out = MslCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER);
|
||||
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER);
|
||||
try {
|
||||
final String msl = StandardCharsets.UTF_8.decode(out.code().duplicate()).toString();
|
||||
final int samplerCount = refl.counts().numSamplers();
|
||||
|
||||
+46
-9
@@ -1,9 +1,15 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader.msl;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
|
||||
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.SpirvTestShaders;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileCache;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.opengl.GL43;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
@@ -15,6 +21,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -28,6 +35,13 @@ import static org.junit.jupiter.api.Assertions.fail;
|
||||
* Tests SPIR-V → MSL cross-compilation for the macOS/Metal backend.
|
||||
*/
|
||||
class MslCrossCompileTest {
|
||||
private static CrossCompileCache cache() { return Reflect.getStatic(MslCrossCompile.class, "CACHE"); }
|
||||
|
||||
@AfterEach
|
||||
void resetDiskCache() {
|
||||
Reflect.setStatic(ShaderDiskCache.class, "root", null);
|
||||
Reflect.<Map<?, ?>>get(cache(), "cache").clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +50,7 @@ class MslCrossCompileTest {
|
||||
final ByteBuffer spirv = compile(SpirvTestShaders.VERTEX_GLSL, Shaderc.shaderc_vertex_shader);
|
||||
try {
|
||||
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_VERTEX_SHADER);
|
||||
final MslCrossCompile.Output out = MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
|
||||
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
|
||||
try {
|
||||
final String msl = decode(out.code());
|
||||
assertTrue(msl.length() > 0, "MSL output non-empty");
|
||||
@@ -59,7 +73,7 @@ class MslCrossCompileTest {
|
||||
final ByteBuffer spirv = compile(SpirvTestShaders.FRAGMENT_GLSL, Shaderc.shaderc_fragment_shader);
|
||||
try {
|
||||
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_FRAGMENT_SHADER);
|
||||
final MslCrossCompile.Output out = MslCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER);
|
||||
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_FRAGMENT_SHADER);
|
||||
try {
|
||||
final String msl = decode(out.code());
|
||||
assertTrue(msl.contains("fragment "), "MSL should contain fragment stage qualifier:\n" + msl);
|
||||
@@ -98,7 +112,7 @@ class MslCrossCompileTest {
|
||||
final ByteBuffer spirv = compile(SpirvTestShaders.LDS_MULTI_MIP_GLSL, Shaderc.shaderc_compute_shader);
|
||||
try {
|
||||
ShaderManager.remapSpirvForComputeSDLGPU(spirv);
|
||||
final MslCrossCompile.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
|
||||
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
|
||||
try {
|
||||
final String msl = decode(out.code());
|
||||
assertTrue(msl.contains("kernel "), "Compute MSL should contain kernel stage qualifier:\n" + msl);
|
||||
@@ -130,7 +144,7 @@ class MslCrossCompileTest {
|
||||
final ByteBuffer spirv = compile(glsl, Shaderc.shaderc_compute_shader);
|
||||
try {
|
||||
ShaderManager.remapSpirvForComputeSDLGPU(spirv);
|
||||
final MslCrossCompile.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
|
||||
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
|
||||
try {
|
||||
final String msl = decode(out.code());
|
||||
assertTrue(msl.contains("kernel "), "compute MSL should contain kernel stage qualifier:\n" + msl);
|
||||
@@ -151,7 +165,7 @@ class MslCrossCompileTest {
|
||||
final ByteBuffer spirv = compile(CHUNK_CULL_GLSL, Shaderc.shaderc_compute_shader);
|
||||
try {
|
||||
ShaderManager.remapSpirvForComputeSDLGPU(spirv);
|
||||
final MslCrossCompile.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
|
||||
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
|
||||
try {
|
||||
final String msl = decode(out.code());
|
||||
assertTrue(msl.contains("kernel "), "Compute MSL should contain kernel stage qualifier:\n" + msl);
|
||||
@@ -186,7 +200,7 @@ class MslCrossCompileTest {
|
||||
final ByteBuffer spirv = compile(VERTEX_WRITES_IMAGE_GLSL, Shaderc.shaderc_vertex_shader);
|
||||
try {
|
||||
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_VERTEX_SHADER);
|
||||
final MslCrossCompile.Output out = MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
|
||||
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
|
||||
try {
|
||||
final String msl = decode(out.code());
|
||||
assertFalse(msl.contains("vertex void main0"),
|
||||
@@ -207,7 +221,7 @@ class MslCrossCompileTest {
|
||||
final ByteBuffer spirv = compile(SpirvTestShaders.VERTEX_GLSL, Shaderc.shaderc_vertex_shader);
|
||||
try {
|
||||
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_VERTEX_SHADER);
|
||||
final MslCrossCompile.Output out = MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
|
||||
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
|
||||
MemoryUtil.memFree(out.code());
|
||||
} finally {
|
||||
MemoryUtil.memFree(spirv);
|
||||
@@ -333,7 +347,7 @@ class MslCrossCompileTest {
|
||||
final ByteBuffer spirv = compile(glsl, Shaderc.shaderc_compute_shader);
|
||||
try {
|
||||
ShaderManager.remapSpirvForComputeSDLGPU(spirv);
|
||||
final MslCrossCompile.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
|
||||
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
|
||||
try {
|
||||
final String msl = decode(out.code());
|
||||
assertUniqueBufferSlots(msl);
|
||||
@@ -388,7 +402,7 @@ class MslCrossCompileTest {
|
||||
final ByteBuffer spirv = compile(glsl, Shaderc.shaderc_compute_shader);
|
||||
try {
|
||||
ShaderManager.remapSpirvForComputeSDLGPU(spirv);
|
||||
final MslCrossCompile.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
|
||||
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL43.GL_COMPUTE_SHADER);
|
||||
try {
|
||||
assertTrue(decode(out.code()).contains("kernel "), name + " should produce compute MSL");
|
||||
} finally {
|
||||
@@ -400,6 +414,29 @@ class MslCrossCompileTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void diskCacheHitReturnsStoredBlob(@TempDir Path dir) {
|
||||
ShaderDiskCache.configure(dir, "t");
|
||||
Reflect.<Map<?, ?>>get(cache(), "cache").clear();
|
||||
final ByteBuffer spirv = compile(SpirvTestShaders.VERTEX_GLSL, Shaderc.shaderc_vertex_shader);
|
||||
try {
|
||||
ShaderManager.remapSpirvForSDLGPU(spirv, GL20.GL_VERTEX_SHADER);
|
||||
final byte[] heap = ShaderCacheIO.toHeap(spirv);
|
||||
MemoryUtil.memFree(MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER).code());
|
||||
Reflect.<Map<?, ?>>get(cache(), "cache").clear();
|
||||
ShaderDiskCache.putBlob(cache().diskKey(heap, GL20.GL_VERTEX_SHADER), "poison_entry", "poison".getBytes(StandardCharsets.UTF_8));
|
||||
final CrossCompileCache.Output out = MslCrossCompile.compile(spirv, GL20.GL_VERTEX_SHADER);
|
||||
try {
|
||||
assertEquals("poison_entry", out.entrypoint());
|
||||
assertEquals("poison", new String(ShaderCacheIO.toHeap(out.code()), StandardCharsets.UTF_8));
|
||||
} finally {
|
||||
MemoryUtil.memFree(out.code());
|
||||
}
|
||||
} finally {
|
||||
MemoryUtil.memFree(spirv);
|
||||
}
|
||||
}
|
||||
|
||||
private static String decode(ByteBuffer buf) {
|
||||
final ByteBuffer slice = buf.duplicate();
|
||||
int len = slice.remaining();
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.gtnewhorizons.angelica.debug.DebugKeyHandler;
|
||||
import com.gtnewhorizons.angelica.rendering.culling.GpuCulling;
|
||||
import static com.gtnewhorizons.angelica.AngelicaMod.MOD_ID;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.util.Locale;
|
||||
@@ -40,6 +41,7 @@ import com.gtnewhorizon.gtnhlib.client.renderer.vao.VAOManager;
|
||||
import com.gtnewhorizons.angelica.client.font.DarkModeUtils;
|
||||
import com.gtnewhorizons.angelica.commands.AngelicaCommand;
|
||||
import com.gtnewhorizons.angelica.AngelicaMod;
|
||||
import com.gtnewhorizons.angelica.Tags;
|
||||
import com.gtnewhorizons.angelica.common.BlockError;
|
||||
import com.gtnewhorizons.angelica.common.BlockIsbrhTest;
|
||||
import com.gtnewhorizons.angelica.compat.ModStatus;
|
||||
@@ -62,6 +64,7 @@ import com.gtnewhorizons.angelica.dynamiclights.config.EntityLightConfig;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
|
||||
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
|
||||
import com.gtnewhorizons.angelica.iris.IrisGLSMBridge;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
|
||||
@@ -148,6 +151,11 @@ public final class ClientProxy extends CommonProxy {
|
||||
BlockIsbrhTest.renderId = RenderingRegistry.getNextAvailableRenderId();
|
||||
RenderingRegistry.registerBlockHandler(BlockIsbrhTest.renderId, new IsbrhTestRenderer());
|
||||
}
|
||||
final File source = event.getSourceFile();
|
||||
if (source != null && source.isFile()) {
|
||||
ShaderDiskCache.configure(Minecraft.getMinecraft().mcDataDir.toPath().resolve("angelica").resolve("shadercache"),
|
||||
Tags.VERSION + "|" + source.length() + "|" + source.lastModified());
|
||||
}
|
||||
if (AngelicaConfig.enableIris) {
|
||||
IrisGLSMBridge.installImmediateExtendedHandler();
|
||||
Iris.warmupShaderTransforms();
|
||||
|
||||
@@ -58,6 +58,10 @@ public final class RwImageStoreExtractor {
|
||||
|
||||
private static volatile Map<String, ImageInformation> activeCustomImages = Map.of();
|
||||
|
||||
static Map<String, ImageInformation> activeCustomImages() {
|
||||
return activeCustomImages;
|
||||
}
|
||||
|
||||
public static void setActiveCustomImages(Map<String, ImageInformation> images) {
|
||||
activeCustomImages = (images == null) ? Map.of() : Map.copyOf(images);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.iris.IrisExtendedChunkVertexType;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
|
||||
import net.coderbot.iris.Iris;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderType;
|
||||
import net.coderbot.iris.pipeline.transform.parameter.AttributeParameters;
|
||||
import net.coderbot.iris.pipeline.transform.parameter.Parameters;
|
||||
@@ -22,13 +23,19 @@ import org.taumc.glsl.Transformer;
|
||||
import org.taumc.glsl.grammar.GLSLLexer;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -226,6 +233,57 @@ public class ShaderTransformer {
|
||||
|
||||
record StageArtifact(GLSLParser.Translation_unitContext tree, int headerLen) {}
|
||||
|
||||
static ShaderDiskCache.Key diskKey(Patch patch, EnumMap<PatchShaderType, String> inputs, Parameters parameters) {
|
||||
final ShaderDiskCache.Key k = ShaderDiskCache.key("iris-transform").str(patch.name());
|
||||
parameters.appendDiskKey(k);
|
||||
k.i(RenderSystem.getMaxGlslVersion()).b(RenderSystem.supportsSSBO()).b(RenderSystem.supportsImageLoadStore())
|
||||
.b(RenderSystem.isGLES()).b(BackendManager.RENDER_BACKEND.isSDLGPU()).i(maxSupportedHoistVersion);
|
||||
if (enabledRequirements == null) {
|
||||
k.i(-1);
|
||||
} else {
|
||||
k.i(enabledRequirements.length);
|
||||
for (VersionRequirement r : enabledRequirements) k.str(r.keyword());
|
||||
}
|
||||
appendSorted(k, SamplerToStorageImageRewriter.activeCandidates(), Object::toString);
|
||||
appendSorted(k, RwImageStoreExtractor.activeCustomImages().entrySet(), e -> e.getKey() + "=" + e.getValue());
|
||||
if (patch == Patch.CELERITAS_TERRAIN) k.str(CeleritasHeader.VALUE);
|
||||
for (PatchShaderType t : PatchShaderType.VALUES) k.str(inputs.get(t));
|
||||
return k;
|
||||
}
|
||||
|
||||
private static <T> void appendSorted(ShaderDiskCache.Key k, Collection<T> items, Function<T, String> render) {
|
||||
final List<String> rendered = new ArrayList<>(items.size());
|
||||
for (T item : items) rendered.add(render.apply(item));
|
||||
k.sortedStrs(rendered);
|
||||
}
|
||||
|
||||
private static Map<PatchShaderType, String> diskOrCompute(Patch patchType, EnumMap<PatchShaderType, String> inputs, Parameters parameters, Supplier<Map<PatchShaderType, String>> compute) {
|
||||
final ShaderDiskCache.Key diskKey = ShaderDiskCache.isEnabled() ? diskKey(patchType, inputs, parameters) : null;
|
||||
final Map<PatchShaderType, String> fromDisk = diskKey != null ? readDisk(diskKey) : null;
|
||||
if (fromDisk != null) return fromDisk;
|
||||
final Map<PatchShaderType, String> result = compute.get();
|
||||
if (diskKey != null) writeDisk(diskKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Map<PatchShaderType, String> readDisk(ShaderDiskCache.Key key) {
|
||||
final Map<String, String> stored = ShaderDiskCache.getStrings(key);
|
||||
if (stored == null) return null;
|
||||
try {
|
||||
final EnumMap<PatchShaderType, String> out = new EnumMap<>(PatchShaderType.class);
|
||||
for (Map.Entry<String, String> e : stored.entrySet()) out.put(PatchShaderType.valueOf(e.getKey()), e.getValue());
|
||||
return out;
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeDisk(ShaderDiskCache.Key key, Map<PatchShaderType, String> result) {
|
||||
final Map<String, String> out = new LinkedHashMap<>();
|
||||
for (Map.Entry<PatchShaderType, String> e : result.entrySet()) out.put(e.getKey().name(), e.getValue());
|
||||
ShaderDiskCache.putStrings(key, out);
|
||||
}
|
||||
|
||||
public static <P extends Parameters> Map<PatchShaderType, String> transform(String vertex, String geometry, String tessControl, String tessEval, String fragment, P parameters) {
|
||||
return transform(vertex, geometry, tessControl, tessEval, fragment, parameters, null);
|
||||
}
|
||||
@@ -251,7 +309,7 @@ public class ShaderTransformer {
|
||||
result = shaderTransformationCache.getAndMoveToLast(key);
|
||||
}
|
||||
if(result == null || !useCache) {
|
||||
result = transformInternal(inputs, patchType, parameters, artifactsOut);
|
||||
result = diskOrCompute(patchType, inputs, parameters, () -> transformInternal(inputs, patchType, parameters, artifactsOut));
|
||||
// Clear this, we don't want whatever random type was last transformed being considered for the key
|
||||
parameters.type = null;
|
||||
synchronized (shaderTransformationCache) {
|
||||
@@ -293,7 +351,7 @@ public class ShaderTransformer {
|
||||
result = shaderTransformationCache.getAndMoveToLast(key);
|
||||
}
|
||||
if (result == null || !useCache) {
|
||||
result = transformComputeInternal(compute, patchType, parameters, artifactsOut);
|
||||
result = diskOrCompute(patchType, inputs, parameters, () -> transformComputeInternal(compute, patchType, parameters, artifactsOut));
|
||||
// Clear this, we don't want whatever random type was last transformed being considered for the key
|
||||
parameters.type = null;
|
||||
synchronized (shaderTransformationCache) {
|
||||
@@ -481,7 +539,7 @@ public class ShaderTransformer {
|
||||
String headerTail = stageHeader.extensions();
|
||||
// For Celeritas terrain vertex shaders, inject chunk_vertex.glsl header
|
||||
if (patchType == Patch.CELERITAS_TERRAIN && shaderType == PatchShaderType.VERTEX) {
|
||||
headerTail += computeCeleritasHeader();
|
||||
headerTail += CeleritasHeader.VALUE;
|
||||
}
|
||||
|
||||
final StringBuilder formattedShaderBuilder = new StringBuilder();
|
||||
@@ -664,6 +722,10 @@ public class ShaderTransformer {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CeleritasHeader {
|
||||
static final String VALUE = computeCeleritasHeader();
|
||||
}
|
||||
|
||||
private static String computeCeleritasHeader() {
|
||||
final ShaderConstants constants = ShaderConstants.builder()
|
||||
.add("VERT_POS_SCALE", "1.0")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.coderbot.iris.pipeline.transform.parameter;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
|
||||
import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability;
|
||||
import net.coderbot.iris.pipeline.transform.Patch;
|
||||
import net.coderbot.iris.shaderpack.texture.TextureStage;
|
||||
@@ -27,6 +28,12 @@ public class AttributeParameters extends Parameters {
|
||||
return TextureStage.GBUFFERS_AND_SHADOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendDiskKey(ShaderDiskCache.Key k) {
|
||||
super.appendDiskKey(k);
|
||||
k.b(hasGeometry).b(hasTesselation).str(inputs == null ? null : inputs.name()).b(scrollGlint).str(instancing.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
|
||||
@@ -2,12 +2,16 @@ package net.coderbot.iris.pipeline.transform.parameter;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
|
||||
import net.coderbot.iris.gl.blending.AlphaTest;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderType;
|
||||
import com.gtnewhorizons.angelica.glsm.texture.TextureType;
|
||||
import net.coderbot.iris.helpers.Tri;
|
||||
import net.coderbot.iris.pipeline.transform.Patch;
|
||||
import net.coderbot.iris.shaderpack.texture.TextureStage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class Parameters {
|
||||
public final Patch patch;
|
||||
private final Object2ObjectMap<Tri<String, TextureType, TextureStage>, String> textureMap;
|
||||
@@ -29,6 +33,20 @@ public abstract class Parameters {
|
||||
return textureMap;
|
||||
}
|
||||
|
||||
public void appendDiskKey(ShaderDiskCache.Key k) {
|
||||
k.str(getClass().getName());
|
||||
if (textureMap == null) {
|
||||
k.i(-1);
|
||||
return;
|
||||
}
|
||||
final List<String> rendered = new ArrayList<>(textureMap.size());
|
||||
for (Object2ObjectMap.Entry<Tri<String, TextureType, TextureStage>, String> e : textureMap.object2ObjectEntrySet()) {
|
||||
final Tri<String, TextureType, TextureStage> tri = e.getKey();
|
||||
rendered.add(tri.first() + "\0" + tri.second().name() + "\0" + tri.third().name() + "\0" + e.getValue());
|
||||
}
|
||||
k.sortedStrs(rendered);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
|
||||
+7
@@ -2,6 +2,7 @@ package net.coderbot.iris.pipeline.transform.parameter;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
|
||||
import net.coderbot.iris.gl.blending.AlphaTest;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
|
||||
import com.gtnewhorizons.angelica.glsm.texture.TextureType;
|
||||
import net.coderbot.iris.helpers.Tri;
|
||||
import net.coderbot.iris.pipeline.transform.Patch;
|
||||
@@ -21,6 +22,12 @@ public class TextureStageParameters extends Parameters {
|
||||
return stage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendDiskKey(ShaderDiskCache.Key k) {
|
||||
super.appendDiskKey(k);
|
||||
k.str(stage == null ? null : stage.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package net.coderbot.iris.pipeline.transform;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.RenderSystem;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
|
||||
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
|
||||
import com.gtnewhorizons.angelica.glsm.texture.TextureType;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import net.coderbot.iris.helpers.Tri;
|
||||
import net.coderbot.iris.pipeline.transform.parameter.ComputeParameters;
|
||||
import net.coderbot.iris.pipeline.transform.parameter.TextureStageParameters;
|
||||
import net.coderbot.iris.shaderpack.texture.TextureStage;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class ShaderTransformerDiskCacheTest {
|
||||
private static final String VERTEX = "#version 330 core\nvoid main(){ gl_Position = vec4(0.0); }";
|
||||
private static final String FRAGMENT = "#version 330 core\nvoid main(){ gl_FragColor = vec4(1.0); }";
|
||||
private static final String COMPUTE = String.join("\n",
|
||||
"#version 330 core",
|
||||
"layout(local_size_x = 8) in;",
|
||||
"out vec4 unused;",
|
||||
"void main(){ unused = vec4(float(gl_GlobalInvocationID.x)); }",
|
||||
"");
|
||||
|
||||
@TempDir
|
||||
Path dir;
|
||||
|
||||
private int savedMaxGlsl;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
savedMaxGlsl = Reflect.getStatic(RenderSystem.class, "maxGlslVersion");
|
||||
Reflect.setStatic(RenderSystem.class, "maxGlslVersion", 460);
|
||||
TransformPatcher.clearCache();
|
||||
ShaderTransformer.clearCache();
|
||||
ShaderTransformer.init();
|
||||
ShaderDiskCache.configure(dir, "t");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws Exception {
|
||||
TransformPatcher.clearCache();
|
||||
ShaderTransformer.clearCache();
|
||||
Reflect.setStatic(ShaderDiskCache.class, "root", null);
|
||||
Reflect.setStatic(RenderSystem.class, "maxGlslVersion", savedMaxGlsl);
|
||||
ShaderTransformer.init();
|
||||
}
|
||||
|
||||
private static EnumMap<PatchShaderType, String> compositeInputs() {
|
||||
final EnumMap<PatchShaderType, String> inputs = new EnumMap<>(PatchShaderType.class);
|
||||
inputs.put(PatchShaderType.VERTEX, VERTEX);
|
||||
inputs.put(PatchShaderType.GEOMETRY, null);
|
||||
inputs.put(PatchShaderType.TESS_CONTROL, null);
|
||||
inputs.put(PatchShaderType.TESS_EVAL, null);
|
||||
inputs.put(PatchShaderType.FRAGMENT, FRAGMENT);
|
||||
return inputs;
|
||||
}
|
||||
|
||||
private static TextureStageParameters compositeParams() {
|
||||
return new TextureStageParameters(Patch.COMPOSITE, TextureStage.COMPOSITE_AND_FINAL, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void compositeDiskHitSkipsTransform() {
|
||||
assertNotNull(TransformPatcher.patchComposite(VERTEX, null, FRAGMENT));
|
||||
TransformPatcher.clearCache();
|
||||
ShaderTransformer.clearCache();
|
||||
|
||||
ShaderDiskCache.putStrings(ShaderTransformer.diskKey(Patch.COMPOSITE, compositeInputs(), compositeParams()), Map.of("VERTEX", "POISON-V", "FRAGMENT", "POISON-F"));
|
||||
|
||||
final Map<PatchShaderType, String> out = TransformPatcher.patchComposite(VERTEX, null, FRAGMENT);
|
||||
assertEquals("POISON-V", out.get(PatchShaderType.VERTEX));
|
||||
assertEquals("POISON-F", out.get(PatchShaderType.FRAGMENT));
|
||||
}
|
||||
|
||||
@Test
|
||||
void computeDiskHitSkipsTransform() {
|
||||
assertNotNull(TransformPatcher.patchCompute("shadowcomp", COMPUTE, null, null));
|
||||
TransformPatcher.clearCache();
|
||||
ShaderTransformer.clearCache();
|
||||
|
||||
final EnumMap<PatchShaderType, String> inputs = new EnumMap<>(PatchShaderType.class);
|
||||
inputs.put(PatchShaderType.COMPUTE, COMPUTE);
|
||||
ShaderDiskCache.putStrings(ShaderTransformer.diskKey(Patch.COMPUTE, inputs, new ComputeParameters(Patch.COMPUTE, null, null)), Map.of("COMPUTE", "POISON-C"));
|
||||
|
||||
assertEquals("POISON-C", TransformPatcher.patchCompute("shadowcomp", COMPUTE, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void keyChangesWithGlslVersion() throws Exception {
|
||||
final String before = ShaderTransformer.diskKey(Patch.COMPOSITE, compositeInputs(), compositeParams()).hex();
|
||||
Reflect.setStatic(RenderSystem.class, "maxGlslVersion", 330);
|
||||
final String after = ShaderTransformer.diskKey(Patch.COMPOSITE, compositeInputs(), compositeParams()).hex();
|
||||
assertNotEquals(before, after);
|
||||
}
|
||||
|
||||
private static Object2ObjectOpenHashMap<Tri<String, TextureType, TextureStage>, String> textureMap(int capacity) {
|
||||
final Object2ObjectOpenHashMap<Tri<String, TextureType, TextureStage>, String> m = new Object2ObjectOpenHashMap<>(capacity);
|
||||
m.put(new Tri<>("colortex0", TextureType.TEXTURE_2D, TextureStage.COMPOSITE_AND_FINAL), "a.png");
|
||||
m.put(new Tri<>("noisetex", TextureType.TEXTURE_3D, TextureStage.GBUFFERS_AND_SHADOW), "b.png");
|
||||
m.put(new Tri<>("gaux1", TextureType.TEXTURE_1D, TextureStage.DEFERRED), "c.png");
|
||||
return m;
|
||||
}
|
||||
|
||||
@Test
|
||||
void keyIgnoresTextureMapIterationOrder() {
|
||||
final Object2ObjectOpenHashMap<Tri<String, TextureType, TextureStage>, String> m1 = textureMap(16);
|
||||
Object2ObjectOpenHashMap<Tri<String, TextureType, TextureStage>, String> m2 = null;
|
||||
for (int capacity = 32; capacity <= (1 << 20) && m2 == null; capacity <<= 1) {
|
||||
final Object2ObjectOpenHashMap<Tri<String, TextureType, TextureStage>, String> candidate = textureMap(capacity);
|
||||
if (!new ArrayList<>(m1.keySet()).equals(new ArrayList<>(candidate.keySet()))) m2 = candidate;
|
||||
}
|
||||
assertNotNull(m2, "no capacity produced a different iteration order");
|
||||
assertNotEquals(new ArrayList<>(m1.keySet()), new ArrayList<>(m2.keySet()));
|
||||
assertEquals(m1, m2);
|
||||
|
||||
final String h1 = ShaderTransformer.diskKey(Patch.COMPOSITE, compositeInputs(), new TextureStageParameters(Patch.COMPOSITE, TextureStage.COMPOSITE_AND_FINAL, m1)).hex();
|
||||
final String h2 = ShaderTransformer.diskKey(Patch.COMPOSITE, compositeInputs(), new TextureStageParameters(Patch.COMPOSITE, TextureStage.COMPOSITE_AND_FINAL, m2)).hex();
|
||||
assertEquals(h1, h2);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user