Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
53ea87f
bench: record CPU frame time excluding present
CanReader Nov 2, 2026
86fec83
shaders: linearize SSAO sample depth instead of two mat4 transforms
CanReader Nov 2, 2026
cd6252f
container: hash HashTable keys by value
CanReader Nov 2, 2026
ce27bd7
render: allocate queued commands from per-frame arenas
CanReader Nov 2, 2026
fe33e44
physics: query broadphase only from movable bodies
CanReader Nov 2, 2026
6553ecf
d3d11: release the replaced depth-stencil state
CanReader Nov 2, 2026
0e32ae8
vulkan: reuse a material's PBR set when it is rebound in the same frame
CanReader Nov 2, 2026
ffe9a47
container: make Queue pop amortized O(1)
CanReader Nov 2, 2026
cc7ad29
physics: enlarge dynamic proxies along velocity
CanReader Nov 2, 2026
6716d8e
transform: cache world matrix by value behind a dirty flag
CanReader Nov 2, 2026
b5e3c80
gameobject: add transform before mesh in plane and cylinder factories
CanReader Nov 2, 2026
591006e
build: enable link-time optimization for Release engine builds
CanReader Nov 2, 2026
5d2160f
build: drop math errno in Release engine builds
CanReader Nov 2, 2026
b2d9b2f
d3d11: cache rasterizer and depth-stencil states
CanReader Nov 2, 2026
3c41c3d
opengl: cache vertex layout state with separate attribute formats
CanReader Nov 2, 2026
3aba7c6
physics: test meshes in place with a bounds early-out
CanReader Nov 2, 2026
7bb24f4
opengl: restore shadow pass viewport from tracked state
CanReader Nov 2, 2026
1632d29
d3d11: bind buffers with static_cast instead of dynamic_cast
CanReader Nov 2, 2026
6389ef6
d3d11: yield instead of spinning on the stats query
CanReader Nov 2, 2026
ba331d0
assets: parse models once and post-process in one pass
CanReader Nov 2, 2026
08da833
opengl: memoize shadow caster light-space transform
CanReader Nov 2, 2026
71dd371
animation: binary search keyframes
CanReader Nov 2, 2026
bbb0bfb
d3d11: memoize shadow caster transforms per world matrix
CanReader Nov 2, 2026
5db747a
opengl: linearize ssao tap depth instead of reconstructing world pos
CanReader Nov 2, 2026
39f5fa0
container: exact-size List copies and std::sort
CanReader Nov 2, 2026
49345b2
d3d12: memoize shadow caster transforms per world matrix
CanReader Nov 2, 2026
6426ae6
math: debug-only Matrix bounds check, drop unused zero test
CanReader Nov 2, 2026
05158b8
culling: vectorize occluder triangle raster with SSE2
CanReader Nov 2, 2026
23fab58
culling: vectorize occlusion rect depth scan with SSE2
CanReader Nov 2, 2026
c96a8cc
gameobject: cache transform, material and mesh component lookups
CanReader Nov 2, 2026
9a73e71
scene: submit transform and material state only for visible meshes
CanReader Nov 2, 2026
ff401fc
mesh: transform cull bounds with Arvo's center-extent method
CanReader Nov 2, 2026
3e4e7ff
culling: fold raster depth clamp into the coverage mask
CanReader Nov 2, 2026
83530dc
style: format changed lines
CanReader Nov 2, 2026
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
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,15 @@ set_target_properties(Engine PROPERTIES
)
target_compile_features(Engine PUBLIC cxx_std_23)

# --- Release link-time optimization ---
include(CheckIPOSupported)
check_ipo_supported(RESULT ENGINE_IPO_SUPPORTED LANGUAGES CXX)
if(ENGINE_IPO_SUPPORTED)
set_property(TARGET Engine PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE ON)
endif()
target_compile_options(Engine PRIVATE
$<$<AND:$<CONFIG:Release>,$<CXX_COMPILER_ID:GNU,Clang,AppleClang>>:-fno-math-errno>)

# --- Generated version header ---
set(ENGINE_GENERATED_INCLUDE_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated/include)
configure_file(
Expand Down
18 changes: 11 additions & 7 deletions assets/shaders/ssao.frag
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ vec3 ReconstructWorldPos(vec2 uv, float depth) {
return world.xyz / world.w;
}

// View-space Z for a depth value, solved from the projection's z and w rows.
float ViewZFromDepth(float depth) {
return (Projection[3][2] - depth * Projection[3][3]) /
(depth * Projection[2][3] - Projection[2][2]);
}

void main() {
// Skip sky pixels — sky should read 1.0 (no occlusion).
float depth = texture(gDepth, fragUV).r;
Expand Down Expand Up @@ -92,24 +98,22 @@ void main() {
if (sampleUV.x < 0.0 || sampleUV.x > 1.0 ||
sampleUV.y < 0.0 || sampleUV.y > 1.0) continue;

// Reconstruct the world-space position at this screen location and
// transform into view space for depth comparison.
// View-space depth at this screen location for the comparison.
float sampleDepth = texture(gDepth, sampleUV).r;
if (sampleDepth >= 0.9999) continue; // sky — no occluder

vec3 sampleWorld = ReconstructWorldPos(sampleUV, sampleDepth);
vec3 sampleViewPos = (View * vec4(sampleWorld, 1.0)).xyz;
float sampleViewZ = ViewZFromDepth(sampleDepth);

// Range check — sample contributes less when the depth difference
// between the frag and the occluder is much larger than the radius.
// (avoids haloing around silhouettes)
float rangeCheck = smoothstep(0.0, 1.0,
Radius / max(abs(viewPos.z - sampleViewPos.z), 0.0001));
Radius / max(abs(viewPos.z - sampleViewZ), 0.0001));

// In view space the camera looks down -Z — "closer to the camera"
// means a LESS negative Z (i.e. larger value). The occluder blocks
// the sample when sampleViewPos.z >= samplePos.z + bias.
occlusion += (sampleViewPos.z >= samplePos.z + Bias ? 1.0 : 0.0) * rangeCheck;
// the sample when sampleViewZ >= samplePos.z + bias.
occlusion += (sampleViewZ >= samplePos.z + Bias ? 1.0 : 0.0) * rangeCheck;
}

float ao = 1.0 - (occlusion / float(kSize)) * Intensity;
Expand Down
Binary file modified assets/shaders/ssao.frag.spv
Binary file not shown.
26 changes: 16 additions & 10 deletions assets/shaders/ssao_gl.frag
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ vec3 ReconstructWorldPos(vec2 uv, float depth) {
return world.xyz / world.w;
}

// View-space z from depth; equals (View * ReconstructWorldPos(uv, depth)).z
// because the projection's z and w rows depend on view z alone.
float LinearizeViewZ(float depth) {
float ndcZ = depth * 2.0 - 1.0;
return (Projection[3][2] - Projection[3][3] * ndcZ)
/ (Projection[2][3] * ndcZ - Projection[2][2]);
}

void main() {
if (ssaoEnabled == 0u) {
outColor = 1.0;
Expand All @@ -76,19 +84,18 @@ void main() {
vec3 bitangent = cross(worldN, tangent);
mat3 TBN = mat3(tangent, bitangent, worldN);

// Reference view-space Z for the current fragment.
// View * worldPos here only needs to be consistent with itself — the
// comparison is self-referential, so even if the view matrix is
// column- vs row-major mis-interpreted, both sides of the compare are
// transformed the same way and the result stays meaningful.
float fragViewZ = (View * vec4(worldPos, 1.0)).z;
// Reference view-space Z for the current fragment; View's z row maps a
// world offset to its view-space z delta.
float fragViewZ = LinearizeViewZ(depth);
vec3 viewZAxis = vec3(View[0][2], View[1][2], View[2][2]);

mat4 VP = Projection * View;

float occlusion = 0.0;

for (uint i = 0u; i < ssaoKernelSize; ++i) {
vec3 sampleWorld = worldPos + (TBN * ssaoKernel[i].xyz) * ssaoRadius;
vec3 sampleOffset = (TBN * ssaoKernel[i].xyz) * ssaoRadius;
vec3 sampleWorld = worldPos + sampleOffset;

vec4 clipSample = VP * vec4(sampleWorld, 1.0);
if (clipSample.w <= 0.0) continue;
Expand All @@ -98,9 +105,8 @@ void main() {

float sampleDepth = texture(depthTexture, uv).r;
if (sampleDepth >= 1.0) continue; // sky — no occluder
vec3 actualWorld = ReconstructWorldPos(uv, sampleDepth);
float actualViewZ = (View * vec4(actualWorld, 1.0)).z;
float sampleViewZ = (View * vec4(sampleWorld, 1.0)).z;
float actualViewZ = LinearizeViewZ(sampleDepth);
float sampleViewZ = fragViewZ + dot(viewZAxis, sampleOffset);

float rangeCheck = smoothstep(0.0, 1.0,
ssaoRadius / max(abs(fragViewZ - actualViewZ), 1e-4));
Expand Down
84 changes: 72 additions & 12 deletions include/private/Graphics/Common/RenderCommandQueue.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
#define _RENDERCOMMANDQUEUE_H

#include <Graphics/Common/Renderer.hpp>
#include "RenderCommands.hpp"
#include <Utility/Container/Queue.hpp>
#include <Memory/ObjectPtr.hpp>
#include <cstddef>
#include <memory>
#include <new>
#include <utility>
#include <vector>

#include "RenderCommands.hpp"

namespace Sleak {
namespace RenderEngine {
Expand Down Expand Up @@ -61,7 +66,7 @@ namespace Sleak {

void ExecuteShadowPass(RenderContext* context);

bool HasCachedShadowDraws() const { return cachedShadowDraws.GetSize() > 0; }
bool HasCachedShadowDraws() const { return !cachedShadowDraws.empty(); }

/// Drops queued commands for the current frame, keeping the cached shadow draw list.
void Clear();
Expand All @@ -82,18 +87,73 @@ namespace Sleak {

/// A draw command paired with the transform/bone buffers it needs to replay in the shadow pass.
struct ShadowDrawEntry {
RefPtr<RenderCommandBase> command;
RefPtr<BufferBase> transformBuffer; // last-bound slot-0 buffer
RefPtr<BufferBase> boneBuffer; // slot-3 bone UBO for skinned draws
RenderCommandBase* command = nullptr;
const RefPtr<BufferBase>* transformBuffer =
nullptr; // last-bound slot-0 buffer
const RefPtr<BufferBase>* boneBuffer =
nullptr; // slot-3 bone UBO for skinned draws
};

private:
static constexpr int RETIRE_FRAMES = 3;
static RenderCommandQueue* Instance;
Queue<RefPtr<RenderCommandBase>> commands;
List<ShadowDrawEntry> cachedShadowDraws;
List<ShadowDrawEntry> m_retiredShadowDraws[RETIRE_FRAMES];
int m_retireIndex = 0;
/// Bump allocator owning one frame's commands until the slot is
/// recycled.
class CommandArena {
public:
CommandArena() = default;
CommandArena(const CommandArena&) = delete;
CommandArena& operator=(const CommandArena&) = delete;
~CommandArena() { Reset(); }

/// Constructs a command in arena memory and appends it to the
/// frame's list.
template <typename T, typename... Args>
T* Create(Args&&... args) {
void* memory = Allocate(sizeof(T), alignof(T));
T* command = new (memory) T(std::forward<Args>(args)...);
m_commands.push_back(command);
return command;
}

/// Destroys every command and rewinds the blocks for reuse.
void Reset();

const std::vector<RenderCommandBase*>& Commands() const {
return m_commands;
}

private:
struct Block {
std::unique_ptr<std::byte[]> data;
size_t size = 0;
};

/// Returns aligned storage from the current block, adding blocks
/// as needed.
void* Allocate(size_t size, size_t alignment);

static constexpr size_t BLOCK_SIZE = 64 * 1024;
std::vector<Block> m_blocks;
size_t m_blockIndex = 0;
size_t m_offset = 0;
std::vector<RenderCommandBase*> m_commands;
};

/// Moves submission to the next arena, destroying the commands it last
/// held.
void AdvanceArena();

CommandArena& CurrentArena() { return m_arenas[m_currentArena]; }

// A frame's commands stay alive while its shadow replay and GPU frames
// in flight may use them
static constexpr int RETIRE_FRAMES = 3;
static constexpr int ARENA_COUNT = RETIRE_FRAMES + 2;
static RenderCommandQueue* Instance;
CommandArena m_arenas[ARENA_COUNT];
int m_currentArena = 0;
std::vector<ShadowDrawEntry> cachedShadowDraws;
std::vector<RenderCommandBase*> m_pendingState;
std::vector<RenderCommandBase*> m_forwardCmds;
};
}
}
Expand Down
4 changes: 3 additions & 1 deletion include/private/Graphics/Common/RenderCommands.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ namespace Sleak {

RENDER_COMMAND(BindConstantBuffer)

RefPtr<BufferBase> GetBuffer() const { return constantBuffer; }
const RefPtr<BufferBase>& GetBuffer() const {
return constantBuffer;
}
int GetSlot() const { return slot; }

private:
Expand Down
43 changes: 33 additions & 10 deletions include/private/Graphics/DirectX11/DirectX11Renderer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@
#ifndef _DIRECTX11RENDERER_H
#define _DIRECTX11RENDERER_H

#include "Graphics/Common/RenderContext.hpp"
#include "Graphics/Common/Renderer.hpp"
#include "Graphics/Common/ResourceManager.hpp"
#include <Utility/Container/List.hpp>
#include <backends/imgui_impl_dx11.h>
#include <d3d11.h>
#include <Core/Window.hpp>
#include <imgui.h>
#include <backends/imgui_impl_dx11.h>
#include <chrono>
#include <wrl/client.h>

#include <Core/Timer.hpp>
#include <Core/Window.hpp>
#include <Utility/Container/List.hpp>
#include <Utility/Container/Queue.hpp>
#include <chrono>
#include <utility>
#include <vector>

#include "Graphics/Common/RenderContext.hpp"
#include "Graphics/Common/Renderer.hpp"
#include "Graphics/Common/ResourceManager.hpp"

namespace Sleak {
namespace RenderEngine {
Expand Down Expand Up @@ -112,6 +117,13 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext {
bool CreateRenderTargetView();
/// Creates and binds the rasterizer state matching current fill/cull settings.
bool SetRasterState();
/// Binds the depth state matching the tracked depth write and compare.
void ApplyDepthState();
/// Returns the cached rasterizer state for desc, creating it on a miss.
ID3D11RasterizerState* GetRasterState(const D3D11_RASTERIZER_DESC& desc);
/// Returns the cached depth-stencil state for desc, creating it on a miss.
ID3D11DepthStencilState* GetDepthState(
const D3D11_DEPTH_STENCIL_DESC& desc);

/// Creates the alpha-blend state used for transparent draws.
bool CreateBlendState();
Expand Down Expand Up @@ -141,7 +153,7 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext {
ID3D11RenderTargetView* renderTargetView; // Render target view
ID3D11InputLayout* layout;

ID3D11DepthStencilState* depthStencilState;
ID3D11DepthStencilState* depthStencilState = nullptr;
ID3D11DepthStencilView* depthStencilView;
ID3D11Texture2D* depthStencilBuffer;

Expand All @@ -151,8 +163,8 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext {
ID3D11RenderTargetView* msaaRenderTargetView = nullptr;
ID3D11Texture2D* msaaDepthStencilBuffer = nullptr;
ID3D11DepthStencilView* msaaDepthStencilView = nullptr;
ID3D11Query* query;

ID3D11Query* query = nullptr;

D3D11_PRIMITIVE_TOPOLOGY topology;
D3D11_FILL_MODE fillMode;
Expand All @@ -166,6 +178,14 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext {
bool m_depthWriteEnabled = true;
D3D11_COMPARISON_FUNC m_depthFunc = D3D11_COMPARISON_LESS;

// Immutable state objects keyed by description, owned until Cleanup
std::vector<std::pair<D3D11_RASTERIZER_DESC,
Microsoft::WRL::ComPtr<ID3D11RasterizerState>>>
m_rasterStates;
std::vector<std::pair<D3D11_DEPTH_STENCIL_DESC,
Microsoft::WRL::ComPtr<ID3D11DepthStencilState>>>
m_depthStates;

ImGuiContext* ImCon;

// Shadow mapping
Expand All @@ -186,6 +206,9 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext {
float m_pendingLightVP[16] = {};
bool m_hasPendingLightVP = false;
ID3D11Buffer* m_shadowTransformCB = nullptr; // Dedicated CB for shadow pass transforms
// World last uploaded to m_shadowTransformCB, valid until lightVP changes
float m_shadowWorldCache[16] = {};
bool m_shadowCBCacheValid = false;
bool m_inShadowPass = false;
void SetLightVP(const float* mat) override;
/// Allocates the shadow-pass depth texture, DSV/SRV, and comparison sampler.
Expand Down
3 changes: 3 additions & 0 deletions include/private/Graphics/DirectX12/DirectX12Renderer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ class ENGINE_API DirectX12Renderer : public Renderer, public RenderContext {
float m_lightVP[16] = {};
float m_pendingLightVP[16] = {};
bool m_hasPendingLightVP = false;
// Last shadow transform uploaded this pass and the World it was built from
float m_shadowWorldCache[16] = {};
D3D12_GPU_VIRTUAL_ADDRESS m_shadowCBAddress = 0;
bool m_inShadowPass = false;
// Depth-only PSO for shadow pass (no PS, no RTV, CULL_NONE)
ID3D12PipelineState* m_shadowPassPSO = nullptr;
Expand Down
Loading
Loading