Add bounded point and spot shadows with fallback diagnostics

This commit is contained in:
Emil
2026-09-24 02:51:11 +03:00
parent 252f7e2e91
commit a5fb2167d6
9 changed files with 384 additions and 23 deletions
+33 -2
View File
@@ -48,6 +48,7 @@ struct ProfileSample {
bool physicsDebug{};
bool gpuVisibilityActive{};
faset::render::VisibilityMode effectiveVisibilityMode{faset::render::VisibilityMode::Direct};
faset::render::FrameStats lighting;
};
Json distribution(std::vector<double> values) {
if (values.empty())
@@ -96,7 +97,35 @@ Json profileFrames(const std::vector<ProfileSample>& samples) {
{"physics_debug", sample.physicsDebug},
{"gpu_visibility_active", sample.gpuVisibilityActive},
{"effective_visibility_mode",
visibility_mode_name(sample.effectiveVisibilityMode)}});
visibility_mode_name(sample.effectiveVisibilityMode)},
{"effective_lighting_path", sample.lighting.effective_lighting_path},
{"submitted_local_lights", sample.lighting.submitted_local_lights},
{"omitted_local_lights", sample.lighting.omitted_local_lights},
{"requested_sun_cascades", sample.lighting.requested_sun_cascades},
{"effective_sun_cascades", sample.lighting.effective_sun_cascades},
{"requested_local_shadow_faces",
sample.lighting.requested_local_shadow_faces},
{"local_shadow_faces", sample.lighting.local_shadow_faces},
{"local_shadow_tiles", sample.lighting.local_shadow_tiles},
{"dropped_shadow_faces", sample.lighting.dropped_shadow_faces},
{"dropped_point_shadow_faces",
sample.lighting.dropped_point_shadow_faces},
{"shadow_atlas_full_drops", sample.lighting.shadow_atlas_full_drops},
{"shadow_caster_budget_drops",
sample.lighting.shadow_caster_budget_drops},
{"shadow_unavailable_drops",
sample.lighting.shadow_unavailable_drops},
{"shadow_caster_draws", sample.lighting.shadow_caster_draws},
{"sun_shadow_atlas_bytes",
sample.lighting.sun_shadow_atlas_bytes},
{"local_shadow_atlas_bytes",
sample.lighting.local_shadow_atlas_bytes},
{"gpu_main_raster_ms",
gpuMeasured ? Json(sample.lighting.gpu_main_raster_ms) : Json(nullptr)},
{"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)}});
}
return {{"samples", std::move(frames)},
{"summary_ms",
@@ -587,7 +616,8 @@ int player_main(int argc, char** argv) {
milliseconds(renderStarted, frameFinished), measured.cpu_ms, measured.gpu_ms,
measured.readback_cpu_ms, runtimeStats, measured.draw_calls, measured.vertices,
measured.gpu_allocated_bytes, measured.texture_count, debugPhysics,
measured.gpu_visibility_active, measured.effective_visibility_mode});
measured.gpu_visibility_active, measured.effective_visibility_mode,
measured});
}
++frames;
}
@@ -619,6 +649,7 @@ int player_main(int argc, char** argv) {
{"visibility_mode", visibilityName},
{"effective_visibility_mode",
visibility_mode_name(stats.effective_visibility_mode)},
{"effective_lighting_path", stats.effective_lighting_path},
{"simulation_mode", "synthetic_fixed_timestep"},
{"fixed_delta_seconds", config.fixedDelta},
{"percentile_method", "nearest_rank_all_completed_frames_no_warmup_exclusion"},
+2
View File
@@ -51,6 +51,8 @@ if(BUILD_TESTING)
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_test(NAME render_lighting_local COMMAND faset_render_lighting_gpu_tests --local)
set_tests_properties(render_lighting_local 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)
+7 -1
View File
@@ -187,6 +187,11 @@ struct FrameStats {
std::uint32_t requested_sun_cascades{}, effective_sun_cascades{};
std::uint32_t sun_shadow_caster_draws{};
std::uint64_t sun_shadow_atlas_bytes{};
std::uint32_t requested_local_shadow_faces{}, local_shadow_faces{}, local_shadow_tiles{};
std::uint32_t dropped_shadow_faces{}, dropped_point_shadow_faces{};
std::uint32_t shadow_atlas_full_drops{}, shadow_caster_budget_drops{};
std::uint32_t shadow_unavailable_drops{}, shadow_caster_draws{};
std::uint64_t local_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};
@@ -198,7 +203,8 @@ 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{};
double gpu_sun_shadow_ms{}, gpu_local_shadow_ms{};
std::string effective_lighting_path{"forward"};
std::string device;
};
struct HzbDebugImage {
+37 -1
View File
@@ -78,6 +78,34 @@ float sampleSunCascade(uint index, float3 world, float nl) {
}
return visible;
}
float sampleLocalFace(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 = localShadowAtlas.SampleLevel(shadowSampler, tap, 0);
visible += projected.z - bias <= depth ? 1.0 / 9.0 : 0.0;
}
return visible;
}
uint pointShadowFace(float3 lightToFragment) {
float3 magnitude = abs(lightToFragment);
if (magnitude.x >= magnitude.y && magnitude.x >= magnitude.z)
return lightToFragment.x >= 0.0 ? 0 : 1;
if (magnitude.y >= magnitude.z)
return lightToFragment.y >= 0.0 ? 2 : 3;
return lightToFragment.z >= 0.0 ? 4 : 5;
}
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);
@@ -160,8 +188,16 @@ float4 fragmentMain(VertexOutput v) : SV_Target {
float cone=saturate((cosAngle-light.directionCosOuter.w)/denominator);
attenuation *= cone*cone*(3.0-2.0*cone);
}
float visibility = 1.0;
float nl = max(dot(n,l), 0.0);
if (light.coneTypeShadowView.w > 0.5 && nl > 0.0 && attenuation > 0.0) {
uint face = light.coneTypeShadowView.w > 1.5 ?
pointShadowFace(v.world - light.positionRange.xyz) : 0;
uint viewIndex = uint(light.coneTypeShadowView.z + 0.5) + face;
visibility = sampleLocalFace(viewIndex, v.world, nl);
}
linear += directBRDF(base.rgb, rough, metal, n, view, l) *
light.colorIntensity.rgb * (light.colorIntensity.w * attenuation);
light.colorIntensity.rgb * (light.colorIntensity.w * attenuation * visibility);
}
linear=linear/(1.0+linear);
return float4(pow(max(linear,0),float3(1.0/2.2)),base.a);
+21
View File
@@ -381,6 +381,27 @@ void DebugOverlay::append(render::Snapshot& output, render::Renderer& renderer,
ImGui::Text("Prepared LOD: %u / %u / %u / %u+", stats.lod_counts[0],
stats.lod_counts[1], stats.lod_counts[2], stats.lod_counts[3]);
ImGui::Separator();
ImGui::TextUnformatted("Lighting and shadows");
ImGui::Text("Lighting path: %s", stats.effective_lighting_path.c_str());
ImGui::Text("Local lights: %u submitted, %u omitted",
stats.submitted_local_lights, stats.omitted_local_lights);
ImGui::Text("Sun cascades: %u / %u effective",
stats.requested_sun_cascades, stats.effective_sun_cascades);
ImGui::Text("Local faces: %u requested, %u rasterized (%u tiles)",
stats.requested_local_shadow_faces, stats.local_shadow_faces,
stats.local_shadow_tiles);
ImGui::Text("Dropped faces: %u (point %u, atlas %u, draw budget %u, unavailable %u)",
stats.dropped_shadow_faces, stats.dropped_point_shadow_faces,
stats.shadow_atlas_full_drops, stats.shadow_caster_budget_drops,
stats.shadow_unavailable_drops);
ImGui::Text("Shadow caster draws: %u / 4096", stats.shadow_caster_draws);
ImGui::Text("Atlas memory: sun %.1f MiB, local %.1f MiB",
double(stats.sun_shadow_atlas_bytes) / 1048576.0,
double(stats.local_shadow_atlas_bytes) / 1048576.0);
if (stats.gpu_ms > 0)
ImGui::Text("Shadow GPU: sun %.2f ms, local %.2f ms",
stats.gpu_sun_shadow_ms, stats.gpu_local_shadow_ms);
ImGui::Separator();
ImGui::Text("Vulkan allocations: %.2f MiB",
double(stats.gpu_allocated_bytes) / 1048576.0);
ImGui::Text("Validation: %s Errors: %u",
+3 -1
View File
@@ -240,8 +240,10 @@ ShadowView local_view(const LocalLight& light, std::uint32_t face,
const auto up = light.kind == LocalLight::Kind::Point ? ups.at(face)
: std::abs(direction[1]) > .98f ? Vec3{0, 0, 1} : Vec3{0, 1, 0};
const auto near_plane = std::max(.0001f, std::min(.05f, light.range * .1f));
// Slight face overlap keeps the dominant-axis choice inside both adjacent
// projections at a cubemap seam; the guarded tile still prevents PCF bleed.
const auto fov = light.kind == LocalLight::Kind::Point
? std::numbers::pi_v<float> / 2 : light.outer_angle * 2;
? std::numbers::pi_v<float> / 2 + .04f : light.outer_angle * 2;
result.view_projection = multiply(
perspective(fov, 1, near_plane, light.range),
look_at(light.position, add(light.position, direction), up));
+116 -14
View File
@@ -231,8 +231,8 @@ struct Renderer::Impl {
VkExtent2D swap_extent{};
std::vector<VkImage> swap_images;
std::vector<VkImageLayout> swap_layouts;
Image color, depth, shadow;
std::uint32_t sun_shadow_size{};
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;
SceneResources scene;
@@ -364,6 +364,7 @@ struct Renderer::Impl {
destroy(color);
destroy(depth);
destroy(shadow);
destroy(local_shadow);
if (device) {
destroy_scene_interfaces();
if (pipeline)
@@ -791,6 +792,18 @@ struct Renderer::Impl {
if (!shadow.handle)
shadow = make_image(1, 1, VK_FORMAT_D32_SFLOAT, shadow_usage,
VK_IMAGE_ASPECT_DEPTH_BIT);
for (const auto size : {2048u, 1024u}) {
if (size > max_image_dimension)
continue;
try {
local_shadow = make_image(size, size, VK_FORMAT_D32_SFLOAT,
shadow_usage, VK_IMAGE_ASPECT_DEPTH_BIT);
local_shadow_size = size;
break;
} catch (const std::exception&) {
// Local shadows are optional; all affected lights remain unshadowed.
}
}
make_targets();
make_descriptors();
make_pipelines();
@@ -1626,7 +1639,8 @@ struct Renderer::Impl {
{lighting_header.handle, 0, lighting_header.size},
{lighting_locals.handle, 0, lighting_locals.size},
{lighting_views.handle, 0, lighting_views.size}}};
const VkDescriptorImageInfo atlas{VK_NULL_HANDLE, shadow.view,
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{};
for (std::uint32_t i = 0; i < writes.size(); ++i) {
@@ -1741,6 +1755,17 @@ struct Renderer::Impl {
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.requested_local_shadow_faces = statistics.local_shadow_faces =
statistics.local_shadow_tiles = statistics.dropped_shadow_faces =
statistics.dropped_point_shadow_faces =
statistics.shadow_atlas_full_drops =
statistics.shadow_caster_budget_drops =
statistics.shadow_unavailable_drops =
statistics.shadow_caster_draws = 0;
statistics.local_shadow_atlas_bytes = local_shadow_size
? local_shadow.allocation_size : 0;
statistics.gpu_local_shadow_ms = 0;
statistics.effective_lighting_path = "forward";
statistics.gpu_bins = statistics.gpu_visible_instances =
statistics.gpu_frustum_rejected = statistics.gpu_occlusion_deferred =
statistics.gpu_post_visible = 0;
@@ -1986,6 +2011,8 @@ struct Renderer::Impl {
ShadowBudget shadow_budget;
shadow_budget.sun_atlas_size = sun_shadow_size;
shadow_budget.sun_atlas_available = sun_shadow_size != 0;
shadow_budget.local_atlas_size = local_shadow_size;
shadow_budget.local_atlas_available = local_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(),
@@ -2000,6 +2027,39 @@ struct Renderer::Impl {
if (view.valid)
statistics.sun_shadow_caster_draws +=
static_cast<std::uint32_t>(view.caster_indices.size());
const bool local_raster = local_shadow_size &&
std::any_of(shadow_plan.local_views.begin(), shadow_plan.local_views.end(),
[](const ShadowView& view) {
return view.valid && !view.caster_indices.empty();
});
statistics.requested_local_shadow_faces = shadow_plan.local_faces_requested;
statistics.local_shadow_tiles = shadow_plan.local_faces_used;
statistics.local_shadow_faces = local_raster ? shadow_plan.local_faces_used : 0;
statistics.dropped_shadow_faces = shadow_plan.dropped_local_faces;
statistics.dropped_point_shadow_faces = shadow_plan.dropped_point_faces;
for (const auto& assignment : shadow_plan.local_assignments) {
if (assignment.valid || assignment.reason == ShadowDropReason::None)
continue;
const auto& light = snapshot.local_lights[assignment.source_index];
const auto faces = light.kind == LocalLight::Kind::Point ? 6u : 1u;
if (assignment.reason == ShadowDropReason::TileBudget)
statistics.shadow_atlas_full_drops += faces;
else if (assignment.reason == ShadowDropReason::CasterBudget)
statistics.shadow_caster_budget_drops += faces;
else if (assignment.reason == ShadowDropReason::Unavailable)
statistics.shadow_unavailable_drops += faces;
}
for (const auto& view : shadow_plan.sun_views) {
if (view.reason == ShadowDropReason::CasterBudget)
++statistics.shadow_caster_budget_drops;
else if (view.reason == ShadowDropReason::Unavailable)
++statistics.shadow_unavailable_drops;
}
statistics.shadow_caster_draws = statistics.sun_shadow_caster_draws;
if (local_raster)
for (const auto& view : shadow_plan.local_views)
statistics.shadow_caster_draws +=
static_cast<std::uint32_t>(view.caster_indices.size());
std::vector<GpuVertex> data;
std::vector<Batch> scene_batches, transparent_batches, sprite_batches, ui_batches;
for (const auto& selected : selected_draws) {
@@ -2110,10 +2170,14 @@ struct Renderer::Impl {
triangles.clip_rect});
}
std::vector<Batch> shadow_batch_by_source(snapshot.draws.size());
if (sun_raster) {
if (sun_raster || local_raster) {
std::vector<std::uint8_t> required(snapshot.draws.size());
for (const auto& view : shadow_plan.sun_views)
if (view.valid)
if (sun_raster && view.valid)
for (const auto source : view.caster_indices)
required.at(source) = 1;
for (const auto& view : shadow_plan.local_views)
if (local_raster && view.valid)
for (const auto source : view.caster_indices)
required.at(source) = 1;
for (std::size_t source = 0; source < required.size(); ++source) {
@@ -2234,6 +2298,19 @@ struct Renderer::Impl {
if (i < lighting.cascade_splits.size())
lighting.cascade_splits[i] = view.split_far;
}
for (const auto& view : shadow_plan.local_views) {
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,
local_shadow_size ? 1.f / float(local_shadow_size) : 0.f,
local_raster && view.valid ? 1.f : 0.f};
gpu_shadow_views.push_back(gpu);
}
std::vector<const LocalShadowAssignment*> assignments(snapshot.local_lights.size());
for (const auto& assignment : shadow_plan.local_assignments)
assignments.at(assignment.source_index) = &assignment;
statistics.omitted_local_lights = shadow_plan.omitted_local_lights;
std::vector<LocalLightGpu> gpu_lights;
gpu_lights.reserve(shadow_plan.submitted_local_indices.size());
@@ -2260,6 +2337,12 @@ struct Renderer::Impl {
local.intensity};
gpu.cone_type_shadow_view = {spot ? std::cos(local.inner_angle) : 1.f,
spot ? 1.f : 0.f, -1, 0};
const auto* assignment = assignments.at(source);
if (local_raster && assignment && assignment->valid) {
gpu.cone_type_shadow_view[2] = float(
shadow_plan.sun_views.size() + assignment->first_view);
gpu.cone_type_shadow_view[3] = float(assignment->face_count);
}
gpu_lights.push_back(gpu);
}
lighting.counts[0] = static_cast<std::uint32_t>(gpu_lights.size());
@@ -2440,26 +2523,26 @@ struct Renderer::Impl {
}
});
};
if (sun_raster)
add_pass("SunShadowAtlas", {}, {"shadow"}, [&] {
transition(command, shadow, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL,
auto raster_shadow_atlas = [&](Image& atlas, std::uint32_t atlas_size,
const std::vector<ShadowView>& views) {
transition(command, atlas, 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.imageView = atlas.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.renderArea = {{0, 0}, {atlas_size, atlas_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) {
for (const auto& view : views) {
if (!view.valid || view.caster_indices.empty())
continue;
const auto guard = (view.tile_size - view.usable_size) / 2;
@@ -2487,8 +2570,12 @@ struct Renderer::Impl {
}
}
vkCmdEndRendering(command);
transition(command, shadow, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL,
transition(command, atlas, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL,
VK_IMAGE_ASPECT_DEPTH_BIT);
};
if (sun_raster)
add_pass("SunShadowAtlas", {}, {"shadow"}, [&] {
raster_shadow_atlas(shadow, sun_shadow_size, shadow_plan.sun_views);
});
else
add_pass("ShadowFallback", {}, {"shadow"}, [&] {
@@ -2497,6 +2584,18 @@ struct Renderer::Impl {
transition(command, shadow, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL,
VK_IMAGE_ASPECT_DEPTH_BIT);
});
if (local_raster)
add_pass("LocalShadowAtlas", {}, {"local_shadow"}, [&] {
raster_shadow_atlas(local_shadow, local_shadow_size,
shadow_plan.local_views);
});
else
add_pass("LocalShadowFallback", {}, {"local_shadow"}, [&] {
if (local_shadow.handle)
transition(command, local_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"}, [&] {
@@ -2556,8 +2655,9 @@ struct Renderer::Impl {
}
});
add_pass(occlusion ? "MainRaster" : "ForwardAndUI",
gpu_active ? std::vector<std::string>{"shadow", "main_indirect", "main_visible"}
: std::vector<std::string>{"shadow"},
gpu_active ? std::vector<std::string>{"shadow", "local_shadow",
"main_indirect", "main_visible"}
: std::vector<std::string>{"shadow", "local_shadow"},
{"color", "depth"}, [&] {
transition(command, color, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
VK_IMAGE_ASPECT_COLOR_BIT);
@@ -2824,6 +2924,7 @@ struct Renderer::Impl {
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 == "LocalShadowAtlas") statistics.gpu_local_shadow_ms = elapsed;
else if (label == "MainRaster" || label == "ForwardAndUI")
statistics.gpu_main_raster_ms = elapsed;
else if (label == "BuildCurrentHZB") statistics.gpu_hzb_ms = elapsed;
@@ -2897,6 +2998,7 @@ struct Renderer::Impl {
statistics.gpu_allocated_bytes = vertices.allocation_size + readback.allocation_size +
color.allocation_size + depth.allocation_size +
shadow.allocation_size +
local_shadow.allocation_size +
lighting_header.allocation_size +
lighting_locals.allocation_size +
lighting_views.allocation_size;
+15
View File
@@ -29,6 +29,21 @@ with tempfile.TemporaryDirectory(prefix="faset-player-diagnostics-") as temporar
report = json.loads(profile.read_text(encoding="utf-8"))
assert report["completed_frames"] == 1 and len(report["samples"]) == 1, report
assert report["samples"][0]["tick"] == 1, report["samples"]
lighting = report["samples"][0]
assert lighting["effective_lighting_path"] == "forward", lighting
for field in ["submitted_local_lights", "omitted_local_lights",
"requested_sun_cascades", "effective_sun_cascades",
"requested_local_shadow_faces", "local_shadow_faces",
"local_shadow_tiles", "dropped_shadow_faces",
"dropped_point_shadow_faces", "shadow_atlas_full_drops",
"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"]:
assert field in lighting, (field, lighting)
assert lighting["submitted_local_lights"] == 0 and \
lighting["effective_sun_cascades"] == 0 and \
lighting["local_shadow_faces"] == 0, lighting
# The same linked v2 schema must validate without registering or invoking behavior.
validated = subprocess.run([sys.argv[1], "--scene", str(scene), "--validate"],
+150 -4
View File
@@ -145,13 +145,159 @@ void sun() {
two_d.stats.gpu_sun_shadow_ms == 0,
"Sprite-only rendering spends no sun shadow GPU work");
}
Snapshot local_scene(LocalLight::Kind kind, bool caster_shadow) {
Snapshot result;
result.view_id = "p3-local-shadow";
result.eye = {0, 5, 8};
const auto view = look_at(result.eye, {0, -1, 0});
const auto projection = orthographic(-3, 3, -2.25f, 2.25f, .1f, 50);
result.projection = projection;
result.view_projection = multiply(projection, view);
result.camera_frustum = CameraFrustum{view, projection, .1f, 50, false};
result.authored_lights_present = true;
DrawItem floor;
floor.mesh = cube_mesh();
floor.model = transform({0, -1, 0}, {}, {8, .1f, 8});
floor.color = {.8f, .8f, .8f, 1};
floor.instance_key = "floor";
result.draws.push_back(floor);
DrawItem caster;
caster.mesh = cube_mesh();
caster.model = transform({0, .7f, 0}, {}, {.8f, .8f, .8f});
caster.color = {.4f, .4f, .4f, 1};
caster.cast_shadow = caster_shadow;
caster.instance_key = "caster";
result.draws.push_back(caster);
LocalLight light;
light.kind = kind;
light.stable_id = "local";
light.position = {0, 3, 0};
light.direction = {0, -1, 0};
light.color = {1, .85f, .65f, 1};
light.intensity = 80;
light.range = 8;
light.inner_angle = .3f;
light.outer_angle = .7f;
result.local_lights.push_back(light);
return result;
}
std::size_t darker_pixels(const Frame& shadowed, const Frame& unshadowed) {
std::size_t count{};
for (std::size_t i = 0; i < shadowed.pixels.size(); i += 4)
count += int(unshadowed.pixels[i]) > int(shadowed.pixels[i]) + 12;
return count;
}
Snapshot point_face_scene(Vec3 axis, bool caster_shadow) {
Snapshot result;
result.view_id = "point-six-faces";
const Vec3 lateral = std::abs(axis[1]) > .9f ? Vec3{0, 0, 1} : Vec3{0, 1, 0};
result.eye = {-axis[0] * .4f + lateral[0] * 2,
-axis[1] * .4f + lateral[1] * 2,
-axis[2] * .4f + lateral[2] * 2};
const Vec3 target{axis[0] * 3, axis[1] * 3, axis[2] * 3};
const auto view = look_at(result.eye, target);
const auto projection = orthographic(-2, 2, -2, 2, .1f, 20);
result.projection = projection;
result.view_projection = multiply(projection, view);
result.camera_frustum = CameraFrustum{view, projection, .1f, 20, false};
result.authored_lights_present = true;
DrawItem receiver;
receiver.mesh = cube_mesh();
receiver.model = transform(target, {}, {1.5f, 1.5f, 1.5f});
receiver.color = {.8f, .8f, .8f, 1};
receiver.instance_key = "point-receiver";
result.draws.push_back(receiver);
DrawItem caster;
caster.mesh = cube_mesh();
caster.model = transform({axis[0] * 1.5f, axis[1] * 1.5f, axis[2] * 1.5f},
{}, {.5f, .5f, .5f});
caster.cast_shadow = caster_shadow;
caster.instance_key = "point-caster";
result.draws.push_back(caster);
LocalLight light;
light.stable_id = "point-face";
light.position = {0, 0, 0};
light.range = 8;
light.intensity = 90;
result.local_lights.push_back(light);
return result;
}
void local() {
auto direct = make_renderer(VisibilityMode::Direct);
auto gpu = make_renderer(VisibilityMode::GpuFrustum);
auto occlusion = make_renderer(VisibilityMode::GpuOcclusion);
for (auto kind : {LocalLight::Kind::Point, LocalLight::Kind::Spot}) {
const auto scene_with_shadow = local_scene(kind, true);
const auto shadowed = capture(direct, scene_with_shadow);
const auto gpu_shadowed = capture(gpu, scene_with_shadow);
const auto occlusion_shadowed = capture(occlusion, scene_with_shadow);
const auto unshadowed = capture(direct, local_scene(kind, false));
const auto faces = kind == LocalLight::Kind::Point ? 6u : 1u;
require(shadowed.stats.local_shadow_faces == faces &&
shadowed.stats.requested_local_shadow_faces == faces &&
shadowed.stats.shadow_caster_draws <= 4096 &&
shadowed.stats.local_shadow_atlas_bytes > 0 &&
shadowed.stats.gpu_local_shadow_ms > 0,
"Point/spot views render within atlas and caster budgets");
require(darker_pixels(shadowed, unshadowed) > 20,
"Caster darkens point/spot-lit receiver (count=" +
std::to_string(darker_pixels(shadowed, unshadowed)) + ")");
require(shadowed.stats.validation_errors == 0 &&
gpu_shadowed.stats.validation_errors == 0 &&
occlusion_shadowed.stats.validation_errors == 0,
"Local shadow rendering passes Vulkan validation");
compare_frames(shadowed, gpu_shadowed);
compare_frames(shadowed, occlusion_shadowed);
}
for (const Vec3 axis : {Vec3{1, 0, 0}, Vec3{-1, 0, 0}, Vec3{0, 1, 0},
Vec3{0, -1, 0}, Vec3{0, 0, 1}, Vec3{0, 0, -1},
Vec3{.7071068f, .7071068f, 0}}) {
const auto shadowed = capture(direct, point_face_scene(axis, true));
const auto unshadowed = capture(direct, point_face_scene(axis, false));
require(shadowed.stats.local_shadow_faces == 6 &&
darker_pixels(shadowed, unshadowed) > 5,
"A point light shadows each face direction and the adjacent-face seam");
}
auto crowded = local_scene(LocalLight::Kind::Point, true);
const auto point = crowded.local_lights.front();
crowded.local_lights.clear();
for (int i = 0; i < 15; ++i) {
LocalLight filler;
filler.kind = LocalLight::Kind::Spot;
filler.stable_id = "filler-" + std::to_string(i);
filler.position = {100, 100, 100};
filler.direction = {0, -1, 0};
filler.range = 8;
filler.intensity = 1;
filler.shadow_priority = 10;
crowded.local_lights.push_back(filler);
}
const auto without_point = capture(direct, crowded);
crowded.local_lights.push_back(point);
const auto overflow = capture(direct, crowded);
require(overflow.stats.requested_local_shadow_faces == 21 &&
overflow.stats.dropped_point_shadow_faces == 6 &&
overflow.stats.shadow_atlas_full_drops == 6 &&
overflow.stats.local_shadow_tiles <= 16,
"Fifteen occupied tiles drop the complete six-face point shadow");
std::size_t brightened{};
for (std::size_t i = 0; i < overflow.pixels.size(); i += 4)
brightened += int(overflow.pixels[i]) > int(without_point.pixels[i]) + 12;
require(brightened > 20,
"Point light with dropped atlas faces still illuminates unshadowed");
}
} // 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";
if (argc != 2)
throw std::invalid_argument("Expected --sun or --local");
if (std::string(argv[1]) == "--sun")
sun();
else if (std::string(argv[1]) == "--local")
local();
else
throw std::invalid_argument("Expected --sun or --local");
std::cout << "Shadow atlas and Direct/GPU lighting parity passed\n";
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;