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

This commit is contained in:
Emil
2026-09-24 03:37:04 +03:00
parent 2dfff62f8c
commit a0a4e29d48
17 changed files with 673 additions and 40 deletions
+9 -1
View File
@@ -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",
+10
View File
@@ -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)
+40 -7
View File
@@ -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)
+9
View File
@@ -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
View File
@@ -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);
+75
View File
@@ -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;
}
+2
View File
@@ -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",
+8
View File
@@ -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
View File
@@ -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);
+49 -7
View File
@@ -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) {
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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" /
+1 -1
View File
@@ -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"})
+3 -1
View File
@@ -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 \
+101 -3
View File
@@ -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';
+37 -3
View File
@@ -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"}) {
+22 -2
View File
@@ -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