From a570741ee279f6d25721b16fa95193f725a8c9c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:42:55 +0000 Subject: [PATCH 01/21] ci: bump actions/github-script from 7 to 9 Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 9. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v7...v9) --- updated-dependencies: - dependency-name: actions/github-script dependency-version: '9' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/engine-update.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/engine-update.yml b/.github/workflows/engine-update.yml index d7083f4..52bcbf3 100644 --- a/.github/workflows/engine-update.yml +++ b/.github/workflows/engine-update.yml @@ -60,7 +60,7 @@ jobs: - name: Open an issue if the engine broke the game if: failure() - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const sha = context.payload.client_payload?.sha || 'main'; From 0128d9a5452e834a5c10954eb9e765befa6d81ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:43:00 +0000 Subject: [PATCH 02/21] ci: bump actions/checkout from 4 to 7 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/engine-update.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55bd10b..5fcb86c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,12 +44,12 @@ jobs: # Engine/ is a tracked symlink to ../SleakEngine, so the two repos must # sit side by side in the workspace for it to resolve. - name: Check out the game - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: path: SleakCraft - name: Check out the engine - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: CanReader/SleakEngine ref: ${{ github.event.inputs.engine_ref || 'main' }} @@ -147,12 +147,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out the game - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: path: SleakCraft - name: Check out the engine - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: CanReader/SleakEngine path: SleakEngine @@ -203,12 +203,12 @@ jobs: timeout-minutes: 90 steps: - name: Check out the game - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: path: SleakCraft - name: Check out the engine - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: CanReader/SleakEngine submodules: recursive diff --git a/.github/workflows/engine-update.yml b/.github/workflows/engine-update.yml index d7083f4..e1cff08 100644 --- a/.github/workflows/engine-update.yml +++ b/.github/workflows/engine-update.yml @@ -20,12 +20,12 @@ jobs: timeout-minutes: 90 steps: - name: Check out the game - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: path: SleakCraft - name: Check out the engine - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: CanReader/SleakEngine ref: ${{ github.event.client_payload.sha || 'main' }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ac608f8..23239db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,12 +19,12 @@ jobs: os: [ubuntu-latest, windows-latest] steps: - name: Check out the game - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: path: SleakCraft - name: Check out the engine - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: CanReader/SleakEngine submodules: recursive From 1b125914a83a5e6f7e4cefba66e706eb4eae785d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:43:02 +0000 Subject: [PATCH 03/21] ci: bump actions/cache from 4 to 6 Bumps [actions/cache](https://github.com/actions/cache) from 4 to 6. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/cache dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- .github/workflows/engine-update.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55bd10b..67b58d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,7 +93,7 @@ jobs: - name: Cache compiler output if: runner.os == 'Linux' - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.ccache key: ccache-craft-${{ runner.os }}-${{ matrix.name }}-${{ github.sha }} @@ -228,7 +228,7 @@ jobs: libasound2-dev libpulse-dev libudev-dev libdbus-1-dev \ glslc || sudo apt-get install -y glslang-tools - - uses: actions/cache@v4 + - uses: actions/cache@v6 with: path: ~/.ccache key: ccache-craft-smoke-${{ github.sha }} diff --git a/.github/workflows/engine-update.yml b/.github/workflows/engine-update.yml index d7083f4..a06375f 100644 --- a/.github/workflows/engine-update.yml +++ b/.github/workflows/engine-update.yml @@ -45,7 +45,7 @@ jobs: glslc || sudo apt-get install -y glslang-tools - name: Cache compiler output - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.ccache key: ccache-craft-engineupdate-${{ github.run_id }} From ad49470d6539236e4685c45801114896407bd370 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:44:52 +0000 Subject: [PATCH 04/21] ci: bump dependabot/fetch-metadata from 2 to 3 Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 2 to 3. - [Release notes](https://github.com/dependabot/fetch-metadata/releases) - [Commits](https://github.com/dependabot/fetch-metadata/compare/v2...v3) --- updated-dependencies: - dependency-name: dependabot/fetch-metadata dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/dependabot-auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 63386e8..810458a 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Fetch update metadata id: meta - uses: dependabot/fetch-metadata@v2 + uses: dependabot/fetch-metadata@v3 with: github-token: ${{ secrets.GITHUB_TOKEN }} From 0acc0384b65bea1b8a4fb11111742ad30bfa0ddf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:45:33 +0000 Subject: [PATCH 05/21] ci: bump softprops/action-gh-release from 2 to 3 Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ac608f8..07b1fd5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,7 +85,7 @@ jobs: ls -la - name: Attach to the release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: files: | SleakCraft/dist/*.tar.gz From d1b0209d925af7b6dbef38de46db8349d3ce94f8 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Thu, 8 Oct 2026 20:07:50 +0300 Subject: [PATCH 06/21] feat: support the engine's gpu profiling and frame capture flags List --gpuprofile, -capture and -capture-frame in the help text, and load chunks on the main thread during a capture so chunk completion order, and with it the captured frame, is the same on every run. --- Client/src/main.cpp | 3 +++ Game/src/MainScene.cpp | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/Client/src/main.cpp b/Client/src/main.cpp index 5d9bd28..3682138 100644 --- a/Client/src/main.cpp +++ b/Client/src/main.cpp @@ -30,6 +30,9 @@ static void PrintHelp(const char* exe) { << " --no-vsync Disable VSync on launch\n" << "\nBenchmark\n" << " --bench Start benchmark recording immediately\n" + << " --gpuprofile Log per-pass GPU timings (Vulkan)\n" + << " -capture Write frame N as a PPM, then exit (Vulkan)\n" + << " -capture-frame Frame index for -capture (default: 600)\n" << "\nDebug\n" << " --validate Enable Vulkan validation layer\n" << "\nMisc\n" diff --git a/Game/src/MainScene.cpp b/Game/src/MainScene.cpp index 4f1fac9..f18a442 100644 --- a/Game/src/MainScene.cpp +++ b/Game/src/MainScene.cpp @@ -73,6 +73,10 @@ bool MainScene::Initialize() { m_chunkManager.Initialize(this, m_blockMaterial); m_blockEffects.Initialize(this, m_blockMaterial); + // Captures load synchronously + if (!Sleak::CommandLine::GetValue("-capture").empty()) + m_multithreadedLoading = false; + if (m_isNewWorld) { m_chunkManager.SetSeed(m_worldSeed); From 7d21d030e78e356eb62892a4a2831700dab181e0 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 00:42:18 +0300 Subject: [PATCH 07/21] feat: add a debug-only --autofly flag for streaming tests --autofly flies the player along +Z at a fixed height with no input, so chunk streaming can be exercised and benchmarked. Like -world it only exists in Debug builds. Help text also lists --syncval. --- Client/src/main.cpp | 13 ++++++++- Game/include/Game.hpp | 6 +++++ Game/include/Player/PlayerController.hpp | 6 +++++ Game/src/MainScene.cpp | 9 ++++++- Game/src/Player/PlayerController.cpp | 34 ++++++++++++++++++++++++ 5 files changed, 66 insertions(+), 2 deletions(-) diff --git a/Client/src/main.cpp b/Client/src/main.cpp index 3682138..6df1e5f 100644 --- a/Client/src/main.cpp +++ b/Client/src/main.cpp @@ -30,11 +30,13 @@ static void PrintHelp(const char* exe) { << " --no-vsync Disable VSync on launch\n" << "\nBenchmark\n" << " --bench Start benchmark recording immediately\n" + << " --autofly Autopilot flight speed (Debug builds)\n" << " --gpuprofile Log per-pass GPU timings (Vulkan)\n" << " -capture Write frame N as a PPM, then exit (Vulkan)\n" << " -capture-frame Frame index for -capture (default: 600)\n" << "\nDebug\n" - << " --validate Enable Vulkan validation layer\n" + << " --validate Enable Vulkan validation (Debug: always on)\n" + << " --syncval Enable Vulkan synchronization validation\n" << "\nMisc\n" << " --help Show this message\n\n"; } @@ -59,6 +61,15 @@ int main(int argc, char** argv) { }; Game* game = new Game(); +#ifdef DEBUG + const std::string autoFly = defaults.CommandLineArgs["--autofly"]; + if (!autoFly.empty()) { + try { + game->SetAutoFlySpeed(std::stof(autoFly)); + } catch (...) { + } + } +#endif Sleak::Application app(defaults); return app.Run(game); diff --git a/Game/include/Game.hpp b/Game/include/Game.hpp index 3a05760..3428dca 100644 --- a/Game/include/Game.hpp +++ b/Game/include/Game.hpp @@ -38,8 +38,14 @@ class SLEAK_API Game : public Sleak::GameBase { inline bool GetIsGameRunning() { return bIsGameRunning; } + /// Autopilot speed in m/s that new game scenes fly at, 0 for none. + void SetAutoFlySpeed(float speed) { m_autoFlySpeed = speed; } + /// Autopilot speed in m/s set by SetAutoFlySpeed(). + float GetAutoFlySpeed() const { return m_autoFlySpeed; } + private: bool bIsGameRunning = true; + float m_autoFlySpeed = 0.0f; MainMenuScene* m_menuScene = nullptr; MainScene* m_gameScene = nullptr; }; diff --git a/Game/include/Player/PlayerController.hpp b/Game/include/Player/PlayerController.hpp index e8ef2ff..1fb8199 100644 --- a/Game/include/Player/PlayerController.hpp +++ b/Game/include/Player/PlayerController.hpp @@ -23,11 +23,17 @@ class PlayerController { void OnKeyReleased(const Sleak::Events::Input::KeyReleasedEvent& e); /// Applies fly vertical input and resolves voxel collision for this frame. void Update(); + /// Starts a hands-free flight along +Z just above the build limit so + /// tests can drive chunk streaming. Speed is in m/s; zero or less is + /// ignored. + void StartAutoFly(float speed); private: ChunkManager& m_chunkManager; MainScene& m_scene; + float m_autoFlySpeed = 0.0f; + // Minecraft-style double-tap space to toggle fly bool m_flying = false; float m_lastSpacePressTime = -1.0f; diff --git a/Game/src/MainScene.cpp b/Game/src/MainScene.cpp index f18a442..2be3b59 100644 --- a/Game/src/MainScene.cpp +++ b/Game/src/MainScene.cpp @@ -113,8 +113,15 @@ bool MainScene::Initialize() { lm->SetFogDistances(fogDist * 0.91f, fogDist); } - // Register game-specific benchmark metrics + // Autopilot hides UI panels auto* app = Sleak::Application::GetInstance(); + auto* game = app ? static_cast(app->GetGame()) : nullptr; + if (game && game->GetAutoFlySpeed() > 0.0f) { + m_playerController.StartAutoFly(game->GetAutoFlySpeed()); + m_showUI = false; + } + + // Register game-specific benchmark metrics if (app && app->GetBenchmark()) { app->GetBenchmark()->RegisterMetric("RenderDistance", [this]() { return static_cast(m_chunkManager.GetRenderDistance()); diff --git a/Game/src/Player/PlayerController.cpp b/Game/src/Player/PlayerController.cpp index 77ab9b7..7fd4c5f 100644 --- a/Game/src/Player/PlayerController.cpp +++ b/Game/src/Player/PlayerController.cpp @@ -10,6 +10,11 @@ using namespace Sleak; +namespace { +// Autopilot view, about 27 degrees below its +Z heading +const Math::Vector3D kAutoFlyLook(0.0f, -0.5f, 1.0f); +} // namespace + void PlayerController::ApplyTuning() { auto* cam = m_scene.GetActiveCamera(); if (!cam) return; @@ -80,10 +85,39 @@ void PlayerController::OnKeyReleased( m_shiftHeld = false; } +void PlayerController::StartAutoFly(float speed) { + auto* cam = m_scene.GetActiveCamera(); + if (!cam || speed <= 0.0f) return; + m_autoFlySpeed = speed; + + // Above the build limit + const float altitude = + static_cast((WorldGenerator::MAX_CHUNK_Y + 1) * Chunk::SIZE) + + 8.0f; + auto pos = cam->GetPosition(); + cam->SetPosition({pos.GetX(), altitude, pos.GetZ()}); + cam->SetDirection(kAutoFlyLook); + + if (auto* fpc = cam->GetComponent()) + fpc->SetEnabled(false); + if (auto* rb = cam->GetComponent()) + rb->SetUseGravity(false); +} + void PlayerController::Update() { auto* cam = m_scene.GetActiveCamera(); if (!cam) return; + // Autopilot: physics integrates velocity + if (m_autoFlySpeed > 0.0f) { + if (auto* rb = cam->GetComponent()) { + rb->SetUseGravity(false); + rb->SetVelocity(Math::Vector3D::Forward() * m_autoFlySpeed); + } + cam->SetDirection(kAutoFlyLook); + return; + } + // Fly: space=up, ctrl=down, shift=sprint if (m_flying) { auto* rb = cam->GetComponent(); From ba423998c436c2a28e2931e0f679efb03ceb9a4e Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 02:01:38 +0300 Subject: [PATCH 08/21] fix: stop tonemapping vulkan water twice The Vulkan water shader applied ACES and then fog in LDR, but it draws into the HDR scene that the composite tonemaps again, so water came out pale and washed out. It now outputs linear HDR with fog applied the way the deferred lighting pass fogs terrain, and the composite does the only tonemap. --- Game/assets/shaders/water_shader.frag | 5 ++--- Game/assets/shaders/water_shader.frag.spv | Bin 23084 -> 22572 bytes 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Game/assets/shaders/water_shader.frag b/Game/assets/shaders/water_shader.frag index b29c149..b4b4474 100644 --- a/Game/assets/shaders/water_shader.frag +++ b/Game/assets/shaders/water_shader.frag @@ -114,7 +114,6 @@ vec3 SampleSky(vec3 dir, vec3 sunDir, vec3 sunColor, vec3 amb) { // ============================================================ // Helpers // ============================================================ -vec3 ACESFilm(vec3 x) { return clamp((x*(2.51*x+0.03))/(x*(2.43*x+0.59)+0.14), vec3(0), vec3(1)); } float FresnelSchlick(float c) { return 0.02 + 0.98 * pow(clamp(1.0 - c, 0.0, 1.0), 5.0); } float GGX(float NdotH, float r) { float a=r*r*r*r; float d=NdotH*NdotH*(a-1.0)+1.0; return a/(3.14159265*d*d); } @@ -191,9 +190,9 @@ void main() { finalColor += sunCol * c * 0.05 * shadow; } - // ---- ACES + fog ---- - finalColor = ACESFilm(finalColor); + // ---- Fog in linear HDR (composite tonemaps) ---- finalColor = ApplyFog(finalColor, fragWorldPos); + finalColor = clamp(finalColor, vec3(0.0), vec3(1000.0)); // ---- Alpha: BSL waterAlpha=0.70 base, Fresnel boosts edges to ~0.95 ---- float alpha = mix(0.72, 0.97, pow(clamp(1.0 - NdotV, 0.0, 1.0), 2.0)); diff --git a/Game/assets/shaders/water_shader.frag.spv b/Game/assets/shaders/water_shader.frag.spv index d1058da4c31a3afa65d6dd5d5e09a2128ac91c3a..5a559c9931c88e39f5c3c34fb551dfc469950475 100644 GIT binary patch literal 22572 zcmZ9T2bf(|)rK$3B(%_bPv{8JLJuV*ga8H-2)%_NnMnpFGht>z3L*(bsuZP207076 z00Pp4grZUs5D`&95eukb2Q0tkf8TS@cR9J|nf>hV`_|fPuU*bQ=N>}K(wi<*t1VSq zp|)b})s|Xuty)_erG_@Js7H?(KW4kW{z=>Iyvq(ctW>Kz>a#Llt<;vOZJpgzwFMYn zSh`kwnetD{bGW}mS%r4`(IKdXUTU>x@olXwUmHDU!sxMMM~@jbVdAuD6Z-mRb+u3E z>g;ZxFsZY*ePVy-8SVNlTk-4cp4c^gQq^Gq9@RVs5O)RoIH|iq_3{0^?cJ06r(j#Quz8=9ZH{Q4$-;EbF7{FST2^~8mbxYSa_Y=?7CX9oTz!0h zTd#u|$ZR=R-XrRxx+gWXL1+^?dM1x+Q>*8?V(E8Wdv|C5l+07zwvx8`y+EWRdR(WB zW921m`?q)Zb@tDy*jHJ?en4ASS5HSr#qRz1Y3xzmZKrg#PeOBl%%k@5p1Fm3T3c^h z_RAgczP7sGUEql&o)7Nr=`Y$pDrStvS_pRIiJztJEbxHZtLnvBa7bHy{|%@u$A;a&m$0?g-dTdT)PAY&&UN4Smaw(f-d}>Z)P4`2 z$pSdGKY?eL_}^gH)&4x(r}Xr8ats!HS@`T?-_+Z!3hz5@RuN+zaT#L_xUHPeBf!W- z`>Ejmo&%VSO#3ynd#)*8TwYTu$4+!8___e3Ac0 z@M^z*hu8PHu?7~;Zu6}lTO)gVdc99tYyWC$H-N*?e#VqJ4uVhW?C6-@*IxLq2A?r; z{OKM#wqHwL&ZKj@zT9W&h@MIH{nJw09Idaf4{J+pcktn5?7hLg?H!D^9%~2W#E&oiM;5sasI|d6I=kDt z8e?s#b)!w>5OG|+;QIb*sZCce$8+c#_2&VwcwexN#;sjwp4HK8vnge8MWc&C+o8}} zi`bk7MEF05$FmfEEi&F7+iS5!2giE1}j zG@plRw^g)3rFLgU^LeP>0~O8ZpxVNU=JQYO`HJS))n2G*KIhaHQATs0_-q?O@e1Dr z?7Yi;E>-Pv?JcF%`+hF>GM?}1^0fE8y=wOzUXEMg?|!QGFW&^4 z*XnrbH-u=&1zro}`VFS6O@FSNT))9^*G$ba1nzoiSJ&V9mYcu+&b7Yk`a93s)wQoq zoj!)J1J@|+#?xlZbxX}0ZL=P=ZPabEeu>p>GX(44il&}o4qL*_q3%1NwoQdc+X2n^ zX}1mSY_lt6T(;_n_EDKW%FgXHV+E6ffJ!{f*-s zhaU`fyoq}lm}}qIkF4{C|1t2TC~FYqc{{|aPz+xtZ(-DeI-{-e+lfFvMzUx=ZCqNuO(OT`mFw9ME#8aO6j2H_~o7@UdDG1{g7h( z4d8E3n~z%Z{So-}LwXm4zp3q$wV%$K{ur)Lp%cUWenMG1Y@bG(pTfJATduKx-YR|6 z6ZdWKvum%_X!A4pjdRvokbZs+*GD~V-T}Wc^Tnsbe*r)LgV!2+@Ljk*>hbv{_==ln zHro6O{@(mijof|>*GD~VegnSnqqm+4e-D1!v>O|9@IG80_4xc2{Day_jeGug@OQ?Z z*ci|6;rghj%?IGypIU04@DJgiZ1wg&nS(#T^-+({AHmN*cv+*(pWtg=x_l$IKg0D= zPn*AhfA+}pjW&OUf7G=_V-EfX*GD~VJ^~+o{9hVvK87E-V8zCGK7s3_o;H66&%Ju} z;{0)6{DZRb`P($c^G~=w>S^;Y@N=(k(`fU5@Ig!8(-_ac;rghj&40juePDQ_&41w^ zb$!z4=YMd0)YHZrXxKXw8f}(>?{d-;Pi3x`hU=rAHZ9-{U!2}((+Yo}<$^{(1K|3o zZ_AUy`)MH9d&E8O9)6eN9&^vQ2V7Iv$T@coofF4s&gNiD@8xBwy*G_%KG##ProF;9 zg7eqNcZj4mXTSJw3U?hco*m&G9q%>PbZ7Xy>#l3?J>f%#KGfhN;Fmu;y}=KLpK$qz z20s%1+{bq|_zCd&qu*`Z11G~fI<{!cUk99LMuVRUA3F4J4c-HPclH_$-dFf9YQ&#a z@Jk!^v)~;a1BYjP=fdZYZfn>tfHR&3zp`jQsNsKgVISY%*BAB=8~!&HeA|ZoHu${j z)@|f-H@sFG-muSycXWKQ!5@MT9lCmhKMG&GXwlxT@6%xZis!#)wcPhSHSG&a?D}Po zzKmuLCsnk!ORk=ISc(VK(4j*c<5(Jg{s)IOcuQgL__PtR?+jo5&0F^h-=*N2 zHulx7bvx&NJ&)bsw$V>p+6@DfN28yS@B`nS-I#|H;cu;cP+|A}YKQxq!S?bVxbIi4 z$22g1#k)uBXCf?L`nyi&!PkEDwcTUC2<~rz@GIaeRrs}V-*sbu1ioB_KMr3J?*4E; z_}))daQBPv{qnSb56)k~&F3$Wp4HCX)BOKY`X-*--wA22|9{}@&&G4_zXc!DSjW|w zjP>4ssId-fz~BAF$&LN6CR`tN&l2-8?nZFVoyNQmDR@VN?*QNWgq0g{c7(gfT}y3= zb9mu*d87Z6;I-P|#=e*c%Q z%V6hLUcJJ7H(Z2|e#T!+?WJZsxxe$gcIBx;c+~o({0|hLHTwE{$8TglkL2r7`#r46 z{q_~E|0@-)zu&%Mm;3#z%KiRT<$nJPH=f_Xs@!j2Rqi*iD)$>$xc8jjy26d;x2|yW z@mp7w`>m_W*J|Q^>x$iYe&-4|p5MBv-0xjg?)R=LAKt|M-c_~xy{pRo-W6{De)Fnw zzj;-;-@K~aZ(iZ%>o>0|Z>w;B&vaL~{rSx+{&K&2g&WUrUsdk6uW+lPJYUXyEE7u#a6XPq|pnLO=Y1?#WPJtyCzd@g(+ybBBEWhOJ|A%lji^)|Xb>Gx;g^sCK&z4z49?>k`o^?fq^{sK)s z{k{uUvv{T7{TSbk@L!@Q#=&soxURoK*LGPsH@^lOOWj<&@70pa`(WevEK4rG1*<2Q z-+|REUd_4u9z8LtxqN`GExCLMHkP`%c;=|tuj}|nu=^$aPhi*VD#qg2{tVVfeGt1@ z?XO_-*{Zbr3^umsm$uBy$6z&Wo?Y@i%DA6^x2C9v-1G(=Hz5w#W4rc8fYr45jwKf#NqsoQcM<)L0&hpHkG8Zw8tnO={df#qU-kGL3$C88 z$HDbex39yf)iT}_z}0i@M7TZ~+XS#H*qI}?K^8f1#BC2^J=43+mHKwA%QxJFb0as6qnP4@)SH))* z+*r<^b1@sNpSt&s+L>UQ6z{y+)92^G_F0|Fv*7CK^9x|LYM*Dr?K3`e!D@3TIhW4? z+g4lhI2W8ew5R=fU~|+S?R>DhYnZWpQG2O5htt7o8P5ga<~h6&O+9fh0;~B=GIMw_ zygHss;cAyqGM+DiZL2N)T?THRzsu3o)8Ca~wJRtY$Ctsz)0VmU3OIA4J$+mSZtml1 zH1+gxEm-XuO2+h6aCJ=A!PR{JC69S<+iFW5*MpOX_VjTB*c`P-yAiBjU8AqT)zy;A zO>oyL{oV{#(>Lp=7XPn<^-n*yfYpqdv+Y*6dHRl^-T96G?O^SC{d;D816(~FYjl8^yV?@A+Ug zeVrR^YX9W)nM&=MWu5j9fc4S$erkDa-voP}X?qYX&u`|3z(eWJHrmpcdfGn>Hg?YP z$H19GZI4hMrKGKP+j{1F8(e))dkXG*ntImcX>j6c&zfY77nXL{Sl3YCZ?X@r3Xj!i#9O*I&D_ z&EYv}ZU13}yHou3=Dhgb*~{<0yW!_I7r!mLu7jxa-E9}J&lzKh_n_oV*bA&SjFK_! z4OW-e+tuS{4)(=nn>^cwgViivjx+B``+@hN45#E=7y;JDT(#$38ws{=?eX0o9AEAK zWo&BIc~}+h<#%!C!Ebl|_iE>1HR{a6Xt3jvJ69J}xX;b2DqQ<56|Vh`CVp3i-v^#w z;rc(?#1~fhGvH?{-2ZmxzbiFA=h^*mAjNUy8^%GUrtamuEKSK;eh#dE_4~ybxO(2L z4gssBTv(nJhoTu@yZgksQp=i;1*>Jv#}!y!zs$w+%l!TKdfGi>$CcXnlB;<>>30N0 zUu_w~QDFNyl9GGnXt2Is=BAHY+8hhEjqe_5a~#+<=4Bgw)Y9ezaQbnK-cKh|)E^^1 z-~A?(nrF$K_^6#sQTJW&6lyQu1y81Kqo}#>#OZ4yxVf)MXzF>->;S8Iu6aHuj`hdP zmVHmA*tdIHfBQ&YQ^4BI>r`ql^HT4msF{~Id7TC}uj)Hn7hFAaFcqxk9GF+)SSPP; zuzj1W{`Qf)dcfMv>vU=_^HQHiQ8O=b^6CXQ=hcU%p84trt7X0t$2xgU2iv!~>Te&( z>kP1V^O{NRWnSttC~D>foxDB|wr_LQ-#(JpSzztv zbvCt^d8vPaqGn#=;2z zCFL@TK8gQjus+rJ`uXg!%~vSqpr7Z(1*Mk$uZCCqzXq;m@iMmSaU=EDDDx;cP;$21 zRO&eoZ-%Q`JcG^Q>lEYOOfiRBsMYeF_Ezu;6!knaZwIU2MzPIR)N1MX8(_6OPwxbq z>xvX@w(+^9o;G)bt8MOq+h!$-Hrw1qt)4dbgVn+x02?>_!IJxJY9RQV6t&nN0^2_G z^Dx+$#)hXE9^m&3h`FtC!o_T%>Y&-R6 z3&846Q|$XuYBljf>XSvr@0OgyWzm)g7w<3B`Y(gF0$BY@eB^_`##GnGeXt_fdCR-QN^t$u=a%hO2Aiig z`&%8n3fOTBqa@aZVHB0PXT8rYmu0x&Q2HO*3nF{y+=?b^az9r9CcYqrwIqw8l^WV?KXK0hp zE^syfOiJ+SA84usLea zoF4{u-V*O{u$sP^Q?=wY9^5>qN1&;vpQFHPM^e)6Xs~Nlozr9B>goGfux-`T?l^Gs z+#Qdmp8ie*tDQi}d3h4pc-k^A6Ttix^P)ZN+rZjRret1D0axc`B3w=1%!^v`oCI#3 zmv%Ju^fMW()TMmRM84&2!a_rk+^Sz-m2|#5x^p zJZ+h)UNC>fTxn1Hez3MaO6F=hxH?y7z}584T&X3`8Q|u*nu(^KerAK!W>M1aOmOoa z`aGI?+I<16b`~Xj;cT$+v}JsA!2A{C)1E%&f}7{?95nUxaUNLhTuR!V4>q^zz4}GC zdfHtGR=a>Qw>(EK0vk_Ta=RGJUy+;kw7&#w-rA#G3Rd5flJ)o!xVj#f!PWH5dZ;D$ z%fZd-aRr)s`uQ?g?Mh18eFfaScdtTIPrGZtYFAS-=4-*m)0Q!R70h2TX6@S=c~SnVcC_R816#?zMEZUOUGC3c{Iv?#|BVXQ|3^*yCl#*$&nsO2cbfPwD_sBgDqR2foA~dW_@7Gdx-JAiLs>wn z-WSi7dcF-k2UqjG%kL`R0ekryPTTVoHRn$3S~%a|1smJ75Np>j_7}mcJ&E<8tn#Idp zyg$t4hiKZ9i)*ZwT;2pH7uQ>!Tz(8T7u#qv7x#>Ma``E^n#)^oHH%kuUVetA{cVb| z+&^l`WtEc@3VCO3QL$K?ZbL$Uaebn>2@Q+~iohiojeo#w{KZETj{4ZeV zATj?6)<-=t{{~i1Oz$bR#P|qoKRMSv2CL~GpHINHi_hP|YWn1N`ai(7^>?Cn&rk2G ze}efd{vD7$-d}3fvu<xNYDCKbGC1#brSKHY*kXW+kyoiorg(?zfHWt;e_ z74Dg|PRa8u`492TrF!Q47fn6?-SdB7{)&Gar!C*4%tf2?=v)jSwmE0c2Ev`g6n}T? zw+uRR@w>YfY&(736UBJIi8%;t%=~xl72#?p6CnMp3}2~I($6Yr>1PG7ZS}Pu=Sh3| zSq*I8;j4r7k^4_cDkahDb z@C#oLz6m`4O>}*@KI+M71F$)5MbVyha?Q0R)`npBS;n>zT%W`l49?iJZA{7h4*}a= zpJO~-^`VEzx!o;)*0V$@TwHgJaNuE@0YD9URzQ;^E`Xpm(I~=sEwiD{D^9!b&1Jz<{*fp?~8Gk1A4cjv;(xlHccR`6%R zj$?c3oXcZ~k#pFw9#Qg)^ANc0!VfKZ@*E4-M?F5{ntTp}>!%){!<&4@!}U{l4b4j} z{zrlxOZZVGPaa3Z^-+({F<|3ljgAGYxlU zoLi@Yy*#(HbyCzEtJpZs&uL)i!?_b{*Dv;NuybTC)4-=w%%!LF@xKf9mU{l(NgrIz z;^qA;bWho5Kg78DxmTQTwe)!gSS`O#W`NZ!URL={I}>935fuH5=X$AUu4aSPGM}FZ zJBBkWK4*c|v+utEww-#~oegeYhdF5K@tIrtRAZlmrk>d6f^DasbM1Vv<0#HY@OhN% z@h^hSN1vRN7l3n4YR@_8_}!n{#qLdQ$?sygW4MSq^K%JUJ^$_hQm~rOE}wyyfxX;I z+P*|lbFYXK`%18}ub@usFN4+faj$;`?0%{~gReqU&-`2sww-$B=NfS4N4sNkeBNu? zlEYWQ=5Q@_a<~qx9zG9jyF7ER2dgE<4RT`m-%HcxMl|)DS6>6$PCa|^Ca^jBokM%Z zb2Hd;QG42d9c*6OvZl9yv!>dsYw9{`_xYT;w@!YyfnE3Xc{|uX)#Gyq*f{a|M(LxT zyzT_sR$Kh;0vk)4bMASjW?b**d%)hW`JHqx*l~<1ZML}^O+EWyK3J`~5AKJnCzl7n zYVO0i>>r=!55m>eGH>66XWq1D-eP~Kw0q9QW*hUmk6OEVdA@y%;@Uq#?f5+39xe5p zZ;!#%EME34OpGTW#(11MF`fjgC&stIY8J1=$XY#xZf<#}dm5~k;@F(~1!%VQJ;uC@ wtFL2wm|A<*X(8A-2!95yPx^co?AoT!=fG+y>GOHC^l41v=<72?o6q0>2X#`{WdHyG literal 23084 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 From fa6709a82c7ecb94c0fd772b7b5e8c8a891893df Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 02:01:38 +0300 Subject: [PATCH 09/21] perf: cheaper wave normals and back-face shadows in the vulkan water shader The wave normal re-evaluated the six-term height field four times for a central difference; the same difference is now computed in closed form, which differs only by float rounding. The 16-tap shadow PCF is skipped where the facing smoothstep zeroes it, with explicit-LOD taps after the branch. --- Game/assets/shaders/water_shader.frag | 45 +++++++++++++++------- Game/assets/shaders/water_shader.frag.spv | Bin 22572 -> 22872 bytes 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/Game/assets/shaders/water_shader.frag b/Game/assets/shaders/water_shader.frag index b4b4474..eae4383 100644 --- a/Game/assets/shaders/water_shader.frag +++ b/Game/assets/shaders/water_shader.frag @@ -61,7 +61,7 @@ float CalcShadow(vec4 sc) { float rad = uShadowTexelSize * uLightSize * 6.0; float s = 0.0; for (int i = 0; i < 16; i++) - s += texture(shadowMap, vec3(p.xy + rot * disk[i] * rad, p.z - uShadowBias)); + s += textureLod(shadowMap, vec3(p.xy + rot * disk[i] * rad, p.z - uShadowBias), 0.0); return mix(1.0, s / 16.0, uShadowStrength * fc.x * fc.y); } @@ -82,19 +82,34 @@ float WaterHeight(vec2 xz, float time) { return h; } -// Finite-difference normal — purely fragment-shader, no vertex displacement +// Central difference (step WD) of WaterHeight in closed form: for +// A*sin(k*x+c) it is A*cos(k*x+c)*sin(k*WD)/WD, so no height re-evaluation. +const float WD = 0.15; vec3 WaveNormal(vec3 wp, float time) { if (fragNormal.y < 0.5) return fragNormal; // side faces keep flat normal - const float d = 0.15; - float h1 = WaterHeight(wp.xz + vec2( d, 0.0), time); - float h2 = WaterHeight(wp.xz - vec2( d, 0.0), time); - float h3 = WaterHeight(wp.xz + vec2(0.0, d), time); - float h4 = WaterHeight(wp.xz - vec2(0.0, d), time); - - float xd = (h2 - h1) / (2.0 * d); - float zd = (h4 - h3) / (2.0 * d); - return normalize(vec3(xd * 0.30, 1.0, zd * 0.30)); + vec2 w1 = vec2( time * 0.45, time * 0.30); + vec2 w2 = vec2(-time * 0.35, time * 0.50); + float a1 = wp.x * 0.24 + w1.x, b1 = wp.z * 0.19 + w1.y; + float a2 = wp.x * 0.33 + w2.x - wp.z * 0.11; + float a3 = wp.x * 0.80 + w1.x * 1.9 + wp.z * 0.58; + float a4 = wp.x * 1.10 - w2.x * 2.1, b4 = wp.z * 0.88 + w2.y * 1.4; + float a5 = wp.x * 2.20 + w1.x * 3.3, b5 = wp.z * 1.85 - w1.y * 2.7; + float a6 = wp.x * 3.00 - w2.x * 4.2 + wp.z * 2.30; + float ca2 = cos(a2), ca3 = cos(a3), ca6 = cos(a6); + float dHdx = cos(a1) * sin(b1) * (sin(0.24 * WD) / WD) + + 0.65 * ca2 * (sin(0.33 * WD) / WD) + + 0.40 * ca3 * (sin(0.80 * WD) / WD) + + 0.35 * cos(a4) * sin(b4) * (sin(1.10 * WD) / WD) + + 0.15 * cos(a5) * sin(b5) * (sin(2.20 * WD) / WD) + + 0.10 * ca6 * (sin(3.00 * WD) / WD); + float dHdz = sin(a1) * cos(b1) * (sin(0.19 * WD) / WD) + - 0.65 * ca2 * (sin(0.11 * WD) / WD) + + 0.40 * ca3 * (sin(0.58 * WD) / WD) + + 0.35 * sin(a4) * cos(b4) * (sin(0.88 * WD) / WD) + + 0.15 * sin(a5) * cos(b5) * (sin(1.85 * WD) / WD) + + 0.10 * ca6 * (sin(2.30 * WD) / WD); + return normalize(vec3(-dHdx * 0.30, 1.0, -dHdz * 0.30)); } // ============================================================ @@ -152,9 +167,11 @@ void main() { float fresnel = FresnelSchlick(NdotV); // ---- Shadow & diffuse ---- - float shadow = CalcShadow(fragShadowCoord); - shadow *= smoothstep(-0.1, 0.2, dot(N, sunDir)); - float NdotL = clamp(dot(N, sunDir) * 0.65 + 0.35, 0.0, 1.0); + float NdotS = dot(N, sunDir); + float shadow = 0.0; + if (NdotS > -0.1) + shadow = CalcShadow(fragShadowCoord) * smoothstep(-0.1, 0.2, NdotS); + float NdotL = clamp(NdotS * 0.65 + 0.35, 0.0, 1.0); vec3 diffuse = sunCol * NdotL * shadow; // ---- BSL water color: R=64 G=160 B=255 I=0.35, then squared ---- diff --git a/Game/assets/shaders/water_shader.frag.spv b/Game/assets/shaders/water_shader.frag.spv index 5a559c9931c88e39f5c3c34fb551dfc469950475..3f9fab828ee0a044dcc7cf62662c34822d2e5947 100644 GIT binary patch literal 22872 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 From 55266c8d24e0861b71e586f5731a1bca69fccbd1 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Fri, 9 Oct 2026 01:59:38 +0300 Subject: [PATCH 10/21] perf: cull shadow casters outside the light's ortho box A visible column within the caster distance now casts only if its bounds are not wholly outside a clip plane of the next shadow pass's light view-projection. Visibility runs after the light update, so the matrix is exactly the one that pass replays this frame's casters with; the 2 m margin only absorbs float error. At -rd 12 shadow draws drop from 167 to 128 at the spawn view and from about 208 to 151 while flying. --- Game/include/World/ChunkManager.hpp | 8 ++++++ Game/src/MainScene.cpp | 3 ++ Game/src/World/ChunkManager.cpp | 5 ++++ Game/src/World/ChunkRenderer.cpp | 44 +++++++++++++++++++++++++++-- 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/Game/include/World/ChunkManager.hpp b/Game/include/World/ChunkManager.hpp index e252743..703f0f8 100644 --- a/Game/include/World/ChunkManager.hpp +++ b/Game/include/World/ChunkManager.hpp @@ -79,6 +79,8 @@ class ChunkManager { float GetDrawDistance() const { return m_drawDistance; } void SetShadowCasterDistance(float dist) { m_shadowCasterDistSq = dist * dist; } + /// Stores the next shadow pass light view-projection (nullptr: none). + void SetShadowLightVP(const float* vp); void SetSeed(uint32_t seed) { m_generator.SetSeed(seed); } uint32_t GetSeed() const { return m_generator.GetSeed(); } @@ -147,6 +149,10 @@ class ChunkManager { void SetOOMThisFrame(bool oom) { m_oomThisFrame = oom; } float GetDrawDistSq() const { return m_drawDistSq; } float GetShadowCasterDistSq() const { return m_shadowCasterDistSq; } + /// Light view-projection used to cull shadow casters, or nullptr. + const float* GetShadowLightVP() const { + return m_hasShadowLightVP ? m_shadowLightVP : nullptr; + } const Sleak::RefPtr& GetMaterial() const { return m_material; } const Sleak::RefPtr& GetWaterMaterial() const { return m_waterMaterial; } @@ -222,6 +228,8 @@ class ChunkManager { float m_drawDistance = 96.0f; float m_drawDistSq = 96.0f * 96.0f; float m_shadowCasterDistSq = 96.0f * 96.0f; + float m_shadowLightVP[16] = {}; + bool m_hasShadowLightVP = false; int m_lastCenterX = INT_MAX; int m_lastCenterY = INT_MAX; int m_lastCenterZ = INT_MAX; diff --git a/Game/src/MainScene.cpp b/Game/src/MainScene.cpp index 2be3b59..d0bd82f 100644 --- a/Game/src/MainScene.cpp +++ b/Game/src/MainScene.cpp @@ -285,6 +285,9 @@ void MainScene::Update(float deltaTime) { m_playerController.Update(); + // Next shadow pass matrix + auto* lm = GetLightManager(); + m_chunkManager.SetShadowLightVP(lm ? lm->GetShadowLightVP() : nullptr); m_chunkManager.Update(pos.GetX(), pos.GetY(), pos.GetZ()); m_chunkManager.RenderColumns(); diff --git a/Game/src/World/ChunkManager.cpp b/Game/src/World/ChunkManager.cpp index 56109cb..c848c22 100644 --- a/Game/src/World/ChunkManager.cpp +++ b/Game/src/World/ChunkManager.cpp @@ -984,6 +984,11 @@ void ChunkManager::ForceReload() { void ChunkManager::UpdateVisibility() { m_renderer.UpdateVisibility(); } +void ChunkManager::SetShadowLightVP(const float* vp) { + m_hasShadowLightVP = vp != nullptr; + if (vp) std::memcpy(m_shadowLightVP, vp, sizeof(m_shadowLightVP)); +} + void ChunkManager::SetCullingEnabled(bool frustum, bool occlusion) { m_renderer.SetCullingEnabled(frustum, occlusion); } diff --git a/Game/src/World/ChunkRenderer.cpp b/Game/src/World/ChunkRenderer.cpp index 407059c..f91fa35 100644 --- a/Game/src/World/ChunkRenderer.cpp +++ b/Game/src/World/ChunkRenderer.cpp @@ -6,10 +6,43 @@ #include #include +namespace { + +/// True unless the box lies wholly outside one clip plane of the light +/// view-projection (row vectors, depth range [0, 1], no depth clamp). +bool OverlapsLightClip(const Sleak::Math::AABB& box, const float* m) { + constexpr float kMargin = 2.0f; + const float lo[3] = {box.min.GetX() - kMargin, box.min.GetY() - kMargin, + box.min.GetZ() - kMargin}; + const float hi[3] = {box.max.GetX() + kMargin, box.max.GetY() + kMargin, + box.max.GetZ() + kMargin}; + bool left = true, right = true, bottom = true, top = true; + bool nearOut = true, farOut = true; + for (int i = 0; i < 8; ++i) { + const float x = (i & 1) ? hi[0] : lo[0]; + const float y = (i & 2) ? hi[1] : lo[1]; + const float z = (i & 4) ? hi[2] : lo[2]; + const float cx = x * m[0] + y * m[4] + z * m[8] + m[12]; + const float cy = x * m[1] + y * m[5] + z * m[9] + m[13]; + const float cz = x * m[2] + y * m[6] + z * m[10] + m[14]; + const float cw = x * m[3] + y * m[7] + z * m[11] + m[15]; + left = left && cx < -cw; + right = right && cx > cw; + bottom = bottom && cy < -cw; + top = top && cy > cw; + nearOut = nearOut && cz < 0.0f; + farOut = farOut && cz > cw; + } + return !(left || right || bottom || top || nearOut || farOut); +} + +} // namespace + void ChunkRenderer::UpdateVisibility() { const auto& camPos = Sleak::Camera::GetMainCameraPosition(); float camX = camPos.GetX(); float camZ = camPos.GetZ(); + const float* lightVP = m_mgr.GetShadowLightVP(); // Force-render columns near the player regardless of camera frustum, so // terrain above caves/enclosed spaces stays in the shadow map. @@ -34,8 +67,8 @@ void ChunkRenderer::UpdateVisibility() { float dz = (camZ < minZ) ? (minZ - camZ) : (camZ > maxZ) ? (camZ - maxZ) : 0.0f; col.distSq = dx * dx + dz * dz; - // Shadow-caster cull: only columns within the configured caster - // distance render into the shadow map. + // Shadow-caster cull: within caster distance, then (once visible) + // inside the next shadow pass light box. col.castsShadow = (col.distSq <= m_mgr.GetShadowCasterDistSq()); if (col.distSq > m_mgr.GetDrawDistSq()) { @@ -53,6 +86,8 @@ void ChunkRenderer::UpdateVisibility() { if (col.distSq <= SHADOW_FORCE_DIST * SHADOW_FORCE_DIST) { col.visible = true; // force-visible, skip occlusion test + if (col.castsShadow && lightVP) + col.castsShadow = OverlapsLightClip(col.bounds, lightVP); continue; } @@ -63,8 +98,11 @@ void ChunkRenderer::UpdateVisibility() { Sleak::CullingSystem::FinalizeOccluders(); // Pass B: frustum + occlusion query for remaining candidates. - for (ColumnMesh* col : m_cullCandidates) + for (ColumnMesh* col : m_cullCandidates) { col->visible = Sleak::CullingSystem::IsVisible(col->bounds); + if (col->visible && col->castsShadow && lightVP) + col->castsShadow = OverlapsLightClip(col->bounds, lightVP); + } } void ChunkRenderer::SetCullingEnabled(bool frustum, bool occlusion) { From de469aed53310fbf9331277da7b712b8ce78d44a Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Sun, 18 Oct 2026 23:11:01 +0300 Subject: [PATCH 11/21] refactor: name the autofly height clearance The altitude margin above the build limit is now AUTOFLY_CLEARANCE instead of a bare number. --- Game/src/Player/PlayerController.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Game/src/Player/PlayerController.cpp b/Game/src/Player/PlayerController.cpp index 7fd4c5f..06ea07e 100644 --- a/Game/src/Player/PlayerController.cpp +++ b/Game/src/Player/PlayerController.cpp @@ -12,6 +12,7 @@ using namespace Sleak; namespace { // Autopilot view, about 27 degrees below its +Z heading +#define AUTOFLY_CLEARANCE 8.0f const Math::Vector3D kAutoFlyLook(0.0f, -0.5f, 1.0f); } // namespace @@ -93,7 +94,7 @@ void PlayerController::StartAutoFly(float speed) { // Above the build limit const float altitude = static_cast((WorldGenerator::MAX_CHUNK_Y + 1) * Chunk::SIZE) + - 8.0f; + AUTOFLY_CLEARANCE; auto pos = cam->GetPosition(); cam->SetPosition({pos.GetX(), altitude, pos.GetZ()}); cam->SetDirection(kAutoFlyLook); From 6b7bf48ac612d053eb57a1373a86b33e56ef1fc0 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 19 Oct 2026 00:07:15 +0300 Subject: [PATCH 12/21] feat: add a debug-only -campose flag that pins the camera -campose x,y,z,yaw,pitch freezes the camera of a new game scene at a fixed pose with gravity, input and the HUD off, so captures and screenshots frame the same view on every backend. --- Client/src/main.cpp | 13 ++++++++++ Game/include/Game.hpp | 10 ++++++++ Game/include/MainScene.hpp | 1 + Game/include/Player/CameraPose.hpp | 19 +++++++++++++++ Game/include/Player/PlayerController.hpp | 9 +++++++ Game/src/MainScene.cpp | 9 +++++-- Game/src/Player/PlayerController.cpp | 30 ++++++++++++++++++++++++ 7 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 Game/include/Player/CameraPose.hpp diff --git a/Client/src/main.cpp b/Client/src/main.cpp index 6df1e5f..66ecd91 100644 --- a/Client/src/main.cpp +++ b/Client/src/main.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include #include @@ -31,6 +33,8 @@ static void PrintHelp(const char* exe) { << "\nBenchmark\n" << " --bench Start benchmark recording immediately\n" << " --autofly Autopilot flight speed (Debug builds)\n" + << " -campose x,y,z,yaw,pitch\n" + << " Pin the camera, degrees (Debug builds)\n" << " --gpuprofile Log per-pass GPU timings (Vulkan)\n" << " -capture Write frame N as a PPM, then exit (Vulkan)\n" << " -capture-frame Frame index for -capture (default: 600)\n" @@ -69,6 +73,15 @@ int main(int argc, char** argv) { } catch (...) { } } + // Raw argv: CommandLine drops values that start with '-' + for (int i = 1; i + 1 < argc; ++i) { + CameraPose pose; + if (std::strcmp(argv[i], "-campose") == 0 && + std::sscanf(argv[i + 1], "%f,%f,%f,%f,%f", &pose.x, &pose.y, + &pose.z, &pose.yawDeg, + &pose.pitchDeg) == CAMERA_POSE_FIELD_COUNT) + game->SetCameraPose(pose); + } #endif Sleak::Application app(defaults); diff --git a/Game/include/Game.hpp b/Game/include/Game.hpp index 3428dca..ac0a7e6 100644 --- a/Game/include/Game.hpp +++ b/Game/include/Game.hpp @@ -5,6 +5,9 @@ #include #include #include +#include + +#include "Player/CameraPose.hpp" class MainMenuScene; class MainScene; @@ -42,10 +45,17 @@ class SLEAK_API Game : public Sleak::GameBase { void SetAutoFlySpeed(float speed) { m_autoFlySpeed = speed; } /// Autopilot speed in m/s set by SetAutoFlySpeed(). float GetAutoFlySpeed() const { return m_autoFlySpeed; } + /// Pins the camera of new game scenes at a fixed pose. + void SetCameraPose(const CameraPose& pose) { m_cameraPose = pose; } + /// Pose set by SetCameraPose(), if any. + const std::optional& GetCameraPose() const { + return m_cameraPose; + } private: bool bIsGameRunning = true; float m_autoFlySpeed = 0.0f; + std::optional m_cameraPose; MainMenuScene* m_menuScene = nullptr; MainScene* m_gameScene = nullptr; }; diff --git a/Game/include/MainScene.hpp b/Game/include/MainScene.hpp index 1dc542f..dc154cb 100644 --- a/Game/include/MainScene.hpp +++ b/Game/include/MainScene.hpp @@ -130,6 +130,7 @@ class MainScene : public Sleak::Scene { // UI state bool m_showUI = true; + bool m_showHud = true; // crosshair and hotbar, off for pinned captures bool m_showCrosshair = true; // Save/load UI feedback diff --git a/Game/include/Player/CameraPose.hpp b/Game/include/Player/CameraPose.hpp new file mode 100644 index 0000000..85e7cf2 --- /dev/null +++ b/Game/include/Player/CameraPose.hpp @@ -0,0 +1,19 @@ +#ifndef _CAMERA_POSE_HPP_ +#define _CAMERA_POSE_HPP_ + +/// A fixed camera placement for Debug captures: world position plus yaw and +/// pitch in degrees, where positive pitch looks down. +/// @ingroup player +struct CameraPose { + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + float yawDeg = 0.0f; + float pitchDeg = 0.0f; +}; + +/// Number of floats in a CameraPose, as written on the command line. +#define CAMERA_POSE_FIELD_COUNT \ + static_cast(sizeof(CameraPose) / sizeof(float)) + +#endif diff --git a/Game/include/Player/PlayerController.hpp b/Game/include/Player/PlayerController.hpp index 1fb8199..064239c 100644 --- a/Game/include/Player/PlayerController.hpp +++ b/Game/include/Player/PlayerController.hpp @@ -2,6 +2,9 @@ #define _PLAYER_CONTROLLER_HPP_ #include +#include + +#include "Player/CameraPose.hpp" class ChunkManager; class MainScene; @@ -27,12 +30,18 @@ class PlayerController { /// tests can drive chunk streaming. Speed is in m/s; zero or less is /// ignored. void StartAutoFly(float speed); + /// Freezes the camera at a fixed pose so captures frame the same view on + /// every backend. + void PinCamera(const CameraPose& pose); private: ChunkManager& m_chunkManager; MainScene& m_scene; float m_autoFlySpeed = 0.0f; + bool m_pinned = false; + Sleak::Math::Vector3D m_pinPosition; + Sleak::Math::Vector3D m_pinDirection; // Minecraft-style double-tap space to toggle fly bool m_flying = false; diff --git a/Game/src/MainScene.cpp b/Game/src/MainScene.cpp index d0bd82f..b2458cb 100644 --- a/Game/src/MainScene.cpp +++ b/Game/src/MainScene.cpp @@ -120,6 +120,11 @@ bool MainScene::Initialize() { m_playerController.StartAutoFly(game->GetAutoFlySpeed()); m_showUI = false; } + if (game && game->GetCameraPose()) { + m_playerController.PinCamera(*game->GetCameraPose()); + m_showUI = false; + m_showHud = false; + } // Register game-specific benchmark metrics if (app && app->GetBenchmark()) { @@ -310,11 +315,11 @@ void MainScene::Update(float deltaTime) { if (m_saveMessageTimer > 0.0f) m_saveMessageTimer -= deltaTime; if (m_loadConfirmTimer > 0.0f) m_loadConfirmTimer -= deltaTime; - m_hudPanel.RenderCrosshair(); + if (m_showHud) m_hudPanel.RenderCrosshair(); if (m_showUI) RenderUI(); - m_hotbar.Render(); + if (m_showHud) m_hotbar.Render(); } } diff --git a/Game/src/Player/PlayerController.cpp b/Game/src/Player/PlayerController.cpp index 06ea07e..563b000 100644 --- a/Game/src/Player/PlayerController.cpp +++ b/Game/src/Player/PlayerController.cpp @@ -5,8 +5,10 @@ #include #include #include