Add measured optional tiled Forward+ lighting
Native and manual checks / native (ubuntu-24.04) (push) Failing after 36s
Native and manual checks / manual (push) Successful in 25s
Windows editor and software Vulkan / windows-graphics (push) Canceled after 0s
Native and manual checks / native (windows-2025) (push) Canceled after 0s
Native and manual checks / native (ubuntu-24.04) (push) Failing after 36s
Native and manual checks / manual (push) Successful in 25s
Windows editor and software Vulkan / windows-graphics (push) Canceled after 0s
Native and manual checks / native (windows-2025) (push) Canceled after 0s
This commit is contained in:
@@ -125,7 +125,15 @@ Json profileFrames(const std::vector<ProfileSample>& samples) {
|
||||
{"gpu_sun_shadow_ms",
|
||||
gpuMeasured ? Json(sample.lighting.gpu_sun_shadow_ms) : Json(nullptr)},
|
||||
{"gpu_local_shadow_ms",
|
||||
gpuMeasured ? Json(sample.lighting.gpu_local_shadow_ms) : Json(nullptr)}});
|
||||
gpuMeasured ? Json(sample.lighting.gpu_local_shadow_ms) : Json(nullptr)},
|
||||
{"gpu_light_tiles_ms",
|
||||
gpuMeasured ? Json(sample.lighting.gpu_light_tiles_ms) : Json(nullptr)},
|
||||
{"light_tile_count", sample.lighting.light_tile_count},
|
||||
{"light_tile_counts_valid", sample.lighting.light_tile_counts_valid},
|
||||
{"light_tile_candidate_count", sample.lighting.light_tile_counts_valid
|
||||
? Json(sample.lighting.light_tile_candidate_count) : Json(nullptr)},
|
||||
{"light_tile_overflow_count", sample.lighting.light_tile_counts_valid
|
||||
? Json(sample.lighting.light_tile_overflow_count) : Json(nullptr)}});
|
||||
}
|
||||
return {{"samples", std::move(frames)},
|
||||
{"summary_ms",
|
||||
|
||||
@@ -17,6 +17,14 @@ foreach(FASET_ENTRY vertexMain fragmentMain shadowMain)
|
||||
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")
|
||||
endforeach()
|
||||
set(FASET_SHADER_OUTPUT "${FASET_SHADER_DIRECTORY}/lightTileMain.spv")
|
||||
add_custom_command(OUTPUT "${FASET_SHADER_OUTPUT}" "${FASET_SHADER_DIRECTORY}/lightTileMain.reflection.json"
|
||||
COMMAND "${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/tools/compile_shader.py"
|
||||
--compiler "${SLANGC_EXECUTABLE}" --source "${PROJECT_SOURCE_DIR}/shaders/light_tiles.slang"
|
||||
--entry lightTileMain --output "${FASET_SHADER_DIRECTORY}"
|
||||
BYPRODUCTS "${FASET_SHADER_DIRECTORY}/lightTileMain.slang-reflection.json"
|
||||
DEPENDS "${PROJECT_SOURCE_DIR}/shaders/light_tiles.slang" "${PROJECT_SOURCE_DIR}/tools/compile_shader.py" VERBATIM)
|
||||
list(APPEND FASET_SHADER_OUTPUTS "${FASET_SHADER_OUTPUT}" "${FASET_SHADER_DIRECTORY}/lightTileMain.reflection.json")
|
||||
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)
|
||||
@@ -53,6 +61,8 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(render_lighting_sun PROPERTIES LABELS "gpu;p3")
|
||||
add_test(NAME render_lighting_local COMMAND faset_render_lighting_gpu_tests --local)
|
||||
set_tests_properties(render_lighting_local PROPERTIES LABELS "gpu;p3")
|
||||
add_test(NAME render_lighting_tiled COMMAND faset_render_lighting_gpu_tests --tiled)
|
||||
set_tests_properties(render_lighting_tiled PROPERTIES LABELS "gpu;p3")
|
||||
add_executable(faset_render_lighting_policy_tests "${PROJECT_SOURCE_DIR}/tests/render_lighting_policy_tests.cpp")
|
||||
target_link_libraries(faset_render_lighting_policy_tests PRIVATE faset_render)
|
||||
add_test(NAME render_lighting_policy COMMAND faset_render_lighting_policy_tests)
|
||||
|
||||
@@ -19,8 +19,10 @@ namespace fs = std::filesystem;
|
||||
namespace {
|
||||
struct Options {
|
||||
unsigned lights{}, width{1920}, height{1080}, warmup{10}, frames{30}, run_index{};
|
||||
bool shadows{}, validation{};
|
||||
bool shadows{}, validation{}, tile_diagnostics{};
|
||||
VisibilityMode visibility{VisibilityMode::Direct};
|
||||
LightingMode lighting{LightingMode::Auto};
|
||||
std::string light_layout{"dense"};
|
||||
std::string commit{"unknown"}, driver{"unknown"};
|
||||
fs::path csv, capture;
|
||||
};
|
||||
@@ -48,6 +50,8 @@ Options parse(int argc, char** argv) {
|
||||
"--shadows on|off --visibility direct|gpu-frustum|gpu-occlusion "
|
||||
"--csv PATH [--width N --height N --warmup N --frames N "
|
||||
"--run-index N --commit SHA --driver NAME --validation on|off "
|
||||
"--lighting auto|forward|tiled --light-layout dense|localized "
|
||||
"--tile-diagnostics on|off "
|
||||
"--capture PATH]\n";
|
||||
std::exit(0);
|
||||
}
|
||||
@@ -72,11 +76,24 @@ Options parse(int argc, char** argv) {
|
||||
if (value != "on" && value != "off")
|
||||
throw std::invalid_argument("--validation must be on or off");
|
||||
options.validation = value == "on";
|
||||
} else if (name == "--tile-diagnostics") {
|
||||
if (value != "on" && value != "off")
|
||||
throw std::invalid_argument("--tile-diagnostics must be on or off");
|
||||
options.tile_diagnostics = value == "on";
|
||||
} else if (name == "--visibility") {
|
||||
if (value == "direct") options.visibility = VisibilityMode::Direct;
|
||||
else if (value == "gpu-frustum") options.visibility = VisibilityMode::GpuFrustum;
|
||||
else if (value == "gpu-occlusion") options.visibility = VisibilityMode::GpuOcclusion;
|
||||
else throw std::invalid_argument("Unknown visibility mode: " + value);
|
||||
} else if (name == "--lighting") {
|
||||
if (value == "auto") options.lighting = LightingMode::Auto;
|
||||
else if (value == "forward") options.lighting = LightingMode::Forward;
|
||||
else if (value == "tiled") options.lighting = LightingMode::Tiled;
|
||||
else throw std::invalid_argument("Unknown lighting mode: " + value);
|
||||
} else if (name == "--light-layout") {
|
||||
if (value != "dense" && value != "localized")
|
||||
throw std::invalid_argument("--light-layout must be dense or localized");
|
||||
options.light_layout = value;
|
||||
} else throw std::invalid_argument("Unknown option: " + name);
|
||||
}
|
||||
constexpr std::array allowed_lights{0u, 4u, 16u, 32u, 64u, 128u};
|
||||
@@ -146,7 +163,7 @@ Snapshot benchmark_scene(const Options& options) {
|
||||
.6f + .4f * float(i % 3 == 1),
|
||||
.6f + .4f * float(i % 3 == 2), 1};
|
||||
light.intensity = 5.f;
|
||||
light.range = 8.f;
|
||||
light.range = options.light_layout == "localized" ? 1.75f : 8.f;
|
||||
light.casts_shadow = options.shadows;
|
||||
scene.local_lights.push_back(std::move(light));
|
||||
}
|
||||
@@ -160,6 +177,8 @@ void benchmark(const Options& options) {
|
||||
config.headless = true;
|
||||
config.validation = options.validation;
|
||||
config.visibility_mode = options.visibility;
|
||||
config.lighting_mode = options.lighting;
|
||||
config.visibility_diagnostics = options.tile_diagnostics;
|
||||
auto renderer = Renderer(config);
|
||||
const auto scene = benchmark_scene(options);
|
||||
for (unsigned i = 0; i < options.warmup; ++i)
|
||||
@@ -169,14 +188,18 @@ void benchmark(const Options& options) {
|
||||
std::ofstream csv(faset::native_io_path(options.csv));
|
||||
if (!csv)
|
||||
throw std::runtime_error("Cannot open benchmark CSV: " + faset::path_to_utf8(options.csv));
|
||||
csv << "light_count,shadows,visibility,effective_visibility,lighting_path,"
|
||||
csv << "light_count,light_layout,shadows,visibility,effective_visibility,lighting_path,"
|
||||
"requested_lighting,"
|
||||
"build_configuration,run_index,frame,"
|
||||
"device,driver,commit,width,height,validation_enabled,validation_errors,"
|
||||
"submitted_local_lights,omitted_local_lights,"
|
||||
"requested_local_shadow_faces,rendered_local_shadow_faces,dropped_shadow_faces,"
|
||||
"shadow_atlas_full_drops,shadow_tiles,draw_calls,gpu_bytes,"
|
||||
"gpu_main_raster_ms,gpu_post_raster_ms,gpu_post_visible,visibility_counters_valid,"
|
||||
"gpu_sun_shadow_ms,gpu_local_shadow_ms,gpu_shadow_ms,gpu_ms,cpu_ms,readback_cpu_ms\n";
|
||||
"gpu_sun_shadow_ms,gpu_local_shadow_ms,gpu_shadow_ms,gpu_light_tiles_ms,"
|
||||
"gpu_build_plus_raster_ms,light_tile_count,light_tile_counts_valid,"
|
||||
"light_tile_candidate_count,light_tile_overflow_count,"
|
||||
"gpu_ms,cpu_ms,readback_cpu_ms\n";
|
||||
csv << std::fixed << std::setprecision(6);
|
||||
for (unsigned frame = 0; frame < options.frames; ++frame) {
|
||||
renderer.render(scene);
|
||||
@@ -189,9 +212,13 @@ void benchmark(const Options& options) {
|
||||
throw std::runtime_error("Requested visibility path fell back during benchmark");
|
||||
if (stats.gpu_main_raster_ms <= 0 || stats.gpu_ms <= 0)
|
||||
throw std::runtime_error("GPU raster or frame timestamp was unavailable");
|
||||
csv << options.lights << ',' << (options.shadows ? "on" : "off") << ','
|
||||
csv << options.lights << ',' << options.light_layout << ','
|
||||
<< (options.shadows ? "on" : "off") << ','
|
||||
<< mode_name(options.visibility) << ',' << mode_name(stats.effective_visibility_mode)
|
||||
<< ',' << stats.effective_lighting_path << ',' << FASET_BENCHMARK_CONFIGURATION << ','
|
||||
<< ',' << stats.effective_lighting_path << ','
|
||||
<< (options.lighting == LightingMode::Forward ? "forward" :
|
||||
options.lighting == LightingMode::Tiled ? "tiled" : "auto") << ','
|
||||
<< FASET_BENCHMARK_CONFIGURATION << ','
|
||||
<< options.run_index << ',' << frame << ',';
|
||||
csv_text(csv, stats.device);
|
||||
csv << ',';
|
||||
@@ -208,7 +235,13 @@ void benchmark(const Options& options) {
|
||||
<< stats.gpu_main_raster_ms << ',' << stats.gpu_post_raster_ms << ','
|
||||
<< stats.gpu_post_visible << ',' << (stats.visibility_counters_valid ? 1 : 0)
|
||||
<< ',' << stats.gpu_sun_shadow_ms << ',' << stats.gpu_local_shadow_ms << ','
|
||||
<< (stats.gpu_sun_shadow_ms + stats.gpu_local_shadow_ms) << ',' << stats.gpu_ms << ','
|
||||
<< (stats.gpu_sun_shadow_ms + stats.gpu_local_shadow_ms) << ','
|
||||
<< stats.gpu_light_tiles_ms << ','
|
||||
<< (stats.gpu_main_raster_ms + stats.gpu_post_raster_ms +
|
||||
stats.gpu_light_tiles_ms) << ','
|
||||
<< stats.light_tile_count << ',' << (stats.light_tile_counts_valid ? 1 : 0)
|
||||
<< ',' << stats.light_tile_candidate_count << ','
|
||||
<< stats.light_tile_overflow_count << ',' << stats.gpu_ms << ','
|
||||
<< stats.cpu_ms << ',' << stats.readback_cpu_ms << '\n';
|
||||
}
|
||||
if (!csv)
|
||||
|
||||
@@ -136,6 +136,7 @@ struct Snapshot {
|
||||
std::optional<CameraFrustum> camera_frustum{};
|
||||
};
|
||||
enum class VisibilityMode { Direct, GpuFrustum, GpuOcclusion };
|
||||
enum class LightingMode { Auto, Forward, Tiled };
|
||||
// CPU-only validation used before publishing a game or creating Vulkan pipelines.
|
||||
void validate_shader_bundle(const std::filesystem::path& directory);
|
||||
void validate_gpu_shader_bundle(const std::filesystem::path& directory);
|
||||
@@ -146,6 +147,9 @@ struct RendererConfig {
|
||||
bool headless{false};
|
||||
bool validation{true};
|
||||
VisibilityMode visibility_mode{VisibilityMode::Direct};
|
||||
// Auto prefers the 16x16 tiled light list at 32+ submitted local lights.
|
||||
// Forward remains the reference and the fallback on unsupported devices.
|
||||
LightingMode lighting_mode{LightingMode::Auto};
|
||||
// GPU counter readback is diagnostic-only; normal visibility uses no CPU feedback.
|
||||
bool visibility_diagnostics{false};
|
||||
// Optional isolated shader bundle, useful for editor preview and shader reload tests.
|
||||
@@ -204,6 +208,11 @@ struct FrameStats {
|
||||
double gpu_main_cull_ms{}, gpu_main_raster_ms{}, gpu_hzb_ms{};
|
||||
double gpu_post_cull_ms{}, gpu_post_raster_ms{};
|
||||
double gpu_sun_shadow_ms{}, gpu_local_shadow_ms{};
|
||||
double gpu_light_tiles_ms{};
|
||||
std::uint32_t light_tile_count{};
|
||||
// Optional tile-list readback, valid only when visibility diagnostics are on.
|
||||
bool light_tile_counts_valid{};
|
||||
std::uint32_t light_tile_candidate_count{}, light_tile_overflow_count{};
|
||||
std::string effective_lighting_path{"forward"};
|
||||
std::string device;
|
||||
};
|
||||
|
||||
+17
-1
@@ -50,6 +50,9 @@ struct ShadowViewGpu {
|
||||
[[vk::binding(1,1)]] StructuredBuffer<LocalLightGpu> localLights;
|
||||
[[vk::binding(2,1)]] StructuredBuffer<ShadowViewGpu> shadowViews;
|
||||
[[vk::binding(3,1)]] Texture2D<float> localShadowAtlas;
|
||||
// 4-word header, then 66 words per 16x16 tile: count, overflow, 64 indices.
|
||||
// An overflowing tile evaluates the full submitted list instead of losing light.
|
||||
[[vk::binding(4,1)]] StructuredBuffer<uint> lightTileWords;
|
||||
[shader("vertex")]
|
||||
VertexOutput vertexMain(VertexInput v) {
|
||||
VertexOutput o;
|
||||
@@ -171,7 +174,20 @@ float4 fragmentMain(VertexOutput v) : SV_Target {
|
||||
linear += directBRDF(base.rgb, rough, metal, n, view, l) *
|
||||
lighting.sunColor.rgb * (lighting.sunDirectionIntensity.w * 3.0 * visibility);
|
||||
}
|
||||
for (uint i=0; i<lighting.counts.x; ++i) {
|
||||
uint candidateCount = lighting.counts.x;
|
||||
uint tileBase = 0;
|
||||
bool tileList = false;
|
||||
if (lightTileWords[1] != 0 && lightTileWords[0] != 0 && lightTileWords[2] != 0) {
|
||||
uint tileX = min(uint(v.position.x) / 16u, lightTileWords[0] - 1u);
|
||||
uint tileY = min(uint(v.position.y) / 16u, lightTileWords[2] - 1u);
|
||||
tileBase = 4u + (tileY * lightTileWords[0] + tileX) * 66u;
|
||||
if (lightTileWords[tileBase + 1u] == 0) {
|
||||
candidateCount = min(lightTileWords[tileBase], lighting.counts.x);
|
||||
tileList = true;
|
||||
}
|
||||
}
|
||||
for (uint candidate=0; candidate<candidateCount; ++candidate) {
|
||||
uint i = tileList ? lightTileWords[tileBase + 2u + candidate] : candidate;
|
||||
LocalLightGpu light=localLights[i];
|
||||
float3 delta=light.positionRange.xyz-v.world;
|
||||
float distanceSquared=max(dot(delta,delta),1e-6);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Conservative depth-free 16x16 Forward+ construction. One invocation owns
|
||||
// one tile, so indices remain in the same sorted order as the forward reference.
|
||||
struct LocalLightGpu {
|
||||
float4 positionRange;
|
||||
float4 directionCosOuter;
|
||||
float4 colorIntensity;
|
||||
float4 coneTypeShadowView;
|
||||
float4 reserved;
|
||||
};
|
||||
struct TileBuildParameters {
|
||||
column_major float4x4 viewProjection;
|
||||
float4 viewport; // x, y, width, height in framebuffer pixels
|
||||
uint4 dimensions; // tilesX, tilesY, submitted local lights, capacity (<= 64)
|
||||
};
|
||||
[[vk::push_constant]] ConstantBuffer<TileBuildParameters> build;
|
||||
[[vk::binding(0,0)]] StructuredBuffer<LocalLightGpu> localLights;
|
||||
[[vk::binding(1,0)]] RWStructuredBuffer<uint> tileWords;
|
||||
|
||||
bool sphereTouchesPlane(float3 center, float radius, float4 plane) {
|
||||
// The final epsilon admits boundary/rounding cases rather than dropping a
|
||||
// light. We intentionally do not use scene depth or reject near-plane cuts.
|
||||
return dot(plane, float4(center, 1.0)) + radius * length(plane.xyz) >= -1e-4;
|
||||
}
|
||||
|
||||
[shader("compute")]
|
||||
[numthreads(64, 1, 1)]
|
||||
void lightTileMain(uint3 dispatchId : SV_DispatchThreadID) {
|
||||
uint tileId = dispatchId.x;
|
||||
uint tilesX = build.dimensions.x;
|
||||
uint tilesY = build.dimensions.y;
|
||||
if (tileId >= tilesX * tilesY) return;
|
||||
if (tileId == 0) {
|
||||
tileWords[0] = tilesX;
|
||||
tileWords[1] = 1;
|
||||
tileWords[2] = tilesY;
|
||||
tileWords[3] = min(build.dimensions.w, 64u);
|
||||
}
|
||||
uint tileX = tileId % tilesX;
|
||||
uint tileY = tileId / tilesX;
|
||||
float x0 = float(tileX * 16u);
|
||||
float y0 = float(tileY * 16u);
|
||||
float x1 = x0 + 16.0;
|
||||
float y1 = y0 + 16.0;
|
||||
float left = 2.0 * (x0 - build.viewport.x) / build.viewport.z - 1.0;
|
||||
float right = 2.0 * (x1 - build.viewport.x) / build.viewport.z - 1.0;
|
||||
float top = 2.0 * (y0 - build.viewport.y) / build.viewport.w - 1.0;
|
||||
float bottom = 2.0 * (y1 - build.viewport.y) / build.viewport.w - 1.0;
|
||||
float4 xRow = mul(float4(1, 0, 0, 0), build.viewProjection);
|
||||
float4 yRow = mul(float4(0, 1, 0, 0), build.viewProjection);
|
||||
float4 wRow = mul(float4(0, 0, 0, 1), build.viewProjection);
|
||||
float4 leftPlane = xRow - left * wRow;
|
||||
float4 rightPlane = right * wRow - xRow;
|
||||
float4 topPlane = yRow - top * wRow;
|
||||
float4 bottomPlane = bottom * wRow - yRow;
|
||||
uint base = 4u + tileId * 66u;
|
||||
uint count = 0;
|
||||
bool overflow = false;
|
||||
for (uint i = 0; i < build.dimensions.z; ++i) {
|
||||
LocalLightGpu light = localLights[i];
|
||||
if (light.colorIntensity.w <= 0.0) continue;
|
||||
float3 center = light.positionRange.xyz;
|
||||
float radius = light.positionRange.w;
|
||||
if (!sphereTouchesPlane(center, radius, leftPlane) ||
|
||||
!sphereTouchesPlane(center, radius, rightPlane) ||
|
||||
!sphereTouchesPlane(center, radius, topPlane) ||
|
||||
!sphereTouchesPlane(center, radius, bottomPlane)) continue;
|
||||
if (count < min(build.dimensions.w, 64u))
|
||||
tileWords[base + 2u + count] = i;
|
||||
else
|
||||
overflow = true;
|
||||
++count;
|
||||
}
|
||||
tileWords[base] = min(count, min(build.dimensions.w, 64u));
|
||||
tileWords[base + 1u] = overflow ? 1u : 0u;
|
||||
}
|
||||
@@ -333,6 +333,7 @@ struct BuildService::Impl {
|
||||
copy_required_file(player, staging / ("faset_player" + executable_suffix()));
|
||||
copy_required_file(exporter, staging / ("faset_schema_exporter" + executable_suffix()));
|
||||
for (const auto* file : {"vertexMain.spv", "fragmentMain.spv", "shadowMain.spv",
|
||||
"lightTileMain.spv", "lightTileMain.reflection.json",
|
||||
"vertexMain.reflection.json", "fragmentMain.reflection.json",
|
||||
"shadowMain.reflection.json", "gpuVertexMain.spv",
|
||||
"gpuShadowMain.spv", "gpuCullMain.spv", "gpuHzbMain.spv",
|
||||
@@ -585,6 +586,7 @@ struct BuildService::Impl {
|
||||
copy_required_file(build_directory / ("faset_player" + executable_suffix()),
|
||||
staging / ("faset_player" + executable_suffix()));
|
||||
for (const auto* shader : {"vertexMain.spv", "fragmentMain.spv", "shadowMain.spv",
|
||||
"lightTileMain.spv", "lightTileMain.reflection.json",
|
||||
"vertexMain.reflection.json", "fragmentMain.reflection.json",
|
||||
"shadowMain.reflection.json", "gpuVertexMain.spv",
|
||||
"gpuShadowMain.spv", "gpuCullMain.spv", "gpuHzbMain.spv",
|
||||
|
||||
@@ -383,6 +383,14 @@ void DebugOverlay::append(render::Snapshot& output, render::Renderer& renderer,
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Lighting and shadows");
|
||||
ImGui::Text("Lighting path: %s", stats.effective_lighting_path.c_str());
|
||||
ImGui::Text("Light tiles: %u; GPU build %.2f ms",
|
||||
stats.light_tile_count, stats.gpu_light_tiles_ms);
|
||||
if (stats.light_tile_counts_valid)
|
||||
ImGui::Text("Tile entries: %u; overflow tiles: %u",
|
||||
stats.light_tile_candidate_count,
|
||||
stats.light_tile_overflow_count);
|
||||
else if (stats.light_tile_count)
|
||||
ImGui::TextDisabled("Tile entry counts unavailable until diagnostics readback");
|
||||
ImGui::Text("Local lights: %u submitted, %u omitted",
|
||||
stats.submitted_local_lights, stats.omitted_local_lights);
|
||||
ImGui::Text("Sun cascades: %u / %u effective",
|
||||
|
||||
+287
-12
@@ -95,6 +95,12 @@ struct ShadowViewGpu {
|
||||
std::array<float, 4> guarded_clamp{};
|
||||
std::array<float, 4> bias_flags{};
|
||||
};
|
||||
struct LightTilePush {
|
||||
Mat4 view_projection;
|
||||
std::array<float, 4> viewport;
|
||||
std::array<std::uint32_t, 4> dimensions;
|
||||
};
|
||||
static_assert(sizeof(LightTilePush) == 96);
|
||||
static_assert(sizeof(LightingHeaderGpu) == 80 &&
|
||||
offsetof(LightingHeaderGpu, sun_direction_intensity) == 16 &&
|
||||
offsetof(LightingHeaderGpu, sun_color) == 32 &&
|
||||
@@ -225,7 +231,7 @@ struct Renderer::Impl {
|
||||
float timestamp_period{};
|
||||
std::uint32_t timestamp_bits{};
|
||||
VkSemaphore acquired{}, present_ready{};
|
||||
std::array<std::string, 3> shader_layouts{};
|
||||
std::array<std::string, 4> shader_layouts{};
|
||||
VkSwapchainKHR swapchain{};
|
||||
VkFormat swap_format{};
|
||||
VkExtent2D swap_extent{};
|
||||
@@ -234,7 +240,9 @@ struct Renderer::Impl {
|
||||
Image color, depth, shadow, local_shadow;
|
||||
std::uint32_t sun_shadow_size{}, local_shadow_size{};
|
||||
Buffer vertices, readback;
|
||||
Buffer lighting_header, lighting_locals, lighting_views;
|
||||
Buffer lighting_header, lighting_locals, lighting_views, light_tile_words,
|
||||
light_tile_readback;
|
||||
bool light_tiles_capable{};
|
||||
SceneResources scene;
|
||||
InstanceTracker instance_tracker;
|
||||
std::unordered_map<std::string, std::size_t> previous_lods;
|
||||
@@ -254,6 +262,11 @@ struct Renderer::Impl {
|
||||
VkDescriptorSetLayout lighting_layout{};
|
||||
VkDescriptorPool lighting_pool{};
|
||||
VkDescriptorSet lighting_set{};
|
||||
VkDescriptorSetLayout light_tile_layout{};
|
||||
VkDescriptorPool light_tile_pool{};
|
||||
VkDescriptorSet light_tile_set{};
|
||||
VkPipelineLayout light_tile_pipeline_layout{};
|
||||
VkPipeline light_tile_pipeline{};
|
||||
VkSampler shadow_sampler{}, color_sampler{};
|
||||
VkPipelineLayout pipeline_layout{};
|
||||
VkPipeline pipeline{}, ui_pipeline{}, shadow_pipeline{}, sprite_pipeline{};
|
||||
@@ -361,12 +374,15 @@ struct Renderer::Impl {
|
||||
destroy(lighting_header);
|
||||
destroy(lighting_locals);
|
||||
destroy(lighting_views);
|
||||
destroy(light_tile_words);
|
||||
destroy(light_tile_readback);
|
||||
destroy(color);
|
||||
destroy(depth);
|
||||
destroy(shadow);
|
||||
destroy(local_shadow);
|
||||
if (device) {
|
||||
destroy_scene_interfaces();
|
||||
destroy_light_tile_interfaces();
|
||||
if (pipeline)
|
||||
vkDestroyPipeline(device, pipeline, nullptr);
|
||||
if (ui_pipeline)
|
||||
@@ -697,6 +713,13 @@ struct Renderer::Impl {
|
||||
max_compute_groups_x = properties.limits.maxComputeWorkGroupCount[0];
|
||||
max_storage_buffer_range = properties.limits.maxStorageBufferRange;
|
||||
max_image_dimension = properties.limits.maxImageDimension2D;
|
||||
light_tiles_capable =
|
||||
(queues[i].queueFlags & VK_QUEUE_COMPUTE_BIT) != 0 &&
|
||||
properties.limits.maxComputeWorkGroupInvocations >= 64 &&
|
||||
properties.limits.maxComputeWorkGroupSize[0] >= 64 &&
|
||||
properties.limits.maxPerStageDescriptorStorageBuffers >= 4 &&
|
||||
properties.limits.maxDescriptorSetStorageBuffers >= 4 &&
|
||||
max_compute_groups_x > 0;
|
||||
scene.available = (queues[i].queueFlags & VK_QUEUE_COMPUTE_BIT) != 0 &&
|
||||
properties.limits.maxPerStageDescriptorStorageBuffers >= 8 &&
|
||||
properties.limits.maxDescriptorSetStorageBuffers >= 8 &&
|
||||
@@ -805,7 +828,19 @@ struct Renderer::Impl {
|
||||
}
|
||||
}
|
||||
make_targets();
|
||||
light_tile_words = make_buffer(16,
|
||||
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
|
||||
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
make_descriptors();
|
||||
if (light_tiles_capable) {
|
||||
try {
|
||||
make_light_tile_descriptors();
|
||||
} catch (const std::exception&) {
|
||||
destroy_light_tile_interfaces();
|
||||
light_tiles_capable = false;
|
||||
}
|
||||
}
|
||||
make_pipelines();
|
||||
if (scene.available && c.visibility_mode != VisibilityMode::Direct)
|
||||
make_scene_descriptors_and_pipelines();
|
||||
@@ -964,7 +999,7 @@ struct Renderer::Impl {
|
||||
pi.pPoolSizes = sizes;
|
||||
check(vkCreateDescriptorPool(device, &pi, nullptr, &descriptor_pool),
|
||||
"Create descriptor pool");
|
||||
std::array<VkDescriptorSetLayoutBinding, 4> lighting_bindings{};
|
||||
std::array<VkDescriptorSetLayoutBinding, 5> lighting_bindings{};
|
||||
for (std::uint32_t i = 0; i < lighting_bindings.size(); ++i)
|
||||
lighting_bindings[i] = {i,
|
||||
i == 3 ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE
|
||||
@@ -975,7 +1010,7 @@ struct Renderer::Impl {
|
||||
check(vkCreateDescriptorSetLayout(device, &li, nullptr, &lighting_layout),
|
||||
"Create lighting descriptor layout");
|
||||
VkDescriptorPoolSize lighting_sizes[] = {
|
||||
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1}};
|
||||
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1}};
|
||||
pi.flags = 0;
|
||||
pi.maxSets = 1;
|
||||
pi.poolSizeCount = 2;
|
||||
@@ -999,6 +1034,59 @@ struct Renderer::Impl {
|
||||
si.magFilter = si.minFilter = VK_FILTER_LINEAR;
|
||||
check(vkCreateSampler(device, &si, nullptr, &color_sampler), "Create color sampler");
|
||||
}
|
||||
void destroy_light_tile_interfaces() {
|
||||
if (!device)
|
||||
return;
|
||||
if (light_tile_pipeline)
|
||||
vkDestroyPipeline(device, light_tile_pipeline, nullptr);
|
||||
if (light_tile_pipeline_layout)
|
||||
vkDestroyPipelineLayout(device, light_tile_pipeline_layout, nullptr);
|
||||
if (light_tile_pool)
|
||||
vkDestroyDescriptorPool(device, light_tile_pool, nullptr);
|
||||
if (light_tile_layout)
|
||||
vkDestroyDescriptorSetLayout(device, light_tile_layout, nullptr);
|
||||
light_tile_pipeline = {};
|
||||
light_tile_pipeline_layout = {};
|
||||
light_tile_pool = {};
|
||||
light_tile_layout = {};
|
||||
light_tile_set = {};
|
||||
}
|
||||
void make_light_tile_descriptors() {
|
||||
const std::array<VkDescriptorSetLayoutBinding, 2> bindings{{
|
||||
{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr},
|
||||
{1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}}};
|
||||
VkDescriptorSetLayoutCreateInfo layout{};
|
||||
layout.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
|
||||
layout.bindingCount = static_cast<std::uint32_t>(bindings.size());
|
||||
layout.pBindings = bindings.data();
|
||||
check(vkCreateDescriptorSetLayout(device, &layout, nullptr, &light_tile_layout),
|
||||
"Create light tile descriptor layout");
|
||||
VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2};
|
||||
VkDescriptorPoolCreateInfo pool_info{};
|
||||
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||
pool_info.maxSets = 1;
|
||||
pool_info.poolSizeCount = 1;
|
||||
pool_info.pPoolSizes = &size;
|
||||
check(vkCreateDescriptorPool(device, &pool_info, nullptr, &light_tile_pool),
|
||||
"Create light tile descriptor pool");
|
||||
VkDescriptorSetAllocateInfo allocation{};
|
||||
allocation.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
||||
allocation.descriptorPool = light_tile_pool;
|
||||
allocation.descriptorSetCount = 1;
|
||||
allocation.pSetLayouts = &light_tile_layout;
|
||||
check(vkAllocateDescriptorSets(device, &allocation, &light_tile_set),
|
||||
"Allocate light tile descriptors");
|
||||
VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0,
|
||||
sizeof(LightTilePush)};
|
||||
VkPipelineLayoutCreateInfo pipeline{};
|
||||
pipeline.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
||||
pipeline.setLayoutCount = 1;
|
||||
pipeline.pSetLayouts = &light_tile_layout;
|
||||
pipeline.pushConstantRangeCount = 1;
|
||||
pipeline.pPushConstantRanges = &push;
|
||||
check(vkCreatePipelineLayout(device, &pipeline, nullptr, &light_tile_pipeline_layout),
|
||||
"Create light tile pipeline layout");
|
||||
}
|
||||
VkDescriptorSet upload_texture(std::shared_ptr<const Texture> source) {
|
||||
if (!source)
|
||||
source = white;
|
||||
@@ -1215,6 +1303,30 @@ struct Renderer::Impl {
|
||||
check(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pi, nullptr, output),
|
||||
"Create graphics pipeline");
|
||||
}
|
||||
if (light_tiles_capable) {
|
||||
VkShaderModule tile_shader{};
|
||||
try {
|
||||
tile_shader = shader(shaders[3]);
|
||||
VkComputePipelineCreateInfo tile{};
|
||||
tile.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
|
||||
tile.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
tile.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
|
||||
tile.stage.module = tile_shader;
|
||||
tile.stage.pName = "main";
|
||||
tile.layout = light_tile_pipeline_layout;
|
||||
check(vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &tile,
|
||||
nullptr, &light_tile_pipeline),
|
||||
"Create light tile compute pipeline");
|
||||
} catch (const std::exception&) {
|
||||
// Optional acceleration: keep the validated forward renderer.
|
||||
if (light_tile_pipeline)
|
||||
vkDestroyPipeline(device, light_tile_pipeline, nullptr);
|
||||
light_tile_pipeline = {};
|
||||
light_tiles_capable = false;
|
||||
}
|
||||
if (tile_shader)
|
||||
vkDestroyShaderModule(device, tile_shader, nullptr);
|
||||
}
|
||||
} catch (...) {
|
||||
vkDestroyShaderModule(device, vertex, nullptr);
|
||||
vkDestroyShaderModule(device, fragment, nullptr);
|
||||
@@ -1635,14 +1747,15 @@ struct Renderer::Impl {
|
||||
upload_scene_buffer(buffer, values.data(), values.size() * sizeof(T), usage);
|
||||
}
|
||||
void update_lighting_descriptors() {
|
||||
const std::array<VkDescriptorBufferInfo, 3> buffers{{
|
||||
const std::array<VkDescriptorBufferInfo, 4> buffers{{
|
||||
{lighting_header.handle, 0, lighting_header.size},
|
||||
{lighting_locals.handle, 0, lighting_locals.size},
|
||||
{lighting_views.handle, 0, lighting_views.size}}};
|
||||
{lighting_views.handle, 0, lighting_views.size},
|
||||
{light_tile_words.handle, 0, light_tile_words.size}}};
|
||||
const VkDescriptorImageInfo atlas{VK_NULL_HANDLE,
|
||||
local_shadow.handle ? local_shadow.view : shadow.view,
|
||||
VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL};
|
||||
std::array<VkWriteDescriptorSet, 4> writes{};
|
||||
std::array<VkWriteDescriptorSet, 5> writes{};
|
||||
for (std::uint32_t i = 0; i < writes.size(); ++i) {
|
||||
writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
writes[i].dstSet = lighting_set;
|
||||
@@ -1653,7 +1766,25 @@ struct Renderer::Impl {
|
||||
if (i == 3)
|
||||
writes[i].pImageInfo = &atlas;
|
||||
else
|
||||
writes[i].pBufferInfo = &buffers[i];
|
||||
writes[i].pBufferInfo = &buffers[i == 4 ? 3 : i];
|
||||
}
|
||||
vkUpdateDescriptorSets(device, static_cast<std::uint32_t>(writes.size()),
|
||||
writes.data(), 0, nullptr);
|
||||
}
|
||||
void update_light_tile_descriptors() {
|
||||
if (!light_tile_set)
|
||||
return;
|
||||
const std::array<VkDescriptorBufferInfo, 2> buffers{{
|
||||
{lighting_locals.handle, 0, lighting_locals.size},
|
||||
{light_tile_words.handle, 0, light_tile_words.size}}};
|
||||
std::array<VkWriteDescriptorSet, 2> writes{};
|
||||
for (std::uint32_t i = 0; i < writes.size(); ++i) {
|
||||
writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
writes[i].dstSet = light_tile_set;
|
||||
writes[i].dstBinding = i;
|
||||
writes[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||
writes[i].descriptorCount = 1;
|
||||
writes[i].pBufferInfo = &buffers[i];
|
||||
}
|
||||
vkUpdateDescriptorSets(device, static_cast<std::uint32_t>(writes.size()),
|
||||
writes.data(), 0, nullptr);
|
||||
@@ -1765,6 +1896,10 @@ struct Renderer::Impl {
|
||||
statistics.local_shadow_atlas_bytes = local_shadow_size
|
||||
? local_shadow.allocation_size : 0;
|
||||
statistics.gpu_local_shadow_ms = 0;
|
||||
statistics.gpu_light_tiles_ms = 0;
|
||||
statistics.light_tile_count = 0;
|
||||
statistics.light_tile_counts_valid = false;
|
||||
statistics.light_tile_candidate_count = statistics.light_tile_overflow_count = 0;
|
||||
statistics.effective_lighting_path = "forward";
|
||||
statistics.gpu_bins = statistics.gpu_visible_instances =
|
||||
statistics.gpu_frustum_rejected = statistics.gpu_occlusion_deferred =
|
||||
@@ -2354,7 +2489,64 @@ struct Renderer::Impl {
|
||||
upload_scene_buffer(lighting_header, &lighting, sizeof(lighting), 0);
|
||||
upload_scene_vector(lighting_locals, gpu_lights);
|
||||
upload_scene_vector(lighting_views, gpu_shadow_views);
|
||||
constexpr std::uint32_t light_tile_side = 16;
|
||||
constexpr std::uint32_t light_tile_stride_words = 66;
|
||||
constexpr std::uint32_t light_tile_capacity = 64;
|
||||
const std::uint32_t light_tiles_x = width / light_tile_side +
|
||||
(width % light_tile_side != 0);
|
||||
const std::uint32_t light_tiles_y = height / light_tile_side +
|
||||
(height % light_tile_side != 0);
|
||||
const std::uint64_t light_tile_count =
|
||||
std::uint64_t(light_tiles_x) * light_tiles_y;
|
||||
const std::uint64_t light_tile_bytes =
|
||||
(4u + light_tile_count * light_tile_stride_words) * sizeof(std::uint32_t);
|
||||
// The fixed 1080p reference scene has broad overlapping lights: 32 and
|
||||
// 64 nearly fill every tile, and 128 overflows every tile. Until a
|
||||
// validated runtime occupancy predictor exists, Auto keeps the measured
|
||||
// faster full scan. The explicit mode supports sparse-light projects.
|
||||
const bool requested_light_tiles =
|
||||
config.lighting_mode == LightingMode::Tiled;
|
||||
bool use_light_tiles = requested_light_tiles && lighting.counts[0] > 0 &&
|
||||
light_tiles_capable && light_tile_pipeline && light_tile_set &&
|
||||
std::all_of(scene_viewport.begin(), scene_viewport.end(),
|
||||
[](float value) { return std::isfinite(value); }) &&
|
||||
scene_viewport[2] > 0 && scene_viewport[3] > 0 &&
|
||||
light_tile_count <= std::numeric_limits<std::uint32_t>::max() &&
|
||||
light_tile_bytes <= max_storage_buffer_range &&
|
||||
(light_tile_count + 63) / 64 <= max_compute_groups_x;
|
||||
if (use_light_tiles && light_tile_words.size < light_tile_bytes) {
|
||||
try {
|
||||
auto replacement = make_buffer(light_tile_bytes,
|
||||
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT |
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
|
||||
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
destroy(light_tile_words);
|
||||
light_tile_words = replacement;
|
||||
} catch (const std::exception&) {
|
||||
use_light_tiles = false;
|
||||
}
|
||||
}
|
||||
if (use_light_tiles) {
|
||||
statistics.effective_lighting_path = "tiled";
|
||||
statistics.light_tile_count = static_cast<std::uint32_t>(light_tile_count);
|
||||
}
|
||||
bool collect_light_tile_counts = use_light_tiles && config.visibility_diagnostics;
|
||||
if (collect_light_tile_counts && light_tile_readback.size < light_tile_bytes) {
|
||||
try {
|
||||
auto replacement = make_buffer(light_tile_bytes,
|
||||
VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
|
||||
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||
VK_MEMORY_PROPERTY_HOST_CACHED_BIT);
|
||||
destroy(light_tile_readback);
|
||||
light_tile_readback = replacement;
|
||||
} catch (const std::exception&) {
|
||||
collect_light_tile_counts = false;
|
||||
}
|
||||
}
|
||||
update_lighting_descriptors();
|
||||
if (use_light_tiles)
|
||||
update_light_tile_descriptors();
|
||||
Vec3 light_eye{-direction[0] * 30, -direction[1] * 30, -direction[2] * 30};
|
||||
Vec3 light_up = std::abs(direction[1]) > .98f ? Vec3{0, 0, 1} : Vec3{0, 1, 0};
|
||||
Push push{sun_raster && !shadow_plan.sun_views.empty()
|
||||
@@ -2596,6 +2788,38 @@ struct Renderer::Impl {
|
||||
VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL,
|
||||
VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
});
|
||||
if (use_light_tiles)
|
||||
add_pass("LightTileBuild", {}, {"light_tiles"}, [&] {
|
||||
scene_barrier(VK_PIPELINE_STAGE_2_HOST_BIT,
|
||||
VK_ACCESS_2_HOST_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
|
||||
VK_ACCESS_2_SHADER_STORAGE_READ_BIT);
|
||||
vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_COMPUTE,
|
||||
light_tile_pipeline);
|
||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_COMPUTE,
|
||||
light_tile_pipeline_layout, 0, 1,
|
||||
&light_tile_set, 0, nullptr);
|
||||
const LightTilePush tile_push{
|
||||
snapshot.view_projection, scene_viewport,
|
||||
{light_tiles_x, light_tiles_y, lighting.counts[0], light_tile_capacity}};
|
||||
vkCmdPushConstants(command, light_tile_pipeline_layout,
|
||||
VK_SHADER_STAGE_COMPUTE_BIT, 0,
|
||||
sizeof(tile_push), &tile_push);
|
||||
vkCmdDispatch(command,
|
||||
static_cast<std::uint32_t>((light_tile_count + 63) / 64), 1, 1);
|
||||
scene_barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
|
||||
VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT,
|
||||
VK_ACCESS_2_SHADER_STORAGE_READ_BIT);
|
||||
});
|
||||
else
|
||||
add_pass("LightTileFallback", {}, {"light_tiles"}, [&] {
|
||||
vkCmdFillBuffer(command, light_tile_words.handle, 0, 16, 0);
|
||||
scene_barrier(VK_PIPELINE_STAGE_2_TRANSFER_BIT,
|
||||
VK_ACCESS_2_TRANSFER_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT,
|
||||
VK_ACCESS_2_SHADER_STORAGE_READ_BIT);
|
||||
});
|
||||
if (gpu_active)
|
||||
add_pass("MainCull", {"shadow"},
|
||||
{"main_indirect", "main_visible", "deferred_ids"}, [&] {
|
||||
@@ -2656,8 +2880,9 @@ struct Renderer::Impl {
|
||||
});
|
||||
add_pass(occlusion ? "MainRaster" : "ForwardAndUI",
|
||||
gpu_active ? std::vector<std::string>{"shadow", "local_shadow",
|
||||
"main_indirect", "main_visible"}
|
||||
: std::vector<std::string>{"shadow", "local_shadow"},
|
||||
"light_tiles", "main_indirect", "main_visible"}
|
||||
: std::vector<std::string>{"shadow", "local_shadow",
|
||||
"light_tiles"},
|
||||
{"color", "depth"}, [&] {
|
||||
transition(command, color, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
|
||||
VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
@@ -2845,7 +3070,9 @@ struct Renderer::Impl {
|
||||
vkCmdEndRendering(command);
|
||||
});
|
||||
}
|
||||
add_pass("Readback", {"color"}, {"capture"}, [&] {
|
||||
add_pass("Readback", collect_light_tile_counts
|
||||
? std::vector<std::string>{"color", "light_tiles"}
|
||||
: std::vector<std::string>{"color"}, {"capture"}, [&] {
|
||||
transition(command, color, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
VkBufferImageCopy copy{};
|
||||
@@ -2882,6 +3109,19 @@ struct Renderer::Impl {
|
||||
VK_PIPELINE_STAGE_2_HOST_BIT,
|
||||
VK_ACCESS_2_HOST_READ_BIT);
|
||||
}
|
||||
if (collect_light_tile_counts) {
|
||||
scene_barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
|
||||
VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_2_TRANSFER_BIT,
|
||||
VK_ACCESS_2_TRANSFER_READ_BIT);
|
||||
const VkBufferCopy tile_copy{0, 0, light_tile_bytes};
|
||||
vkCmdCopyBuffer(command, light_tile_words.handle,
|
||||
light_tile_readback.handle, 1, &tile_copy);
|
||||
scene_barrier(VK_PIPELINE_STAGE_2_TRANSFER_BIT,
|
||||
VK_ACCESS_2_TRANSFER_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_2_HOST_BIT,
|
||||
VK_ACCESS_2_HOST_READ_BIT);
|
||||
}
|
||||
});
|
||||
if (swap_index)
|
||||
add_pass("Presentation", {"color"}, {"swapchain"}, [&] {
|
||||
@@ -2925,6 +3165,7 @@ struct Renderer::Impl {
|
||||
if (label == "MainCull") statistics.gpu_main_cull_ms = elapsed;
|
||||
else if (label == "SunShadowAtlas") statistics.gpu_sun_shadow_ms = elapsed;
|
||||
else if (label == "LocalShadowAtlas") statistics.gpu_local_shadow_ms = elapsed;
|
||||
else if (label == "LightTileBuild") statistics.gpu_light_tiles_ms = elapsed;
|
||||
else if (label == "MainRaster" || label == "ForwardAndUI")
|
||||
statistics.gpu_main_raster_ms = elapsed;
|
||||
else if (label == "BuildCurrentHZB") statistics.gpu_hzb_ms = elapsed;
|
||||
@@ -2988,6 +3229,29 @@ struct Renderer::Impl {
|
||||
statistics.culled_meshes += statistics.gpu_frustum_rejected;
|
||||
statistics.visibility_counters_valid = true;
|
||||
}
|
||||
if (collect_light_tile_counts) {
|
||||
void* mapped_tiles{};
|
||||
check(vkMapMemory(device, light_tile_readback.memory, 0,
|
||||
light_tile_bytes, 0, &mapped_tiles),
|
||||
"Read light tile diagnostics");
|
||||
const auto* words = static_cast<const std::uint32_t*>(mapped_tiles);
|
||||
if (words[0] != light_tiles_x || words[1] != 1 ||
|
||||
words[2] != light_tiles_y || words[3] != light_tile_capacity) {
|
||||
vkUnmapMemory(device, light_tile_readback.memory);
|
||||
throw std::runtime_error("Light tile diagnostic header is inconsistent");
|
||||
}
|
||||
for (std::uint32_t tile = 0; tile < light_tile_count; ++tile) {
|
||||
const auto base = 4u + tile * light_tile_stride_words;
|
||||
if (words[base] > light_tile_capacity || words[base + 1] > 1) {
|
||||
vkUnmapMemory(device, light_tile_readback.memory);
|
||||
throw std::runtime_error("Light tile diagnostic record is invalid");
|
||||
}
|
||||
statistics.light_tile_candidate_count += words[base];
|
||||
statistics.light_tile_overflow_count += words[base + 1];
|
||||
}
|
||||
vkUnmapMemory(device, light_tile_readback.memory);
|
||||
statistics.light_tile_counts_valid = true;
|
||||
}
|
||||
instance_tracker.finish_frame();
|
||||
scene.previous_vp = snapshot.view_projection;
|
||||
scene.previous_projection = snapshot.projection;
|
||||
@@ -3001,7 +3265,9 @@ struct Renderer::Impl {
|
||||
local_shadow.allocation_size +
|
||||
lighting_header.allocation_size +
|
||||
lighting_locals.allocation_size +
|
||||
lighting_views.allocation_size;
|
||||
lighting_views.allocation_size +
|
||||
light_tile_words.allocation_size +
|
||||
light_tile_readback.allocation_size;
|
||||
statistics.texture_count = static_cast<std::uint32_t>(textures.size());
|
||||
for (const auto& [_, texture] : textures)
|
||||
statistics.gpu_allocated_bytes += texture.image.allocation_size;
|
||||
@@ -3039,6 +3305,8 @@ bool Renderer::reload_shaders(std::string& error) {
|
||||
auto previous_ui = r.ui_pipeline;
|
||||
auto previous_shadow = r.shadow_pipeline;
|
||||
auto previous_sprite = r.sprite_pipeline;
|
||||
auto previous_light_tile = r.light_tile_pipeline;
|
||||
auto previous_light_tiles_capable = r.light_tiles_capable;
|
||||
auto previous_layout_fingerprints = r.shader_layouts;
|
||||
auto previous_gpu_fingerprints = r.scene.shader_layouts;
|
||||
auto previous_gpu = r.scene.graphics_pipeline;
|
||||
@@ -3050,6 +3318,7 @@ bool Renderer::reload_shaders(std::string& error) {
|
||||
r.ui_pipeline = {};
|
||||
r.shadow_pipeline = {};
|
||||
r.sprite_pipeline = {};
|
||||
r.light_tile_pipeline = {};
|
||||
r.scene.graphics_pipeline = r.scene.cull_pipeline = r.scene.post_pipeline =
|
||||
r.scene.hzb_pipeline = {};
|
||||
try {
|
||||
@@ -3065,6 +3334,8 @@ bool Renderer::reload_shaders(std::string& error) {
|
||||
vkDestroyPipeline(r.device, r.shadow_pipeline, nullptr);
|
||||
if (r.sprite_pipeline)
|
||||
vkDestroyPipeline(r.device, r.sprite_pipeline, nullptr);
|
||||
if (r.light_tile_pipeline)
|
||||
vkDestroyPipeline(r.device, r.light_tile_pipeline, nullptr);
|
||||
if (r.pipeline_layout)
|
||||
vkDestroyPipelineLayout(r.device, r.pipeline_layout, nullptr);
|
||||
for (auto pipeline : {r.scene.graphics_pipeline, r.scene.cull_pipeline,
|
||||
@@ -3076,6 +3347,8 @@ bool Renderer::reload_shaders(std::string& error) {
|
||||
r.ui_pipeline = previous_ui;
|
||||
r.shadow_pipeline = previous_shadow;
|
||||
r.sprite_pipeline = previous_sprite;
|
||||
r.light_tile_pipeline = previous_light_tile;
|
||||
r.light_tiles_capable = previous_light_tiles_capable;
|
||||
r.scene.graphics_pipeline = previous_gpu;
|
||||
r.scene.cull_pipeline = previous_cull;
|
||||
r.scene.post_pipeline = previous_post;
|
||||
@@ -3089,6 +3362,8 @@ bool Renderer::reload_shaders(std::string& error) {
|
||||
vkDestroyPipeline(r.device, previous_ui, nullptr);
|
||||
vkDestroyPipeline(r.device, previous_shadow, nullptr);
|
||||
vkDestroyPipeline(r.device, previous_sprite, nullptr);
|
||||
if (previous_light_tile)
|
||||
vkDestroyPipeline(r.device, previous_light_tile, nullptr);
|
||||
for (auto pipeline : {previous_gpu, previous_cull, previous_post, previous_hzb})
|
||||
if (pipeline)
|
||||
vkDestroyPipeline(r.device, pipeline, nullptr);
|
||||
|
||||
@@ -30,15 +30,54 @@ void locations(const Json& fields, std::initializer_list<const char*> types, con
|
||||
++index;
|
||||
}
|
||||
}
|
||||
void validate_tile_layout(const Json& layout) {
|
||||
require(layout.at("stage") == "compute", "light tile shader stage changed");
|
||||
const auto& descriptors = layout.at("descriptors");
|
||||
require(descriptors.is_array() && descriptors.size() == 2,
|
||||
"light tile descriptor count changed");
|
||||
for (std::size_t i = 0; i < 2; ++i)
|
||||
require(descriptors[i].at("set") == 0 && descriptors[i].at("binding") == i &&
|
||||
descriptors[i].at("count") == 1 &&
|
||||
descriptors[i].at("type") == "storage_buffer" &&
|
||||
descriptors[i].at("element_stride") == (i == 0 ? 80 : 4),
|
||||
"light tile descriptor ABI changed");
|
||||
const auto& constants = layout.at("push_constants");
|
||||
require(constants.is_array() && constants.size() == 1 &&
|
||||
constants[0].at("offset") == 0 && constants[0].at("size") == 96,
|
||||
"light tile push size changed");
|
||||
const auto& members = constants[0].at("members");
|
||||
require(members.is_array() && members.size() == 3,
|
||||
"light tile push members changed");
|
||||
const int offsets[] = {0, 64, 80};
|
||||
const char* types[] = {"float32x4x4", "float32x4", "uint32x4"};
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
require(members[i].at("offset") == offsets[i] &&
|
||||
members[i].at("size") == (i == 0 ? 64 : 16) &&
|
||||
members[i].at("type") == types[i],
|
||||
"light tile push field changed");
|
||||
const auto& blocks = layout.at("spirv_push_constants");
|
||||
require(blocks.is_array() && blocks.size() == 1 &&
|
||||
blocks[0].at("members").size() == 3,
|
||||
"light tile SPIR-V push block changed");
|
||||
const auto& actual = blocks[0].at("members");
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
require(actual[i].at("member") == i && actual[i].at("offset") == offsets[i],
|
||||
"light tile SPIR-V push offset changed");
|
||||
require(actual[0].at("matrix_layout") == "row-major" &&
|
||||
actual[0].at("matrix_stride") == 16,
|
||||
"light tile SPIR-V matrix storage convention changed");
|
||||
locations(layout.at("inputs"), {}, "light tile inputs");
|
||||
locations(layout.at("outputs"), {}, "light tile outputs");
|
||||
}
|
||||
void validate_layout(const Json& layout, std::string_view entry) {
|
||||
const bool fragment = entry == "fragmentMain";
|
||||
require(layout.at("stage") == (fragment ? "fragment" : "vertex"), "shader stage changed");
|
||||
const auto& descriptors = layout.at("descriptors");
|
||||
require(descriptors.is_array() && descriptors.size() == 8, "descriptor count changed");
|
||||
require(descriptors.is_array() && descriptors.size() == 9, "descriptor count changed");
|
||||
for (std::size_t i = 0; i < descriptors.size(); ++i) {
|
||||
const auto& binding = descriptors[i];
|
||||
const auto set = i < 4 ? 0 : 1;
|
||||
const auto slot = i % 4;
|
||||
const auto slot = set == 0 ? i : i - 4;
|
||||
require(binding.at("set") == set && binding.at("binding") == slot &&
|
||||
binding.at("count") == 1,
|
||||
"descriptor set, binding or array count changed");
|
||||
@@ -46,8 +85,8 @@ void validate_layout(const Json& layout, std::string_view entry) {
|
||||
: slot == 3 ? "sampled_image_2d" : "storage_buffer";
|
||||
require(binding.at("type") == expected_type,
|
||||
"descriptor type changed");
|
||||
if (set == 1 && slot < 3)
|
||||
require(binding.at("element_stride") == (slot == 2 ? 112 : 80),
|
||||
if (set == 1 && slot != 3)
|
||||
require(binding.at("element_stride") == (slot == 4 ? 4 : slot == 2 ? 112 : 80),
|
||||
"lighting storage record stride changed");
|
||||
require(fragment || !binding.at("used").get<bool>(),
|
||||
"vertex texture bindings are unsupported");
|
||||
@@ -198,22 +237,25 @@ detail::ShaderCode load(const std::filesystem::path& directory, const char* entr
|
||||
require(metadata.at("layout_fingerprint") == fingerprint, "layout fingerprint mismatch");
|
||||
if (gpu)
|
||||
validate_gpu_layout(layout, entry);
|
||||
else if (std::string_view(entry) == "lightTileMain")
|
||||
validate_tile_layout(layout);
|
||||
else
|
||||
validate_layout(layout, entry);
|
||||
detail::ShaderCode result;
|
||||
result.layout_fingerprint = fingerprint;
|
||||
result.words.resize(bytes.size() / 4);
|
||||
std::memcpy(result.words.data(), bytes.data(), bytes.size());
|
||||
validate_spirv(result.words, gpu ? ((std::string_view(entry) == "gpuVertexMain" ||
|
||||
validate_spirv(result.words, std::string_view(entry) == "lightTileMain" ? 5u :
|
||||
gpu ? ((std::string_view(entry) == "gpuVertexMain" ||
|
||||
std::string_view(entry) == "gpuShadowMain") ? 0u : 5u)
|
||||
: (std::string_view(entry) == "fragmentMain" ? 4u : 0u));
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
std::array<detail::ShaderCode, 3>
|
||||
std::array<detail::ShaderCode, 4>
|
||||
detail::load_shader_bundle(const std::filesystem::path& directory) {
|
||||
return {load(directory, "vertexMain"), load(directory, "fragmentMain"),
|
||||
load(directory, "shadowMain")};
|
||||
load(directory, "shadowMain"), load(directory, "lightTileMain")};
|
||||
}
|
||||
std::array<detail::ShaderCode, 5>
|
||||
detail::load_gpu_shader_bundle(const std::filesystem::path& directory) {
|
||||
|
||||
@@ -10,7 +10,8 @@ struct ShaderCode {
|
||||
std::vector<std::uint32_t> words;
|
||||
std::string layout_fingerprint;
|
||||
};
|
||||
std::array<ShaderCode, 3> load_shader_bundle(const std::filesystem::path& directory);
|
||||
// Direct graphics plus the independent 16x16 light-tile compute entry.
|
||||
std::array<ShaderCode, 4> 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
|
||||
|
||||
@@ -148,7 +148,7 @@ int test_main(int argc, char** argv) {
|
||||
const auto first = builds.wait(builds.start_build());
|
||||
check(first.state == "succeeded", "Valid custom schema v2 publishes: " + first.error);
|
||||
const auto directory = path_from_utf8(first.result.at("directory").get<std::string>());
|
||||
for (const auto* entry : {"gpuVertexMain", "gpuShadowMain", "gpuCullMain",
|
||||
for (const auto* entry : {"lightTileMain", "gpuVertexMain", "gpuShadowMain", "gpuCullMain",
|
||||
"gpuHzbMain", "gpuPostCullMain"})
|
||||
for (const auto* extension : {".spv", ".reflection.json"})
|
||||
check(fs::is_regular_file(directory / "shaders" /
|
||||
|
||||
@@ -60,7 +60,7 @@ int tool_main(int argc, char** argv) {
|
||||
for (const auto* target : {"faset_player", "faset_schema_exporter"})
|
||||
fs::copy_file(self, build / (std::string(target) + suffix),
|
||||
fs::copy_options::overwrite_existing);
|
||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain",
|
||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain", "lightTileMain",
|
||||
"gpuVertexMain", "gpuShadowMain", "gpuCullMain",
|
||||
"gpuHzbMain", "gpuPostCullMain"})
|
||||
for (const auto* extension : {".spv", ".reflection.json"})
|
||||
|
||||
@@ -39,7 +39,9 @@ with tempfile.TemporaryDirectory(prefix="faset-player-diagnostics-") as temporar
|
||||
"shadow_caster_budget_drops", "shadow_unavailable_drops",
|
||||
"shadow_caster_draws", "sun_shadow_atlas_bytes",
|
||||
"local_shadow_atlas_bytes", "gpu_main_raster_ms",
|
||||
"gpu_sun_shadow_ms", "gpu_local_shadow_ms"]:
|
||||
"gpu_sun_shadow_ms", "gpu_local_shadow_ms",
|
||||
"gpu_light_tiles_ms", "light_tile_count", "light_tile_counts_valid",
|
||||
"light_tile_candidate_count", "light_tile_overflow_count"]:
|
||||
assert field in lighting, (field, lighting)
|
||||
assert lighting["submitted_local_lights"] == 0 and \
|
||||
lighting["effective_sun_cascades"] == 0 and \
|
||||
|
||||
@@ -21,13 +21,14 @@ Frame capture(Renderer& renderer, const Snapshot& scene) {
|
||||
renderer.render(scene);
|
||||
return {renderer.pixels(), renderer.stats()};
|
||||
}
|
||||
Renderer make_renderer(VisibilityMode mode) {
|
||||
Renderer make_renderer(VisibilityMode mode, LightingMode lighting = LightingMode::Auto) {
|
||||
RendererConfig config;
|
||||
config.width = 320;
|
||||
config.height = 240;
|
||||
config.headless = true;
|
||||
config.validation = true;
|
||||
config.visibility_mode = mode;
|
||||
config.lighting_mode = lighting;
|
||||
config.visibility_diagnostics = true;
|
||||
return Renderer(config);
|
||||
}
|
||||
@@ -286,17 +287,114 @@ void local() {
|
||||
require(brightened > 20,
|
||||
"Point light with dropped atlas faces still illuminates unshadowed");
|
||||
}
|
||||
void tiled() {
|
||||
for (auto visibility : {VisibilityMode::Direct, VisibilityMode::GpuFrustum,
|
||||
VisibilityMode::GpuOcclusion}) {
|
||||
auto forward = make_renderer(visibility, LightingMode::Forward);
|
||||
auto tiles = make_renderer(visibility, LightingMode::Tiled);
|
||||
auto fixture = local_scene(LocalLight::Kind::Point, false);
|
||||
auto no_lights = fixture;
|
||||
no_lights.local_lights.clear();
|
||||
const auto empty_tiled = capture(tiles, no_lights);
|
||||
require(empty_tiled.stats.effective_lighting_path == "forward" &&
|
||||
empty_tiled.stats.light_tile_count == 0,
|
||||
"Forced tiles correctly fall back when no local lights are submitted");
|
||||
fixture.scene_rect = {32, 24, 256, 192};
|
||||
fixture.local_lights.front().casts_shadow = false;
|
||||
auto spot = fixture.local_lights.front();
|
||||
spot.kind = LocalLight::Kind::Spot;
|
||||
spot.stable_id = "second-spot";
|
||||
spot.position = {1.5f, 2, 0};
|
||||
spot.direction = {0, -1, 0};
|
||||
spot.intensity = 7;
|
||||
spot.range = 4;
|
||||
fixture.local_lights.push_back(spot);
|
||||
auto outside = spot;
|
||||
outside.stable_id = "offscreen-light";
|
||||
outside.position = {100, 100, 100};
|
||||
outside.range = 2;
|
||||
fixture.local_lights.push_back(outside);
|
||||
const auto expected = capture(forward, fixture);
|
||||
const auto actual = capture(tiles, fixture);
|
||||
require(expected.stats.effective_lighting_path == "forward" &&
|
||||
actual.stats.effective_lighting_path == "tiled" &&
|
||||
actual.stats.gpu_light_tiles_ms > 0 &&
|
||||
actual.stats.light_tile_count > 0,
|
||||
"Forced 16x16 tile construction reports its actual GPU work");
|
||||
require(actual.stats.validation_errors == 0,
|
||||
"Forward+ tile build and fragment reads pass Vulkan validation");
|
||||
require(actual.stats.light_tile_overflow_count == 0 &&
|
||||
actual.stats.light_tile_candidate_count <
|
||||
actual.stats.light_tile_count * 3,
|
||||
"Depth-free tile lists exclude an offscreen light without overflow");
|
||||
compare_frames(expected, actual);
|
||||
|
||||
auto near_plane = local_scene(LocalLight::Kind::Point, true);
|
||||
near_plane.local_lights.front().position = {0, 5, 7.95f};
|
||||
near_plane.local_lights.front().range = 15;
|
||||
const auto near_forward = capture(forward, near_plane);
|
||||
const auto near_tiled = capture(tiles, near_plane);
|
||||
require(near_tiled.stats.effective_lighting_path == "tiled" &&
|
||||
near_tiled.stats.light_tile_counts_valid,
|
||||
"Near-plane crossing light and its shadow use actual tile lists");
|
||||
compare_frames(near_forward, near_tiled);
|
||||
|
||||
forward.resize(336, 256);
|
||||
tiles.resize(336, 256);
|
||||
fixture.scene_rect = {40, 32, 248, 176};
|
||||
const auto resized_forward = capture(forward, fixture);
|
||||
const auto resized_tiled = capture(tiles, fixture);
|
||||
require(resized_tiled.stats.light_tile_count == 21 * 16,
|
||||
"Forward+ rebuilds its grid after a drawable resize");
|
||||
compare_frames(resized_forward, resized_tiled);
|
||||
|
||||
// Eighty coincident lights cover the same central tiles. A 64-index tile
|
||||
// must evaluate the entire submitted list instead of losing late lights.
|
||||
fixture.local_lights.clear();
|
||||
for (int i = 0; i < 80; ++i) {
|
||||
auto light = point_face_scene({0, 0, 1}, false).local_lights.front();
|
||||
light.stable_id = "overflow-" + std::to_string(i);
|
||||
light.position = {0, 3, 0};
|
||||
light.intensity = .45f;
|
||||
light.range = 8;
|
||||
light.casts_shadow = false;
|
||||
fixture.local_lights.push_back(light);
|
||||
}
|
||||
const auto all_forward = capture(forward, fixture);
|
||||
const auto all_tiled = capture(tiles, fixture);
|
||||
auto automatic = make_renderer(visibility, LightingMode::Auto);
|
||||
const auto dense_auto = capture(automatic, fixture);
|
||||
require(dense_auto.stats.effective_lighting_path == "forward" &&
|
||||
dense_auto.stats.light_tile_count == 0,
|
||||
"Auto avoids tile construction for unmeasured dense overlap");
|
||||
require(all_tiled.stats.submitted_local_lights == 80 &&
|
||||
all_tiled.stats.effective_lighting_path == "tiled" &&
|
||||
all_tiled.stats.light_tile_overflow_count > 0,
|
||||
"Overflow fixture submits all eighty lights through Forward+");
|
||||
compare_frames(all_forward, all_tiled);
|
||||
fixture.local_lights.resize(64);
|
||||
const auto first_sixty_four = capture(forward, fixture);
|
||||
std::size_t extra_light_pixels{};
|
||||
for (std::size_t i = 0; i < all_forward.pixels.size(); i += 4)
|
||||
extra_light_pixels += int(all_forward.pixels[i]) >
|
||||
int(first_sixty_four.pixels[i]) + 2;
|
||||
require(extra_light_pixels > 20,
|
||||
"Overflow fixture visibly depends on lights past index 63");
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
if (argc != 2)
|
||||
throw std::invalid_argument("Expected --sun or --local");
|
||||
throw std::invalid_argument("Expected --sun, --local, or --tiled");
|
||||
if (std::string(argv[1]) == "--sun")
|
||||
sun();
|
||||
else if (std::string(argv[1]) == "--local")
|
||||
local();
|
||||
else if (std::string(argv[1]) == "--tiled")
|
||||
tiled();
|
||||
else
|
||||
throw std::invalid_argument("Expected --sun or --local");
|
||||
throw std::invalid_argument("Expected --sun, --local, or --tiled");
|
||||
std::cout << "Shadow atlas and Direct/GPU lighting parity passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
|
||||
@@ -46,7 +46,7 @@ int main() {
|
||||
try {
|
||||
const auto bundle = temporary / "shaders";
|
||||
fs::create_directories(bundle);
|
||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain",
|
||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain", "lightTileMain",
|
||||
"gpuVertexMain", "gpuShadowMain", "gpuCullMain",
|
||||
"gpuHzbMain", "gpuPostCullMain"})
|
||||
for (const auto* extension : {".spv", ".reflection.json"}) {
|
||||
@@ -88,7 +88,7 @@ int main() {
|
||||
render::Renderer renderer(configuration);
|
||||
const auto baseline_only = temporary / "baseline-only";
|
||||
fs::create_directories(baseline_only);
|
||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain"})
|
||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain", "lightTileMain"})
|
||||
for (const auto* extension : {".spv", ".reflection.json"}) {
|
||||
const auto name = std::string(entry) + extension;
|
||||
fs::copy_file(bundle / name, baseline_only / name);
|
||||
@@ -126,6 +126,40 @@ int main() {
|
||||
opaque_scene.draws.push_back(opaque_cube);
|
||||
gpu_renderer.render(opaque_scene);
|
||||
const auto gpu_expected = gpu_renderer.pixels();
|
||||
auto tiled_configuration = configuration;
|
||||
tiled_configuration.lighting_mode = render::LightingMode::Tiled;
|
||||
render::Renderer tiled_renderer(tiled_configuration);
|
||||
auto lit_scene = opaque_scene;
|
||||
render::LocalLight point;
|
||||
point.stable_id = "reload-point";
|
||||
point.position = {1, 1, 3};
|
||||
point.intensity = 5;
|
||||
point.range = 8;
|
||||
point.casts_shadow = false;
|
||||
lit_scene.local_lights.push_back(point);
|
||||
tiled_renderer.render(lit_scene);
|
||||
require(tiled_renderer.stats().effective_lighting_path == "tiled" &&
|
||||
tiled_renderer.stats().validation_errors == 0,
|
||||
"Tiled lighting is active before shader reload");
|
||||
const auto tiled_expected = tiled_renderer.pixels();
|
||||
const auto original_tile_spirv = read_text(bundle / "lightTileMain.spv");
|
||||
atomic_write(bundle / "lightTileMain.spv", "damaged tile bytecode");
|
||||
std::string tile_error;
|
||||
require(!tiled_renderer.reload_shaders(tile_error) && !tile_error.empty(),
|
||||
"Rejected light tile shader preserves the working pipeline");
|
||||
tiled_renderer.render(lit_scene);
|
||||
require(tiled_renderer.stats().effective_lighting_path == "tiled" &&
|
||||
tiled_renderer.pixels() == tiled_expected &&
|
||||
tiled_renderer.stats().validation_errors == 0,
|
||||
"Rejected light tile shader retains tiled lighting and pixels");
|
||||
atomic_write(bundle / "lightTileMain.spv", original_tile_spirv);
|
||||
require(tiled_renderer.reload_shaders(tile_error),
|
||||
"Compatible light tile shader reloads successfully");
|
||||
tiled_renderer.render(lit_scene);
|
||||
require(tiled_renderer.stats().effective_lighting_path == "tiled" &&
|
||||
tiled_renderer.pixels() == tiled_expected &&
|
||||
tiled_renderer.stats().validation_errors == 0,
|
||||
"Compatible light tile reload preserves tiled pixels");
|
||||
render::Snapshot scene;
|
||||
scene.ui_quads.push_back({0, 0, 32, 64, {1, .8f, .4f, 1}});
|
||||
scene.sprites.push_back({{.5f, 0, .5f}, {1, 2}, {.2f, 1, .4f, 1}});
|
||||
@@ -139,7 +173,7 @@ int main() {
|
||||
require(deep_bundle.native().size() > 300,
|
||||
"Shader file fixture must exceed the legacy Windows path limit");
|
||||
fs::create_directories(native_io_path(deep_bundle));
|
||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain",
|
||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain", "lightTileMain",
|
||||
"gpuVertexMain", "gpuShadowMain", "gpuCullMain",
|
||||
"gpuHzbMain", "gpuPostCullMain"})
|
||||
for (const auto* extension : {".spv", ".reflection.json"}) {
|
||||
|
||||
@@ -56,15 +56,35 @@ class ReflectionTests(unittest.TestCase):
|
||||
(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)],
|
||||
self.assertEqual([lighting[1, i] for i in range(5)],
|
||||
[("storage_buffer", 80), ("storage_buffer", 80),
|
||||
("storage_buffer", 112), ("sampled_image_2d", None)])
|
||||
("storage_buffer", 112), ("sampled_image_2d", None),
|
||||
("storage_buffer", 4)])
|
||||
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_light_tile_compute_reflection(self):
|
||||
compiler = os.environ["FASET_TEST_SLANGC"]
|
||||
with tempfile.TemporaryDirectory(prefix="faset-light-tiles-abi-") as directory:
|
||||
process = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "--compiler", compiler, "--source",
|
||||
str(SCRIPT.parents[1] / "shaders" / "light_tiles.slang"), "--entry",
|
||||
"lightTileMain", "--output", directory],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
self.assertEqual(process.returncode, 0, process.stderr)
|
||||
layout = json.loads((Path(directory) / "lightTileMain.reflection.json").read_text())["layout"]
|
||||
self.assertEqual(layout["stage"], "compute")
|
||||
self.assertEqual(
|
||||
[(item["set"], item["binding"], item["type"], item.get("element_stride"))
|
||||
for item in layout["descriptors"]],
|
||||
[(0, 0, "storage_buffer", 80), (0, 1, "storage_buffer", 4)],
|
||||
)
|
||||
self.assertEqual(layout["push_constants"][0]["size"], 96)
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user