Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions assets/shaders/deferred/vk_geometry_indirect.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#version 450

/**
* Deferred Geometry Pass - GPU-Driven Fragment Shader
*
* v1: shares the default material set (set 1) across every indirect draw.
* Per-object material overrides will return when bindless materials land.
*/

layout(location = 0) in vec3 v_WorldPos;
layout(location = 1) in vec3 v_Normal;
layout(location = 2) in vec2 v_TexCoord;
layout(location = 3) in vec4 v_Tangent;
layout(location = 4) flat in uint v_ObjectId;

layout(set = 1, binding = 0) uniform sampler2D u_AlbedoMap;
layout(set = 1, binding = 1) uniform sampler2D u_NormalMap;
layout(set = 1, binding = 2) uniform sampler2D u_ARMMap;

layout(location = 0) out vec4 gAlbedoMetallic;
layout(location = 1) out vec4 gNormalRoughness;
layout(location = 2) out vec4 gEmissionID;
layout(location = 3) out vec2 gVelocity;

vec2 encode_octahedron(vec3 n) {
n /= (abs(n.x) + abs(n.y) + abs(n.z));
if (n.z < 0.0) {
n.xy = (1.0 - abs(n.yx)) * vec2(
n.x >= 0.0 ? 1.0 : -1.0,
n.y >= 0.0 ? 1.0 : -1.0
);
}
return n.xy * 0.5 + 0.5;
}

vec3 apply_normal_map(vec3 normal_sample, vec3 N, vec4 T) {
vec3 ts_normal = normal_sample * 2.0 - 1.0;
vec3 T3 = normalize(T.xyz);
vec3 N3 = normalize(N);
T3 = normalize(T3 - dot(T3, N3) * N3);
vec3 B = cross(N3, T3) * T.w;
mat3 TBN = mat3(T3, B, N3);
return normalize(TBN * ts_normal);
}

void main() {
vec3 albedo_sample = texture(u_AlbedoMap, v_TexCoord).rgb;
vec3 normal_sample = texture(u_NormalMap, v_TexCoord).rgb;
vec3 arm_sample = texture(u_ARMMap, v_TexCoord).rgb;

float ao = arm_sample.r;
float roughness = arm_sample.g;
float metallic = arm_sample.b;

vec3 world_normal = apply_normal_map(normal_sample, v_Normal, v_Tangent);
vec2 encoded_normal = encode_octahedron(world_normal);

gAlbedoMetallic = vec4(albedo_sample, metallic);
gNormalRoughness = vec4(encoded_normal, roughness, ao);
gEmissionID = vec4(0.0, 0.0, 0.0, 0.0);
gVelocity = vec2(0.0, 0.0);
}
61 changes: 61 additions & 0 deletions assets/shaders/deferred/vk_geometry_indirect.vert
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#version 450

/**
* Deferred Geometry Pass - GPU-Driven Vertex Shader
*
* Same outputs as vk_geometry.vert, but reads the per-object model matrix
* from a storage buffer indexed by gl_BaseInstance (set by the cull shader's
* generated indirect draw command).
*/

layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec3 a_Normal;
layout(location = 2) in vec2 a_TexCoord;
layout(location = 3) in vec4 a_Tangent;
layout(location = 4) in ivec4 a_BoneIds;
layout(location = 5) in vec4 a_BoneWeights;

// Camera UBO (set 0, binding 0)
layout(set = 0, binding = 0) uniform CameraUBO {
mat4 view;
mat4 projection;
mat4 view_projection;
vec4 camera_position;
} camera;

// Per-object data SSBO (set 2, binding 0). Layout must match GPUObjectData.
struct GPUObjectData {
mat4 model;
mat4 inverse_transpose_model;
vec4 bounding_sphere;
uint mesh_index;
uint material_index;
uint flags;
uint _padding;
};

layout(std430, set = 2, binding = 0) readonly buffer ObjectBuffer {
GPUObjectData objects[];
};

// Outputs to fragment shader
layout(location = 0) out vec3 v_WorldPos;
layout(location = 1) out vec3 v_Normal;
layout(location = 2) out vec2 v_TexCoord;
layout(location = 3) out vec4 v_Tangent;
layout(location = 4) flat out uint v_ObjectId;

void main() {
GPUObjectData obj = objects[gl_BaseInstance];

vec4 worldPos = obj.model * vec4(a_Position, 1.0);
v_WorldPos = worldPos.xyz;

mat3 normalMatrix = mat3(obj.inverse_transpose_model);
v_Normal = normalize(normalMatrix * a_Normal);
v_TexCoord = a_TexCoord;
v_Tangent = vec4(normalize(normalMatrix * a_Tangent.xyz), a_Tangent.w);
v_ObjectId = gl_BaseInstance;

gl_Position = camera.view_projection * worldPos;
}
1 change: 1 addition & 0 deletions engine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ set(ENGINE_SOURCES
renderer/debug_renderer.cpp
renderer/cinematic_camera.cpp
renderer/gpu_scene.cpp
renderer/gpu_cull_pass.cpp

# Audio
audio/audio_engine.cpp
Expand Down
45 changes: 45 additions & 0 deletions engine/renderer/deferred_renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,51 @@
m_geometry_pipeline = m_device.create_graphics_pipeline(desc);
}

// =========================================================================
// Geometry Pipeline (GPU-driven / indirect)
// =========================================================================
{
// Object SSBO descriptor set layout (set 2)
rhi::DescriptorSetLayoutDesc obj_layout_desc;
obj_layout_desc.bindings.push_back(
rhi::DescriptorBinding::storage_buffer(0, rhi::ShaderStage::Vertex));
obj_layout_desc.debug_name = "ObjectSSBOLayout";
m_object_set_layout = m_device.create_descriptor_set_layout(obj_layout_desc);

// Pipeline layout: set 0 (camera), set 1 (material), set 2 (object SSBO).
// No push constants — model matrix comes from the SSBO.
rhi::PipelineLayoutDesc pl_desc;
pl_desc.set_layouts.push_back(m_camera_layout.get());
pl_desc.set_layouts.push_back(m_material_layout.get());
pl_desc.set_layouts.push_back(m_object_set_layout.get());
m_geometry_layout_indirect = m_device.create_pipeline_layout(pl_desc);

auto vs = m_device.create_shader_from_file(
"assets/shaders/deferred/vk_geometry_indirect.vert", rhi::ShaderStage::Vertex,
"GeometryIndirectVert");
auto fs = m_device.create_shader_from_file(
"assets/shaders/deferred/vk_geometry_indirect.frag", rhi::ShaderStage::Fragment,
"GeometryIndirectFrag");

if (vs && fs) {

Check warning on line 1331 in engine/renderer/deferred_renderer.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Reduce verbosity with "using enum" for "hz::rhi::Format".

See more on https://sonarcloud.io/project/issues?id=jackthepunished_horizon-engine&issues=AZ3QyUDNMYklAafZ3Nvm&open=AZ3QyUDNMYklAafZ3Nvm&pullRequest=3
rhi::GraphicsPipelineDesc desc;
desc.vertex_shader = vs.get();
desc.fragment_shader = fs.get();
desc.vertex_layout = rhi::VertexInputLayout::standard_vertex();
desc.topology = rhi::PrimitiveTopology::TriangleList;
desc.rasterization = rhi::RasterizationState::default_state();
desc.depth_stencil = rhi::DepthStencilState::default_state();
desc.blend = rhi::BlendState::disabled(4);
desc.multisample = {};
desc.layout = m_geometry_layout_indirect.get();
desc.color_attachment_formats = {rhi::Format::RGBA16_FLOAT, rhi::Format::RGBA16_FLOAT,
rhi::Format::RGBA16_FLOAT, rhi::Format::RG16_FLOAT};
desc.depth_attachment_format = rhi::Format::D32_FLOAT;
desc.debug_name = "GeometryIndirectPipeline";
m_geometry_pipeline_indirect = m_device.create_graphics_pipeline(desc);
}
}

// =========================================================================
// Lighting Pipeline
// =========================================================================
Expand Down
16 changes: 16 additions & 0 deletions engine/renderer/deferred_renderer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,18 @@ class DeferredRenderer {
return m_geometry_layout.get();
}

// GPU-driven indirect rendering accessors.
[[nodiscard]] const rhi::PipelineLayout* get_geometry_layout_indirect() const {
return m_geometry_layout_indirect.get();
}
[[nodiscard]] const rhi::Pipeline* get_geometry_pipeline_indirect() const {
return m_geometry_pipeline_indirect.get();
}
[[nodiscard]] const rhi::DescriptorSetLayout* get_object_set_layout() const {
return m_object_set_layout.get();
}
[[nodiscard]] const rhi::DescriptorSet* get_camera_set() const { return m_camera_set.get(); }

private:
// Debug accessors (returning Views)
[[nodiscard]] rhi::TextureView* get_gbuffer_albedo() const {
Expand Down Expand Up @@ -473,6 +485,10 @@ class DeferredRenderer {
// Pipelines
std::unique_ptr<rhi::Pipeline> m_geometry_pipeline;
std::unique_ptr<rhi::PipelineLayout> m_geometry_layout;
// GPU-driven variant: reads model matrix from object SSBO via gl_BaseInstance.
std::unique_ptr<rhi::Pipeline> m_geometry_pipeline_indirect;
std::unique_ptr<rhi::PipelineLayout> m_geometry_layout_indirect;
std::unique_ptr<rhi::DescriptorSetLayout> m_object_set_layout;
std::unique_ptr<rhi::Pipeline> m_lighting_pipeline;
std::unique_ptr<rhi::PipelineLayout> m_lighting_layout;
std::unique_ptr<rhi::Pipeline> m_composite_pipeline;
Expand Down
163 changes: 163 additions & 0 deletions engine/renderer/gpu_cull_pass.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#include "gpu_cull_pass.hpp"

#include "engine/core/log.hpp"
#include "engine/renderer/gpu_scene.hpp"
#include "engine/rhi/rhi_command_list.hpp"

namespace hz {

namespace {

// Extract 6 frustum planes (left, right, bottom, top, near, far) from a row-major
// view-projection matrix using Gribb-Hartmann. GLM stores matrices column-major,
// so we read the rows by indexing the columns.
std::array<glm::vec4, 6> extract_frustum_planes(const glm::mat4& vp) {
auto row = [&](int i) {
return glm::vec4(vp[0][i], vp[1][i], vp[2][i], vp[3][i]);
};
const glm::vec4 r0 = row(0);
const glm::vec4 r1 = row(1);
const glm::vec4 r2 = row(2);
const glm::vec4 r3 = row(3);

std::array<glm::vec4, 6> planes{
r3 + r0, // Left
r3 - r0, // Right
r3 + r1, // Bottom
r3 - r1, // Top
r3 + r2, // Near
r3 - r2, // Far
};

for (auto& p : planes) {
const float len = glm::length(glm::vec3(p));
if (len > 0.0f) p /= len;
}
return planes;
}

} // namespace

GpuCullPass::GpuCullPass(rhi::Device& device, const GPUScene& scene) : m_device(device) {
// Descriptor set layout: 4 storage buffers (object, mesh_info, draw_cmd, draw_count)
{

Check warning on line 43 in engine/renderer/gpu_cull_pass.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Reduce verbosity with "using enum" for "hz::rhi::ShaderStage".

See more on https://sonarcloud.io/project/issues?id=jackthepunished_horizon-engine&issues=AZ3QyT_tMYklAafZ3Nvg&open=AZ3QyT_tMYklAafZ3Nvg&pullRequest=3
rhi::DescriptorSetLayoutDesc desc;
desc.bindings.push_back(rhi::DescriptorBinding::storage_buffer(0, rhi::ShaderStage::Compute));
desc.bindings.push_back(rhi::DescriptorBinding::storage_buffer(1, rhi::ShaderStage::Compute));
desc.bindings.push_back(rhi::DescriptorBinding::storage_buffer(2, rhi::ShaderStage::Compute));
desc.bindings.push_back(rhi::DescriptorBinding::storage_buffer(3, rhi::ShaderStage::Compute));
desc.debug_name = "GpuCullSetLayout";
m_set_layout = device.create_descriptor_set_layout(desc);
}

// Pipeline layout with push constants
{
rhi::PipelineLayoutDesc desc;
desc.set_layouts = {m_set_layout.get()};
desc.push_constant_ranges.push_back(
{rhi::ShaderStage::Compute, 0, sizeof(PushConstants)});
desc.debug_name = "GpuCullPipelineLayout";
m_pipeline_layout = device.create_pipeline_layout(desc);
}

// Compute pipeline
auto cs = device.create_shader_from_file("assets/shaders/compute/cull.comp",
rhi::ShaderStage::Compute, "GpuCullCS");
if (!cs) {
HZ_LOG_ERROR("GpuCullPass: failed to compile cull.comp");
return;
}

rhi::ComputePipelineDesc pdesc{};
pdesc.compute_shader = cs.get();
pdesc.layout = m_pipeline_layout.get();
pdesc.debug_name = "GpuCullPipeline";
m_pipeline = device.create_compute_pipeline(pdesc);

// Descriptor pool + set
{
rhi::DescriptorPoolDesc desc{};
desc.pool_sizes = {{rhi::DescriptorType::StorageBuffer, 4}};
desc.max_sets = 1;
desc.debug_name = "GpuCullPool";
m_pool = device.create_descriptor_pool(desc);
}
m_set = m_pool->allocate(*m_set_layout);

rhi::DescriptorWrite writes[4]{

Check warning on line 87 in engine/renderer/gpu_cull_pass.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "std::array" or "std::vector" instead of a C-style array.

See more on https://sonarcloud.io/project/issues?id=jackthepunished_horizon-engine&issues=AZ3QyT_tMYklAafZ3Nvi&open=AZ3QyT_tMYklAafZ3Nvi&pullRequest=3
rhi::DescriptorWrite::storage_buffer(0, *scene.object_buffer()),
rhi::DescriptorWrite::storage_buffer(1, *scene.mesh_info_buffer()),
rhi::DescriptorWrite::storage_buffer(2, *scene.draw_command_buffer()),
rhi::DescriptorWrite::storage_buffer(3, *scene.draw_count_buffer()),
};
m_set->write({writes, 4});

HZ_LOG_INFO("GpuCullPass initialized");
}

GpuCullPass::~GpuCullPass() = default;

void GpuCullPass::execute(rhi::CommandList& cmd, const glm::mat4& view,
const glm::mat4& projection, const GPUScene& scene) {

Check warning on line 101 in engine/renderer/gpu_cull_pass.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Reduce verbosity with "using enum" for "hz::rhi::ResourceState".

See more on https://sonarcloud.io/project/issues?id=jackthepunished_horizon-engine&issues=AZ3QyT_tMYklAafZ3Nvh&open=AZ3QyT_tMYklAafZ3Nvh&pullRequest=3
if (!m_pipeline || scene.object_count() == 0) return;

HZ_DEBUG_MARKER(cmd, "GPU Cull");

// 1. Transition count buffer to CopyDest, then clear to 0.
// Source state may be Undefined (first frame) or IndirectArgument (subsequent frames).
rhi::BufferBarrier to_copy{};
to_copy.buffer = scene.draw_count_buffer();
to_copy.old_state = m_count_in_indirect_state ? rhi::ResourceState::IndirectArgument
: rhi::ResourceState::Undefined;
to_copy.new_state = rhi::ResourceState::CopyDest;
cmd.barrier(to_copy);

cmd.clear_buffer(*scene.draw_count_buffer(), 0, sizeof(u32), 0u);

// 2. Barrier: CopyDest -> UnorderedAccess (compute reads/writes count atomically)
rhi::BufferBarrier to_uav{};
to_uav.buffer = scene.draw_count_buffer();
to_uav.old_state = rhi::ResourceState::CopyDest;
to_uav.new_state = rhi::ResourceState::UnorderedAccess;
cmd.barrier(to_uav);

// Also transition draw command buffer to UnorderedAccess if it was previously IndirectArgument.
if (m_cmd_in_indirect_state) {
rhi::BufferBarrier to_uav_cmd{};
to_uav_cmd.buffer = scene.draw_command_buffer();
to_uav_cmd.old_state = rhi::ResourceState::IndirectArgument;
to_uav_cmd.new_state = rhi::ResourceState::UnorderedAccess;
cmd.barrier(to_uav_cmd);
}

// 3. Bind pipeline + descriptor set
cmd.bind_pipeline(*m_pipeline);
cmd.bind_descriptor_set(*m_pipeline_layout, 0, *m_set);

// 4. Push constants (view-proj + planes + count)
PushConstants pc{};
pc.view_proj = projection * view;
auto planes = extract_frustum_planes(pc.view_proj);
for (int i = 0; i < 6; ++i) pc.frustum_planes[i] = planes[i];
pc.object_count = scene.object_count();
cmd.push_constants(*m_pipeline_layout, rhi::ShaderStage::Compute, 0, sizeof(pc), &pc);

// 5. Dispatch (64 threads per group, ceil-div)
const u32 group_count = (pc.object_count + 63) / 64;
cmd.dispatch(group_count, 1, 1);

// 6. Barriers: compute writes -> indirect reads (for the upcoming draw_indexed_indirect_count)
rhi::BufferBarrier post_barriers[2]{};

Check warning on line 150 in engine/renderer/gpu_cull_pass.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "std::array" or "std::vector" instead of a C-style array.

See more on https://sonarcloud.io/project/issues?id=jackthepunished_horizon-engine&issues=AZ3QyT_tMYklAafZ3Nvj&open=AZ3QyT_tMYklAafZ3Nvj&pullRequest=3
post_barriers[0].buffer = scene.draw_command_buffer();
post_barriers[0].old_state = rhi::ResourceState::UnorderedAccess;
post_barriers[0].new_state = rhi::ResourceState::IndirectArgument;
post_barriers[1].buffer = scene.draw_count_buffer();
post_barriers[1].old_state = rhi::ResourceState::UnorderedAccess;
post_barriers[1].new_state = rhi::ResourceState::IndirectArgument;
cmd.barriers({post_barriers, 2});

m_count_in_indirect_state = true;
m_cmd_in_indirect_state = true;
}

} // namespace hz
Loading
Loading