Render stable cascaded sun shadows into a bounded atlas
This commit is contained in:
@@ -47,6 +47,10 @@ target_link_libraries(faset_render PRIVATE Vulkan::Vulkan SDL3::SDL3 faset_core)
|
||||
target_compile_definitions(faset_render PRIVATE FASET_SHADER_DIRECTORY="${FASET_SHADER_DIRECTORY}")
|
||||
add_dependencies(faset_render faset_shaders)
|
||||
if(BUILD_TESTING)
|
||||
add_executable(faset_render_lighting_gpu_tests "${PROJECT_SOURCE_DIR}/tests/render_lighting_gpu_tests.cpp")
|
||||
target_link_libraries(faset_render_lighting_gpu_tests PRIVATE faset_render)
|
||||
add_test(NAME render_lighting_sun COMMAND faset_render_lighting_gpu_tests --sun)
|
||||
set_tests_properties(render_lighting_sun 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)
|
||||
|
||||
@@ -184,6 +184,9 @@ struct FrameStats {
|
||||
std::uint32_t texture_count{};
|
||||
std::uint32_t vertices{}, draw_calls{}, culled_meshes{}, validation_errors{};
|
||||
std::uint32_t submitted_local_lights{}, omitted_local_lights{};
|
||||
std::uint32_t requested_sun_cascades{}, effective_sun_cascades{};
|
||||
std::uint32_t sun_shadow_caster_draws{};
|
||||
std::uint64_t sun_shadow_atlas_bytes{};
|
||||
bool gpu_visibility_active{}, hzb_valid{};
|
||||
// Requested and actual paths for the last frame; actual may be less capable.
|
||||
VisibilityMode requested_visibility_mode{VisibilityMode::Direct};
|
||||
@@ -195,6 +198,7 @@ struct FrameStats {
|
||||
double cpu_ms{}, gpu_ms{}, readback_cpu_ms{};
|
||||
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{};
|
||||
std::string device;
|
||||
};
|
||||
struct HzbDebugImage {
|
||||
|
||||
+44
-10
@@ -27,7 +27,7 @@ struct FrameParameters {
|
||||
// Shared Direct/P2 graphics ABI. The legacy material set remains set 0;
|
||||
// GPU-only instance/visibility records occupy set 2.
|
||||
struct LightingHeader {
|
||||
uint4 counts; // local count, sun enabled, sun shadow enabled, view count
|
||||
uint4 counts; // local count, sun enabled, sun shadow enabled, sun view count
|
||||
float4 sunDirectionIntensity; // xyz world-space ray direction, w intensity
|
||||
float4 sunColor;
|
||||
float4 cameraForwardShadowDistance;
|
||||
@@ -58,6 +58,26 @@ VertexOutput vertexMain(VertexInput v) {
|
||||
}
|
||||
[shader("vertex")]
|
||||
float4 shadowMain(VertexInput v) : SV_Position { return mul(frame.lightViewProjection, float4(v.world,1)); }
|
||||
float sampleSunCascade(uint index, float3 world, float nl) {
|
||||
ShadowViewGpu record = shadowViews[index];
|
||||
if (record.biasFlags.w < 0.5) return 1.0;
|
||||
float4 clip = mul(record.viewProjection, float4(world,1));
|
||||
if (clip.w <= 0.0) return 1.0;
|
||||
float3 projected = clip.xyz / clip.w;
|
||||
float2 localUV = projected.xy * 0.5 + 0.5;
|
||||
if (any(localUV < 0.0) || any(localUV > 1.0) ||
|
||||
projected.z < 0.0 || projected.z > 1.0) return 1.0;
|
||||
float2 atlasUV = localUV * record.tileScaleOffset.xy + record.tileScaleOffset.zw;
|
||||
float bias = max(record.biasFlags.x, record.biasFlags.y * (1.0 - nl));
|
||||
float visible = 0.0;
|
||||
for (int y=-1; y<=1; ++y) for (int x=-1; x<=1; ++x) {
|
||||
float2 tap = clamp(atlasUV + float2(x,y) * record.biasFlags.z,
|
||||
record.guardedClamp.xy, record.guardedClamp.zw);
|
||||
float depth = shadowMap.SampleLevel(shadowSampler, tap, 0);
|
||||
visible += projected.z - bias <= depth ? 1.0 / 9.0 : 0.0;
|
||||
}
|
||||
return visible;
|
||||
}
|
||||
float3 directBRDF(float3 base, float rough, float metal, float3 n, float3 view, float3 l) {
|
||||
const float pi = 3.14159265;
|
||||
float nl = max(dot(n,l),0.0);
|
||||
@@ -94,15 +114,29 @@ float4 fragmentMain(VertexOutput v) : SV_Target {
|
||||
float3 l=normalize(-lighting.sunDirectionIntensity.xyz);
|
||||
float nl=max(dot(n,l),0.0);
|
||||
float visibility=1.0;
|
||||
if (lighting.counts.z != 0 && nl > 0) {
|
||||
float4 lightClip=mul(frame.lightViewProjection,float4(v.world,1));
|
||||
float3 projected=lightClip.xyz/lightClip.w;
|
||||
float2 uv=projected.xy*.5+.5;
|
||||
if(all(uv>=0.0)&&all(uv<=1.0)&&projected.z>=0.0&&projected.z<=1.0) {
|
||||
visibility=0.0;
|
||||
for(int y=-1;y<=1;++y) for(int x=-1;x<=1;++x) {
|
||||
float depth=shadowMap.SampleLevel(shadowSampler,uv+float2(x,y)/1024.0,0);
|
||||
visibility += projected.z-max(0.0008,0.003*(1.0-nl)) <= depth ? 1.0/9.0 : 0.0;
|
||||
if (lighting.counts.z != 0 && lighting.counts.w != 0 && nl > 0) {
|
||||
if (lighting.counts.w == 1) {
|
||||
visibility = sampleSunCascade(0, v.world, nl);
|
||||
} else {
|
||||
float cameraDepth = dot(v.world - frame.eye.xyz,
|
||||
lighting.cameraForwardShadowDistance.xyz);
|
||||
if (cameraDepth >= 0.0 &&
|
||||
cameraDepth <= lighting.cameraForwardShadowDistance.w) {
|
||||
uint cascade = 0;
|
||||
while (cascade + 1 < lighting.counts.w &&
|
||||
cameraDepth > lighting.cascadeSplits[cascade]) ++cascade;
|
||||
visibility = sampleSunCascade(cascade, v.world, nl);
|
||||
if (cascade + 1 < lighting.counts.w) {
|
||||
float previousSplit = cascade == 0 ? 0.0 :
|
||||
lighting.cascadeSplits[cascade-1];
|
||||
float blendWidth = max(0.2,
|
||||
0.1 * (lighting.cascadeSplits[cascade] - previousSplit));
|
||||
float blend = saturate((cameraDepth -
|
||||
(lighting.cascadeSplits[cascade] - blendWidth)) / blendWidth);
|
||||
if (blend > 0.0)
|
||||
visibility = lerp(visibility,
|
||||
sampleSunCascade(cascade+1, v.world, nl), blend);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+169
-57
@@ -201,7 +201,7 @@ struct SceneResources {
|
||||
std::array<float, 4> previous_viewport{};
|
||||
std::string previous_view_id;
|
||||
};
|
||||
constexpr std::uint32_t shadow_size = 1024;
|
||||
constexpr std::uint32_t timestamp_capacity = 24;
|
||||
} // namespace
|
||||
struct Renderer::Impl {
|
||||
RendererConfig config;
|
||||
@@ -232,6 +232,7 @@ struct Renderer::Impl {
|
||||
std::vector<VkImage> swap_images;
|
||||
std::vector<VkImageLayout> swap_layouts;
|
||||
Image color, depth, shadow;
|
||||
std::uint32_t sun_shadow_size{};
|
||||
Buffer vertices, readback;
|
||||
Buffer lighting_header, lighting_locals, lighting_views;
|
||||
SceneResources scene;
|
||||
@@ -769,14 +770,27 @@ struct Renderer::Impl {
|
||||
VkQueryPoolCreateInfo query{};
|
||||
query.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
|
||||
query.queryType = VK_QUERY_TYPE_TIMESTAMP;
|
||||
query.queryCount = 12;
|
||||
query.queryCount = timestamp_capacity;
|
||||
check(vkCreateQueryPool(device, &query, nullptr, ×tamp_pool),
|
||||
"Create GPU timestamp queries");
|
||||
}
|
||||
shadow =
|
||||
make_image(shadow_size, shadow_size, VK_FORMAT_D32_SFLOAT,
|
||||
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
|
||||
VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
const auto shadow_usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT |
|
||||
VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
for (const auto size : {2048u, 1024u}) {
|
||||
if (size > max_image_dimension)
|
||||
continue;
|
||||
try {
|
||||
shadow = make_image(size, size, VK_FORMAT_D32_SFLOAT, shadow_usage,
|
||||
VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
sun_shadow_size = size;
|
||||
break;
|
||||
} catch (const std::exception&) {
|
||||
// Optional atlas allocation may fail; try the bounded half-size profile.
|
||||
}
|
||||
}
|
||||
if (!shadow.handle)
|
||||
shadow = make_image(1, 1, VK_FORMAT_D32_SFLOAT, shadow_usage,
|
||||
VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
make_targets();
|
||||
make_descriptors();
|
||||
make_pipelines();
|
||||
@@ -1723,6 +1737,10 @@ struct Renderer::Impl {
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
statistics.draw_calls = statistics.culled_meshes = statistics.gpu_label_count = 0;
|
||||
statistics.submitted_local_lights = statistics.omitted_local_lights = 0;
|
||||
statistics.requested_sun_cascades = statistics.effective_sun_cascades =
|
||||
statistics.sun_shadow_caster_draws = 0;
|
||||
statistics.sun_shadow_atlas_bytes = sun_shadow_size ? shadow.allocation_size : 0;
|
||||
statistics.gpu_sun_shadow_ms = 0;
|
||||
statistics.gpu_bins = statistics.gpu_visible_instances =
|
||||
statistics.gpu_frustum_rejected = statistics.gpu_occlusion_deferred =
|
||||
statistics.gpu_post_visible = 0;
|
||||
@@ -1965,12 +1983,28 @@ struct Renderer::Impl {
|
||||
gpu_frame.textures.push_back(bin.texture);
|
||||
}
|
||||
gpu_frame.candidate_count = static_cast<std::uint32_t>(gpu_frame.candidates.size());
|
||||
ShadowBudget shadow_budget;
|
||||
shadow_budget.sun_atlas_size = sun_shadow_size;
|
||||
shadow_budget.sun_atlas_available = sun_shadow_size != 0;
|
||||
const auto shadow_plan = build_shadow_plan(snapshot, shadow_casters, shadow_budget);
|
||||
const bool sun_raster = sun_shadow_size &&
|
||||
std::any_of(shadow_plan.sun_views.begin(), shadow_plan.sun_views.end(),
|
||||
[](const ShadowView& view) {
|
||||
return view.valid && !view.caster_indices.empty();
|
||||
});
|
||||
statistics.requested_sun_cascades = shadow_plan.requested_sun_cascades;
|
||||
statistics.effective_sun_cascades = sun_raster
|
||||
? shadow_plan.effective_sun_cascades : 0;
|
||||
if (sun_raster)
|
||||
for (const auto& view : shadow_plan.sun_views)
|
||||
if (view.valid)
|
||||
statistics.sun_shadow_caster_draws +=
|
||||
static_cast<std::uint32_t>(view.caster_indices.size());
|
||||
std::vector<GpuVertex> data;
|
||||
std::vector<Batch> scene_batches, transparent_batches, shadow_batches,
|
||||
sprite_batches, ui_batches;
|
||||
std::vector<Batch> scene_batches, transparent_batches, sprite_batches, ui_batches;
|
||||
for (const auto& selected : selected_draws) {
|
||||
const auto& item = *selected.source;
|
||||
if (selected.gpu && !item.cast_shadow)
|
||||
if (selected.gpu)
|
||||
continue;
|
||||
auto first = data.size();
|
||||
const auto& mesh = *selected.mesh;
|
||||
@@ -1993,16 +2027,12 @@ struct Renderer::Impl {
|
||||
continue;
|
||||
Batch batch{static_cast<std::uint32_t>(first), count,
|
||||
item.texture ? item.texture.get() : white.get()};
|
||||
if (item.cast_shadow)
|
||||
shadow_batches.push_back(batch);
|
||||
if (!selected.gpu) {
|
||||
if (outside(data, first))
|
||||
++statistics.culled_meshes;
|
||||
else if (gpu_active && !selected.opaque)
|
||||
transparent_batches.push_back(batch);
|
||||
else
|
||||
scene_batches.push_back(batch);
|
||||
}
|
||||
if (outside(data, first))
|
||||
++statistics.culled_meshes;
|
||||
else if (gpu_active && !selected.opaque)
|
||||
transparent_batches.push_back(batch);
|
||||
else
|
||||
scene_batches.push_back(batch);
|
||||
}
|
||||
struct OrderedSprite {
|
||||
const Sprite* sprite;
|
||||
@@ -2079,6 +2109,41 @@ struct Renderer::Impl {
|
||||
triangles.texture ? triangles.texture.get() : white.get(),
|
||||
triangles.clip_rect});
|
||||
}
|
||||
std::vector<Batch> shadow_batch_by_source(snapshot.draws.size());
|
||||
if (sun_raster) {
|
||||
std::vector<std::uint8_t> required(snapshot.draws.size());
|
||||
for (const auto& view : shadow_plan.sun_views)
|
||||
if (view.valid)
|
||||
for (const auto source : view.caster_indices)
|
||||
required.at(source) = 1;
|
||||
for (std::size_t source = 0; source < required.size(); ++source) {
|
||||
if (!required[source])
|
||||
continue;
|
||||
const auto& item = snapshot.draws[source];
|
||||
if (!item.mesh)
|
||||
continue;
|
||||
const auto first = data.size();
|
||||
const auto& mesh = *item.mesh; // Source LOD 0, independent of camera/P2 LOD.
|
||||
auto emit = [&](std::uint32_t index) {
|
||||
if (index >= mesh.vertices.size())
|
||||
throw std::out_of_range("Shadow mesh index outside vertex range");
|
||||
data.push_back(gpu_vertex(mesh.vertices[index], item,
|
||||
snapshot.view_projection));
|
||||
};
|
||||
if (mesh.indices.empty())
|
||||
for (std::uint32_t i = 0; i < mesh.vertices.size(); ++i)
|
||||
emit(i);
|
||||
else
|
||||
for (const auto index : mesh.indices)
|
||||
emit(index);
|
||||
const auto count = data.size() - first;
|
||||
if (count % 3 || first > UINT32_MAX || count > UINT32_MAX)
|
||||
throw std::invalid_argument("Shadow mesh must fit complete triangles");
|
||||
shadow_batch_by_source[source] =
|
||||
{static_cast<std::uint32_t>(first), static_cast<std::uint32_t>(count),
|
||||
white.get()};
|
||||
}
|
||||
}
|
||||
statistics.vertices = static_cast<std::uint32_t>(data.size() + gpu_frame.vertices.size());
|
||||
auto byte_count = std::max<std::size_t>(sizeof(GpuVertex), data.size() * sizeof(GpuVertex));
|
||||
if (vertices.size < byte_count) {
|
||||
@@ -2140,7 +2205,7 @@ struct Renderer::Impl {
|
||||
v /= length;
|
||||
LightingHeaderGpu lighting{};
|
||||
lighting.counts[1] = sun ? 1u : 0u;
|
||||
lighting.counts[2] = sun && sun->casts_shadow ? 1u : 0u;
|
||||
lighting.counts[2] = sun_raster ? 1u : 0u;
|
||||
lighting.sun_direction_intensity = {direction[0], direction[1], direction[2],
|
||||
sun ? sun->intensity : 0};
|
||||
lighting.sun_color = sun ? sun->color : Color{0, 0, 0, 1};
|
||||
@@ -2153,7 +2218,22 @@ struct Renderer::Impl {
|
||||
const auto& view = snapshot.camera_frustum->view;
|
||||
lighting.camera_forward_shadow_distance = {-view[2], -view[6], -view[10], 80};
|
||||
}
|
||||
const auto shadow_plan = build_shadow_plan(snapshot, shadow_casters);
|
||||
lighting.counts[3] = static_cast<std::uint32_t>(shadow_plan.sun_views.size());
|
||||
std::vector<ShadowViewGpu> gpu_shadow_views;
|
||||
gpu_shadow_views.reserve(std::max<std::size_t>(1, shadow_plan.sun_views.size()));
|
||||
for (std::size_t i = 0; i < shadow_plan.sun_views.size(); ++i) {
|
||||
const auto& view = shadow_plan.sun_views[i];
|
||||
ShadowViewGpu gpu{};
|
||||
gpu.view_projection = view.view_projection;
|
||||
gpu.tile_scale_offset = view.atlas_scale_offset;
|
||||
gpu.guarded_clamp = view.guarded_clamp;
|
||||
gpu.bias_flags = {.0008f, .003f,
|
||||
sun_shadow_size ? 1.f / float(sun_shadow_size) : 0.f,
|
||||
sun_raster && view.valid ? 1.f : 0.f};
|
||||
gpu_shadow_views.push_back(gpu);
|
||||
if (i < lighting.cascade_splits.size())
|
||||
lighting.cascade_splits[i] = view.split_far;
|
||||
}
|
||||
statistics.omitted_local_lights = shadow_plan.omitted_local_lights;
|
||||
std::vector<LocalLightGpu> gpu_lights;
|
||||
gpu_lights.reserve(shadow_plan.submitted_local_indices.size());
|
||||
@@ -2186,15 +2266,18 @@ struct Renderer::Impl {
|
||||
statistics.submitted_local_lights = lighting.counts[0];
|
||||
if (gpu_lights.empty())
|
||||
gpu_lights.push_back({}); // Descriptors always point at a full initialized record.
|
||||
const ShadowViewGpu empty_shadow_view{};
|
||||
if (gpu_shadow_views.empty())
|
||||
gpu_shadow_views.push_back({}); // Always bind an initialized record.
|
||||
upload_scene_buffer(lighting_header, &lighting, sizeof(lighting), 0);
|
||||
upload_scene_vector(lighting_locals, gpu_lights);
|
||||
upload_scene_buffer(lighting_views, &empty_shadow_view, sizeof(empty_shadow_view), 0);
|
||||
upload_scene_vector(lighting_views, gpu_shadow_views);
|
||||
update_lighting_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{multiply(orthographic(-20, 20, -20, 20, .1f, 80),
|
||||
look_at(light_eye, {0, 0, 0}, light_up)),
|
||||
Push push{sun_raster && !shadow_plan.sun_views.empty()
|
||||
? shadow_plan.sun_views.front().view_projection
|
||||
: multiply(orthographic(-20, 20, -20, 20, .1f, 80),
|
||||
look_at(light_eye, {0, 0, 0}, light_up)),
|
||||
{direction[0], direction[1], direction[2], 0},
|
||||
{snapshot.eye[0], snapshot.eye[1], snapshot.eye[2], 1}};
|
||||
std::optional<std::uint32_t> swap_index;
|
||||
@@ -2218,7 +2301,7 @@ struct Renderer::Impl {
|
||||
std::uint32_t timestamp_cursor = 0;
|
||||
std::vector<std::string> timestamp_labels;
|
||||
if (timestamp_pool) {
|
||||
vkCmdResetQueryPool(command, timestamp_pool, 0, 12);
|
||||
vkCmdResetQueryPool(command, timestamp_pool, 0, timestamp_capacity);
|
||||
vkCmdWriteTimestamp2(command, VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT,
|
||||
timestamp_pool, timestamp_cursor++);
|
||||
}
|
||||
@@ -2349,7 +2432,7 @@ struct Renderer::Impl {
|
||||
}
|
||||
} end{*this};
|
||||
callback();
|
||||
if (timestamp_pool && timestamp_cursor < 12) {
|
||||
if (timestamp_pool && timestamp_cursor < timestamp_capacity) {
|
||||
vkCmdWriteTimestamp2(command,
|
||||
VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT,
|
||||
timestamp_pool, timestamp_cursor++);
|
||||
@@ -2357,35 +2440,63 @@ struct Renderer::Impl {
|
||||
}
|
||||
});
|
||||
};
|
||||
add_pass("ShadowMap", {}, {"shadow"}, [&] {
|
||||
transition(command, shadow, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL,
|
||||
VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
VkRenderingAttachmentInfo attachment{};
|
||||
attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
|
||||
attachment.imageView = shadow.view;
|
||||
attachment.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL;
|
||||
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.clearValue.depthStencil = {1, 0};
|
||||
VkRenderingInfo rendering{};
|
||||
rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
|
||||
rendering.renderArea = {{0, 0}, {shadow_size, shadow_size}};
|
||||
rendering.layerCount = 1;
|
||||
rendering.pDepthAttachment = &attachment;
|
||||
vkCmdBeginRendering(command, &rendering);
|
||||
set_viewport(shadow_size, shadow_size);
|
||||
vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, shadow_pipeline);
|
||||
vkCmdPushConstants(command, pipeline_layout,
|
||||
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0,
|
||||
sizeof(push), &push);
|
||||
for (auto batch : shadow_batches) {
|
||||
vkCmdDraw(command, batch.count, 1, batch.first, 0);
|
||||
++statistics.draw_calls;
|
||||
}
|
||||
vkCmdEndRendering(command);
|
||||
transition(command, shadow, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL,
|
||||
VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
});
|
||||
if (sun_raster)
|
||||
add_pass("SunShadowAtlas", {}, {"shadow"}, [&] {
|
||||
transition(command, shadow, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL,
|
||||
VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
VkRenderingAttachmentInfo attachment{};
|
||||
attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
|
||||
attachment.imageView = shadow.view;
|
||||
attachment.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL;
|
||||
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachment.clearValue.depthStencil = {1, 0};
|
||||
VkRenderingInfo rendering{};
|
||||
rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
|
||||
rendering.renderArea = {{0, 0}, {sun_shadow_size, sun_shadow_size}};
|
||||
rendering.layerCount = 1;
|
||||
rendering.pDepthAttachment = &attachment;
|
||||
vkCmdBeginRendering(command, &rendering);
|
||||
vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
shadow_pipeline);
|
||||
for (const auto& view : shadow_plan.sun_views) {
|
||||
if (!view.valid || view.caster_indices.empty())
|
||||
continue;
|
||||
const auto guard = (view.tile_size - view.usable_size) / 2;
|
||||
const auto x = view.tile_origin_x + guard;
|
||||
const auto y = view.tile_origin_y + guard;
|
||||
const VkViewport viewport{float(x), float(y), float(view.usable_size),
|
||||
float(view.usable_size), 0, 1};
|
||||
const VkRect2D scissor{{static_cast<std::int32_t>(x),
|
||||
static_cast<std::int32_t>(y)},
|
||||
{view.usable_size, view.usable_size}};
|
||||
vkCmdSetViewport(command, 0, 1, &viewport);
|
||||
vkCmdSetScissor(command, 0, 1, &scissor);
|
||||
auto view_push = push;
|
||||
view_push.light_view_projection = view.view_projection;
|
||||
vkCmdPushConstants(command, pipeline_layout,
|
||||
VK_SHADER_STAGE_VERTEX_BIT |
|
||||
VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
0, sizeof(view_push), &view_push);
|
||||
for (const auto source : view.caster_indices) {
|
||||
const auto& batch = shadow_batch_by_source.at(source);
|
||||
if (!batch.count)
|
||||
continue;
|
||||
vkCmdDraw(command, batch.count, 1, batch.first, 0);
|
||||
++statistics.draw_calls;
|
||||
}
|
||||
}
|
||||
vkCmdEndRendering(command);
|
||||
transition(command, shadow, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL,
|
||||
VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
});
|
||||
else
|
||||
add_pass("ShadowFallback", {}, {"shadow"}, [&] {
|
||||
// A bound descriptor still needs a matching image layout, even when
|
||||
// every graphics shader branch treats its shadow as unshadowed.
|
||||
transition(command, shadow, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL,
|
||||
VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
});
|
||||
if (gpu_active)
|
||||
add_pass("MainCull", {"shadow"},
|
||||
{"main_indirect", "main_visible", "deferred_ids"}, [&] {
|
||||
@@ -2692,7 +2803,7 @@ struct Renderer::Impl {
|
||||
graph.execute();
|
||||
submit(swap_index.has_value());
|
||||
if (timestamp_pool) {
|
||||
std::array<std::uint64_t, 12> stamps{};
|
||||
std::array<std::uint64_t, timestamp_capacity> stamps{};
|
||||
check(vkGetQueryPoolResults(device, timestamp_pool, 0, timestamp_cursor,
|
||||
timestamp_cursor * sizeof(std::uint64_t), stamps.data(),
|
||||
sizeof(std::uint64_t),
|
||||
@@ -2712,6 +2823,7 @@ struct Renderer::Impl {
|
||||
const auto elapsed = milliseconds(stamps[i], stamps[i + 1]);
|
||||
const auto& label = timestamp_labels[i];
|
||||
if (label == "MainCull") statistics.gpu_main_cull_ms = elapsed;
|
||||
else if (label == "SunShadowAtlas") statistics.gpu_sun_shadow_ms = elapsed;
|
||||
else if (label == "MainRaster" || label == "ForwardAndUI")
|
||||
statistics.gpu_main_raster_ms = elapsed;
|
||||
else if (label == "BuildCurrentHZB") statistics.gpu_hzb_ms = elapsed;
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#include <faset/render/renderer.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace faset::render;
|
||||
namespace {
|
||||
void require(bool condition, const std::string& message) {
|
||||
if (!condition)
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
struct Frame {
|
||||
std::vector<std::uint8_t> pixels;
|
||||
FrameStats stats;
|
||||
};
|
||||
Frame capture(Renderer& renderer, const Snapshot& scene) {
|
||||
renderer.render(scene);
|
||||
return {renderer.pixels(), renderer.stats()};
|
||||
}
|
||||
Renderer make_renderer(VisibilityMode mode) {
|
||||
RendererConfig config;
|
||||
config.width = 320;
|
||||
config.height = 240;
|
||||
config.headless = true;
|
||||
config.validation = true;
|
||||
config.visibility_mode = mode;
|
||||
config.visibility_diagnostics = true;
|
||||
return Renderer(config);
|
||||
}
|
||||
void compare_frames(const Frame& direct, const Frame& gpu) {
|
||||
require(direct.pixels.size() == gpu.pixels.size(), "Lighting image dimensions match");
|
||||
std::uint64_t error{};
|
||||
std::size_t bad{};
|
||||
for (std::size_t i = 0; i < direct.pixels.size(); i += 4) {
|
||||
int worst{};
|
||||
for (int channel = 0; channel < 3; ++channel) {
|
||||
const int difference = std::abs(int(direct.pixels[i + channel]) -
|
||||
int(gpu.pixels[i + channel]));
|
||||
error += difference;
|
||||
worst = std::max(worst, difference);
|
||||
}
|
||||
bad += worst > 16;
|
||||
}
|
||||
const auto count = direct.pixels.size() / 4;
|
||||
require(bad <= std::max<std::size_t>(24, count / 200) &&
|
||||
double(error) / double(count * 3) <= 2.0,
|
||||
"Direct and GPU sun lighting images agree (bad=" + std::to_string(bad) +
|
||||
", mean=" + std::to_string(double(error) / double(count * 3)) + ")");
|
||||
}
|
||||
Snapshot scene(bool caster) {
|
||||
Snapshot result;
|
||||
result.view_id = "p3-offscreen-sun";
|
||||
result.eye = {0, 5, 8};
|
||||
const auto view = look_at(result.eye, {0, -1, 0});
|
||||
const auto projection = orthographic(-2.5f, 2.5f, -2, 2, .1f, 50);
|
||||
result.projection = projection;
|
||||
result.view_projection = multiply(projection, view);
|
||||
result.camera_frustum = CameraFrustum{view, projection, .1f, 50.f, false};
|
||||
DrawItem receiver;
|
||||
receiver.mesh = cube_mesh();
|
||||
receiver.model = transform({0, -1, 0}, {}, {8, .1f, 8});
|
||||
receiver.color = {.8f, .8f, .8f, 1};
|
||||
receiver.instance_key = "receiver";
|
||||
result.draws.push_back(receiver);
|
||||
if (caster) {
|
||||
DrawItem shadow_caster;
|
||||
shadow_caster.mesh = cube_mesh();
|
||||
shadow_caster.model = transform({3, 1, 0}, {}, {.8f, .8f, .8f});
|
||||
shadow_caster.color = {.2f, .2f, .8f, 1};
|
||||
shadow_caster.instance_key = "offscreen-caster";
|
||||
result.draws.push_back(shadow_caster);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
void sun() {
|
||||
auto direct = make_renderer(VisibilityMode::Direct);
|
||||
auto gpu = make_renderer(VisibilityMode::GpuFrustum);
|
||||
auto occlusion = make_renderer(VisibilityMode::GpuOcclusion);
|
||||
auto with_caster = scene(true);
|
||||
const auto direct_frame = capture(direct, with_caster);
|
||||
const auto gpu_frame = capture(gpu, with_caster);
|
||||
const auto occlusion_frame = capture(occlusion, with_caster);
|
||||
require(direct_frame.stats.effective_sun_cascades == 4 &&
|
||||
gpu_frame.stats.effective_sun_cascades == 4 &&
|
||||
occlusion_frame.stats.effective_sun_cascades == 4,
|
||||
"Explicit 3D camera renders four sun cascades on every graphics path");
|
||||
require(direct_frame.stats.requested_sun_cascades == 4 &&
|
||||
direct_frame.stats.sun_shadow_caster_draws > 0 &&
|
||||
direct_frame.stats.sun_shadow_caster_draws <= 4096 &&
|
||||
direct_frame.stats.sun_shadow_atlas_bytes > 0 &&
|
||||
direct_frame.stats.gpu_sun_shadow_ms > 0,
|
||||
"Sun cascade stats describe bounded actual raster work and GPU time");
|
||||
require(gpu_frame.stats.gpu_frustum_rejected > 0,
|
||||
"Offscreen caster fixture is outside GPU camera frustum");
|
||||
require(direct_frame.stats.validation_errors == 0 &&
|
||||
gpu_frame.stats.validation_errors == 0 &&
|
||||
occlusion_frame.stats.validation_errors == 0,
|
||||
"Sun atlas rendering reports no Vulkan validation errors");
|
||||
compare_frames(direct_frame, gpu_frame);
|
||||
compare_frames(direct_frame, occlusion_frame);
|
||||
auto without = scene(false);
|
||||
const auto no_caster = capture(direct, without);
|
||||
std::size_t darkened{};
|
||||
for (std::size_t i = 0; i < direct_frame.pixels.size(); i += 4)
|
||||
darkened += int(no_caster.pixels[i]) > int(direct_frame.pixels[i]) + 12;
|
||||
require(darkened > 20,
|
||||
"Offscreen source-LOD0 caster darkens visible receiver (count=" +
|
||||
std::to_string(darkened) + ")");
|
||||
auto coarser = with_caster;
|
||||
auto degenerate_lod = std::make_shared<Mesh>(*cube_mesh());
|
||||
for (auto& vertex : degenerate_lod->vertices)
|
||||
vertex.position = {0, 0, 0};
|
||||
coarser.draws.back().lod_meshes.push_back(degenerate_lod);
|
||||
const auto source_lod_shadow = capture(gpu, coarser);
|
||||
std::size_t lod_darkened{};
|
||||
for (std::size_t i = 0; i < source_lod_shadow.pixels.size(); i += 4)
|
||||
lod_darkened += int(no_caster.pixels[i]) >
|
||||
int(source_lod_shadow.pixels[i]) + 12;
|
||||
require(source_lod_shadow.stats.lod_counts[1] > 0 && lod_darkened > 20,
|
||||
"Shadow raster uses source LOD0 even when camera chooses a coarse LOD");
|
||||
auto no_shadow = with_caster;
|
||||
no_shadow.authored_lights_present = true;
|
||||
no_shadow.sun = SunLight{"sun", no_shadow.light_direction, {1, 1, 1, 1}, 1, false};
|
||||
const auto disabled = capture(direct, no_shadow);
|
||||
require(disabled.stats.effective_sun_cascades == 0,
|
||||
"Disabled sun shadow does no shadow raster work");
|
||||
require(disabled.stats.sun_shadow_caster_draws == 0 &&
|
||||
disabled.stats.gpu_sun_shadow_ms == 0,
|
||||
"Disabled sun does not draw a hidden legacy shadow pass");
|
||||
auto legacy = with_caster;
|
||||
legacy.camera_frustum.reset();
|
||||
const auto fallback = capture(direct, legacy);
|
||||
require(fallback.stats.effective_sun_cascades == 1,
|
||||
"Low-level snapshot without explicit camera retains one reported shadow view");
|
||||
Snapshot sprite_only;
|
||||
sprite_only.sprites.push_back({{0, 0, 0}, {1, 1}});
|
||||
const auto two_d = capture(direct, sprite_only);
|
||||
require(two_d.stats.effective_sun_cascades == 0,
|
||||
"Sprite-only scene skips the sun atlas raster");
|
||||
require(two_d.stats.sun_shadow_caster_draws == 0 &&
|
||||
two_d.stats.gpu_sun_shadow_ms == 0,
|
||||
"Sprite-only rendering spends no sun shadow GPU work");
|
||||
}
|
||||
} // namespace
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
if (argc != 2 || std::string(argv[1]) != "--sun")
|
||||
throw std::invalid_argument("Expected --sun");
|
||||
sun();
|
||||
std::cout << "Sun cascade atlas and Direct/GPU lighting parity passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user