diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ed77e8..9152957 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,7 @@ if(FASET_BUILD_RENDERER AND EXISTS "${PROJECT_SOURCE_DIR}/cmake/Renderer.cmake") include(cmake/Renderer.cmake) include(cmake/Visibility.cmake) include(cmake/GpuVisibility.cmake) + include(cmake/GpuVisibilityAcceptance.cmake) endif() if(TARGET faset_gameplay AND EXISTS "${PROJECT_SOURCE_DIR}/cmake/Player.cmake") include(cmake/Player.cmake) diff --git a/include/faset/render/renderer.hpp b/include/faset/render/renderer.hpp index b9bd3d9..ad352ed 100644 --- a/include/faset/render/renderer.hpp +++ b/include/faset/render/renderer.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -84,6 +85,9 @@ struct Snapshot { // target. std::array scene_rect{}; Mat4 view_projection{identity}; + // Supplying the projection separately lets temporal visibility invalidate + // history on FOV/near/far changes without invalidating normal camera motion. + Mat4 projection{identity}; Vec3 eye{4, 3, 5}; Vec3 light_direction{-0.5f, -1, -0.3f}; Color clear_color{0.055f, 0.065f, 0.085f, 1}; @@ -107,6 +111,8 @@ struct RendererConfig { bool headless{false}; bool validation{true}; VisibilityMode visibility_mode{VisibilityMode::Direct}; + // 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. std::filesystem::path shader_directory{}; }; @@ -143,12 +149,19 @@ struct FrameStats { std::uint32_t texture_count{}; std::uint32_t vertices{}, draw_calls{}, culled_meshes{}, validation_errors{}; bool gpu_visibility_active{}, hzb_valid{}; + bool visibility_counters_valid{}; std::uint32_t gpu_bins{}, gpu_visible_instances{}, gpu_frustum_rejected{}; std::uint32_t gpu_occlusion_deferred{}, gpu_post_visible{}; std::array lod_counts{}; 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{}; std::string device; }; +struct HzbDebugImage { + std::uint32_t width{}, height{}; + std::vector rgba; +}; class Renderer { public: explicit Renderer(const RendererConfig& = {}); @@ -162,6 +175,10 @@ class Renderer { void resize(std::uint32_t width, std::uint32_t height); void set_visibility_mode(VisibilityMode); VisibilityMode visibility_mode() const; + void set_visibility_diagnostics(bool enabled); + // Reads the most recently completed HZB mip for editor diagnostics only. + // Normal visibility decisions remain entirely on the GPU. + std::optional hzb_debug_image(std::uint32_t mip = 0); // Rebuilds graphics pipelines from SPIR-V; a failure preserves the current pipelines. bool reload_shaders(std::string& error); // Saves the latest completed frame as a portable RGB PPM image. diff --git a/include/faset/render/visibility.hpp b/include/faset/render/visibility.hpp index ac01b8b..44936c3 100644 --- a/include/faset/render/visibility.hpp +++ b/include/faset/render/visibility.hpp @@ -19,6 +19,7 @@ struct Bounds { // corners of the local AABB, including rotation, reflection and nonuniform scale. // Empty or nonfinite geometry/matrices are rejected instead of being culled. Bounds local_bounds(const Mesh& mesh); +Bounds transformed_bounds(const Bounds& local, const Mat4& model); Bounds transformed_bounds(const Mesh& mesh, const Mat4& model); struct InstanceUpdate { diff --git a/src/player/SceneView.cpp b/src/player/SceneView.cpp index 570e25f..103f975 100644 --- a/src/player/SceneView.cpp +++ b/src/player/SceneView.cpp @@ -377,10 +377,10 @@ render::Snapshot SceneView::build(const Json& scene, float aspect, CameraSetting if (!std::isfinite(height) || height <= 0) throw std::invalid_argument("Orthographic height must be positive"); const auto center = foundCamera ? camera.eye : camera.target; - out.view_projection = - render::multiply(render::orthographic(-height * aspect / 2, height * aspect / 2, - -height / 2, height / 2, -100, 100), - render::transform({-center[0], -center[1], 0})); + out.projection = render::orthographic(-height * aspect / 2, height * aspect / 2, + -height / 2, height / 2, -100, 100); + out.view_projection = render::multiply( + out.projection, render::transform({-center[0], -center[1], 0})); out.eye = {center[0], center[1], 10}; } else { if (!std::isfinite(camera.verticalFovDegrees) || camera.verticalFovDegrees <= 0 || @@ -405,10 +405,11 @@ render::Snapshot SceneView::build(const Json& scene, float aspect, CameraSetting cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2] < 1e-10f) throw std::invalid_argument("Camera basis is degenerate"); out.eye = camera.eye; + out.projection = render::perspective( + camera.verticalFovDegrees * std::numbers::pi_v / 180, aspect, + camera.nearPlane, camera.farPlane); out.view_projection = render::multiply( - render::perspective(camera.verticalFovDegrees * std::numbers::pi_v / 180, aspect, - camera.nearPlane, camera.farPlane), - render::look_at(camera.eye, camera.target, cameraUp)); + out.projection, render::look_at(camera.eye, camera.target, cameraUp)); } std::sort(impl_->messages.begin(), impl_->messages.end()); impl_->messages.erase(std::unique(impl_->messages.begin(), impl_->messages.end()), diff --git a/src/render/renderer.cpp b/src/render/renderer.cpp index f9ff7eb..10934a3 100644 --- a/src/render/renderer.cpp +++ b/src/render/renderer.cpp @@ -156,8 +156,9 @@ struct SceneResources { std::array shader_layouts{}; std::uint32_t hzb_mips{}, hzb_current{}; bool hzb_history_valid{}, available{}; - bool hzb_supported{}; + bool hzb_supported{}, hzb_extent_supported{}; Mat4 previous_vp{identity}; + Mat4 previous_projection{identity}; std::array previous_viewport{}; std::string previous_view_id; }; @@ -176,6 +177,8 @@ struct Renderer::Impl { VkDevice device{}; VkQueue queue{}; std::uint32_t queue_family{}; + std::uint32_t max_compute_groups_x{}, max_storage_buffer_range{}, + max_image_dimension{}; VkCommandPool pool{}; VkCommandBuffer command{}; VkFence fence{}; @@ -194,6 +197,17 @@ struct Renderer::Impl { SceneResources scene; InstanceTracker instance_tracker; std::unordered_map previous_lods; + struct CachedBounds { + std::weak_ptr owner; + Bounds local; + }; + struct CachedOpacity { + std::weak_ptr owner; + std::uint64_t revision{}; + bool opaque{}; + }; + std::unordered_map bounds_cache; + std::unordered_map opacity_cache; VkDescriptorSetLayout descriptor_layout{}; VkDescriptorPool descriptor_pool{}; VkSampler shadow_sampler{}, color_sampler{}; @@ -247,6 +261,36 @@ struct Renderer::Impl { } i = {}; } + void destroy_scene_interfaces() { + if (!device) + return; + for (auto* pipeline : {&scene.graphics_pipeline, &scene.cull_pipeline, + &scene.post_pipeline, &scene.hzb_pipeline}) { + if (*pipeline) + vkDestroyPipeline(device, *pipeline, nullptr); + *pipeline = {}; + } + for (auto* layout : {&scene.graphics_pipeline_layout, &scene.cull_pipeline_layout, + &scene.hzb_pipeline_layout}) { + if (*layout) + vkDestroyPipelineLayout(device, *layout, nullptr); + *layout = {}; + } + if (scene.descriptor_pool) + vkDestroyDescriptorPool(device, scene.descriptor_pool, nullptr); + scene.descriptor_pool = {}; + for (auto* layout : {&scene.graphics_layout, &scene.cull_layout, + &scene.hzb_layout}) { + if (*layout) + vkDestroyDescriptorSetLayout(device, *layout, nullptr); + *layout = {}; + } + scene.graphics_main = scene.graphics_post = scene.cull_main = + scene.cull_post = {}; + for (auto& sets : scene.hzb_sets) + sets.clear(); + scene.shader_layouts = {}; + } void cleanup() { if (device) vkDeviceWaitIdle(device); @@ -271,19 +315,7 @@ struct Renderer::Impl { destroy(depth); destroy(shadow); if (device) { - for (auto pipeline : {scene.graphics_pipeline, scene.cull_pipeline, - scene.post_pipeline, scene.hzb_pipeline}) - if (pipeline) - vkDestroyPipeline(device, pipeline, nullptr); - for (auto layout : {scene.graphics_pipeline_layout, scene.cull_pipeline_layout, - scene.hzb_pipeline_layout}) - if (layout) - vkDestroyPipelineLayout(device, layout, nullptr); - if (scene.descriptor_pool) - vkDestroyDescriptorPool(device, scene.descriptor_pool, nullptr); - for (auto layout : {scene.graphics_layout, scene.cull_layout, scene.hzb_layout}) - if (layout) - vkDestroyDescriptorSetLayout(device, layout, nullptr); + destroy_scene_interfaces(); if (pipeline) vkDestroyPipeline(device, pipeline, nullptr); if (ui_pipeline) @@ -610,14 +642,26 @@ struct Renderer::Impl { statistics.device = properties.deviceName; timestamp_period = properties.limits.timestampPeriod; timestamp_bits = queues[i].timestampValidBits; + max_compute_groups_x = properties.limits.maxComputeWorkGroupCount[0]; + max_storage_buffer_range = properties.limits.maxStorageBufferRange; + max_image_dimension = properties.limits.maxImageDimension2D; scene.available = (queues[i].queueFlags & VK_QUEUE_COMPUTE_BIT) != 0 && - f11.shaderDrawParameters; + f11.shaderDrawParameters && + properties.limits.maxPerStageDescriptorStorageBuffers >= 8 && + properties.limits.maxDescriptorSetStorageBuffers >= 8 && + properties.limits.maxComputeWorkGroupInvocations >= 64 && + properties.limits.maxComputeWorkGroupSize[0] >= 64 && + max_compute_groups_x > 0; scene.hzb_supported = scene.available && (hzb_props.optimalTilingFeatures & (VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | - VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT)) == + VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | + VK_FORMAT_FEATURE_TRANSFER_SRC_BIT | + VK_FORMAT_FEATURE_TRANSFER_DST_BIT)) == (VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | - VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT); + VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | + VK_FORMAT_FEATURE_TRANSFER_SRC_BIT | + VK_FORMAT_FEATURE_TRANSFER_DST_BIT); } } } @@ -680,7 +724,7 @@ struct Renderer::Impl { VkQueryPoolCreateInfo query{}; query.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; query.queryType = VK_QUERY_TYPE_TIMESTAMP; - query.queryCount = 2; + query.queryCount = 12; check(vkCreateQueryPool(device, &query, nullptr, ×tamp_pool), "Create GPU timestamp queries"); } @@ -691,7 +735,7 @@ struct Renderer::Impl { make_targets(); make_descriptors(); make_pipelines(); - if (scene.available) + if (scene.available && c.visibility_mode != VisibilityMode::Direct) make_scene_descriptors_and_pipelines(); white = std::make_shared(); white->width = white->height = 1; @@ -715,17 +759,24 @@ struct Renderer::Impl { VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_DEPTH_BIT); - if (scene.hzb_supported) { - const auto padded_width = std::bit_ceil(width); - const auto padded_height = std::bit_ceil(height); + const auto padded_width = std::bit_ceil(width); + const auto padded_height = std::bit_ceil(height); + scene.hzb_extent_supported = scene.hzb_supported && + padded_width <= max_image_dimension && padded_height <= max_image_dimension && + (padded_width + 7u) / 8u <= max_compute_groups_x; + if (scene.hzb_extent_supported && + config.visibility_mode == VisibilityMode::GpuOcclusion) { scene.hzb_mips = std::bit_width(std::max(padded_width, padded_height)); for (auto& pyramid : scene.hzb) pyramid = make_image(padded_width, padded_height, VK_FORMAT_R32_SFLOAT, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, VK_IMAGE_ASPECT_COLOR_BIT, scene.hzb_mips); scene.hzb_current = 0; + } else { + scene.hzb_mips = 0; } // CPU reads this allocation every frame. Prefer cached coherent memory when available. readback = @@ -1086,7 +1137,7 @@ struct Renderer::Impl { scene.graphics_main = scene.graphics_post = scene.cull_main = scene.cull_post = {}; for (auto& sets : scene.hzb_sets) sets.clear(); - const std::uint32_t hzb_sets = scene.hzb_supported ? scene.hzb_mips * 2 : 0; + const std::uint32_t hzb_sets = scene.hzb_mips * 2; const std::array sizes{{ {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 64}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 16 + hzb_sets}, @@ -1094,7 +1145,7 @@ struct Renderer::Impl { VkDescriptorPoolCreateInfo pool{}; pool.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; pool.maxSets = 4 + hzb_sets; - pool.poolSizeCount = static_cast(sizes.size()); + pool.poolSizeCount = hzb_sets ? static_cast(sizes.size()) : 2; pool.pPoolSizes = sizes.data(); check(vkCreateDescriptorPool(device, &pool, nullptr, &scene.descriptor_pool), "Create GPU visibility descriptor pool"); @@ -1112,7 +1163,7 @@ struct Renderer::Impl { scene.graphics_post = sets[1]; scene.cull_main = sets[2]; scene.cull_post = sets[3]; - if (!scene.hzb_supported) + if (!scene.hzb_mips) return; for (std::uint32_t image = 0; image < 2; ++image) { auto& target_sets = scene.hzb_sets[image]; @@ -1197,6 +1248,10 @@ struct Renderer::Impl { check(vkCreatePipelineLayout(device, &pipeline_info, nullptr, &scene.hzb_pipeline_layout), "Create HZB pipeline layout"); + make_scene_pipelines(); + refresh_scene_descriptors(); + } + void make_scene_pipelines() { const auto gpu_shaders = detail::load_gpu_shader_bundle(shader_directory()); const auto baseline_shaders = detail::load_shader_bundle(shader_directory()); for (std::size_t i = 0; i < gpu_shaders.size(); ++i) @@ -1206,7 +1261,8 @@ struct Renderer::Impl { VkShaderModule vertex = shader(gpu_shaders[0]); VkShaderModule fragment = shader(baseline_shaders[1]); VkShaderModule main_cull = shader(gpu_shaders[2]); - VkShaderModule hzb_shader = shader(gpu_shaders[3]); + VkShaderModule hzb_shader = scene.hzb_supported ? shader(gpu_shaders[3]) + : VK_NULL_HANDLE; VkShaderModule post_cull = shader(gpu_shaders[4]); try { VkPipelineShaderStageCreateInfo stages[2]{}; @@ -1309,7 +1365,6 @@ struct Renderer::Impl { make_compute(hzb_shader, scene.hzb_pipeline_layout, scene.hzb_pipeline); for (std::size_t i = 0; i < gpu_shaders.size(); ++i) scene.shader_layouts[i] = gpu_shaders[i].layout_fingerprint; - refresh_scene_descriptors(); } catch (...) { for (auto module : {vertex, fragment, main_cull, hzb_shader, post_cull}) vkDestroyShaderModule(device, module, nullptr); @@ -1446,6 +1501,8 @@ struct Renderer::Impl { void upload_scene_buffer(Buffer& buffer, const void* bytes, std::size_t count, VkBufferUsageFlags usage) { const VkDeviceSize required = std::max(16, count); + if (required > max_storage_buffer_range) + throw std::overflow_error("GPU scene buffer exceeds maxStorageBufferRange"); if (buffer.size < required) { destroy(buffer); buffer = make_buffer(required, usage | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, @@ -1535,6 +1592,26 @@ struct Renderer::Impl { dependency.pMemoryBarriers = &barrier; vkCmdPipelineBarrier2(command, &dependency); } + Bounds world_bounds(const std::shared_ptr& mesh, const Mat4& model) { + auto found = bounds_cache.find(mesh.get()); + if (found == bounds_cache.end() || found->second.owner.lock() != mesh) { + auto local = local_bounds(*mesh); + found = bounds_cache.insert_or_assign(mesh.get(), CachedBounds{mesh, local}).first; + } + return transformed_bounds(found->second.local, model); + } + bool is_opaque(const std::shared_ptr& texture) { + if (!texture) + return true; + auto found = opacity_cache.find(texture.get()); + if (found == opacity_cache.end() || found->second.owner.lock() != texture || + found->second.revision != texture->revision) { + const bool opaque = opaque_texture(texture.get()); + found = opacity_cache.insert_or_assign( + texture.get(), CachedOpacity{texture, texture->revision, opaque}).first; + } + return found->second.opaque; + } void render(const Snapshot& snapshot) { auto start = std::chrono::steady_clock::now(); statistics.draw_calls = statistics.culled_meshes = statistics.gpu_label_count = 0; @@ -1542,9 +1619,14 @@ struct Renderer::Impl { statistics.gpu_frustum_rejected = statistics.gpu_occlusion_deferred = statistics.gpu_post_visible = 0; statistics.lod_counts = {}; + statistics.visibility_counters_valid = false; + for (auto it = bounds_cache.begin(); it != bounds_cache.end();) + it = it->second.owner.expired() ? bounds_cache.erase(it) : std::next(it); + for (auto it = opacity_cache.begin(); it != opacity_cache.end();) + it = it->second.owner.expired() ? opacity_cache.erase(it) : std::next(it); const bool gpu_active = scene.available && config.visibility_mode != VisibilityMode::Direct; - const bool occlusion = gpu_active && scene.hzb_supported && + const bool occlusion = gpu_active && scene.hzb_supported && scene.hzb_mips && config.visibility_mode == VisibilityMode::GpuOcclusion; statistics.gpu_visibility_active = gpu_active; statistics.hzb_valid = false; @@ -1594,7 +1676,8 @@ struct Renderer::Impl { const std::string view_id = snapshot.view_id.empty() ? "default" : snapshot.view_id; const bool history_compatible = occlusion && scene.hzb_history_valid && !snapshot.camera_cut && scene.previous_view_id == view_id && - scene.previous_viewport == scene_viewport; + scene.previous_viewport == scene_viewport && + scene.previous_projection == snapshot.projection; if (!history_compatible) instance_tracker.invalidate_view(view_id); if (occlusion) @@ -1604,7 +1687,7 @@ struct Renderer::Impl { gpu_frame.view.previous_vp = scene.previous_vp; gpu_frame.view.viewport = scene_viewport; gpu_frame.view.previous_viewport = scene.previous_viewport; - if (scene.hzb_supported) { + if (scene.hzb_mips) { const auto dimensions = std::array{ std::bit_ceil(width), std::bit_ceil(height), scene.hzb_mips, 0}; gpu_frame.view.hzb_dimensions = dimensions; @@ -1616,6 +1699,7 @@ struct Renderer::Impl { const DrawItem* source; std::shared_ptr mesh; bool gpu{}; + bool opaque{}; }; std::vector selected_draws; selected_draws.reserve(snapshot.draws.size()); @@ -1631,7 +1715,7 @@ struct Renderer::Impl { for (const auto& item : snapshot.draws) { if (!item.mesh || item.mesh->vertices.empty()) continue; - const auto source_bounds = transformed_bounds(*item.mesh, item.model); + const auto source_bounds = world_bounds(item.mesh, item.model); std::vector thresholds; std::vector available; std::shared_ptr selected_mesh = item.mesh; @@ -1657,14 +1741,14 @@ struct Renderer::Impl { if (!item.instance_key.empty()) current_lods[item.instance_key] = lod; const auto bounds = selected_mesh == item.mesh - ? source_bounds : transformed_bounds(*selected_mesh, item.model); + ? source_bounds : world_bounds(selected_mesh, item.model); InstanceUpdate previous{}; if (!item.instance_key.empty()) previous = instance_tracker.update(item.instance_key, selected_mesh, item.model, bounds, view_id); - const bool eligible = gpu_active && item.color[3] >= 1.f && - opaque_texture(item.texture.get()); - selected_draws.push_back({&item, selected_mesh, eligible}); + const bool opaque = item.color[3] >= 1.f && is_opaque(item.texture); + const bool eligible = gpu_active && opaque; + selected_draws.push_back({&item, selected_mesh, eligible, opaque}); if (!eligible) continue; auto [range_it, inserted] = mesh_ranges.try_emplace(selected_mesh.get()); @@ -1761,7 +1845,8 @@ struct Renderer::Impl { } gpu_frame.candidate_count = static_cast(gpu_frame.candidates.size()); std::vector data; - std::vector scene_batches, shadow_batches, sprite_batches, ui_batches; + std::vector scene_batches, transparent_batches, shadow_batches, + sprite_batches, ui_batches; for (const auto& selected : selected_draws) { const auto& item = *selected.source; if (selected.gpu && !item.cast_shadow) @@ -1792,6 +1877,8 @@ struct Renderer::Impl { 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); } @@ -1938,9 +2025,12 @@ struct Renderer::Impl { } } begin(); + std::uint32_t timestamp_cursor = 0; + std::vector timestamp_labels; if (timestamp_pool) { - vkCmdResetQueryPool(command, timestamp_pool, 0, 2); - vkCmdWriteTimestamp2(command, VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, timestamp_pool, 0); + vkCmdResetQueryPool(command, timestamp_pool, 0, 12); + vkCmdWriteTimestamp2(command, VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, + timestamp_pool, timestamp_cursor++); } VkDeviceSize offset{}; vkCmdBindVertexBuffers(command, 0, 1, &vertices.handle, &offset); @@ -1953,12 +2043,100 @@ struct Renderer::Impl { vkCmdSetViewport(command, 0, 1, &viewport); vkCmdSetScissor(command, 0, 1, &scissor); }; + auto set_scene_viewport = [&] { + VkViewport viewport{scene_viewport[0], scene_viewport[1], + scene_viewport[2], scene_viewport[3], 0, 1}; + VkRect2D scissor{{static_cast(scene_viewport[0]), + static_cast(scene_viewport[1])}, + {static_cast(scene_viewport[2]), + static_cast(scene_viewport[3])}}; + vkCmdSetViewport(command, 0, 1, &viewport); + vkCmdSetScissor(command, 0, 1, &scissor); + }; + auto draw_transparent = [&] { + if (transparent_batches.empty()) + return; + vkCmdBindVertexBuffers(command, 0, 1, &vertices.handle, &offset); + vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + vkCmdPushConstants(command, pipeline_layout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(push), &push); + for (auto batch : transparent_batches) { + auto descriptor = textures.at(batch.texture).descriptor; + vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, + pipeline_layout, 0, 1, &descriptor, 0, nullptr); + vkCmdDraw(command, batch.count, 1, batch.first, 0); + ++statistics.draw_calls; + } + }; + auto draw_sprites_and_ui = [&] { + vkCmdBindVertexBuffers(command, 0, 1, &vertices.handle, &offset); + vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline); + vkCmdPushConstants(command, pipeline_layout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, + sizeof(push), &push); + for (auto batch : sprite_batches) { + auto descriptor = textures.at(batch.texture).descriptor; + vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, + 0, 1, &descriptor, 0, nullptr); + vkCmdDraw(command, batch.count, 1, batch.first, 0); + ++statistics.draw_calls; + } + set_viewport(width, height); + vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, ui_pipeline); + for (auto batch : ui_batches) { + VkRect2D scissor{{0, 0}, {width, height}}; + if (batch.clip_rect[2] > 0 && batch.clip_rect[3] > 0) { + const auto& clip = batch.clip_rect; + const float x = std::clamp(clip[0], 0.f, float(width)); + const float y = std::clamp(clip[1], 0.f, float(height)); + const float right = std::clamp(clip[0] + clip[2], x, float(width)); + const float bottom = std::clamp(clip[1] + clip[3], y, float(height)); + scissor.offset = {static_cast(x), static_cast(y)}; + scissor.extent = {static_cast(right) - static_cast(x), + static_cast(bottom) - static_cast(y)}; + } + if (!scissor.extent.width || !scissor.extent.height) + continue; + vkCmdSetScissor(command, 0, 1, &scissor); + auto descriptor = textures.at(batch.texture).descriptor; + vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, + 0, 1, &descriptor, 0, nullptr); + vkCmdDraw(command, batch.count, 1, batch.first, 0); + ++statistics.draw_calls; + } + }; + auto dispatch_scene = [&](VkPipeline compute_pipeline, VkDescriptorSet descriptor) { + if (!gpu_frame.candidate_count) + return; + vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_COMPUTE, + compute_pipeline); + vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_COMPUTE, + scene.cull_pipeline_layout, 0, 1, &descriptor, 0, + nullptr); + const std::uint64_t max_threads = std::uint64_t(max_compute_groups_x) * 64; + for (std::uint64_t base = 0; base < gpu_frame.candidate_count; + base += max_threads) { + const auto remaining = std::uint64_t(gpu_frame.candidate_count) - base; + const auto groups = static_cast( + std::min(max_compute_groups_x, + (remaining + 63) / 64)); + const std::array parameters{ + gpu_frame.candidate_count, gpu_frame.candidate_count, + static_cast(base), 0}; + vkCmdPushConstants(command, scene.cull_pipeline_layout, + VK_SHADER_STAGE_COMPUTE_BIT, 0, + sizeof(parameters), parameters.data()); + vkCmdDispatch(command, groups, 1, 1); + } + }; RenderGraph graph; auto add_pass = [&](std::string name, std::vector reads, std::vector writes, RenderGraph::Callback callback) { const auto label_name = name; graph.add(std::move(name), std::move(reads), std::move(writes), - [this, label_name, callback = std::move(callback)] { + [this, ×tamp_cursor, ×tamp_labels, label_name, + callback = std::move(callback)] { if (statistics.gpu_labels_enabled) { VkDebugUtilsLabelEXT label{}; label.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT; @@ -1978,6 +2156,12 @@ struct Renderer::Impl { } } end{*this}; callback(); + if (timestamp_pool && timestamp_cursor < 12) { + vkCmdWriteTimestamp2(command, + VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT, + timestamp_pool, timestamp_cursor++); + timestamp_labels.push_back(label_name); + } }); }; add_pass("ShadowMap", {}, {"shadow"}, [&] { @@ -2010,22 +2194,33 @@ struct Renderer::Impl { VK_IMAGE_ASPECT_DEPTH_BIT); }); if (gpu_active) - add_pass("MainCull", {"shadow"}, {"main_indirect", "main_visible"}, [&] { + add_pass("MainCull", {"shadow"}, + {"main_indirect", "main_visible", "deferred_ids"}, [&] { + if (occlusion) { + for (auto& pyramid : scene.hzb) { + if (pyramid.layout == VK_IMAGE_LAYOUT_UNDEFINED) { + transition(command, pyramid, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_ASPECT_COLOR_BIT); + VkClearColorValue far_depth{}; + far_depth.float32[0] = 1.f; + VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, + pyramid.mip_levels, 0, 1}; + vkCmdClearColorImage(command, pyramid.handle, + VK_IMAGE_LAYOUT_GENERAL, &far_depth, 1, + &range); + } + } + scene_barrier(VK_PIPELINE_STAGE_2_TRANSFER_BIT, + VK_ACCESS_2_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); + } 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 | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT); if (gpu_frame.candidate_count) { - vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_COMPUTE, - scene.cull_pipeline); - vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_COMPUTE, - scene.cull_pipeline_layout, 0, 1, - &scene.cull_main, 0, nullptr); - const std::array push{ - gpu_frame.candidate_count, gpu_frame.candidate_count, 0, 0}; - vkCmdPushConstants(command, scene.cull_pipeline_layout, - VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), push.data()); - vkCmdDispatch(command, (gpu_frame.candidate_count + 63) / 64, 1, 1); + dispatch_scene(scene.cull_pipeline, scene.cull_main); scene_barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT | @@ -2034,7 +2229,10 @@ struct Renderer::Impl { VK_ACCESS_2_SHADER_STORAGE_READ_BIT); } }); - add_pass("ForwardAndUI", {"shadow"}, {"color", "depth"}, [&] { + add_pass(occlusion ? "MainRaster" : "ForwardAndUI", + gpu_active ? std::vector{"shadow", "main_indirect", "main_visible"} + : std::vector{"shadow"}, + {"color", "depth"}, [&] { transition(command, color, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); transition(command, depth, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, @@ -2052,7 +2250,8 @@ struct Renderer::Impl { da.imageView = depth.view; da.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; da.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - da.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + da.storeOp = occlusion ? VK_ATTACHMENT_STORE_OP_STORE + : VK_ATTACHMENT_STORE_OP_DONT_CARE; da.clearValue.depthStencil = {1, 0}; VkRenderingInfo rendering{}; rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; @@ -2062,18 +2261,7 @@ struct Renderer::Impl { rendering.pColorAttachments = &ca; rendering.pDepthAttachment = &da; vkCmdBeginRendering(command, &rendering); - set_viewport(width, height); - if (snapshot.scene_rect[2] > 0 && snapshot.scene_rect[3] > 0) { - auto r = snapshot.scene_rect; - float x = std::clamp(r[0], 0.f, float(width)), - y = std::clamp(r[1], 0.f, float(height)); - float w = std::min(r[2], float(width) - x), h = std::min(r[3], float(height) - y); - VkViewport viewport{x, y, w, h, 0, 1}; - VkRect2D scissor{{static_cast(x), static_cast(y)}, - {static_cast(w), static_cast(h)}}; - vkCmdSetViewport(command, 0, 1, &viewport); - vkCmdSetScissor(command, 0, 1, &scissor); - } + set_scene_viewport(); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); vkCmdPushConstants(command, pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, @@ -2116,42 +2304,123 @@ struct Renderer::Impl { vkCmdDraw(command, batch.count, 1, batch.first, 0); ++statistics.draw_calls; } - vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline); - for (auto batch : sprite_batches) { - auto descriptor = textures.at(batch.texture).descriptor; - vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, - 0, 1, &descriptor, 0, nullptr); - vkCmdDraw(command, batch.count, 1, batch.first, 0); - ++statistics.draw_calls; - } - set_viewport(width, height); - vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, ui_pipeline); - vkCmdPushConstants(command, pipeline_layout, - VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, - sizeof(push), &push); - for (auto batch : ui_batches) { - VkRect2D scissor{{0, 0}, {width, height}}; - if (batch.clip_rect[2] > 0 && batch.clip_rect[3] > 0) { - const auto& clip = batch.clip_rect; - const float x = std::clamp(clip[0], 0.f, float(width)); - const float y = std::clamp(clip[1], 0.f, float(height)); - const float right = std::clamp(clip[0] + clip[2], x, float(width)); - const float bottom = std::clamp(clip[1] + clip[3], y, float(height)); - scissor.offset = {static_cast(x), static_cast(y)}; - scissor.extent = {static_cast(right) - static_cast(x), - static_cast(bottom) - static_cast(y)}; - } - if (!scissor.extent.width || !scissor.extent.height) - continue; - vkCmdSetScissor(command, 0, 1, &scissor); - auto descriptor = textures.at(batch.texture).descriptor; - vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, - 0, 1, &descriptor, 0, nullptr); - vkCmdDraw(command, batch.count, 1, batch.first, 0); - ++statistics.draw_calls; + if (!occlusion) { + draw_transparent(); + draw_sprites_and_ui(); } vkCmdEndRendering(command); }); + if (occlusion) { + add_pass("BuildCurrentHZB", {"depth"}, {"current_hzb"}, [&] { + transition(command, depth, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL, + VK_IMAGE_ASPECT_DEPTH_BIT); + auto& pyramid = scene.hzb[scene.hzb_current]; + transition(command, pyramid, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_ASPECT_COLOR_BIT); + vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_COMPUTE, + scene.hzb_pipeline); + const auto padded_width = std::bit_ceil(width); + const auto padded_height = std::bit_ceil(height); + for (std::uint32_t mip = 0; mip < scene.hzb_mips; ++mip) { + const std::uint32_t source_width = mip == 0 ? width + : std::max(1u, padded_width >> (mip - 1)); + const std::uint32_t source_height = mip == 0 ? height + : std::max(1u, padded_height >> (mip - 1)); + const std::uint32_t output_width = + std::max(1u, padded_width >> mip); + const std::uint32_t output_height = + std::max(1u, padded_height >> mip); + const auto descriptor = scene.hzb_sets[scene.hzb_current][mip]; + vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_COMPUTE, + scene.hzb_pipeline_layout, 0, 1, + &descriptor, 0, nullptr); + const std::array dimensions{ + source_width, source_height, output_width, output_height}; + vkCmdPushConstants(command, scene.hzb_pipeline_layout, + VK_SHADER_STAGE_COMPUTE_BIT, 0, + sizeof(dimensions), dimensions.data()); + vkCmdDispatch(command, (output_width + 7) / 8, + (output_height + 7) / 8, 1); + scene_barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); + } + }); + add_pass("PostCull", {"deferred_ids", "current_hzb"}, + {"post_indirect", "post_visible"}, [&] { + scene_barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + VK_ACCESS_2_SHADER_STORAGE_READ_BIT | + VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); + if (gpu_frame.candidate_count) { + dispatch_scene(scene.post_pipeline, scene.cull_post); + scene_barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, + VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT | + VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT, + VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT | + VK_ACCESS_2_SHADER_STORAGE_READ_BIT); + } + }); + add_pass("PostRasterAndUI", {"color", "depth", "post_indirect", "post_visible"}, + {"color", "depth"}, [&] { + transition(command, color, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_ASPECT_COLOR_BIT); + transition(command, depth, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, + VK_IMAGE_ASPECT_DEPTH_BIT); + VkRenderingAttachmentInfo ca{}; + ca.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; + ca.imageView = color.view; + ca.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + ca.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + ca.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + VkRenderingAttachmentInfo da{}; + da.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; + da.imageView = depth.view; + da.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; + da.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + da.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + rendering.renderArea = {{0, 0}, {width, height}}; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &ca; + rendering.pDepthAttachment = &da; + vkCmdBeginRendering(command, &rendering); + set_scene_viewport(); + if (!gpu_frame.bins.empty()) { + VkDeviceSize scene_offset{}; + vkCmdBindVertexBuffers(command, 0, 1, &scene.vertices.handle, + &scene_offset); + vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, + scene.graphics_pipeline); + for (std::uint32_t bin = 0; bin < gpu_frame.bins.size(); ++bin) { + auto descriptor = textures.at(gpu_frame.textures[bin]).descriptor; + const std::array sets{descriptor, + scene.graphics_post}; + vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, + scene.graphics_pipeline_layout, 0, + sets.size(), sets.data(), 0, nullptr); + ScenePush draw_push{push, {gpu_frame.bins[bin].visible_base, + 0, 0, 0}}; + vkCmdPushConstants(command, scene.graphics_pipeline_layout, + VK_SHADER_STAGE_VERTEX_BIT | + VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof(draw_push), &draw_push); + vkCmdDrawIndirect(command, scene.post_args.handle, + VkDeviceSize(bin) * sizeof(SceneIndirect), 1, + sizeof(SceneIndirect)); + ++statistics.draw_calls; + } + } + draw_transparent(); + draw_sprites_and_ui(); + vkCmdEndRendering(command); + }); + } add_pass("Readback", {"color"}, {"capture"}, [&] { transition(command, color, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); @@ -2179,20 +2448,35 @@ struct Renderer::Impl { VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_ASPECT_COLOR_BIT); }); graph.execute(); - if (timestamp_pool) - vkCmdWriteTimestamp2(command, VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT, timestamp_pool, - 1); submit(swap_index.has_value()); if (timestamp_pool) { - std::uint64_t stamps[2]{}; - check(vkGetQueryPoolResults(device, timestamp_pool, 0, 2, sizeof(stamps), stamps, + std::array stamps{}; + check(vkGetQueryPoolResults(device, timestamp_pool, 0, timestamp_cursor, + timestamp_cursor * sizeof(std::uint64_t), stamps.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "Read GPU timestamps"); - auto delta = stamps[1] - stamps[0]; - if (timestamp_bits < 64) - delta &= (std::uint64_t(1) << timestamp_bits) - 1; - statistics.gpu_ms = double(delta) * timestamp_period / 1000000.0; + auto milliseconds = [&](std::uint64_t before, std::uint64_t after) { + auto delta = after - before; + if (timestamp_bits < 64) + delta &= (std::uint64_t(1) << timestamp_bits) - 1; + return double(delta) * timestamp_period / 1000000.0; + }; + statistics.gpu_ms = milliseconds(stamps[0], stamps[timestamp_cursor - 1]); + statistics.gpu_main_cull_ms = statistics.gpu_main_raster_ms = + statistics.gpu_hzb_ms = statistics.gpu_post_cull_ms = + statistics.gpu_post_raster_ms = 0; + for (std::size_t i = 0; i < timestamp_labels.size(); ++i) { + 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 == "MainRaster" || label == "ForwardAndUI") + statistics.gpu_main_raster_ms = elapsed; + else if (label == "BuildCurrentHZB") statistics.gpu_hzb_ms = elapsed; + else if (label == "PostCull") statistics.gpu_post_cull_ms = elapsed; + else if (label == "PostRasterAndUI") + statistics.gpu_post_raster_ms = elapsed; + } } if (swap_index) { VkPresentInfoKHR present{}; @@ -2218,7 +2502,7 @@ struct Renderer::Impl { statistics.readback_cpu_ms = std::chrono::duration( std::chrono::steady_clock::now() - readback_started) .count(); - if (gpu_active) { + if (gpu_active && config.visibility_diagnostics) { void* counts{}; check(vkMapMemory(device, scene.main_args.memory, 0, scene.main_args.size, 0, &counts), "Read GPU scene indirect counts"); @@ -2244,13 +2528,14 @@ struct Renderer::Impl { std::min(gpu_frame.candidate_count, main_visible + statistics.gpu_occlusion_deferred); statistics.culled_meshes += statistics.gpu_frustum_rejected; + statistics.visibility_counters_valid = true; } instance_tracker.finish_frame(); scene.previous_vp = snapshot.view_projection; + scene.previous_projection = snapshot.projection; scene.previous_viewport = scene_viewport; scene.previous_view_id = view_id; - if (!occlusion) - scene.hzb_history_valid = false; + scene.hzb_history_valid = occlusion; ++statistics.frame; statistics.gpu_allocated_bytes = vertices.allocation_size + readback.allocation_size + color.allocation_size + depth.allocation_size + @@ -2290,13 +2575,23 @@ 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_layout_fingerprints = r.shader_layouts; + auto previous_gpu_fingerprints = r.scene.shader_layouts; + auto previous_gpu = r.scene.graphics_pipeline; + auto previous_cull = r.scene.cull_pipeline; + auto previous_post = r.scene.post_pipeline; + auto previous_hzb = r.scene.hzb_pipeline; r.pipeline_layout = {}; r.pipeline = {}; r.ui_pipeline = {}; r.shadow_pipeline = {}; r.sprite_pipeline = {}; + r.scene.graphics_pipeline = r.scene.cull_pipeline = r.scene.post_pipeline = + r.scene.hzb_pipeline = {}; try { r.make_pipelines(); + if (r.scene.graphics_pipeline_layout) + r.make_scene_pipelines(); } catch (const std::exception& exception) { if (r.pipeline) vkDestroyPipeline(r.device, r.pipeline, nullptr); @@ -2308,11 +2603,21 @@ bool Renderer::reload_shaders(std::string& error) { vkDestroyPipeline(r.device, r.sprite_pipeline, nullptr); if (r.pipeline_layout) vkDestroyPipelineLayout(r.device, r.pipeline_layout, nullptr); + for (auto pipeline : {r.scene.graphics_pipeline, r.scene.cull_pipeline, + r.scene.post_pipeline, r.scene.hzb_pipeline}) + if (pipeline) + vkDestroyPipeline(r.device, pipeline, nullptr); r.pipeline_layout = previous_layout; r.pipeline = previous; r.ui_pipeline = previous_ui; r.shadow_pipeline = previous_shadow; r.sprite_pipeline = previous_sprite; + r.scene.graphics_pipeline = previous_gpu; + r.scene.cull_pipeline = previous_cull; + r.scene.post_pipeline = previous_post; + r.scene.hzb_pipeline = previous_hzb; + r.shader_layouts = previous_layout_fingerprints; + r.scene.shader_layouts = previous_gpu_fingerprints; error = exception.what(); return false; } @@ -2320,6 +2625,9 @@ 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); + for (auto pipeline : {previous_gpu, previous_cull, previous_post, previous_hzb}) + if (pipeline) + vkDestroyPipeline(r.device, pipeline, nullptr); vkDestroyPipelineLayout(r.device, previous_layout, nullptr); error.clear(); return true; @@ -2339,13 +2647,78 @@ void Renderer::resize(std::uint32_t w, std::uint32_t h) { void Renderer::set_visibility_mode(VisibilityMode mode) { if (impl_->config.visibility_mode == mode) return; + if (mode != VisibilityMode::Direct && impl_->scene.available && + !impl_->scene.graphics_layout) { + try { + impl_->make_scene_descriptors_and_pipelines(); + } catch (...) { + impl_->destroy_scene_interfaces(); + throw; + } + } impl_->config.visibility_mode = mode; + if (mode == VisibilityMode::GpuOcclusion && impl_->scene.hzb_supported && + !impl_->scene.hzb[0].handle) + impl_->make_targets(); impl_->scene.hzb_history_valid = false; impl_->instance_tracker.invalidate_view(impl_->scene.previous_view_id); } VisibilityMode Renderer::visibility_mode() const { return impl_->config.visibility_mode; } +void Renderer::set_visibility_diagnostics(bool enabled) { + impl_->config.visibility_diagnostics = enabled; +} +std::optional Renderer::hzb_debug_image(std::uint32_t mip) { + auto& renderer = *impl_; + if (!renderer.scene.hzb_history_valid || !renderer.scene.hzb_supported) + return std::nullopt; + if (mip >= renderer.scene.hzb_mips) + throw std::out_of_range("HZB debug mip outside pyramid"); + const std::uint32_t w = std::max(1u, std::bit_ceil(renderer.width) >> mip); + const std::uint32_t h = std::max(1u, std::bit_ceil(renderer.height) >> mip); + auto staging = renderer.make_buffer(VkDeviceSize(w) * h * sizeof(float), + VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + VK_MEMORY_PROPERTY_HOST_CACHED_BIT); + try { + renderer.begin(); + auto& pyramid = renderer.scene.hzb[renderer.scene.hzb_current]; + renderer.transition(renderer.command, pyramid, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_ASPECT_COLOR_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 0, 1}; + copy.imageExtent = {w, h, 1}; + vkCmdCopyImageToBuffer(renderer.command, pyramid.handle, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staging.handle, 1, + ©); + renderer.transition(renderer.command, pyramid, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_ASPECT_COLOR_BIT); + renderer.submit(); + HzbDebugImage result; + result.width = w; + result.height = h; + result.rgba.resize(std::size_t(w) * h * 4); + void* mapped{}; + check(vkMapMemory(renderer.device, staging.memory, 0, staging.size, 0, &mapped), + "Map HZB debug image"); + const auto* depth = static_cast(mapped); + for (std::size_t i = 0; i < std::size_t(w) * h; ++i) { + const float value = std::isfinite(depth[i]) ? std::clamp(depth[i], 0.f, 1.f) : 1.f; + const auto gray = static_cast(std::lround(std::pow(value, 32.f) * 255)); + result.rgba[4 * i] = result.rgba[4 * i + 1] = + result.rgba[4 * i + 2] = gray; + result.rgba[4 * i + 3] = 255; + } + vkUnmapMemory(renderer.device, staging.memory); + renderer.destroy(staging); + return result; + } catch (...) { + renderer.destroy(staging); + throw; + } +} std::uint32_t Renderer::width() const { return impl_->width; } diff --git a/src/render/visibility.cpp b/src/render/visibility.cpp index 7331fbc..2b19145 100644 --- a/src/render/visibility.cpp +++ b/src/render/visibility.cpp @@ -33,7 +33,11 @@ Bounds local_bounds(const Mesh& mesh) { } Bounds transformed_bounds(const Mesh& mesh, const Mat4& model) { - const auto local = local_bounds(mesh); + return transformed_bounds(local_bounds(mesh), model); +} + +Bounds transformed_bounds(const Bounds& local, const Mat4& model) { + validate(local); for (float value : model) if (!std::isfinite(value)) throw std::invalid_argument("Nonfinite mesh transform"); diff --git a/tests/render_gpu_visibility_tests.cpp b/tests/render_gpu_visibility_tests.cpp index ccec782..ddb7266 100644 --- a/tests/render_gpu_visibility_tests.cpp +++ b/tests/render_gpu_visibility_tests.cpp @@ -4,12 +4,13 @@ #include #include #include +#include #include using namespace faset::render; namespace { -void require(bool condition, const char* message) { +void require(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); } @@ -21,6 +22,7 @@ RendererConfig config(VisibilityMode mode) { result.headless = true; result.validation = true; result.visibility_mode = mode; + result.visibility_diagnostics = true; return result; } @@ -68,6 +70,10 @@ int main() { gpu.stats().gpu_frustum_rejected == 1, "GPU frustum/indirect counts are wrong"); require(gpu.stats().validation_errors == 0, "Vulkan validation rejected GPU path"); + if (gpu.stats().gpu_ms > 0) + require(gpu.stats().gpu_main_cull_ms > 0 && + gpu.stats().gpu_main_raster_ms > 0, + "GPU visibility pass timings are missing"); require(different_pixels(direct.pixels(), gpu.pixels(), 5) < 160, "GPU and direct opaque images differ"); @@ -76,6 +82,32 @@ int main() { require(gpu.stats().gpu_visible_instances == 0 && gpu.stats().gpu_bins == 0, "Empty frame must reset indirect counts"); require(gpu.stats().validation_errors == 0, "Empty GPU frame failed validation"); + gpu.set_visibility_diagnostics(false); + gpu.render(scene()); + require(!gpu.stats().visibility_counters_valid && + gpu.stats().gpu_visibility_active, + "Normal GPU rendering must not read diagnostic visibility counters"); + gpu.set_visibility_diagnostics(true); + gpu.render(scene()); + require(gpu.stats().visibility_counters_valid && + gpu.stats().gpu_visible_instances == 1, + "Visibility counters can be enabled for diagnostics"); + Renderer occlusion(config(VisibilityMode::GpuOcclusion)); + occlusion.render(scene()); + auto hzb = occlusion.hzb_debug_image(0); + if (occlusion.stats().gpu_ms > 0) + require(occlusion.stats().gpu_hzb_ms > 0 && + occlusion.stats().gpu_post_cull_ms > 0 && + occlusion.stats().gpu_post_raster_ms > 0, + "HZB/post pass timings are missing"); + require(hzb && hzb->width == 512 && hzb->height == 256 && + hzb->rgba.size() == std::size_t(hzb->width) * hzb->height * 4, + "Current HZB debug image has wrong dimensions"); + require(std::any_of(hzb->rgba.begin(), hzb->rgba.end(), + [](std::uint8_t value) { return value < 250; }), + "Current HZB debug image contains no scene depth (min=" + + std::to_string(*std::min_element(hzb->rgba.begin(), hzb->rgba.end())) + + ")"); std::cout << "GPU frustum, fixed indirect and direct-image comparison passed\n"; return 0; } catch (const std::exception& error) { diff --git a/tests/render_reload_tests.cpp b/tests/render_reload_tests.cpp index e18c664..da9cade 100644 --- a/tests/render_reload_tests.cpp +++ b/tests/render_reload_tests.cpp @@ -65,6 +65,46 @@ int main() { configuration.validation = true; configuration.shader_directory = bundle; 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* extension : {".spv", ".reflection.json"}) { + const auto name = std::string(entry) + extension; + fs::copy_file(bundle / name, baseline_only / name); + } + auto direct_only_configuration = configuration; + direct_only_configuration.shader_directory = baseline_only; + render::Renderer direct_only(direct_only_configuration); + direct_only.render(render::Snapshot{}); + require(direct_only.stats().validation_errors == 0, + "Direct renderer starts without optional GPU shader bundle"); + bool unavailable_gpu_bundle = false; + try { + direct_only.set_visibility_mode(render::VisibilityMode::GpuFrustum); + } catch (const std::exception&) { + unavailable_gpu_bundle = true; + } + require(unavailable_gpu_bundle, + "Switching to GPU visibility reports missing optional shader bundle"); + direct_only.render(render::Snapshot{}); + require(direct_only.visibility_mode() == render::VisibilityMode::Direct && + direct_only.stats().validation_errors == 0, + "Failed GPU initialization preserves the direct renderer"); + auto gpu_configuration = configuration; + gpu_configuration.visibility_mode = render::VisibilityMode::GpuFrustum; + render::Renderer gpu_renderer(gpu_configuration); + render::Snapshot opaque_scene; + opaque_scene.eye = {0, 0, 5}; + opaque_scene.view_projection = render::multiply( + render::perspective(.8f, 1.f, .1f, 20.f), + render::look_at(opaque_scene.eye, {0, 0, 0})); + render::DrawItem opaque_cube; + opaque_cube.mesh = render::cube_mesh(); + opaque_cube.instance_key = "shader-reload-cube"; + opaque_cube.cast_shadow = false; + opaque_scene.draws.push_back(opaque_cube); + gpu_renderer.render(opaque_scene); + const auto gpu_expected = gpu_renderer.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}}); @@ -161,6 +201,12 @@ int main() { "Source-only behavior edit preserves normalized layout fingerprint"); std::string error; require(renderer.reload_shaders(error), "Compatible shader edit reloads successfully"); + require(gpu_renderer.reload_shaders(error), + "Compatible fragment edit reloads GPU scene pipeline"); + gpu_renderer.render(opaque_scene); + const auto gpu_changed = gpu_renderer.pixels(); + require(gpu_changed != gpu_expected, + "Compatible fragment reload must change GPU opaque pixels"); renderer.render(scene); const auto changed = renderer.pixels(); require(changed[0] + 50 < expected[0], "Compatible reload changes actual rendered pixels");