Add checked Slang shaders for GPU visibility and HZB
This commit is contained in:
@@ -17,6 +17,23 @@ foreach(FASET_ENTRY vertexMain fragmentMain shadowMain)
|
|||||||
DEPENDS "${PROJECT_SOURCE_DIR}/shaders/baseline.slang" "${PROJECT_SOURCE_DIR}/tools/compile_shader.py" VERBATIM)
|
DEPENDS "${PROJECT_SOURCE_DIR}/shaders/baseline.slang" "${PROJECT_SOURCE_DIR}/tools/compile_shader.py" VERBATIM)
|
||||||
list(APPEND FASET_SHADER_OUTPUTS "${FASET_SHADER_OUTPUT}" "${FASET_SHADER_DIRECTORY}/${FASET_ENTRY}.reflection.json")
|
list(APPEND FASET_SHADER_OUTPUTS "${FASET_SHADER_OUTPUT}" "${FASET_SHADER_DIRECTORY}/${FASET_ENTRY}.reflection.json")
|
||||||
endforeach()
|
endforeach()
|
||||||
|
foreach(FASET_ENTRY gpuVertexMain gpuShadowMain gpuCullMain gpuHzbMain gpuPostCullMain)
|
||||||
|
if(FASET_ENTRY STREQUAL "gpuVertexMain" OR FASET_ENTRY STREQUAL "gpuShadowMain")
|
||||||
|
set(FASET_GPU_DEFINE FASET_GPU_GRAPHICS=1)
|
||||||
|
elseif(FASET_ENTRY STREQUAL "gpuHzbMain")
|
||||||
|
set(FASET_GPU_DEFINE FASET_GPU_HZB=1)
|
||||||
|
else()
|
||||||
|
set(FASET_GPU_DEFINE FASET_GPU_CULL=1)
|
||||||
|
endif()
|
||||||
|
set(FASET_SHADER_OUTPUT "${FASET_SHADER_DIRECTORY}/${FASET_ENTRY}.spv")
|
||||||
|
add_custom_command(OUTPUT "${FASET_SHADER_OUTPUT}" "${FASET_SHADER_DIRECTORY}/${FASET_ENTRY}.reflection.json"
|
||||||
|
COMMAND "${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/tools/compile_shader.py"
|
||||||
|
--compiler "${SLANGC_EXECUTABLE}" --source "${PROJECT_SOURCE_DIR}/shaders/gpu_scene.slang"
|
||||||
|
--entry "${FASET_ENTRY}" --define "${FASET_GPU_DEFINE}" --output "${FASET_SHADER_DIRECTORY}"
|
||||||
|
BYPRODUCTS "${FASET_SHADER_DIRECTORY}/${FASET_ENTRY}.slang-reflection.json"
|
||||||
|
DEPENDS "${PROJECT_SOURCE_DIR}/shaders/gpu_scene.slang" "${PROJECT_SOURCE_DIR}/tools/compile_shader.py" VERBATIM)
|
||||||
|
list(APPEND FASET_SHADER_OUTPUTS "${FASET_SHADER_OUTPUT}" "${FASET_SHADER_DIRECTORY}/${FASET_ENTRY}.reflection.json")
|
||||||
|
endforeach()
|
||||||
add_custom_command(OUTPUT "${FASET_SHADER_DIRECTORY}/compatibility.spv"
|
add_custom_command(OUTPUT "${FASET_SHADER_DIRECTORY}/compatibility.spv"
|
||||||
COMMAND "${SLANGC_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/shaders/compatibility.hlsl"
|
COMMAND "${SLANGC_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/shaders/compatibility.hlsl"
|
||||||
-entry compatibilityMain -stage compute -target spirv -profile spirv_1_6
|
-entry compatibilityMain -stage compute -target spirv -profile spirv_1_6
|
||||||
@@ -49,6 +66,14 @@ if(BUILD_TESTING)
|
|||||||
FASET_PYTHON_EXECUTABLE="${Python3_EXECUTABLE}")
|
FASET_PYTHON_EXECUTABLE="${Python3_EXECUTABLE}")
|
||||||
add_test(NAME render_shader_reload COMMAND faset_render_reload_tests)
|
add_test(NAME render_shader_reload COMMAND faset_render_reload_tests)
|
||||||
set_tests_properties(render_shader_reload PROPERTIES LABELS "gpu")
|
set_tests_properties(render_shader_reload PROPERTIES LABELS "gpu")
|
||||||
|
add_executable(faset_render_gpu_shader_contract_tests "${PROJECT_SOURCE_DIR}/tests/render_gpu_shader_contract_tests.cpp")
|
||||||
|
target_include_directories(faset_render_gpu_shader_contract_tests PRIVATE "${PROJECT_SOURCE_DIR}/src/render")
|
||||||
|
target_link_libraries(faset_render_gpu_shader_contract_tests PRIVATE faset_render faset_core)
|
||||||
|
target_compile_definitions(faset_render_gpu_shader_contract_tests PRIVATE FASET_TEST_SHADER_DIRECTORY="${FASET_SHADER_DIRECTORY}")
|
||||||
|
add_test(NAME render_gpu_shader_contract COMMAND faset_render_gpu_shader_contract_tests)
|
||||||
|
add_test(NAME render_shader_reflection COMMAND "${CMAKE_COMMAND}" -E env
|
||||||
|
"FASET_TEST_SLANGC=${SLANGC_EXECUTABLE}"
|
||||||
|
"${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/tests/test_shader_reflection.py")
|
||||||
add_executable(faset_render_window_tests "${PROJECT_SOURCE_DIR}/tests/render_window_tests.cpp")
|
add_executable(faset_render_window_tests "${PROJECT_SOURCE_DIR}/tests/render_window_tests.cpp")
|
||||||
target_link_libraries(faset_render_window_tests PRIVATE faset_render SDL3::SDL3)
|
target_link_libraries(faset_render_window_tests PRIVATE faset_render SDL3::SDL3)
|
||||||
add_test(NAME render_window_lifecycle COMMAND faset_render_window_tests "${CMAKE_BINARY_DIR}/window-test")
|
add_test(NAME render_window_lifecycle COMMAND faset_render_window_tests "${CMAKE_BINARY_DIR}/window-test")
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
// GPU-visible opaque scene. All host records use 16-byte lanes; reflected strides
|
||||||
|
// are validated before pipelines are created. Sprites/UI and shadow caster selection
|
||||||
|
// remain independent of camera culling.
|
||||||
|
struct GpuSceneVertex {
|
||||||
|
float3 position : POSITION;
|
||||||
|
float3 normal : NORMAL;
|
||||||
|
float4 color : COLOR0;
|
||||||
|
float2 uv : TEXCOORD0;
|
||||||
|
};
|
||||||
|
struct GpuSceneOutput {
|
||||||
|
float4 position : SV_Position;
|
||||||
|
float3 world : TEXCOORD0;
|
||||||
|
float3 normal : NORMAL;
|
||||||
|
float4 color : COLOR0;
|
||||||
|
float2 material : TEXCOORD1;
|
||||||
|
float2 uv : TEXCOORD2;
|
||||||
|
};
|
||||||
|
struct InstanceRecord {
|
||||||
|
column_major float4x4 model; // 0..63
|
||||||
|
float4 normalRow0; // 64..79, inverse-transpose 3x3
|
||||||
|
float4 normalRow1; // 80..95
|
||||||
|
float4 normalRow2; // 96..111
|
||||||
|
float4 color; // 112..127
|
||||||
|
float4 material; // 128..143: roughness, metallic, texture flags
|
||||||
|
float4 currentCenter; // 144..159: world AABB center
|
||||||
|
float4 currentExtent; // 160..175: world AABB half extents
|
||||||
|
float4 previousCenter; // 176..191
|
||||||
|
float4 previousExtent; // 192..207
|
||||||
|
uint4 metadata; // 208..223: x=previousValid, others reserved
|
||||||
|
};
|
||||||
|
struct ViewRecord {
|
||||||
|
column_major float4x4 currentViewProjection; // 0..63
|
||||||
|
column_major float4x4 previousViewProjection; // 64..127
|
||||||
|
float4 currentViewport; // 128..143: x/y/width/height in target pixels
|
||||||
|
float4 previousViewport; // 144..159
|
||||||
|
uint4 currentHzbSize; // 160..175: width/height/mipCount/reserved
|
||||||
|
uint4 previousHzbSize; // 176..191
|
||||||
|
uint4 flags; // 192..207: x=historyValid
|
||||||
|
};
|
||||||
|
struct BinRecord {
|
||||||
|
uint candidateFirst, candidateCount, visibleBase, capacity;
|
||||||
|
};
|
||||||
|
struct Candidate {
|
||||||
|
uint instanceId, binIndex, flags, reserved;
|
||||||
|
};
|
||||||
|
// Exactly VkDrawIndirectCommand: vertexCount, instanceCount, firstVertex, firstInstance.
|
||||||
|
struct IndirectArgs {
|
||||||
|
uint vertexCount, instanceCount, firstVertex, firstInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
#if defined(FASET_GPU_GRAPHICS)
|
||||||
|
struct GpuFrameParameters {
|
||||||
|
column_major float4x4 lightViewProjection; // same first 96 bytes as baseline fragment
|
||||||
|
float4 lightDirection;
|
||||||
|
float4 eye;
|
||||||
|
uint4 drawInfo; // x=visible ID range base; firstInstance is always zero
|
||||||
|
};
|
||||||
|
[[vk::push_constant]] ConstantBuffer<GpuFrameParameters> gpuFrame;
|
||||||
|
[[vk::binding(0,1)]] StructuredBuffer<InstanceRecord> gfxInstances;
|
||||||
|
[[vk::binding(1,1)]] StructuredBuffer<uint> gfxVisibleIds;
|
||||||
|
[[vk::binding(2,1)]] StructuredBuffer<ViewRecord> gfxViews;
|
||||||
|
|
||||||
|
[shader("vertex")]
|
||||||
|
GpuSceneOutput gpuVertexMain(GpuSceneVertex vertex, uint drawInstance : SV_InstanceID) {
|
||||||
|
InstanceRecord instance = gfxInstances[gfxVisibleIds[gpuFrame.drawInfo.x + drawInstance]];
|
||||||
|
float4 world = mul(instance.model, float4(vertex.position, 1));
|
||||||
|
GpuSceneOutput output;
|
||||||
|
output.position = mul(gfxViews[0].currentViewProjection, world);
|
||||||
|
output.world = world.xyz;
|
||||||
|
float3 normal = float3(dot(instance.normalRow0.xyz, vertex.normal),
|
||||||
|
dot(instance.normalRow1.xyz, vertex.normal),
|
||||||
|
dot(instance.normalRow2.xyz, vertex.normal));
|
||||||
|
float normalLength = length(normal);
|
||||||
|
output.normal = normalLength > 1e-8 ? normal / normalLength : float3(0, 0, 0);
|
||||||
|
output.color = vertex.color * instance.color;
|
||||||
|
output.material = instance.material.xy;
|
||||||
|
output.uv = vertex.uv;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
[shader("vertex")]
|
||||||
|
float4 gpuShadowMain(GpuSceneVertex vertex, uint drawInstance : SV_InstanceID) : SV_Position {
|
||||||
|
InstanceRecord instance = gfxInstances[gfxVisibleIds[gpuFrame.drawInfo.x + drawInstance]];
|
||||||
|
return mul(gpuFrame.lightViewProjection, mul(instance.model, float4(vertex.position, 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#elif defined(FASET_GPU_CULL)
|
||||||
|
struct CullParameters {
|
||||||
|
uint candidateCount;
|
||||||
|
uint deferredCapacity;
|
||||||
|
uint reserved0;
|
||||||
|
uint reserved1;
|
||||||
|
};
|
||||||
|
[[vk::push_constant]] ConstantBuffer<CullParameters> cullParameters;
|
||||||
|
[[vk::binding(0,0)]] StructuredBuffer<InstanceRecord> cullInstances;
|
||||||
|
[[vk::binding(1,0)]] StructuredBuffer<Candidate> candidates;
|
||||||
|
[[vk::binding(2,0)]] StructuredBuffer<BinRecord> bins;
|
||||||
|
[[vk::binding(3,0)]] RWStructuredBuffer<uint> visibleIds;
|
||||||
|
[[vk::binding(4,0)]] RWStructuredBuffer<IndirectArgs> args;
|
||||||
|
[[vk::binding(5,0)]] RWStructuredBuffer<uint> deferredIds;
|
||||||
|
[[vk::binding(6,0)]] RWStructuredBuffer<uint> deferredCount;
|
||||||
|
[[vk::binding(7,0)]] Texture2D<float> previousHzb;
|
||||||
|
[[vk::binding(8,0)]] Texture2D<float> currentHzb;
|
||||||
|
[[vk::binding(9,0)]] StructuredBuffer<ViewRecord> cullViews;
|
||||||
|
|
||||||
|
float4 boundsCorner(float4 center, float4 extent, uint corner) {
|
||||||
|
return float4(center.xyz + extent.xyz * float3((corner & 1) != 0 ? 1 : -1,
|
||||||
|
(corner & 2) != 0 ? 1 : -1,
|
||||||
|
(corner & 4) != 0 ? 1 : -1), 1);
|
||||||
|
}
|
||||||
|
bool finiteClip(float4 clip) {
|
||||||
|
return all(isfinite(clip));
|
||||||
|
}
|
||||||
|
// A plane may reject an AABB only when all eight corners are strictly outside.
|
||||||
|
// Nonfinite arithmetic fails open so malformed data cannot cause disappearing meshes.
|
||||||
|
bool inFrustum(float4 center, float4 extent, float4x4 viewProjection) {
|
||||||
|
uint rejected[6] = {0, 0, 0, 0, 0, 0};
|
||||||
|
[unroll] for (uint corner = 0; corner < 8; ++corner) {
|
||||||
|
float4 clip = mul(viewProjection, boundsCorner(center, extent, corner));
|
||||||
|
if (!finiteClip(clip)) return true;
|
||||||
|
float planes[6] = {clip.x + clip.w, clip.w - clip.x,
|
||||||
|
clip.y + clip.w, clip.w - clip.y,
|
||||||
|
clip.z, clip.w - clip.z};
|
||||||
|
[unroll] for (uint plane = 0; plane < 6; ++plane)
|
||||||
|
rejected[plane] += planes[plane] < 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
[unroll] for (uint plane = 0; plane < 6; ++plane)
|
||||||
|
if (rejected[plane] == 8) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Ordinary Z: the HZB contains the furthest depth (maximum) in every footprint.
|
||||||
|
// The nearest candidate depth must be farther than *all* those samples to occlude.
|
||||||
|
bool occluded(float4 center, float4 extent, float4x4 viewProjection,
|
||||||
|
float4 viewport, uint4 pyramidSize, Texture2D<float> pyramid) {
|
||||||
|
if (pyramidSize.x == 0 || pyramidSize.y == 0 || pyramidSize.z == 0 ||
|
||||||
|
viewport.z <= 0 || viewport.w <= 0) return false;
|
||||||
|
float2 minPixel = float2(1e30, 1e30), maxPixel = float2(-1e30, -1e30);
|
||||||
|
float nearestDepth = 1;
|
||||||
|
[unroll] for (uint corner = 0; corner < 8; ++corner) {
|
||||||
|
float4 clip = mul(viewProjection, boundsCorner(center, extent, corner));
|
||||||
|
// Near-plane crossings and perspective singularities are always visible.
|
||||||
|
if (!finiteClip(clip) || clip.w <= 0 || clip.z <= 0 || clip.z >= clip.w) return false;
|
||||||
|
float3 projected = clip.xyz / clip.w;
|
||||||
|
float2 pixel = viewport.xy + (projected.xy * 0.5 + 0.5) * viewport.zw;
|
||||||
|
if (!all(isfinite(pixel)) || !isfinite(projected.z)) return false;
|
||||||
|
minPixel = min(minPixel, pixel);
|
||||||
|
maxPixel = max(maxPixel, pixel);
|
||||||
|
nearestDepth = min(nearestDepth, projected.z);
|
||||||
|
}
|
||||||
|
if (minPixel.x < 0 || minPixel.y < 0 || maxPixel.x >= pyramidSize.x ||
|
||||||
|
maxPixel.y >= pyramidSize.y) return false;
|
||||||
|
float extentPixels = max(maxPixel.x - minPixel.x, maxPixel.y - minPixel.y);
|
||||||
|
uint mip = min((uint)ceil(log2(max(extentPixels, 1.0))), pyramidSize.z - 1);
|
||||||
|
uint2 size = max(uint2(1, 1), (pyramidSize.xy + ((1u << mip) - 1)) >> mip);
|
||||||
|
uint2 first = min((uint2)floor(minPixel / (1u << mip)), size - 1);
|
||||||
|
uint2 last = min((uint2)floor(maxPixel / (1u << mip)), size - 1);
|
||||||
|
float furthest = 0;
|
||||||
|
for (uint y = first.y; y <= last.y; ++y)
|
||||||
|
for (uint x = first.x; x <= last.x; ++x)
|
||||||
|
furthest = max(furthest, pyramid.Load(int3(x, y, mip)));
|
||||||
|
return nearestDepth > furthest + 0.0001;
|
||||||
|
}
|
||||||
|
bool appendVisible(uint binIndex, uint instanceId) {
|
||||||
|
BinRecord bin = bins[binIndex];
|
||||||
|
uint observed = args[binIndex].instanceCount;
|
||||||
|
while (observed < bin.capacity) {
|
||||||
|
uint previous;
|
||||||
|
InterlockedCompareExchange(args[binIndex].instanceCount, observed, observed + 1, previous);
|
||||||
|
if (previous == observed) {
|
||||||
|
visibleIds[bin.visibleBase + observed] = instanceId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
observed = previous;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool appendDeferred(uint candidateIndex) {
|
||||||
|
uint observed = deferredCount[0];
|
||||||
|
while (observed < cullParameters.deferredCapacity) {
|
||||||
|
uint previous;
|
||||||
|
InterlockedCompareExchange(deferredCount[0], observed, observed + 1, previous);
|
||||||
|
if (previous == observed) {
|
||||||
|
deferredIds[observed] = candidateIndex;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
observed = previous;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
[shader("compute")]
|
||||||
|
[numthreads(64, 1, 1)]
|
||||||
|
void gpuCullMain(uint3 dispatchId : SV_DispatchThreadID) {
|
||||||
|
uint index = dispatchId.x;
|
||||||
|
if (index >= cullParameters.candidateCount) return;
|
||||||
|
Candidate candidate = candidates[index];
|
||||||
|
InstanceRecord instance = cullInstances[candidate.instanceId];
|
||||||
|
ViewRecord view = cullViews[0];
|
||||||
|
if (!inFrustum(instance.currentCenter, instance.currentExtent,
|
||||||
|
view.currentViewProjection)) return;
|
||||||
|
bool guessedHidden = view.flags.x != 0 && instance.metadata.x != 0 &&
|
||||||
|
occluded(instance.previousCenter, instance.previousExtent,
|
||||||
|
view.previousViewProjection, view.previousViewport,
|
||||||
|
view.previousHzbSize, previousHzb);
|
||||||
|
if (guessedHidden && appendDeferred(index)) return;
|
||||||
|
appendVisible(candidate.binIndex, candidate.instanceId);
|
||||||
|
}
|
||||||
|
[shader("compute")]
|
||||||
|
[numthreads(64, 1, 1)]
|
||||||
|
void gpuPostCullMain(uint3 dispatchId : SV_DispatchThreadID) {
|
||||||
|
uint index = dispatchId.x;
|
||||||
|
if (index >= cullParameters.deferredCapacity || index >= deferredCount[0]) return;
|
||||||
|
Candidate candidate = candidates[deferredIds[index]];
|
||||||
|
InstanceRecord instance = cullInstances[candidate.instanceId];
|
||||||
|
ViewRecord view = cullViews[0];
|
||||||
|
if (!occluded(instance.currentCenter, instance.currentExtent,
|
||||||
|
view.currentViewProjection, view.currentViewport,
|
||||||
|
view.currentHzbSize, currentHzb))
|
||||||
|
appendVisible(candidate.binIndex, candidate.instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
#elif defined(FASET_GPU_HZB)
|
||||||
|
struct HzbParameters {
|
||||||
|
uint sourceWidth, sourceHeight, outputWidth, outputHeight;
|
||||||
|
};
|
||||||
|
[[vk::push_constant]] ConstantBuffer<HzbParameters> hzbParameters;
|
||||||
|
[[vk::binding(0,0)]] Texture2D<float> hzbSource;
|
||||||
|
[[vk::binding(1,0)]] RWTexture2D<float> hzbOutput;
|
||||||
|
[shader("compute")]
|
||||||
|
[numthreads(8, 8, 1)]
|
||||||
|
void gpuHzbMain(uint3 dispatchId : SV_DispatchThreadID) {
|
||||||
|
uint2 pixel = dispatchId.xy;
|
||||||
|
if (pixel.x >= hzbParameters.outputWidth || pixel.y >= hzbParameters.outputHeight) return;
|
||||||
|
if (hzbParameters.outputWidth >= hzbParameters.sourceWidth &&
|
||||||
|
hzbParameters.outputHeight >= hzbParameters.sourceHeight) {
|
||||||
|
// Mip 0 copies depth into a power-of-two base. Missing edge texels are
|
||||||
|
// ordinary-Z far depth, so a padded region can never hide geometry.
|
||||||
|
hzbOutput[pixel] = pixel.x < hzbParameters.sourceWidth &&
|
||||||
|
pixel.y < hzbParameters.sourceHeight
|
||||||
|
? hzbSource.Load(int3(pixel, 0)) : 1.0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float furthest = 0;
|
||||||
|
[unroll] for (uint y = 0; y < 2; ++y)
|
||||||
|
[unroll] for (uint x = 0; x < 2; ++x) {
|
||||||
|
uint2 child = pixel * 2 + uint2(x, y);
|
||||||
|
// Ordinary-Z clear/far depth is 1. Padding therefore cannot occlude.
|
||||||
|
float depth = child.x < hzbParameters.sourceWidth &&
|
||||||
|
child.y < hzbParameters.sourceHeight
|
||||||
|
? hzbSource.Load(int3(child, 0)) : 1.0;
|
||||||
|
furthest = max(furthest, depth);
|
||||||
|
}
|
||||||
|
hzbOutput[pixel] = furthest;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
#error Select FASET_GPU_GRAPHICS, FASET_GPU_CULL, or FASET_GPU_HZB.
|
||||||
|
#endif
|
||||||
@@ -334,7 +334,11 @@ struct BuildService::Impl {
|
|||||||
copy_required_file(exporter, staging / ("faset_schema_exporter" + executable_suffix()));
|
copy_required_file(exporter, staging / ("faset_schema_exporter" + executable_suffix()));
|
||||||
for (const auto* file : {"vertexMain.spv", "fragmentMain.spv", "shadowMain.spv",
|
for (const auto* file : {"vertexMain.spv", "fragmentMain.spv", "shadowMain.spv",
|
||||||
"vertexMain.reflection.json", "fragmentMain.reflection.json",
|
"vertexMain.reflection.json", "fragmentMain.reflection.json",
|
||||||
"shadowMain.reflection.json"})
|
"shadowMain.reflection.json", "gpuVertexMain.spv",
|
||||||
|
"gpuShadowMain.spv", "gpuCullMain.spv", "gpuHzbMain.spv",
|
||||||
|
"gpuPostCullMain.spv", "gpuVertexMain.reflection.json",
|
||||||
|
"gpuShadowMain.reflection.json", "gpuCullMain.reflection.json",
|
||||||
|
"gpuHzbMain.reflection.json", "gpuPostCullMain.reflection.json"})
|
||||||
copy_required_file(native_directory / "shaders" / file, staging / "shaders" / file);
|
copy_required_file(native_directory / "shaders" / file, staging / "shaders" / file);
|
||||||
copy_runtime_libraries(job, player, staging, native_directory, configuration);
|
copy_runtime_libraries(job, player, staging, native_directory, configuration);
|
||||||
Json manifest{{"format", "faset.build"},
|
Json manifest{{"format", "faset.build"},
|
||||||
@@ -582,7 +586,11 @@ struct BuildService::Impl {
|
|||||||
staging / ("faset_player" + executable_suffix()));
|
staging / ("faset_player" + executable_suffix()));
|
||||||
for (const auto* shader : {"vertexMain.spv", "fragmentMain.spv", "shadowMain.spv",
|
for (const auto* shader : {"vertexMain.spv", "fragmentMain.spv", "shadowMain.spv",
|
||||||
"vertexMain.reflection.json", "fragmentMain.reflection.json",
|
"vertexMain.reflection.json", "fragmentMain.reflection.json",
|
||||||
"shadowMain.reflection.json"})
|
"shadowMain.reflection.json", "gpuVertexMain.spv",
|
||||||
|
"gpuShadowMain.spv", "gpuCullMain.spv", "gpuHzbMain.spv",
|
||||||
|
"gpuPostCullMain.spv", "gpuVertexMain.reflection.json",
|
||||||
|
"gpuShadowMain.reflection.json", "gpuCullMain.reflection.json",
|
||||||
|
"gpuHzbMain.reflection.json", "gpuPostCullMain.reflection.json"})
|
||||||
copy_required_file(build_directory / "shaders" / shader,
|
copy_required_file(build_directory / "shaders" / shader,
|
||||||
staging / "shaders" / shader);
|
staging / "shaders" / shader);
|
||||||
for (const auto& entry : fs::directory_iterator(build_directory)) {
|
for (const auto& entry : fs::directory_iterator(build_directory)) {
|
||||||
|
|||||||
@@ -85,7 +85,72 @@ void validate_layout(const Json& layout, std::string_view entry) {
|
|||||||
locations(layout.at("outputs"), {}, "shadow outputs");
|
locations(layout.at("outputs"), {}, "shadow outputs");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void validate_spirv(const std::vector<std::uint32_t>& words, bool fragment) {
|
void validate_gpu_layout(const Json& layout, std::string_view entry) {
|
||||||
|
const bool graphics = entry == "gpuVertexMain" || entry == "gpuShadowMain";
|
||||||
|
const bool hzb = entry == "gpuHzbMain";
|
||||||
|
const bool compute = !graphics;
|
||||||
|
require(layout.at("stage") == (compute ? "compute" : "vertex"), "GPU shader stage changed");
|
||||||
|
const auto& descriptors = layout.at("descriptors");
|
||||||
|
const std::size_t expected_count = graphics ? 3 : hzb ? 2 : 10;
|
||||||
|
require(descriptors.is_array() && descriptors.size() == expected_count,
|
||||||
|
"GPU descriptor count changed");
|
||||||
|
const std::array<int, 10> compute_strides{224, 16, 16, 4, 16, 4, 4, 0, 0, 208};
|
||||||
|
const std::array<int, 3> graphics_strides{224, 4, 208};
|
||||||
|
for (std::size_t i = 0; i < expected_count; ++i) {
|
||||||
|
const auto& binding = descriptors[i];
|
||||||
|
require(binding.at("set") == (graphics ? 1 : 0) && binding.at("binding") == i &&
|
||||||
|
binding.at("count") == 1,
|
||||||
|
"GPU descriptor set, binding or count changed");
|
||||||
|
const int stride = graphics ? graphics_strides[i] : hzb ? 0 : compute_strides[i];
|
||||||
|
const char* type = hzb ? (i == 0 ? "sampled_image_2d" : "storage_image_2d")
|
||||||
|
: stride > 0 ? "storage_buffer" : "sampled_image_2d";
|
||||||
|
require(binding.at("type") == type, "GPU descriptor type changed");
|
||||||
|
if (stride > 0)
|
||||||
|
require(binding.at("element_stride") == stride, "GPU storage record stride changed");
|
||||||
|
}
|
||||||
|
const auto& constants = layout.at("push_constants");
|
||||||
|
require(constants.is_array() && constants.size() == 1 &&
|
||||||
|
constants[0].at("offset") == 0 &&
|
||||||
|
constants[0].at("size") == (graphics ? 112 : 16),
|
||||||
|
"GPU push-constant block changed");
|
||||||
|
const auto& members = constants[0].at("members");
|
||||||
|
require(members.is_array() && members.size() == 4, "GPU push-constant fields changed");
|
||||||
|
const int graphics_offsets[] = {0, 64, 80, 96};
|
||||||
|
const int graphics_sizes[] = {64, 16, 16, 16};
|
||||||
|
const char* graphics_types[] = {"float32x4x4", "float32x4", "float32x4", "uint32x4"};
|
||||||
|
for (std::size_t i = 0; i < 4; ++i) {
|
||||||
|
require(members[i].at("offset") == (graphics ? graphics_offsets[i] : int(i) * 4) &&
|
||||||
|
members[i].at("size") == (graphics ? graphics_sizes[i] : 4) &&
|
||||||
|
members[i].at("type") == (graphics ? graphics_types[i] : "uint32"),
|
||||||
|
"GPU push-constant layout changed");
|
||||||
|
}
|
||||||
|
const auto& blocks = layout.at("spirv_push_constants");
|
||||||
|
require(blocks.is_array() && blocks.size() == 1, "GPU SPIR-V push block changed");
|
||||||
|
const auto& actual = blocks[0].at("members");
|
||||||
|
require(actual.is_array() && actual.size() == 4, "GPU SPIR-V push members changed");
|
||||||
|
for (std::size_t i = 0; i < 4; ++i)
|
||||||
|
require(actual[i].at("member") == i &&
|
||||||
|
actual[i].at("offset") == (graphics ? graphics_offsets[i] : int(i) * 4),
|
||||||
|
"GPU SPIR-V push offsets changed");
|
||||||
|
if (graphics) {
|
||||||
|
require(actual[0].at("matrix_layout") == "row-major" &&
|
||||||
|
actual[0].at("matrix_stride") == 16,
|
||||||
|
"GPU SPIR-V matrix storage convention changed");
|
||||||
|
locations(layout.at("inputs"),
|
||||||
|
{"float32x3", "float32x3", "float32x4", "float32x2"},
|
||||||
|
"GPU vertex inputs");
|
||||||
|
if (entry == "gpuVertexMain")
|
||||||
|
locations(layout.at("outputs"),
|
||||||
|
{"float32x3", "float32x3", "float32x4", "float32x2", "float32x2"},
|
||||||
|
"GPU vertex outputs");
|
||||||
|
else
|
||||||
|
locations(layout.at("outputs"), {}, "GPU shadow outputs");
|
||||||
|
} else {
|
||||||
|
locations(layout.at("inputs"), {}, "GPU compute inputs");
|
||||||
|
locations(layout.at("outputs"), {}, "GPU compute outputs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void validate_spirv(const std::vector<std::uint32_t>& words, std::uint32_t execution_model) {
|
||||||
require(words.size() >= 5 && words[0] == 0x07230203 && words[1] >= 0x00010000 &&
|
require(words.size() >= 5 && words[0] == 0x07230203 && words[1] >= 0x00010000 &&
|
||||||
words[1] <= 0x00010600 && words[3] > 0 && words[3] < (1u << 20) && words[4] == 0,
|
words[1] <= 0x00010600 && words[3] > 0 && words[3] < (1u << 20) && words[4] == 0,
|
||||||
"invalid SPIR-V header");
|
"invalid SPIR-V header");
|
||||||
@@ -101,7 +166,7 @@ void validate_spirv(const std::vector<std::uint32_t>& words, bool fragment) {
|
|||||||
const auto* terminator = static_cast<const char*>(std::memchr(name, 0, available));
|
const auto* terminator = static_cast<const char*>(std::memchr(name, 0, available));
|
||||||
require(terminator != nullptr, "unterminated SPIR-V entry name");
|
require(terminator != nullptr, "unterminated SPIR-V entry name");
|
||||||
if (std::string_view(name, terminator - name) == "main") {
|
if (std::string_view(name, terminator - name) == "main") {
|
||||||
require(words[offset + 1] == (fragment ? 4u : 0u), "SPIR-V entry stage changed");
|
require(words[offset + 1] == execution_model, "SPIR-V entry stage changed");
|
||||||
entry_found = true;
|
entry_found = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +174,8 @@ void validate_spirv(const std::vector<std::uint32_t>& words, bool fragment) {
|
|||||||
}
|
}
|
||||||
require(entry_found, "SPIR-V main entry point missing");
|
require(entry_found, "SPIR-V main entry point missing");
|
||||||
}
|
}
|
||||||
detail::ShaderCode load(const std::filesystem::path& directory, const char* entry) {
|
detail::ShaderCode load(const std::filesystem::path& directory, const char* entry,
|
||||||
|
bool gpu = false) {
|
||||||
const auto bytes = read_bounded(directory / (std::string(entry) + ".spv"), 16 * 1024 * 1024);
|
const auto bytes = read_bounded(directory / (std::string(entry) + ".spv"), 16 * 1024 * 1024);
|
||||||
require(bytes.size() >= 20 && bytes.size() % 4 == 0, "invalid SPIR-V byte length");
|
require(bytes.size() >= 20 && bytes.size() % 4 == 0, "invalid SPIR-V byte length");
|
||||||
const auto metadata = Json::parse(
|
const auto metadata = Json::parse(
|
||||||
@@ -122,12 +188,17 @@ detail::ShaderCode load(const std::filesystem::path& directory, const char* entr
|
|||||||
const auto& layout = metadata.at("layout");
|
const auto& layout = metadata.at("layout");
|
||||||
const auto fingerprint = faset::sha256(layout.dump());
|
const auto fingerprint = faset::sha256(layout.dump());
|
||||||
require(metadata.at("layout_fingerprint") == fingerprint, "layout fingerprint mismatch");
|
require(metadata.at("layout_fingerprint") == fingerprint, "layout fingerprint mismatch");
|
||||||
validate_layout(layout, entry);
|
if (gpu)
|
||||||
|
validate_gpu_layout(layout, entry);
|
||||||
|
else
|
||||||
|
validate_layout(layout, entry);
|
||||||
detail::ShaderCode result;
|
detail::ShaderCode result;
|
||||||
result.layout_fingerprint = fingerprint;
|
result.layout_fingerprint = fingerprint;
|
||||||
result.words.resize(bytes.size() / 4);
|
result.words.resize(bytes.size() / 4);
|
||||||
std::memcpy(result.words.data(), bytes.data(), bytes.size());
|
std::memcpy(result.words.data(), bytes.data(), bytes.size());
|
||||||
validate_spirv(result.words, std::string_view(entry) == "fragmentMain");
|
validate_spirv(result.words, gpu ? ((std::string_view(entry) == "gpuVertexMain" ||
|
||||||
|
std::string_view(entry) == "gpuShadowMain") ? 0u : 5u)
|
||||||
|
: (std::string_view(entry) == "fragmentMain" ? 4u : 0u));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -136,6 +207,12 @@ detail::load_shader_bundle(const std::filesystem::path& directory) {
|
|||||||
return {load(directory, "vertexMain"), load(directory, "fragmentMain"),
|
return {load(directory, "vertexMain"), load(directory, "fragmentMain"),
|
||||||
load(directory, "shadowMain")};
|
load(directory, "shadowMain")};
|
||||||
}
|
}
|
||||||
|
std::array<detail::ShaderCode, 5>
|
||||||
|
detail::load_gpu_shader_bundle(const std::filesystem::path& directory) {
|
||||||
|
return {load(directory, "gpuVertexMain", true), load(directory, "gpuShadowMain", true),
|
||||||
|
load(directory, "gpuCullMain", true), load(directory, "gpuHzbMain", true),
|
||||||
|
load(directory, "gpuPostCullMain", true)};
|
||||||
|
}
|
||||||
void validate_shader_bundle(const std::filesystem::path& directory) {
|
void validate_shader_bundle(const std::filesystem::path& directory) {
|
||||||
(void)detail::load_shader_bundle(directory);
|
(void)detail::load_shader_bundle(directory);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,6 @@ struct ShaderCode {
|
|||||||
std::string layout_fingerprint;
|
std::string layout_fingerprint;
|
||||||
};
|
};
|
||||||
std::array<ShaderCode, 3> load_shader_bundle(const std::filesystem::path& directory);
|
std::array<ShaderCode, 3> load_shader_bundle(const std::filesystem::path& directory);
|
||||||
|
// Order: opaque vertex, optional instanced shadow vertex, main cull, HZB, post cull.
|
||||||
|
std::array<ShaderCode, 5> load_gpu_shader_bundle(const std::filesystem::path& directory);
|
||||||
} // namespace faset::render::detail
|
} // namespace faset::render::detail
|
||||||
|
|||||||
@@ -148,6 +148,12 @@ int test_main(int argc, char** argv) {
|
|||||||
const auto first = builds.wait(builds.start_build());
|
const auto first = builds.wait(builds.start_build());
|
||||||
check(first.state == "succeeded", "Valid custom schema v2 publishes: " + first.error);
|
check(first.state == "succeeded", "Valid custom schema v2 publishes: " + first.error);
|
||||||
const auto directory = path_from_utf8(first.result.at("directory").get<std::string>());
|
const auto directory = path_from_utf8(first.result.at("directory").get<std::string>());
|
||||||
|
for (const auto* entry : {"gpuVertexMain", "gpuShadowMain", "gpuCullMain",
|
||||||
|
"gpuHzbMain", "gpuPostCullMain"})
|
||||||
|
for (const auto* extension : {".spv", ".reflection.json"})
|
||||||
|
check(fs::is_regular_file(directory / "shaders" /
|
||||||
|
(std::string(entry) + extension)),
|
||||||
|
"Published Player contains every checked P2 shader artifact");
|
||||||
const auto player = path_from_utf8(first.result.at("player").get<std::string>());
|
const auto player = path_from_utf8(first.result.at("player").get<std::string>());
|
||||||
const auto schema = path_from_utf8(first.result.at("schema").get<std::string>());
|
const auto schema = path_from_utf8(first.result.at("schema").get<std::string>());
|
||||||
const auto last_build = builds.config().cache_root / "last_build.json";
|
const auto last_build = builds.config().cache_root / "last_build.json";
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ int tool_main(int argc, char** argv) {
|
|||||||
for (const auto* target : {"faset_player", "faset_schema_exporter"})
|
for (const auto* target : {"faset_player", "faset_schema_exporter"})
|
||||||
fs::copy_file(self, build / (std::string(target) + suffix),
|
fs::copy_file(self, build / (std::string(target) + suffix),
|
||||||
fs::copy_options::overwrite_existing);
|
fs::copy_options::overwrite_existing);
|
||||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain"})
|
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain",
|
||||||
|
"gpuVertexMain", "gpuShadowMain", "gpuCullMain",
|
||||||
|
"gpuHzbMain", "gpuPostCullMain"})
|
||||||
for (const auto* extension : {".spv", ".reflection.json"})
|
for (const auto* extension : {".spv", ".reflection.json"})
|
||||||
atomic_write(build / "shaders" / (std::string(entry) + extension), "fixture\n");
|
atomic_write(build / "shaders" / (std::string(entry) + extension), "fixture\n");
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#include "shader_contract.hpp"
|
||||||
|
#include <faset/core/hash.hpp>
|
||||||
|
#include <faset/core/io.hpp>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
namespace {
|
||||||
|
void require(bool valid, const char* message) {
|
||||||
|
if (!valid)
|
||||||
|
throw std::runtime_error(message);
|
||||||
|
}
|
||||||
|
template <class Function> void must_reject(Function&& function, const char* message) {
|
||||||
|
try {
|
||||||
|
function();
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw std::runtime_error(message);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
int main() {
|
||||||
|
const auto original = fs::path(FASET_TEST_SHADER_DIRECTORY);
|
||||||
|
const auto temporary = fs::temp_directory_path() / "faset-gpu-shader-contract-test";
|
||||||
|
struct Cleanup {
|
||||||
|
fs::path path;
|
||||||
|
~Cleanup() { std::error_code ignored; fs::remove_all(path, ignored); }
|
||||||
|
} cleanup{temporary};
|
||||||
|
try {
|
||||||
|
fs::create_directories(temporary);
|
||||||
|
constexpr const char* entries[] = {"gpuVertexMain", "gpuShadowMain", "gpuCullMain",
|
||||||
|
"gpuHzbMain", "gpuPostCullMain"};
|
||||||
|
for (const auto* entry : entries)
|
||||||
|
for (const auto* extension : {".spv", ".reflection.json"}) {
|
||||||
|
const auto file = std::string(entry) + extension;
|
||||||
|
fs::copy_file(original / file, temporary / file, fs::copy_options::overwrite_existing);
|
||||||
|
}
|
||||||
|
auto shaders = faset::render::detail::load_gpu_shader_bundle(temporary);
|
||||||
|
for (const auto& shader : shaders)
|
||||||
|
require(!shader.words.empty() && !shader.layout_fingerprint.empty(),
|
||||||
|
"Every P2 entry is valid SPIR-V with checked metadata");
|
||||||
|
auto shader_file = temporary / "gpuCullMain.spv";
|
||||||
|
const auto shader_bytes = faset::read_text(shader_file);
|
||||||
|
faset::atomic_write(shader_file, "corrupt");
|
||||||
|
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
||||||
|
"Corrupt P2 SPIR-V must be rejected");
|
||||||
|
faset::atomic_write(shader_file, shader_bytes);
|
||||||
|
auto reflection_file = temporary / "gpuPostCullMain.reflection.json";
|
||||||
|
auto metadata = faset::read_json(reflection_file);
|
||||||
|
metadata["layout_fingerprint"] = "tampered";
|
||||||
|
faset::atomic_write_json(reflection_file, metadata);
|
||||||
|
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
||||||
|
"Tampered P2 layout fingerprint must be rejected");
|
||||||
|
metadata = faset::read_json(original / "gpuPostCullMain.reflection.json");
|
||||||
|
metadata["layout"]["descriptors"][0]["element_stride"] = 208;
|
||||||
|
metadata["layout_fingerprint"] = faset::sha256(metadata["layout"].dump());
|
||||||
|
faset::atomic_write_json(reflection_file, metadata);
|
||||||
|
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
||||||
|
"A consistently rehashed but incompatible GPU record stride must be rejected");
|
||||||
|
fs::remove(temporary / "gpuHzbMain.spv");
|
||||||
|
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
||||||
|
"Missing P2 entry must be rejected");
|
||||||
|
std::cout << "GPU shader bundle validates every entry and rejects corrupt or missing artifacts\n";
|
||||||
|
return 0;
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
std::cerr << error.what() << '\n';
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,9 @@ int main() {
|
|||||||
try {
|
try {
|
||||||
const auto bundle = temporary / "shaders";
|
const auto bundle = temporary / "shaders";
|
||||||
fs::create_directories(bundle);
|
fs::create_directories(bundle);
|
||||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain"})
|
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain",
|
||||||
|
"gpuVertexMain", "gpuShadowMain", "gpuCullMain",
|
||||||
|
"gpuHzbMain", "gpuPostCullMain"})
|
||||||
for (const auto* extension : {".spv", ".reflection.json"}) {
|
for (const auto* extension : {".spv", ".reflection.json"}) {
|
||||||
const auto name = std::string(entry) + extension;
|
const auto name = std::string(entry) + extension;
|
||||||
fs::copy_file(path_from_utf8(FASET_TEST_SHADER_DIRECTORY) / name, bundle / name);
|
fs::copy_file(path_from_utf8(FASET_TEST_SHADER_DIRECTORY) / name, bundle / name);
|
||||||
@@ -76,7 +78,9 @@ int main() {
|
|||||||
require(deep_bundle.native().size() > 300,
|
require(deep_bundle.native().size() > 300,
|
||||||
"Shader file fixture must exceed the legacy Windows path limit");
|
"Shader file fixture must exceed the legacy Windows path limit");
|
||||||
fs::create_directories(native_io_path(deep_bundle));
|
fs::create_directories(native_io_path(deep_bundle));
|
||||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain"})
|
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain",
|
||||||
|
"gpuVertexMain", "gpuShadowMain", "gpuCullMain",
|
||||||
|
"gpuHzbMain", "gpuPostCullMain"})
|
||||||
for (const auto* extension : {".spv", ".reflection.json"}) {
|
for (const auto* extension : {".spv", ".reflection.json"}) {
|
||||||
const auto name = std::string(entry) + extension;
|
const auto name = std::string(entry) + extension;
|
||||||
atomic_write(deep_bundle / name, read_text(bundle / name));
|
atomic_write(deep_bundle / name, read_text(bundle / name));
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
"""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_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"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
+18
-3
@@ -109,13 +109,22 @@ def normalize(raw: dict, bytecode: bytes, entry_name: str) -> dict:
|
|||||||
if ty["kind"] == "array":
|
if ty["kind"] == "array":
|
||||||
count = ty["elementCount"]
|
count = ty["elementCount"]
|
||||||
ty = ty["elementType"]
|
ty = ty["elementType"]
|
||||||
|
element_stride = None
|
||||||
if ty["kind"] == "samplerState":
|
if ty["kind"] == "samplerState":
|
||||||
descriptor_type = "sampler"
|
descriptor_type = "sampler"
|
||||||
elif ty["kind"] == "resource" and ty.get("baseShape") == "texture2D":
|
elif ty["kind"] == "resource" and ty.get("baseShape") == "texture2D":
|
||||||
descriptor_type = "sampled_image_2d"
|
descriptor_type = "storage_image_2d" if ty.get("access") == "readWrite" else "sampled_image_2d"
|
||||||
|
elif ty["kind"] == "resource" and ty.get("baseShape") == "structuredBuffer":
|
||||||
|
descriptor_type = "storage_buffer"
|
||||||
|
element_stride = next((item["value"] for item in ty["resultType"].get("sizes", []) if item["kind"] == "uniform"), None)
|
||||||
|
if not isinstance(element_stride, int) or element_stride <= 0:
|
||||||
|
raise ValueError(f"Structured buffer lacks a valid element stride: {parameter['name']}")
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported descriptor kind: {ty}")
|
raise ValueError(f"Unsupported descriptor kind: {ty}")
|
||||||
descriptors.append({"name": parameter["name"], "set": binding.get("space", 0), "binding": binding["index"], "type": descriptor_type, "count": count, "used": used.get(parameter["name"], True)})
|
descriptor = {"name": parameter["name"], "set": binding.get("space", 0), "binding": binding["index"], "type": descriptor_type, "count": count, "used": used.get(parameter["name"], True)}
|
||||||
|
if element_stride is not None:
|
||||||
|
descriptor["element_stride"] = element_stride
|
||||||
|
descriptors.append(descriptor)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported global shader binding: {binding['kind']}")
|
raise ValueError(f"Unsupported global shader binding: {binding['kind']}")
|
||||||
inputs, input_builtins = interface(entry.get("parameters", []), "varyingInput")
|
inputs, input_builtins = interface(entry.get("parameters", []), "varyingInput")
|
||||||
@@ -130,13 +139,19 @@ def main() -> int:
|
|||||||
parser.add_argument("--source", required=True, type=Path)
|
parser.add_argument("--source", required=True, type=Path)
|
||||||
parser.add_argument("--entry", required=True)
|
parser.add_argument("--entry", required=True)
|
||||||
parser.add_argument("--output", required=True, type=Path)
|
parser.add_argument("--output", required=True, type=Path)
|
||||||
|
parser.add_argument("--define", action="append", default=[],
|
||||||
|
help="Slang preprocessor definition, NAME or NAME=VALUE")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
args.output.mkdir(parents=True, exist_ok=True)
|
args.output.mkdir(parents=True, exist_ok=True)
|
||||||
with tempfile.TemporaryDirectory(prefix=".shader-", dir=args.output) as temporary:
|
with tempfile.TemporaryDirectory(prefix=".shader-", dir=args.output) as temporary:
|
||||||
directory = Path(temporary)
|
directory = Path(temporary)
|
||||||
spirv = directory / f"{args.entry}.spv"
|
spirv = directory / f"{args.entry}.spv"
|
||||||
raw = directory / f"{args.entry}.slang-reflection.json"
|
raw = directory / f"{args.entry}.slang-reflection.json"
|
||||||
process = subprocess.run([args.compiler, str(args.source), "-entry", args.entry, "-target", "spirv", "-profile", "spirv_1_6", "-matrix-layout-column-major", "-o", str(spirv), "-reflection-json", str(raw)])
|
command = [args.compiler, str(args.source), "-entry", args.entry, "-target", "spirv",
|
||||||
|
"-profile", "spirv_1_6", "-matrix-layout-column-major"]
|
||||||
|
command += [f"-D{definition}" for definition in args.define]
|
||||||
|
command += ["-o", str(spirv), "-reflection-json", str(raw)]
|
||||||
|
process = subprocess.run(command)
|
||||||
if process.returncode:
|
if process.returncode:
|
||||||
return process.returncode
|
return process.returncode
|
||||||
normalized = normalize(json.loads(raw.read_text(encoding="utf-8")), spirv.read_bytes(), args.entry)
|
normalized = normalize(json.loads(raw.read_text(encoding="utf-8")), spirv.read_bytes(), args.entry)
|
||||||
|
|||||||
Reference in New Issue
Block a user