Add instanced GPU frustum culling and indirect draws
This commit is contained in:
@@ -60,6 +60,7 @@ endif()
|
||||
if(FASET_BUILD_RENDERER AND EXISTS "${PROJECT_SOURCE_DIR}/cmake/Renderer.cmake")
|
||||
include(cmake/Renderer.cmake)
|
||||
include(cmake/Visibility.cmake)
|
||||
include(cmake/GpuVisibility.cmake)
|
||||
endif()
|
||||
if(TARGET faset_gameplay AND EXISTS "${PROJECT_SOURCE_DIR}/cmake/Player.cmake")
|
||||
include(cmake/Player.cmake)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
if(TARGET faset_render AND BUILD_TESTING)
|
||||
add_executable(faset_render_gpu_visibility_tests
|
||||
"${PROJECT_SOURCE_DIR}/tests/render_gpu_visibility_tests.cpp")
|
||||
target_link_libraries(faset_render_gpu_visibility_tests PRIVATE faset_render)
|
||||
add_test(NAME gpu_visibility COMMAND faset_render_gpu_visibility_tests)
|
||||
endif()
|
||||
@@ -97,6 +97,7 @@ struct Snapshot {
|
||||
std::string view_id{};
|
||||
bool camera_cut{};
|
||||
};
|
||||
enum class VisibilityMode { Direct, GpuFrustum, GpuOcclusion };
|
||||
// CPU-only validation used before publishing a game or creating Vulkan pipelines.
|
||||
void validate_shader_bundle(const std::filesystem::path& directory);
|
||||
|
||||
@@ -105,6 +106,7 @@ struct RendererConfig {
|
||||
std::string title{"Faset Engine"};
|
||||
bool headless{false};
|
||||
bool validation{true};
|
||||
VisibilityMode visibility_mode{VisibilityMode::Direct};
|
||||
// Optional isolated shader bundle, useful for editor preview and shader reload tests.
|
||||
std::filesystem::path shader_directory{};
|
||||
};
|
||||
@@ -140,6 +142,10 @@ struct FrameStats {
|
||||
std::uint64_t gpu_allocated_bytes{};
|
||||
std::uint32_t texture_count{};
|
||||
std::uint32_t vertices{}, draw_calls{}, culled_meshes{}, validation_errors{};
|
||||
bool gpu_visibility_active{}, hzb_valid{};
|
||||
std::uint32_t gpu_bins{}, gpu_visible_instances{}, gpu_frustum_rejected{};
|
||||
std::uint32_t gpu_occlusion_deferred{}, gpu_post_visible{};
|
||||
std::array<std::uint32_t, 4> lod_counts{};
|
||||
double cpu_ms{}, gpu_ms{}, readback_cpu_ms{};
|
||||
std::string device;
|
||||
};
|
||||
@@ -154,6 +160,8 @@ class Renderer {
|
||||
std::vector<Event> poll_events();
|
||||
void render(const Snapshot&);
|
||||
void resize(std::uint32_t width, std::uint32_t height);
|
||||
void set_visibility_mode(VisibilityMode);
|
||||
VisibilityMode visibility_mode() const;
|
||||
// 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.
|
||||
|
||||
+850
-18
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <faset/render/renderer.hpp>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
using namespace faset::render;
|
||||
|
||||
namespace {
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition)
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
RendererConfig config(VisibilityMode mode) {
|
||||
RendererConfig result;
|
||||
result.width = 320;
|
||||
result.height = 240;
|
||||
result.headless = true;
|
||||
result.validation = true;
|
||||
result.visibility_mode = mode;
|
||||
return result;
|
||||
}
|
||||
|
||||
Snapshot scene() {
|
||||
Snapshot frame;
|
||||
frame.view_id = "gpu-visibility-test";
|
||||
frame.eye = {4, 3, 6};
|
||||
frame.view_projection =
|
||||
multiply(perspective(.85f, 320.f / 240.f, .1f, 100.f), look_at(frame.eye, {0, 0, 0}));
|
||||
DrawItem visible;
|
||||
visible.mesh = cube_mesh();
|
||||
visible.model = transform({0, 0, 0});
|
||||
visible.color = {.8f, .6f, .3f, 1};
|
||||
visible.instance_key = "visible-cube";
|
||||
frame.draws.push_back(visible);
|
||||
DrawItem hidden = visible;
|
||||
hidden.model = transform({100, 0, 0});
|
||||
hidden.instance_key = "outside-cube";
|
||||
frame.draws.push_back(hidden);
|
||||
return frame;
|
||||
}
|
||||
|
||||
std::size_t different_pixels(const std::vector<std::uint8_t>& a,
|
||||
const std::vector<std::uint8_t>& b, int tolerance) {
|
||||
require(a.size() == b.size(), "Image sizes differ");
|
||||
std::size_t result{};
|
||||
for (std::size_t i = 0; i < a.size(); i += 4)
|
||||
if (std::abs(int(a[i]) - int(b[i])) > tolerance ||
|
||||
std::abs(int(a[i + 1]) - int(b[i + 1])) > tolerance ||
|
||||
std::abs(int(a[i + 2]) - int(b[i + 2])) > tolerance)
|
||||
++result;
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
try {
|
||||
Renderer direct(config(VisibilityMode::Direct));
|
||||
Renderer gpu(config(VisibilityMode::GpuFrustum));
|
||||
auto frame = scene();
|
||||
direct.render(frame);
|
||||
gpu.render(frame);
|
||||
require(gpu.stats().gpu_visibility_active, "GPU visibility path did not run");
|
||||
require(gpu.stats().gpu_bins == 1 && gpu.stats().gpu_visible_instances == 1 &&
|
||||
gpu.stats().gpu_frustum_rejected == 1,
|
||||
"GPU frustum/indirect counts are wrong");
|
||||
require(gpu.stats().validation_errors == 0, "Vulkan validation rejected GPU path");
|
||||
require(different_pixels(direct.pixels(), gpu.pixels(), 5) < 160,
|
||||
"GPU and direct opaque images differ");
|
||||
|
||||
frame.draws.clear();
|
||||
gpu.render(frame);
|
||||
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");
|
||||
std::cout << "GPU frustum, fixed indirect and direct-image comparison passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user