Files
Faset_Engine/tests/test_shader_reflection.py
T

165 lines
8.0 KiB
Python

"""The cooked shader interface must preserve GPU resource kinds and element strides."""
import importlib.util
import json
import os
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
import unittest
SCRIPT = Path(__file__).resolve().parents[1] / "tools" / "compile_shader.py"
SPEC = importlib.util.spec_from_file_location("faset_compile_shader", SCRIPT)
assert SPEC and SPEC.loader
shader = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(shader)
def parameter(name: str, index: int, shape: str, access: str, stride: int | None = None):
result = {"kind": "scalar", "scalarType": "uint32", "sizes": []}
if stride is not None:
result["sizes"] = [{"kind": "uniform", "value": stride, "alignment": 16}]
return {
"name": name,
"binding": {"kind": "descriptorTableSlot", "index": index, "space": 0},
"type": {
"kind": "resource",
"baseShape": shape,
"access": access,
"resultType": result,
"sizes": [{"kind": "descriptorTableSlot", "value": 1}],
},
}
class ReflectionTests(unittest.TestCase):
def test_graphics_lighting_abi(self):
compiler = os.environ["FASET_TEST_SLANGC"]
with tempfile.TemporaryDirectory(prefix="faset-lighting-abi-") as directory:
for source, entry, defines in (
("baseline.slang", "fragmentMain", []),
("gpu_scene.slang", "gpuVertexMain", ["--define", "FASET_GPU_GRAPHICS=1"]),
):
process = subprocess.run(
[sys.executable, str(SCRIPT), "--compiler", compiler, "--source",
str(SCRIPT.parents[1] / "shaders" / source), "--entry", entry,
*defines, "--output", directory],
capture_output=True, text=True,
)
self.assertEqual(process.returncode, 0, process.stderr)
fragment = json.loads((Path(directory) / "fragmentMain.reflection.json").read_text())
gpu_vertex = json.loads((Path(directory) / "gpuVertexMain.reflection.json").read_text())
lighting = {
(d["set"], d["binding"]): (d["type"], d.get("element_stride"))
for d in fragment["layout"]["descriptors"]
}
self.assertEqual([lighting[1, i] for i in range(4)],
[("storage_buffer", 80), ("storage_buffer", 80),
("storage_buffer", 112), ("sampled_image_2d", None)])
graphics = {
(d["set"], d["binding"]): d["element_stride"]
for d in gpu_vertex["layout"]["descriptors"]
}
self.assertEqual([graphics[2, i] for i in range(3)], [224, 4, 208])
def test_gpu_vertex_paths_do_not_require_shader_draw_parameters(self):
# SV_InstanceID makes Slang subtract BaseInstance and emit DrawParameters.
# Our indirect commands always use firstInstance=0, so the Vulkan instance
# index is sufficient and also runs on devices without that optional feature.
compiler = os.environ["FASET_TEST_SLANGC"]
with tempfile.TemporaryDirectory(prefix="faset-gpu-instance-index-") as directory:
for entry in ("gpuVertexMain", "gpuShadowMain"):
process = subprocess.run(
[sys.executable, str(SCRIPT), "--compiler", compiler, "--source",
str(SCRIPT.parents[1] / "shaders" / "gpu_scene.slang"), "--entry",
entry, "--define", "FASET_GPU_GRAPHICS=1", "--output", directory],
capture_output=True, text=True,
)
self.assertEqual(process.returncode, 0, process.stderr)
bytecode = (Path(directory) / f"{entry}.spv").read_bytes()
words = struct.unpack(f"<{len(bytecode) // 4}I", bytecode)
capabilities = set()
offset = 5
while offset < len(words):
count, opcode = words[offset] >> 16, words[offset] & 0xffff
self.assertGreater(count, 0)
if opcode == 17: # OpCapability
capabilities.add(words[offset + 1])
offset += count
self.assertIn(1, capabilities) # Shader
self.assertNotIn(4427, capabilities) # DrawParameters
def test_gpu_storage_resources_keep_kind_and_stride(self):
parameters = [
parameter("instances", 0, "structuredBuffer", "read", 224),
parameter("visibleIds", 1, "structuredBuffer", "readWrite", 4),
parameter("depthOutput", 2, "texture2D", "readWrite"),
parameter("depthInput", 3, "texture2D", "read"),
]
raw = {
"parameters": parameters,
"entryPoints": [{
"name": "gpuCullMain", "stage": "compute",
"bindings": [{"name": p["name"], "binding": {"used": 1}} for p in parameters],
}],
}
spirv = struct.pack("<5I", 0x07230203, 0x00010600, 0, 1, 0)
layout = shader.normalize(raw, spirv, "gpuCullMain")["layout"]
descriptors = layout["descriptors"]
self.assertEqual(
[(d["type"], d.get("element_stride")) for d in descriptors],
[("storage_buffer", 224), ("storage_buffer", 4),
("storage_image_2d", None), ("sampled_image_2d", None)],
)
def test_real_slang_compute_interface(self):
compiler = os.environ["FASET_TEST_SLANGC"]
with tempfile.TemporaryDirectory(prefix="faset-shader-reflection-") as directory:
process = subprocess.run(
[sys.executable, str(SCRIPT), "--compiler", compiler, "--source",
str(SCRIPT.parents[1] / "shaders" / "gpu_scene.slang"), "--entry",
"gpuCullMain", "--define", "FASET_GPU_CULL=1", "--output", directory],
capture_output=True, text=True,
)
self.assertEqual(process.returncode, 0, process.stderr)
metadata = json.loads((Path(directory) / "gpuCullMain.reflection.json").read_text())
bindings = {item["binding"]: item for item in metadata["layout"]["descriptors"]}
self.assertEqual([bindings[n]["element_stride"] for n in (0, 1, 2, 3, 4, 5, 6, 9)],
[224, 16, 16, 4, 16, 4, 4, 208])
self.assertEqual([bindings[n]["type"] for n in (7, 8)],
["sampled_image_2d", "sampled_image_2d"])
def test_hzb_storage_image_has_explicit_r32f_format(self):
compiler = os.environ["FASET_TEST_SLANGC"]
with tempfile.TemporaryDirectory(prefix="faset-hzb-format-") as directory:
process = subprocess.run(
[sys.executable, str(SCRIPT), "--compiler", compiler, "--source",
str(SCRIPT.parents[1] / "shaders" / "gpu_scene.slang"), "--entry",
"gpuHzbMain", "--define", "FASET_GPU_HZB=1", "--output", directory],
capture_output=True, text=True,
)
self.assertEqual(process.returncode, 0, process.stderr)
bytecode = (Path(directory) / "gpuHzbMain.spv").read_bytes()
words = struct.unpack(f"<{len(bytecode) // 4}I", bytecode)
capabilities = set()
image_formats = []
offset = 5
while offset < len(words):
count, opcode = words[offset] >> 16, words[offset] & 0xffff
self.assertGreater(count, 0)
if opcode == 17: # OpCapability
capabilities.add(words[offset + 1])
elif opcode == 25: # OpTypeImage; last operand is ImageFormat
image_formats.append((words[offset + 7], words[offset + 8]))
offset += count
self.assertNotIn(55, capabilities) # StorageImageReadWithoutFormat
self.assertNotIn(56, capabilities) # StorageImageWriteWithoutFormat
self.assertIn((2, 3), image_formats) # storage image, R32f
if __name__ == "__main__":
unittest.main()