From 3fd33ecff7472e4d6fc9e6b3905f56982e497ef2 Mon Sep 17 00:00:00 2001 From: hachem Date: Sun, 23 Aug 2026 17:21:37 +0200 Subject: [feat]: complete RHI api --- README.md | 6 +- assets/hdri/HDR_blue_nebulae-1.hdr | Bin 62208386 -> 0 bytes assets/hdri/HDR_subdued_blue_nebulae.hdr | Bin 62041084 -> 0 bytes assets/hdri/HDR_subdued_multi_nebulae.hdr | Bin 64541302 -> 0 bytes assets/hdri/hdr_blue_nebulae_1.hdr | Bin 0 -> 62208386 bytes assets/hdri/hdr_subdued_blue_nebulae.hdr | Bin 0 -> 62041084 bytes assets/hdri/hdr_subdued_multi_nebulae.hdr | Bin 0 -> 64541302 bytes assets/shaders/Blur.slang | 60 - assets/shaders/EquirectToCubemap.slang | 33 - assets/shaders/Geodesic.slang | 438 ------- assets/shaders/Grid.slang | 34 - assets/shaders/Skybox.slang | 23 - assets/shaders/Sphere.slang | 67 - assets/shaders/TexturedQuad.slang | 28 - assets/shaders/VkPipelineTest.slang | 25 - assets/shaders/blur.slang | 60 + assets/shaders/equirect_to_cubemap.slang | 33 + assets/shaders/geodesic.slang | 438 +++++++ assets/shaders/grid.slang | 39 + assets/shaders/skybox.slang | 29 + assets/shaders/sphere.slang | 74 ++ assets/shaders/textured_quad.slang | 28 + assets/shaders/vk_pipeline_test.slang | 25 + branding/Logo-Black.jpg | Bin 31049 -> 0 bytes branding/Logo-Monochrome-Black.jpg | Bin 25555 -> 0 bytes branding/Logo-Monochrome-White.jpg | Bin 26074 -> 0 bytes branding/Logo-White.jpg | Bin 31870 -> 0 bytes branding/Screenshot1.png | Bin 566012 -> 0 bytes branding/Screenshot2.png | Bin 916806 -> 0 bytes branding/logo_black.jpg | Bin 0 -> 31049 bytes branding/logo_monochrome_black.jpg | Bin 0 -> 25555 bytes branding/logo_monochrome_white.jpg | Bin 0 -> 26074 bytes branding/logo_white.jpg | Bin 0 -> 31870 bytes branding/screenshot_1.png | Bin 0 -> 566012 bytes branding/screenshot_2.png | Bin 0 -> 916806 bytes src/core/application.cpp | 422 +++--- src/core/application.h | 64 +- src/core/hdri_manager.h | 6 +- src/core/settings_manager.cpp | 1 - src/core/state.h | 19 - src/core/state_manager.cpp | 88 -- src/core/state_manager.h | 41 - src/core/window.cpp | 105 +- src/core/window.h | 12 - src/engine/engine.cpp | 625 --------- src/engine/engine.h | 191 --- src/engine/object.h | 76 -- src/engine/scene.h | 70 - src/platform/metal/metal_context.h | 16 - src/platform/metal/metal_context.mm | 124 -- src/platform/opengl/opengl_device.cpp | 279 ++++ src/platform/opengl/opengl_device.h | 11 + src/platform/opengl/opengl_renderer.cpp | 377 ------ src/platform/opengl/opengl_renderer.h | 72 - src/platform/vulkan/vulkan_context.cpp | 789 ----------- src/platform/vulkan/vulkan_context.h | 41 - src/platform/vulkan/vulkan_device.cpp | 1254 ++++++++++++++++++ src/platform/vulkan/vulkan_device.h | 17 + src/platform/vulkan/vulkan_renderer.cpp | 2038 ----------------------------- src/platform/vulkan/vulkan_renderer.h | 50 - src/rendering/black_hole_renderer.cpp | 144 ++ src/rendering/black_hole_renderer.h | 55 + src/rendering/framebuffer.cpp | 251 ---- src/rendering/framebuffer.h | 80 -- src/rendering/index_buffer.cpp | 34 - src/rendering/index_buffer.h | 23 - src/rendering/render_api.h | 25 + src/rendering/renderer.cpp | 168 --- src/rendering/renderer.h | 155 --- src/rendering/renderer_backend.h | 53 +- src/rendering/rhi.h | 132 ++ src/rendering/rhi/rhi.h | 131 -- src/rendering/scene_renderer.cpp | 217 +++ src/rendering/scene_renderer.h | 56 + src/rendering/texture_manager.cpp | 34 - src/rendering/texture_manager.h | 19 - src/rendering/uniform_buffer.cpp | 36 - src/rendering/uniform_buffer.h | 24 - src/rendering/vertex_array.cpp | 61 - src/rendering/vertex_array.h | 35 - src/rendering/vertex_buffer.cpp | 50 - src/rendering/vertex_buffer.h | 78 -- src/states/config_state.cpp | 307 ----- src/states/config_state.h | 47 - src/states/simulation_state.cpp | 386 ------ src/states/simulation_state.h | 27 - src/states/world_builder_state.cpp | 1113 ---------------- src/states/world_builder_state.h | 96 -- tools/compile-shaders.sh | 2 +- 89 files changed, 3241 insertions(+), 8826 deletions(-) delete mode 100644 assets/hdri/HDR_blue_nebulae-1.hdr delete mode 100644 assets/hdri/HDR_subdued_blue_nebulae.hdr delete mode 100644 assets/hdri/HDR_subdued_multi_nebulae.hdr create mode 100644 assets/hdri/hdr_blue_nebulae_1.hdr create mode 100644 assets/hdri/hdr_subdued_blue_nebulae.hdr create mode 100644 assets/hdri/hdr_subdued_multi_nebulae.hdr delete mode 100644 assets/shaders/Blur.slang delete mode 100644 assets/shaders/EquirectToCubemap.slang delete mode 100644 assets/shaders/Geodesic.slang delete mode 100644 assets/shaders/Grid.slang delete mode 100644 assets/shaders/Skybox.slang delete mode 100644 assets/shaders/Sphere.slang delete mode 100644 assets/shaders/TexturedQuad.slang delete mode 100644 assets/shaders/VkPipelineTest.slang create mode 100644 assets/shaders/blur.slang create mode 100644 assets/shaders/equirect_to_cubemap.slang create mode 100644 assets/shaders/geodesic.slang create mode 100644 assets/shaders/grid.slang create mode 100644 assets/shaders/skybox.slang create mode 100644 assets/shaders/sphere.slang create mode 100644 assets/shaders/textured_quad.slang create mode 100644 assets/shaders/vk_pipeline_test.slang delete mode 100644 branding/Logo-Black.jpg delete mode 100644 branding/Logo-Monochrome-Black.jpg delete mode 100644 branding/Logo-Monochrome-White.jpg delete mode 100644 branding/Logo-White.jpg delete mode 100644 branding/Screenshot1.png delete mode 100644 branding/Screenshot2.png create mode 100644 branding/logo_black.jpg create mode 100644 branding/logo_monochrome_black.jpg create mode 100644 branding/logo_monochrome_white.jpg create mode 100644 branding/logo_white.jpg create mode 100644 branding/screenshot_1.png create mode 100644 branding/screenshot_2.png delete mode 100644 src/core/state.h delete mode 100644 src/core/state_manager.cpp delete mode 100644 src/core/state_manager.h delete mode 100644 src/engine/engine.cpp delete mode 100644 src/engine/engine.h delete mode 100644 src/engine/object.h delete mode 100644 src/engine/scene.h delete mode 100644 src/platform/metal/metal_context.h delete mode 100644 src/platform/metal/metal_context.mm create mode 100644 src/platform/opengl/opengl_device.cpp create mode 100644 src/platform/opengl/opengl_device.h delete mode 100644 src/platform/opengl/opengl_renderer.cpp delete mode 100644 src/platform/opengl/opengl_renderer.h delete mode 100644 src/platform/vulkan/vulkan_context.cpp delete mode 100644 src/platform/vulkan/vulkan_context.h create mode 100644 src/platform/vulkan/vulkan_device.cpp create mode 100644 src/platform/vulkan/vulkan_device.h delete mode 100644 src/platform/vulkan/vulkan_renderer.cpp delete mode 100644 src/platform/vulkan/vulkan_renderer.h create mode 100644 src/rendering/black_hole_renderer.cpp create mode 100644 src/rendering/black_hole_renderer.h delete mode 100644 src/rendering/framebuffer.cpp delete mode 100644 src/rendering/framebuffer.h delete mode 100644 src/rendering/index_buffer.cpp delete mode 100644 src/rendering/index_buffer.h create mode 100644 src/rendering/render_api.h delete mode 100644 src/rendering/renderer.cpp delete mode 100644 src/rendering/renderer.h create mode 100644 src/rendering/rhi.h delete mode 100644 src/rendering/rhi/rhi.h create mode 100644 src/rendering/scene_renderer.cpp create mode 100644 src/rendering/scene_renderer.h delete mode 100644 src/rendering/texture_manager.cpp delete mode 100644 src/rendering/texture_manager.h delete mode 100644 src/rendering/uniform_buffer.cpp delete mode 100644 src/rendering/uniform_buffer.h delete mode 100644 src/rendering/vertex_array.cpp delete mode 100644 src/rendering/vertex_array.h delete mode 100644 src/rendering/vertex_buffer.cpp delete mode 100644 src/rendering/vertex_buffer.h delete mode 100644 src/states/config_state.cpp delete mode 100644 src/states/config_state.h delete mode 100644 src/states/simulation_state.cpp delete mode 100644 src/states/simulation_state.h delete mode 100644 src/states/world_builder_state.cpp delete mode 100644 src/states/world_builder_state.h diff --git a/README.md b/README.md index 4931a65..bf6df12 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -![Logo](branding/Logo-Monochrome-White.jpg) +![Logo](branding/logo_monochrome_white.jpg) Donut is a real-time renderer for Sagittarius A* (Sgr A*) that traces light through the curved spacetime surrounding the black hole. Each pixel is represented by a light ray originating from the camera. The ray is integrated through the Schwarzschild metric, allowing the renderer to reproduce gravitational lensing and the distortion of the surrounding accretion disk and background. The renderer runs on the GPU using compute shaders, allowing the scene to be explored interactively in real time. ## Screenshots -![Black Hole with Accretion Disk](branding/Screenshot1.png) -![Black Hole with HDRI](branding/Screenshot2.png) +![Black Hole with Accretion Disk](branding/screenshot_1.png) +![Black Hole with HDRI](branding/screenshot_2.png) ## Physics Donut uses the Schwarzschild solution to describe the spacetime around Sgr A*. The metric is (with $r_s = \frac{2GM}{c^2}$): diff --git a/assets/hdri/HDR_blue_nebulae-1.hdr b/assets/hdri/HDR_blue_nebulae-1.hdr deleted file mode 100644 index f34b25d..0000000 Binary files a/assets/hdri/HDR_blue_nebulae-1.hdr and /dev/null differ diff --git a/assets/hdri/HDR_subdued_blue_nebulae.hdr b/assets/hdri/HDR_subdued_blue_nebulae.hdr deleted file mode 100644 index ea34298..0000000 Binary files a/assets/hdri/HDR_subdued_blue_nebulae.hdr and /dev/null differ diff --git a/assets/hdri/HDR_subdued_multi_nebulae.hdr b/assets/hdri/HDR_subdued_multi_nebulae.hdr deleted file mode 100644 index da88396..0000000 Binary files a/assets/hdri/HDR_subdued_multi_nebulae.hdr and /dev/null differ diff --git a/assets/hdri/hdr_blue_nebulae_1.hdr b/assets/hdri/hdr_blue_nebulae_1.hdr new file mode 100644 index 0000000..f34b25d Binary files /dev/null and b/assets/hdri/hdr_blue_nebulae_1.hdr differ diff --git a/assets/hdri/hdr_subdued_blue_nebulae.hdr b/assets/hdri/hdr_subdued_blue_nebulae.hdr new file mode 100644 index 0000000..ea34298 Binary files /dev/null and b/assets/hdri/hdr_subdued_blue_nebulae.hdr differ diff --git a/assets/hdri/hdr_subdued_multi_nebulae.hdr b/assets/hdri/hdr_subdued_multi_nebulae.hdr new file mode 100644 index 0000000..da88396 Binary files /dev/null and b/assets/hdri/hdr_subdued_multi_nebulae.hdr differ diff --git a/assets/shaders/Blur.slang b/assets/shaders/Blur.slang deleted file mode 100644 index 17e4eae..0000000 --- a/assets/shaders/Blur.slang +++ /dev/null @@ -1,60 +0,0 @@ -struct VSInput -{ - float2 position : POSITION; - float2 texCoord : TEXCOORD0; -}; - -struct VSOutput -{ - float4 position : SV_Position; - float2 texCoord : TEXCOORD0; -}; - -[shader("vertex")] -VSOutput vertexMain(VSInput input) -{ - VSOutput output; - output.position = float4(input.position, 0.0, 1.0); - output.texCoord = input.texCoord; - return output; -} - -Sampler2D u_ScreenTexture; -uniform float2 u_Resolution; -uniform float u_BlurStrength; -uniform float u_GlowIntensity; - -[shader("fragment")] -float4 fragmentMain(VSOutput input) : SV_Target -{ - float2 texelSize = 1.0 / u_Resolution; - float4 centerColor = u_ScreenTexture.Sample(input.texCoord); - - float brightness = (centerColor.r + centerColor.g + centerColor.b) / 3.0; - float glowMask = smoothstep(0.05, 0.3, brightness); - - float4 blurredColor = float4(0.0, 0.0, 0.0, 0.0); - float totalWeight = 0.0; - - for (int x = -8; x <= 8; x++) - { - for (int y = -8; y <= 8; y++) - { - float2 offset = float2(x, y) * texelSize * u_BlurStrength; - float4 sampleColor = u_ScreenTexture.Sample(input.texCoord + offset); - - float weight = exp(-float(x * x + y * y) / (2.0 * 8.0 * 8.0)); - blurredColor += sampleColor * weight; - totalWeight += weight; - } - } - - blurredColor /= totalWeight; - - float3 emissionColor = float3(1.0, 0.8, 0.6); - float3 glowColor = emissionColor * glowMask * u_GlowIntensity * 2.0; - float3 auraColor = blurredColor.rgb * glowMask * u_GlowIntensity * 0.8; - float3 finalColor = centerColor.rgb + glowColor + auraColor; - - return float4(finalColor, centerColor.a); -} diff --git a/assets/shaders/EquirectToCubemap.slang b/assets/shaders/EquirectToCubemap.slang deleted file mode 100644 index 9b2ea4b..0000000 --- a/assets/shaders/EquirectToCubemap.slang +++ /dev/null @@ -1,33 +0,0 @@ -uniform float4x4 u_Projection; -uniform float4x4 u_View; - -struct VSInput { float3 position : POSITION; }; -struct VSOutput { float4 position : SV_Position; float3 worldPos : TEXCOORD0; }; - -[shader("vertex")] -VSOutput vertexMain(VSInput input) -{ - VSOutput output; - output.worldPos = input.position; - output.position = mul(u_Projection, mul(u_View, float4(input.position, 1.0))); - return output; -} - -Sampler2D u_EquirectangularMap; -static const float2 invAtan = float2(0.1591, 0.3183); - -float2 SampleSphericalMap(float3 v) -{ - float2 uv = float2(atan2(v.z, v.x), asin(v.y)); - uv *= invAtan; - uv += 0.5; - return uv; -} - -[shader("fragment")] -float4 fragmentMain(VSOutput input) : SV_Target -{ - float2 uv = SampleSphericalMap(normalize(input.worldPos)); - float3 color = u_EquirectangularMap.Sample(uv).rgb; - return float4(color, 1.0); -} diff --git a/assets/shaders/Geodesic.slang b/assets/shaders/Geodesic.slang deleted file mode 100644 index 7acbd78..0000000 --- a/assets/shaders/Geodesic.slang +++ /dev/null @@ -1,438 +0,0 @@ -struct VSInput { float2 position : POSITION; float2 texCoord : TEXCOORD0; }; -struct VSOutput { float4 position : SV_Position; float2 texCoord : TEXCOORD0; }; - -[shader("vertex")] -VSOutput vertexMain(VSInput input) -{ - VSOutput output; - output.position = float4(input.position, 0.0, 1.0); - output.texCoord = input.texCoord; - return output; -} - -struct Camera -{ - float3 camPos; float _pad0; - float3 camRight; float _pad1; - float3 camUp; float _pad2; - float3 camForward; float _pad3; - float tanHalfFov; - float aspect; - bool moving; - int _pad4; -}; -ConstantBuffer cam; - -struct Disk -{ - float disk_r1; // inner edge (clamped to the ISCO, 3 r_s, below) - float disk_r2; // outer edge - float disk_num; // turbulence strength (0 = smooth physical disk) - float thickness; // slab half-height (anti-aliases the edge-on disk) - float disk_density; // disk brightness / exposure - float temperature; // Kelvin at the flux peak (disk colour) -}; -ConstantBuffer disk; - -struct Objects -{ - int numObjects; - float4 objPosRadius[16]; - float4 objColor[16]; - float mass[16]; -}; -ConstantBuffer obj; - -struct Simulation -{ - int maxStepsMoving; - int maxStepsStatic; - float earlyExitDistance; - float time; -}; -ConstantBuffer sim; - -SamplerCube u_HDRIEnvironment; - -// Schwarzschild radius of Sgr A* (metres). Geometric units with c = G = 1 are -// used throughout the geodesic integration; the black-hole mass is M = r_s / 2. -static const float SagA_rs = 1.269e10; -static const float D_LAMBDA = 1e7; -static const float ESCAPE_R = 1e30; - -static const float R_ISCO = 3.0 * SagA_rs; // innermost stable circular orbit (6M) -static const float R_PHOTON = 1.5 * SagA_rs; // photon sphere (3M) -static const float FLUX_PEAK = 0.0569; // peak of the r^-3(1-sqrt(r_in/r)) profile (at r/r_in ~ 1.36) - -static const int DEFAULT_MAX_STEPS_MOVING = 12000; -static const int DEFAULT_MAX_STEPS_STATIC = 8000; -static const float DEFAULT_EARLY_EXIT_DISTANCE = 2e12; - -static const float MIN_STEP_SIZE = 1e6; -static const float MAX_STEP_SIZE = 2e10; - -// Display mapping for the (relative) Novikov-Thorne flux -> visible colour. -// The RADIAL PROFILE is physical; the absolute temperature scale is a display -// choice (a real Sgr A* disk is far cooler / redder than this). - -struct Hit -{ - float4 objectColor; - float3 hitCenter; - float hitRadius; -}; - -float hash(float3 p) -{ - p = frac(p * float3(0.1031, 0.1030, 0.0973)); - p += dot(p, p.yxz + 33.33); - return frac((p.x + p.y) * p.z); -} - -float noise(float3 x) -{ - float3 i = floor(x); - float3 fr = frac(x); - float3 u = fr * fr * (3.0 - 2.0 * fr); - - float a = hash(i); - float b = hash(i + float3(1.0, 0.0, 0.0)); - float c = hash(i + float3(0.0, 1.0, 0.0)); - float d = hash(i + float3(1.0, 1.0, 0.0)); - float e = hash(i + float3(0.0, 0.0, 1.0)); - float f = hash(i + float3(1.0, 0.0, 1.0)); - float g = hash(i + float3(0.0, 1.0, 1.0)); - float h = hash(i + float3(1.0, 1.0, 1.0)); - - return lerp(lerp(lerp(a, b, u.x), lerp(c, d, u.x), u.y), - lerp(lerp(e, f, u.x), lerp(g, h, u.x), u.y), u.z); -} - -float fbm(float3 x, int octaves) -{ - float v = 0.0; - float a = 0.5; - float3 shift = float3(100, 200, 300); - for (int i = 0; i < octaves; ++i) - { - v += a * noise(x); - x = x * 2.0 + shift; - a *= 0.5; - } - return v; -} - -// Planckian-locus blackbody colour (Tanner Helland approximation), T in Kelvin. -// Returns an sRGB-ish chromaticity normalised so the brightest channel ~ 1. -float3 Blackbody(float T) -{ - T = clamp(T, 1000.0, 40000.0); - float t = T / 100.0; - float3 c; - - c.r = (t <= 66.0) ? 1.0 - : clamp(1.292936186 * pow(t - 60.0, -0.1332047592), 0.0, 1.0); - - c.g = (t <= 66.0) ? clamp(0.3900815788 * log(t) - 0.6318414438, 0.0, 1.0) - : clamp(1.1298908609 * pow(t - 60.0, -0.0755148492), 0.0, 1.0); - - c.b = (t >= 66.0) ? 1.0 - : (t <= 19.0) ? 0.0 - : clamp(0.5432067891 * log(t - 10.0) - 1.1962540891, 0.0, 1.0); - return c; -} - -// Emission from the thin accretion disk at an equatorial crossing point P, seen -// along the (backward-traced) ray direction rayDir. Combines a Novikov-Thorne -// temperature profile with the full gravitational + Doppler redshift. -// g = sqrt(1 - 3M/r) / (1 - beta . nhat) (verified: g -> sqrt(1/2) at ISCO) -// Brightness follows relativistic beaming (I_obs = g^4 I_emit); colour follows -// the redshifted blackbody at T_obs = g * T_emit. -float3 DiskEmission(float3 P, float3 rayDir) -{ - float rc = length(float2(P.x, P.z)); // cylindrical radius (disk axis = +Y) - float rin = max(disk.disk_r1, R_ISCO); - float rout = disk.disk_r2; - if (rc < rin || rc > rout) - return float3(0.0); - - // Novikov-Thorne-style radial flux: F(r) ~ r^-3 (1 - sqrt(r_in/r)), zero at - // the inner edge, peaking just outside it, then declining. T ~ F^(1/4). - float xr = rc / rin; - float flux = max((1.0 - sqrt(1.0 / xr)) / (xr * xr * xr), 0.0); - float Tn = pow(flux / FLUX_PEAK, 0.25); // normalised temperature, peak ~ 1 - float Temit = disk.temperature * Tn; - - // Keplerian orbit (prograde about +Y). Locally-measured orbital speed for a - // Schwarzschild circular geodesic: v = sqrt( M / (r - 2M) ) = 0.5 c at ISCO. - float3 rhat = normalize(float3(P.x, 0.0, P.z)); - float3 phiHat = normalize(cross(float3(0.0, 1.0, 0.0), rhat)); - float v = sqrt((SagA_rs * 0.5) / max(rc - SagA_rs, 1.0)); - float3 beta = v * phiHat; - float3 nhat = -normalize(rayDir); // photon direction toward the observer - - float g = sqrt(max(1.0 - 1.5 * SagA_rs / rc, 0.0)) / max(1.0 - dot(beta, nhat), 1e-3); - - float Tobs = g * Temit; - float3 colour = Blackbody(Tobs); - // Physical bolometric intensity is ~ T_emit^4 * g^4, an enormous dynamic - // range. The g^4 relativistic beaming (the physical asymmetry) is kept; the - // radial falloff is display-compressed (Tn^2) so the colour gradient across - // the disk stays visible instead of collapsing to a thin saturated ring. - float bright = pow(Tn, 2.0) * pow(g, 4.0); - - // Soft inner/outer edges (disks have no hard rim); also tames rim aliasing. - float edge = smoothstep(rin, rin * 1.12, rc) * (1.0 - smoothstep(rout * 0.88, rout, rc)); - bright *= edge; - - // Optional turbulence overlay (disk.disk_num = strength; 0 = smooth). - if (disk.disk_num > 0.0) - { - float ang = sim.time * 0.3 / sqrt(xr); - float3 rp = float3(P.x * cos(ang) - P.z * sin(ang), - 0.0, - P.x * sin(ang) + P.z * cos(ang)) * 1e-10; - float turb = 1.0 + disk.disk_num * (fbm(rp * 3.0, 3) - 0.5); - bright *= max(turb, 0.0); - } - - return colour * bright * max(disk.disk_density, 0.0); -} - -struct Ray -{ - float x, y, z; - float r, theta, phi; - float dr, dtheta, dphi; - float E, L; -}; - -Ray InitRay(float3 pos, float3 dir) -{ - Ray ray; - ray.x = pos.x; - ray.y = pos.y; - ray.z = pos.z; - ray.r = length(pos); - ray.theta = acos(pos.z / ray.r); - ray.phi = atan2(pos.y, pos.x); - - float dx = dir.x, dy = dir.y, dz = dir.z; - - ray.dr = sin(ray.theta)*cos(ray.phi)*dx + - sin(ray.theta)*sin(ray.phi)*dy + - cos(ray.theta)*dz; - ray.dtheta = (cos(ray.theta)*cos(ray.phi)*dx + - cos(ray.theta)*sin(ray.phi)*dy - - sin(ray.theta)*dz) / ray.r; - ray.dphi = (-sin(ray.phi)*dx + cos(ray.phi)*dy) / - (ray.r * sin(ray.theta)); - - ray.L = ray.r * ray.r * sin(ray.theta) * ray.dphi; - float f = 1.0 - SagA_rs / ray.r; - float dt_dL = sqrt((ray.dr*ray.dr)/f + - ray.r*ray.r*(ray.dtheta*ray.dtheta + - sin(ray.theta)*sin(ray.theta)*ray.dphi*ray.dphi)); - ray.E = f * dt_dL; - return ray; -} - -bool Intercept(Ray ray, float rs) { return ray.r <= rs; } - -bool InterceptObject(Ray ray, inout Hit hit) -{ - float3 P = float3(ray.x, ray.y, ray.z); - for (int i = 0; i < obj.numObjects; ++i) - { - float3 center = obj.objPosRadius[i].xyz; - float radius = obj.objPosRadius[i].w; - float distSq = dot(P - center, P - center); - if (distSq > radius * radius * 4.0) continue; - if (distSq <= radius * radius) - { - hit.objectColor = obj.objColor[i]; - hit.hitCenter = center; - hit.hitRadius = radius; - return true; - } - } - return false; -} - -void GeodesicRHS(Ray ray, out float3 d1, out float3 d2) -{ - float r = ray.r; - float theta = ray.theta; - float dr = ray.dr; - float dtheta = ray.dtheta; - float dphi = ray.dphi; - float f = 1.0 - SagA_rs / r; - float dt_dL = ray.E / f; - - d1 = float3(dr, dtheta, dphi); - d2.x = - (SagA_rs / (2.0 * r*r)) * f * dt_dL * dt_dL - + (SagA_rs / (2.0 * r*r * f)) * dr * dr - + r * (dtheta*dtheta + sin(theta)*sin(theta)*dphi*dphi); - d2.y = -2.0*dr*dtheta/r + sin(theta)*cos(theta)*dphi*dphi; - d2.z = -2.0*dr*dphi/r - 2.0*cos(theta)/(sin(theta)) * dtheta * dphi; -} - -void RK4Step(inout Ray ray, float dL) -{ - float3 k1a, k1b; - GeodesicRHS(ray, k1a, k1b); - ray.r += dL * k1a.x; - ray.theta += dL * k1a.y; - ray.phi += dL * k1a.z; - ray.dr += dL * k1b.x; - ray.dtheta += dL * k1b.y; - ray.dphi += dL * k1b.z; - - ray.x = ray.r * sin(ray.theta) * cos(ray.phi); - ray.y = ray.r * sin(ray.theta) * sin(ray.phi); - ray.z = ray.r * cos(ray.theta); -} - -float CalculateAdaptiveStepSize(Ray ray, float baseStepSize) -{ - // Step proportional to the distance from the photon sphere: near-flat space - // far from the hole is crossed in a few huge steps, while the sharply curved - // region near the photon sphere is resolved with tiny ones. This keeps the - // integration accurate near the hole regardless of how far the camera is. - float step = 0.02 * max(ray.r - R_PHOTON, 0.0); - - // Slow down when near the disk plane (within its radial extent) so the thin - // slab is never stepped over -- otherwise grazing rays leak through it. - float rc = length(float2(ray.x, ray.z)); - if (rc < disk.disk_r2 * 3.0 && abs(ray.y) < disk.thickness * 8.0) - step = min(step, disk.thickness); - - return clamp(step, MIN_STEP_SIZE, MAX_STEP_SIZE); -} - -float3 ACESFilm(float3 x) -{ - return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0); -} - -// Trace one primary ray for the given image UV and return its linear, -// pre-tone-map radiance. Called once per sub-sample by fragmentMain. -float3 TracePixel(float2 texCoord) -{ - float u = (2.0 * texCoord.x - 1.0) * cam.aspect * cam.tanHalfFov; - float v = (1.0 - 2.0 * texCoord.y) * cam.tanHalfFov; - float3 dir = normalize(u * cam.camRight - v * cam.camUp + cam.camForward); - Ray ray = InitRay(cam.camPos, dir); - - bool hitBlackHole = false; - bool hitObject = false; - Hit hit; - hit.objectColor = float4(0.0); - hit.hitCenter = float3(0.0); - hit.hitRadius = 0.0; - - bool hitDisk = false; - float3 diskColor = float3(0.0); // emission of the first (opaque) disk surface hit - - int maxSteps = cam.moving ? sim.maxStepsMoving : sim.maxStepsStatic; - if (maxSteps <= 0) - maxSteps = cam.moving ? DEFAULT_MAX_STEPS_MOVING : DEFAULT_MAX_STEPS_STATIC; - - float exitDistance = sim.earlyExitDistance > 0.0 ? sim.earlyExitDistance : DEFAULT_EARLY_EXIT_DISTANCE; - int objectCheckInterval = 5; - - for (int i = 0; i < maxSteps; ++i) - { - if (Intercept(ray, SagA_rs)) { hitBlackHole = true; break; } - if (ray.r > exitDistance || ray.r > ESCAPE_R) break; - - float3 prevPos = float3(ray.x, ray.y, ray.z); - float stepSize = CalculateAdaptiveStepSize(ray, D_LAMBDA); - RK4Step(ray, stepSize); - float3 newPos = float3(ray.x, ray.y, ray.z); - - // Opaque disk of small half-thickness H (a slab about the midplane y=0). - // The ray hits when it first crosses the midplane OR enters the slab - // while grazing along it. Real (nonzero) thickness stops the zero-height - // edge-on "razor" from aliasing into a beam streaking across the frame. - { - float H = disk.thickness; - bool crossed = prevPos.y * newPos.y < 0.0; - bool inSlab = abs(newPos.y) <= H; - if (crossed || inSlab) - { - float3 hitP = crossed - ? lerp(prevPos, newPos, prevPos.y / (prevPos.y - newPos.y)) - : newPos; - float rc = length(float2(hitP.x, hitP.z)); - if (rc >= max(disk.disk_r1, R_ISCO) && rc <= disk.disk_r2) - { - diskColor = DiskEmission(hitP, newPos - prevPos); - hitDisk = true; - break; - } - } - } - - if (i % objectCheckInterval == 0 && InterceptObject(ray, hit)) { hitObject = true; break; } - - // Principled escape: once outbound in near-flat spacetime (r >> r_s) the - // ray direction no longer changes, so stop and read the background. - if (ray.dr > 0.0 && ray.r > 50.0 * SagA_rs) break; - } - - // Escape direction + environment mip LOD from the ray's angular divergence. - // Computed UNCONDITIONALLY (before the branch) so ddx/ddy are valid; strongly - // lensed background rays diverge fast, so they read a blurred cubemap mip and - // the starfield stops aliasing into a fan along the equatorial plane. - float3 rayDir = normalize(float3(ray.x, ray.y, ray.z) - cam.camPos); - float footprint = max(length(ddx(rayDir)), length(ddy(rayDir))); - float envLod = clamp(log2(max(footprint / 0.0015, 1.0)), 0.0, 10.0); - - float3 shade; - if (hitDisk) - { - shade = diskColor; // opaque, self-luminous disk surface - } - else if (hitBlackHole) - { - shade = float3(0.0); // event-horizon shadow - } - else if (hitObject) - { - float3 P = float3(ray.x, ray.y, ray.z); - float3 N = normalize(P - hit.hitCenter); - float3 V = normalize(cam.camPos - P); - float intensity = 0.1 + 0.9 * max(dot(N, V), 0.0); - shade = hit.objectColor.rgb * intensity; - } - else - { - shade = u_HDRIEnvironment.SampleLevel(rayDir, envLod).rgb; - } - - return shade; -} - -[shader("fragment")] -float4 fragmentMain(VSOutput input) : SV_Target -{ - // Moving frame: one sample for responsiveness. Settled frame: rotated-grid - // 4x supersampling (the 4-rook pattern gives 4 distinct sub-pixel positions - // on BOTH axes, far better on the near-horizontal lensed edges than an - // ordered grid). Radiance is averaged before tone-mapping; ddx/ddy give the - // resolution-correct per-pixel UV footprint. - if (cam.moving) - return float4(ACESFilm(TracePixel(input.texCoord)), 1.0); - - float2 dUV = float2(ddx(input.texCoord.x), ddy(input.texCoord.y)); - float2 offs[4] = { - float2( 0.125, 0.375), float2( 0.375, -0.125), - float2(-0.125, -0.375), float2(-0.375, 0.125), - }; - float3 sum = float3(0.0); - for (int i = 0; i < 4; ++i) - sum += TracePixel(input.texCoord + offs[i] * dUV); - - return float4(ACESFilm(sum * 0.25), 1.0); -} diff --git a/assets/shaders/Grid.slang b/assets/shaders/Grid.slang deleted file mode 100644 index ea7d263..0000000 --- a/assets/shaders/Grid.slang +++ /dev/null @@ -1,34 +0,0 @@ -uniform float4x4 u_ViewProjection; -uniform float4x4 u_Transform; -uniform float u_GridSize; - -struct VSInput { float3 position : POSITION; }; -struct VSOutput { float4 position : SV_Position; }; - -[shader("vertex")] -VSOutput vertexMain(VSInput input) -{ - VSOutput output; - float3 scaledPosition = input.position * (u_GridSize / 50.0); - output.position = mul(u_ViewProjection, mul(u_Transform, float4(scaledPosition, 1.0))); - return output; -} - -uniform float3 u_GridColor; -uniform float u_GridAlpha; -uniform float3 u_CameraPos; - -[shader("fragment")] -float4 fragmentMain(VSOutput input) : SV_Target -{ - float dist = length(u_CameraPos); - float fadeFactor = 1.0 - clamp(dist / 100.0, 0.0, 0.8); - - float3 color = u_GridColor; - float alpha = u_GridAlpha * fadeFactor; - - float gridFade = 1.0 - clamp(dist / 50.0, 0.0, 0.9); - alpha *= gridFade; - - return float4(color, alpha); -} diff --git a/assets/shaders/Skybox.slang b/assets/shaders/Skybox.slang deleted file mode 100644 index 6ba81bc..0000000 --- a/assets/shaders/Skybox.slang +++ /dev/null @@ -1,23 +0,0 @@ -uniform float4x4 u_Projection; -uniform float4x4 u_View; - -struct VSInput { float3 position : POSITION; }; -struct VSOutput { float4 position : SV_Position; float3 texCoords : TEXCOORD0; }; - -[shader("vertex")] -VSOutput vertexMain(VSInput input) -{ - VSOutput output; - output.texCoords = input.position; - float4 pos = mul(u_Projection, mul(u_View, float4(input.position, 1.0))); - output.position = pos.xyww; // force depth = 1 so the skybox stays behind everything - return output; -} - -SamplerCube u_Skybox; - -[shader("fragment")] -float4 fragmentMain(VSOutput input) : SV_Target -{ - return u_Skybox.Sample(input.texCoords); -} diff --git a/assets/shaders/Sphere.slang b/assets/shaders/Sphere.slang deleted file mode 100644 index 0871438..0000000 --- a/assets/shaders/Sphere.slang +++ /dev/null @@ -1,67 +0,0 @@ -uniform float4x4 u_ViewProjection; -uniform float4x4 u_Transform; - -struct VSInput -{ - float3 position : POSITION; - float3 normal : NORMAL; -}; - -struct VSOutput -{ - float4 position : SV_Position; - float3 normal : TEXCOORD0; - float3 worldPos : TEXCOORD1; -}; - -[shader("vertex")] -VSOutput vertexMain(VSInput input) -{ - VSOutput output; - output.worldPos = mul(u_Transform, float4(input.position, 1.0)).xyz; - float3x3 normalMatrix = (float3x3)u_Transform; - output.normal = mul(normalMatrix, input.normal); - output.position = mul(u_ViewProjection, float4(output.worldPos, 1.0)); - return output; -} - -uniform float3 u_Color; -uniform float u_Specular; -uniform float u_Emission; -uniform float3 u_LightPos; -uniform float3 u_CameraPos; -uniform int u_IsSelected; -uniform float3 u_OutlineColor; -uniform float u_OutlineWidth; -SamplerCube u_HDRIEnvironment; - -[shader("fragment")] -float4 fragmentMain(VSOutput input) : SV_Target -{ - float3 normal = normalize(input.normal); - float3 lightDir = normalize(u_LightPos - input.worldPos); - float3 viewDir = normalize(u_CameraPos - input.worldPos); - float3 reflectDir = reflect(-lightDir, normal); - - float3 hdriLight = u_HDRIEnvironment.Sample(normal).rgb; - float hdriIntensity = 0.3; - - float diff = max(dot(normal, lightDir), 0.0); - float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0); - - float3 diffuse = u_Color * (diff + hdriIntensity * hdriLight); - float3 specular = float3(u_Specular, u_Specular, u_Specular) * spec; - float3 emission = u_Color * u_Emission; - - float3 result = diffuse + specular + emission; - - if (u_IsSelected > 0) - { - float ndotv = max(dot(normal, viewDir), 0.0); - float rim = 1.0 - ndotv; - float outlineMask = step(1.0 - u_OutlineWidth, rim); - result = lerp(result, u_OutlineColor, outlineMask); - } - - return float4(result, 1.0); -} diff --git a/assets/shaders/TexturedQuad.slang b/assets/shaders/TexturedQuad.slang deleted file mode 100644 index 52d46ed..0000000 --- a/assets/shaders/TexturedQuad.slang +++ /dev/null @@ -1,28 +0,0 @@ -struct VSInput -{ - float2 position : POSITION; - float2 texCoord : TEXCOORD0; -}; - -struct VSOutput -{ - float4 position : SV_Position; - float2 texCoord : TEXCOORD0; -}; - -[shader("vertex")] -VSOutput vertexMain(VSInput input) -{ - VSOutput output; - output.position = float4(input.position, 0.0, 1.0); - output.texCoord = input.texCoord; - return output; -} - -Sampler2D u_ScreenTexture; - -[shader("fragment")] -float4 fragmentMain(VSOutput input) : SV_Target -{ - return u_ScreenTexture.Sample(input.texCoord); -} diff --git a/assets/shaders/VkPipelineTest.slang b/assets/shaders/VkPipelineTest.slang deleted file mode 100644 index f4d1897..0000000 --- a/assets/shaders/VkPipelineTest.slang +++ /dev/null @@ -1,25 +0,0 @@ -// Minimal shader used only to verify the Vulkan graphics-pipeline path (SPIR-V -// module -> pipeline -> draw -> read-back). Draws a full-screen gradient -// triangle from the vertex id, so it needs no vertex buffer, UBO, or sampler. - -struct VSOutput -{ - float4 position : SV_Position; - float3 color : COLOR0; -}; - -[shader("vertex")] -VSOutput vertexMain(uint vid : SV_VertexID) -{ - float2 p = float2(float((vid << 1) & 2), float(vid & 2)); - VSOutput output; - output.position = float4(p * 2.0 - 1.0, 0.0, 1.0); - output.color = float3(p, 0.5); - return output; -} - -[shader("fragment")] -float4 fragmentMain(VSOutput input) : SV_Target -{ - return float4(input.color, 1.0); -} diff --git a/assets/shaders/blur.slang b/assets/shaders/blur.slang new file mode 100644 index 0000000..17e4eae --- /dev/null +++ b/assets/shaders/blur.slang @@ -0,0 +1,60 @@ +struct VSInput +{ + float2 position : POSITION; + float2 texCoord : TEXCOORD0; +}; + +struct VSOutput +{ + float4 position : SV_Position; + float2 texCoord : TEXCOORD0; +}; + +[shader("vertex")] +VSOutput vertexMain(VSInput input) +{ + VSOutput output; + output.position = float4(input.position, 0.0, 1.0); + output.texCoord = input.texCoord; + return output; +} + +Sampler2D u_ScreenTexture; +uniform float2 u_Resolution; +uniform float u_BlurStrength; +uniform float u_GlowIntensity; + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_Target +{ + float2 texelSize = 1.0 / u_Resolution; + float4 centerColor = u_ScreenTexture.Sample(input.texCoord); + + float brightness = (centerColor.r + centerColor.g + centerColor.b) / 3.0; + float glowMask = smoothstep(0.05, 0.3, brightness); + + float4 blurredColor = float4(0.0, 0.0, 0.0, 0.0); + float totalWeight = 0.0; + + for (int x = -8; x <= 8; x++) + { + for (int y = -8; y <= 8; y++) + { + float2 offset = float2(x, y) * texelSize * u_BlurStrength; + float4 sampleColor = u_ScreenTexture.Sample(input.texCoord + offset); + + float weight = exp(-float(x * x + y * y) / (2.0 * 8.0 * 8.0)); + blurredColor += sampleColor * weight; + totalWeight += weight; + } + } + + blurredColor /= totalWeight; + + float3 emissionColor = float3(1.0, 0.8, 0.6); + float3 glowColor = emissionColor * glowMask * u_GlowIntensity * 2.0; + float3 auraColor = blurredColor.rgb * glowMask * u_GlowIntensity * 0.8; + float3 finalColor = centerColor.rgb + glowColor + auraColor; + + return float4(finalColor, centerColor.a); +} diff --git a/assets/shaders/equirect_to_cubemap.slang b/assets/shaders/equirect_to_cubemap.slang new file mode 100644 index 0000000..9b2ea4b --- /dev/null +++ b/assets/shaders/equirect_to_cubemap.slang @@ -0,0 +1,33 @@ +uniform float4x4 u_Projection; +uniform float4x4 u_View; + +struct VSInput { float3 position : POSITION; }; +struct VSOutput { float4 position : SV_Position; float3 worldPos : TEXCOORD0; }; + +[shader("vertex")] +VSOutput vertexMain(VSInput input) +{ + VSOutput output; + output.worldPos = input.position; + output.position = mul(u_Projection, mul(u_View, float4(input.position, 1.0))); + return output; +} + +Sampler2D u_EquirectangularMap; +static const float2 invAtan = float2(0.1591, 0.3183); + +float2 SampleSphericalMap(float3 v) +{ + float2 uv = float2(atan2(v.z, v.x), asin(v.y)); + uv *= invAtan; + uv += 0.5; + return uv; +} + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_Target +{ + float2 uv = SampleSphericalMap(normalize(input.worldPos)); + float3 color = u_EquirectangularMap.Sample(uv).rgb; + return float4(color, 1.0); +} diff --git a/assets/shaders/geodesic.slang b/assets/shaders/geodesic.slang new file mode 100644 index 0000000..7acbd78 --- /dev/null +++ b/assets/shaders/geodesic.slang @@ -0,0 +1,438 @@ +struct VSInput { float2 position : POSITION; float2 texCoord : TEXCOORD0; }; +struct VSOutput { float4 position : SV_Position; float2 texCoord : TEXCOORD0; }; + +[shader("vertex")] +VSOutput vertexMain(VSInput input) +{ + VSOutput output; + output.position = float4(input.position, 0.0, 1.0); + output.texCoord = input.texCoord; + return output; +} + +struct Camera +{ + float3 camPos; float _pad0; + float3 camRight; float _pad1; + float3 camUp; float _pad2; + float3 camForward; float _pad3; + float tanHalfFov; + float aspect; + bool moving; + int _pad4; +}; +ConstantBuffer cam; + +struct Disk +{ + float disk_r1; // inner edge (clamped to the ISCO, 3 r_s, below) + float disk_r2; // outer edge + float disk_num; // turbulence strength (0 = smooth physical disk) + float thickness; // slab half-height (anti-aliases the edge-on disk) + float disk_density; // disk brightness / exposure + float temperature; // Kelvin at the flux peak (disk colour) +}; +ConstantBuffer disk; + +struct Objects +{ + int numObjects; + float4 objPosRadius[16]; + float4 objColor[16]; + float mass[16]; +}; +ConstantBuffer obj; + +struct Simulation +{ + int maxStepsMoving; + int maxStepsStatic; + float earlyExitDistance; + float time; +}; +ConstantBuffer sim; + +SamplerCube u_HDRIEnvironment; + +// Schwarzschild radius of Sgr A* (metres). Geometric units with c = G = 1 are +// used throughout the geodesic integration; the black-hole mass is M = r_s / 2. +static const float SagA_rs = 1.269e10; +static const float D_LAMBDA = 1e7; +static const float ESCAPE_R = 1e30; + +static const float R_ISCO = 3.0 * SagA_rs; // innermost stable circular orbit (6M) +static const float R_PHOTON = 1.5 * SagA_rs; // photon sphere (3M) +static const float FLUX_PEAK = 0.0569; // peak of the r^-3(1-sqrt(r_in/r)) profile (at r/r_in ~ 1.36) + +static const int DEFAULT_MAX_STEPS_MOVING = 12000; +static const int DEFAULT_MAX_STEPS_STATIC = 8000; +static const float DEFAULT_EARLY_EXIT_DISTANCE = 2e12; + +static const float MIN_STEP_SIZE = 1e6; +static const float MAX_STEP_SIZE = 2e10; + +// Display mapping for the (relative) Novikov-Thorne flux -> visible colour. +// The RADIAL PROFILE is physical; the absolute temperature scale is a display +// choice (a real Sgr A* disk is far cooler / redder than this). + +struct Hit +{ + float4 objectColor; + float3 hitCenter; + float hitRadius; +}; + +float hash(float3 p) +{ + p = frac(p * float3(0.1031, 0.1030, 0.0973)); + p += dot(p, p.yxz + 33.33); + return frac((p.x + p.y) * p.z); +} + +float noise(float3 x) +{ + float3 i = floor(x); + float3 fr = frac(x); + float3 u = fr * fr * (3.0 - 2.0 * fr); + + float a = hash(i); + float b = hash(i + float3(1.0, 0.0, 0.0)); + float c = hash(i + float3(0.0, 1.0, 0.0)); + float d = hash(i + float3(1.0, 1.0, 0.0)); + float e = hash(i + float3(0.0, 0.0, 1.0)); + float f = hash(i + float3(1.0, 0.0, 1.0)); + float g = hash(i + float3(0.0, 1.0, 1.0)); + float h = hash(i + float3(1.0, 1.0, 1.0)); + + return lerp(lerp(lerp(a, b, u.x), lerp(c, d, u.x), u.y), + lerp(lerp(e, f, u.x), lerp(g, h, u.x), u.y), u.z); +} + +float fbm(float3 x, int octaves) +{ + float v = 0.0; + float a = 0.5; + float3 shift = float3(100, 200, 300); + for (int i = 0; i < octaves; ++i) + { + v += a * noise(x); + x = x * 2.0 + shift; + a *= 0.5; + } + return v; +} + +// Planckian-locus blackbody colour (Tanner Helland approximation), T in Kelvin. +// Returns an sRGB-ish chromaticity normalised so the brightest channel ~ 1. +float3 Blackbody(float T) +{ + T = clamp(T, 1000.0, 40000.0); + float t = T / 100.0; + float3 c; + + c.r = (t <= 66.0) ? 1.0 + : clamp(1.292936186 * pow(t - 60.0, -0.1332047592), 0.0, 1.0); + + c.g = (t <= 66.0) ? clamp(0.3900815788 * log(t) - 0.6318414438, 0.0, 1.0) + : clamp(1.1298908609 * pow(t - 60.0, -0.0755148492), 0.0, 1.0); + + c.b = (t >= 66.0) ? 1.0 + : (t <= 19.0) ? 0.0 + : clamp(0.5432067891 * log(t - 10.0) - 1.1962540891, 0.0, 1.0); + return c; +} + +// Emission from the thin accretion disk at an equatorial crossing point P, seen +// along the (backward-traced) ray direction rayDir. Combines a Novikov-Thorne +// temperature profile with the full gravitational + Doppler redshift. +// g = sqrt(1 - 3M/r) / (1 - beta . nhat) (verified: g -> sqrt(1/2) at ISCO) +// Brightness follows relativistic beaming (I_obs = g^4 I_emit); colour follows +// the redshifted blackbody at T_obs = g * T_emit. +float3 DiskEmission(float3 P, float3 rayDir) +{ + float rc = length(float2(P.x, P.z)); // cylindrical radius (disk axis = +Y) + float rin = max(disk.disk_r1, R_ISCO); + float rout = disk.disk_r2; + if (rc < rin || rc > rout) + return float3(0.0); + + // Novikov-Thorne-style radial flux: F(r) ~ r^-3 (1 - sqrt(r_in/r)), zero at + // the inner edge, peaking just outside it, then declining. T ~ F^(1/4). + float xr = rc / rin; + float flux = max((1.0 - sqrt(1.0 / xr)) / (xr * xr * xr), 0.0); + float Tn = pow(flux / FLUX_PEAK, 0.25); // normalised temperature, peak ~ 1 + float Temit = disk.temperature * Tn; + + // Keplerian orbit (prograde about +Y). Locally-measured orbital speed for a + // Schwarzschild circular geodesic: v = sqrt( M / (r - 2M) ) = 0.5 c at ISCO. + float3 rhat = normalize(float3(P.x, 0.0, P.z)); + float3 phiHat = normalize(cross(float3(0.0, 1.0, 0.0), rhat)); + float v = sqrt((SagA_rs * 0.5) / max(rc - SagA_rs, 1.0)); + float3 beta = v * phiHat; + float3 nhat = -normalize(rayDir); // photon direction toward the observer + + float g = sqrt(max(1.0 - 1.5 * SagA_rs / rc, 0.0)) / max(1.0 - dot(beta, nhat), 1e-3); + + float Tobs = g * Temit; + float3 colour = Blackbody(Tobs); + // Physical bolometric intensity is ~ T_emit^4 * g^4, an enormous dynamic + // range. The g^4 relativistic beaming (the physical asymmetry) is kept; the + // radial falloff is display-compressed (Tn^2) so the colour gradient across + // the disk stays visible instead of collapsing to a thin saturated ring. + float bright = pow(Tn, 2.0) * pow(g, 4.0); + + // Soft inner/outer edges (disks have no hard rim); also tames rim aliasing. + float edge = smoothstep(rin, rin * 1.12, rc) * (1.0 - smoothstep(rout * 0.88, rout, rc)); + bright *= edge; + + // Optional turbulence overlay (disk.disk_num = strength; 0 = smooth). + if (disk.disk_num > 0.0) + { + float ang = sim.time * 0.3 / sqrt(xr); + float3 rp = float3(P.x * cos(ang) - P.z * sin(ang), + 0.0, + P.x * sin(ang) + P.z * cos(ang)) * 1e-10; + float turb = 1.0 + disk.disk_num * (fbm(rp * 3.0, 3) - 0.5); + bright *= max(turb, 0.0); + } + + return colour * bright * max(disk.disk_density, 0.0); +} + +struct Ray +{ + float x, y, z; + float r, theta, phi; + float dr, dtheta, dphi; + float E, L; +}; + +Ray InitRay(float3 pos, float3 dir) +{ + Ray ray; + ray.x = pos.x; + ray.y = pos.y; + ray.z = pos.z; + ray.r = length(pos); + ray.theta = acos(pos.z / ray.r); + ray.phi = atan2(pos.y, pos.x); + + float dx = dir.x, dy = dir.y, dz = dir.z; + + ray.dr = sin(ray.theta)*cos(ray.phi)*dx + + sin(ray.theta)*sin(ray.phi)*dy + + cos(ray.theta)*dz; + ray.dtheta = (cos(ray.theta)*cos(ray.phi)*dx + + cos(ray.theta)*sin(ray.phi)*dy - + sin(ray.theta)*dz) / ray.r; + ray.dphi = (-sin(ray.phi)*dx + cos(ray.phi)*dy) / + (ray.r * sin(ray.theta)); + + ray.L = ray.r * ray.r * sin(ray.theta) * ray.dphi; + float f = 1.0 - SagA_rs / ray.r; + float dt_dL = sqrt((ray.dr*ray.dr)/f + + ray.r*ray.r*(ray.dtheta*ray.dtheta + + sin(ray.theta)*sin(ray.theta)*ray.dphi*ray.dphi)); + ray.E = f * dt_dL; + return ray; +} + +bool Intercept(Ray ray, float rs) { return ray.r <= rs; } + +bool InterceptObject(Ray ray, inout Hit hit) +{ + float3 P = float3(ray.x, ray.y, ray.z); + for (int i = 0; i < obj.numObjects; ++i) + { + float3 center = obj.objPosRadius[i].xyz; + float radius = obj.objPosRadius[i].w; + float distSq = dot(P - center, P - center); + if (distSq > radius * radius * 4.0) continue; + if (distSq <= radius * radius) + { + hit.objectColor = obj.objColor[i]; + hit.hitCenter = center; + hit.hitRadius = radius; + return true; + } + } + return false; +} + +void GeodesicRHS(Ray ray, out float3 d1, out float3 d2) +{ + float r = ray.r; + float theta = ray.theta; + float dr = ray.dr; + float dtheta = ray.dtheta; + float dphi = ray.dphi; + float f = 1.0 - SagA_rs / r; + float dt_dL = ray.E / f; + + d1 = float3(dr, dtheta, dphi); + d2.x = - (SagA_rs / (2.0 * r*r)) * f * dt_dL * dt_dL + + (SagA_rs / (2.0 * r*r * f)) * dr * dr + + r * (dtheta*dtheta + sin(theta)*sin(theta)*dphi*dphi); + d2.y = -2.0*dr*dtheta/r + sin(theta)*cos(theta)*dphi*dphi; + d2.z = -2.0*dr*dphi/r - 2.0*cos(theta)/(sin(theta)) * dtheta * dphi; +} + +void RK4Step(inout Ray ray, float dL) +{ + float3 k1a, k1b; + GeodesicRHS(ray, k1a, k1b); + ray.r += dL * k1a.x; + ray.theta += dL * k1a.y; + ray.phi += dL * k1a.z; + ray.dr += dL * k1b.x; + ray.dtheta += dL * k1b.y; + ray.dphi += dL * k1b.z; + + ray.x = ray.r * sin(ray.theta) * cos(ray.phi); + ray.y = ray.r * sin(ray.theta) * sin(ray.phi); + ray.z = ray.r * cos(ray.theta); +} + +float CalculateAdaptiveStepSize(Ray ray, float baseStepSize) +{ + // Step proportional to the distance from the photon sphere: near-flat space + // far from the hole is crossed in a few huge steps, while the sharply curved + // region near the photon sphere is resolved with tiny ones. This keeps the + // integration accurate near the hole regardless of how far the camera is. + float step = 0.02 * max(ray.r - R_PHOTON, 0.0); + + // Slow down when near the disk plane (within its radial extent) so the thin + // slab is never stepped over -- otherwise grazing rays leak through it. + float rc = length(float2(ray.x, ray.z)); + if (rc < disk.disk_r2 * 3.0 && abs(ray.y) < disk.thickness * 8.0) + step = min(step, disk.thickness); + + return clamp(step, MIN_STEP_SIZE, MAX_STEP_SIZE); +} + +float3 ACESFilm(float3 x) +{ + return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0); +} + +// Trace one primary ray for the given image UV and return its linear, +// pre-tone-map radiance. Called once per sub-sample by fragmentMain. +float3 TracePixel(float2 texCoord) +{ + float u = (2.0 * texCoord.x - 1.0) * cam.aspect * cam.tanHalfFov; + float v = (1.0 - 2.0 * texCoord.y) * cam.tanHalfFov; + float3 dir = normalize(u * cam.camRight - v * cam.camUp + cam.camForward); + Ray ray = InitRay(cam.camPos, dir); + + bool hitBlackHole = false; + bool hitObject = false; + Hit hit; + hit.objectColor = float4(0.0); + hit.hitCenter = float3(0.0); + hit.hitRadius = 0.0; + + bool hitDisk = false; + float3 diskColor = float3(0.0); // emission of the first (opaque) disk surface hit + + int maxSteps = cam.moving ? sim.maxStepsMoving : sim.maxStepsStatic; + if (maxSteps <= 0) + maxSteps = cam.moving ? DEFAULT_MAX_STEPS_MOVING : DEFAULT_MAX_STEPS_STATIC; + + float exitDistance = sim.earlyExitDistance > 0.0 ? sim.earlyExitDistance : DEFAULT_EARLY_EXIT_DISTANCE; + int objectCheckInterval = 5; + + for (int i = 0; i < maxSteps; ++i) + { + if (Intercept(ray, SagA_rs)) { hitBlackHole = true; break; } + if (ray.r > exitDistance || ray.r > ESCAPE_R) break; + + float3 prevPos = float3(ray.x, ray.y, ray.z); + float stepSize = CalculateAdaptiveStepSize(ray, D_LAMBDA); + RK4Step(ray, stepSize); + float3 newPos = float3(ray.x, ray.y, ray.z); + + // Opaque disk of small half-thickness H (a slab about the midplane y=0). + // The ray hits when it first crosses the midplane OR enters the slab + // while grazing along it. Real (nonzero) thickness stops the zero-height + // edge-on "razor" from aliasing into a beam streaking across the frame. + { + float H = disk.thickness; + bool crossed = prevPos.y * newPos.y < 0.0; + bool inSlab = abs(newPos.y) <= H; + if (crossed || inSlab) + { + float3 hitP = crossed + ? lerp(prevPos, newPos, prevPos.y / (prevPos.y - newPos.y)) + : newPos; + float rc = length(float2(hitP.x, hitP.z)); + if (rc >= max(disk.disk_r1, R_ISCO) && rc <= disk.disk_r2) + { + diskColor = DiskEmission(hitP, newPos - prevPos); + hitDisk = true; + break; + } + } + } + + if (i % objectCheckInterval == 0 && InterceptObject(ray, hit)) { hitObject = true; break; } + + // Principled escape: once outbound in near-flat spacetime (r >> r_s) the + // ray direction no longer changes, so stop and read the background. + if (ray.dr > 0.0 && ray.r > 50.0 * SagA_rs) break; + } + + // Escape direction + environment mip LOD from the ray's angular divergence. + // Computed UNCONDITIONALLY (before the branch) so ddx/ddy are valid; strongly + // lensed background rays diverge fast, so they read a blurred cubemap mip and + // the starfield stops aliasing into a fan along the equatorial plane. + float3 rayDir = normalize(float3(ray.x, ray.y, ray.z) - cam.camPos); + float footprint = max(length(ddx(rayDir)), length(ddy(rayDir))); + float envLod = clamp(log2(max(footprint / 0.0015, 1.0)), 0.0, 10.0); + + float3 shade; + if (hitDisk) + { + shade = diskColor; // opaque, self-luminous disk surface + } + else if (hitBlackHole) + { + shade = float3(0.0); // event-horizon shadow + } + else if (hitObject) + { + float3 P = float3(ray.x, ray.y, ray.z); + float3 N = normalize(P - hit.hitCenter); + float3 V = normalize(cam.camPos - P); + float intensity = 0.1 + 0.9 * max(dot(N, V), 0.0); + shade = hit.objectColor.rgb * intensity; + } + else + { + shade = u_HDRIEnvironment.SampleLevel(rayDir, envLod).rgb; + } + + return shade; +} + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_Target +{ + // Moving frame: one sample for responsiveness. Settled frame: rotated-grid + // 4x supersampling (the 4-rook pattern gives 4 distinct sub-pixel positions + // on BOTH axes, far better on the near-horizontal lensed edges than an + // ordered grid). Radiance is averaged before tone-mapping; ddx/ddy give the + // resolution-correct per-pixel UV footprint. + if (cam.moving) + return float4(ACESFilm(TracePixel(input.texCoord)), 1.0); + + float2 dUV = float2(ddx(input.texCoord.x), ddy(input.texCoord.y)); + float2 offs[4] = { + float2( 0.125, 0.375), float2( 0.375, -0.125), + float2(-0.125, -0.375), float2(-0.375, 0.125), + }; + float3 sum = float3(0.0); + for (int i = 0; i < 4; ++i) + sum += TracePixel(input.texCoord + offs[i] * dUV); + + return float4(ACESFilm(sum * 0.25), 1.0); +} diff --git a/assets/shaders/grid.slang b/assets/shaders/grid.slang new file mode 100644 index 0000000..f549cb1 --- /dev/null +++ b/assets/shaders/grid.slang @@ -0,0 +1,39 @@ +// Reference grid on the XZ plane. Uniforms live in a ConstantBuffer (UBO) so both +// backends drive it through the same RHI path (bind a uniform buffer at binding 0). +struct GridU +{ + float4x4 u_ViewProjection; + float4x4 u_Transform; + float u_GridSize; + float3 u_GridColor; + float u_GridAlpha; + float3 u_CameraPos; +}; +ConstantBuffer gu; + +struct VSInput { float3 position : POSITION; }; +struct VSOutput { float4 position : SV_Position; }; + +[shader("vertex")] +VSOutput vertexMain(VSInput input) +{ + VSOutput output; + float3 scaledPosition = input.position * (gu.u_GridSize / 50.0); + output.position = mul(gu.u_ViewProjection, mul(gu.u_Transform, float4(scaledPosition, 1.0))); + return output; +} + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_Target +{ + float dist = length(gu.u_CameraPos); + float fadeFactor = 1.0 - clamp(dist / 100.0, 0.0, 0.8); + + float3 color = gu.u_GridColor; + float alpha = gu.u_GridAlpha * fadeFactor; + + float gridFade = 1.0 - clamp(dist / 50.0, 0.0, 0.9); + alpha *= gridFade; + + return float4(color, alpha); +} diff --git a/assets/shaders/skybox.slang b/assets/shaders/skybox.slang new file mode 100644 index 0000000..6447bea --- /dev/null +++ b/assets/shaders/skybox.slang @@ -0,0 +1,29 @@ +// Fullscreen skybox cube. View/projection in a ConstantBuffer (UBO, binding 0) +// so both backends drive it through the RHI; the cubemap is a sampler at binding 1. +struct SkyboxU +{ + float4x4 u_Projection; + float4x4 u_View; +}; +ConstantBuffer sb; + +struct VSInput { float3 position : POSITION; }; +struct VSOutput { float4 position : SV_Position; float3 texCoords : TEXCOORD0; }; + +[shader("vertex")] +VSOutput vertexMain(VSInput input) +{ + VSOutput output; + output.texCoords = input.position; + float4 pos = mul(sb.u_Projection, mul(sb.u_View, float4(input.position, 1.0))); + output.position = pos.xyww; // force depth = 1 so the skybox stays behind everything + return output; +} + +SamplerCube u_Skybox; + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_Target +{ + return u_Skybox.Sample(input.texCoords); +} diff --git a/assets/shaders/sphere.slang b/assets/shaders/sphere.slang new file mode 100644 index 0000000..e37700d --- /dev/null +++ b/assets/shaders/sphere.slang @@ -0,0 +1,74 @@ +// Lit sphere (Blinn-Phong + HDRI ambient + selection rim). Uniforms in a +// ConstantBuffer (UBO, binding 0) so both backends drive it through the RHI; +// the environment cubemap is a separate sampler at binding 1. +struct SphereU +{ + float4x4 u_ViewProjection; + float4x4 u_Transform; + float3 u_Color; + float u_Specular; + float u_Emission; + float3 u_LightPos; + float3 u_CameraPos; + int u_IsSelected; + float3 u_OutlineColor; + float u_OutlineWidth; +}; +ConstantBuffer su; + +struct VSInput +{ + float3 position : POSITION; + float3 normal : NORMAL; +}; + +struct VSOutput +{ + float4 position : SV_Position; + float3 normal : TEXCOORD0; + float3 worldPos : TEXCOORD1; +}; + +[shader("vertex")] +VSOutput vertexMain(VSInput input) +{ + VSOutput output; + output.worldPos = mul(su.u_Transform, float4(input.position, 1.0)).xyz; + float3x3 normalMatrix = (float3x3)su.u_Transform; + output.normal = mul(normalMatrix, input.normal); + output.position = mul(su.u_ViewProjection, float4(output.worldPos, 1.0)); + return output; +} + +SamplerCube u_HDRIEnvironment; + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_Target +{ + float3 normal = normalize(input.normal); + float3 lightDir = normalize(su.u_LightPos - input.worldPos); + float3 viewDir = normalize(su.u_CameraPos - input.worldPos); + float3 reflectDir = reflect(-lightDir, normal); + + float3 hdriLight = u_HDRIEnvironment.Sample(normal).rgb; + float hdriIntensity = 0.3; + + float diff = max(dot(normal, lightDir), 0.0); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0); + + float3 diffuse = su.u_Color * (diff + hdriIntensity * hdriLight); + float3 specular = float3(su.u_Specular, su.u_Specular, su.u_Specular) * spec; + float3 emission = su.u_Color * su.u_Emission; + + float3 result = diffuse + specular + emission; + + if (su.u_IsSelected > 0) + { + float ndotv = max(dot(normal, viewDir), 0.0); + float rim = 1.0 - ndotv; + float outlineMask = step(1.0 - su.u_OutlineWidth, rim); + result = lerp(result, su.u_OutlineColor, outlineMask); + } + + return float4(result, 1.0); +} diff --git a/assets/shaders/textured_quad.slang b/assets/shaders/textured_quad.slang new file mode 100644 index 0000000..52d46ed --- /dev/null +++ b/assets/shaders/textured_quad.slang @@ -0,0 +1,28 @@ +struct VSInput +{ + float2 position : POSITION; + float2 texCoord : TEXCOORD0; +}; + +struct VSOutput +{ + float4 position : SV_Position; + float2 texCoord : TEXCOORD0; +}; + +[shader("vertex")] +VSOutput vertexMain(VSInput input) +{ + VSOutput output; + output.position = float4(input.position, 0.0, 1.0); + output.texCoord = input.texCoord; + return output; +} + +Sampler2D u_ScreenTexture; + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_Target +{ + return u_ScreenTexture.Sample(input.texCoord); +} diff --git a/assets/shaders/vk_pipeline_test.slang b/assets/shaders/vk_pipeline_test.slang new file mode 100644 index 0000000..f4d1897 --- /dev/null +++ b/assets/shaders/vk_pipeline_test.slang @@ -0,0 +1,25 @@ +// Minimal shader used only to verify the Vulkan graphics-pipeline path (SPIR-V +// module -> pipeline -> draw -> read-back). Draws a full-screen gradient +// triangle from the vertex id, so it needs no vertex buffer, UBO, or sampler. + +struct VSOutput +{ + float4 position : SV_Position; + float3 color : COLOR0; +}; + +[shader("vertex")] +VSOutput vertexMain(uint vid : SV_VertexID) +{ + float2 p = float2(float((vid << 1) & 2), float(vid & 2)); + VSOutput output; + output.position = float4(p * 2.0 - 1.0, 0.0, 1.0); + output.color = float3(p, 0.5); + return output; +} + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_Target +{ + return float4(input.color, 1.0); +} diff --git a/branding/Logo-Black.jpg b/branding/Logo-Black.jpg deleted file mode 100644 index 5960c8e..0000000 Binary files a/branding/Logo-Black.jpg and /dev/null differ diff --git a/branding/Logo-Monochrome-Black.jpg b/branding/Logo-Monochrome-Black.jpg deleted file mode 100644 index 65181e4..0000000 Binary files a/branding/Logo-Monochrome-Black.jpg and /dev/null differ diff --git a/branding/Logo-Monochrome-White.jpg b/branding/Logo-Monochrome-White.jpg deleted file mode 100644 index 4397006..0000000 Binary files a/branding/Logo-Monochrome-White.jpg and /dev/null differ diff --git a/branding/Logo-White.jpg b/branding/Logo-White.jpg deleted file mode 100644 index 7981c4b..0000000 Binary files a/branding/Logo-White.jpg and /dev/null differ diff --git a/branding/Screenshot1.png b/branding/Screenshot1.png deleted file mode 100644 index 7175ab6..0000000 Binary files a/branding/Screenshot1.png and /dev/null differ diff --git a/branding/Screenshot2.png b/branding/Screenshot2.png deleted file mode 100644 index 2aece5d..0000000 Binary files a/branding/Screenshot2.png and /dev/null differ diff --git a/branding/logo_black.jpg b/branding/logo_black.jpg new file mode 100644 index 0000000..5960c8e Binary files /dev/null and b/branding/logo_black.jpg differ diff --git a/branding/logo_monochrome_black.jpg b/branding/logo_monochrome_black.jpg new file mode 100644 index 0000000..65181e4 Binary files /dev/null and b/branding/logo_monochrome_black.jpg differ diff --git a/branding/logo_monochrome_white.jpg b/branding/logo_monochrome_white.jpg new file mode 100644 index 0000000..4397006 Binary files /dev/null and b/branding/logo_monochrome_white.jpg differ diff --git a/branding/logo_white.jpg b/branding/logo_white.jpg new file mode 100644 index 0000000..7981c4b Binary files /dev/null and b/branding/logo_white.jpg differ diff --git a/branding/screenshot_1.png b/branding/screenshot_1.png new file mode 100644 index 0000000..7175ab6 Binary files /dev/null and b/branding/screenshot_1.png differ diff --git a/branding/screenshot_2.png b/branding/screenshot_2.png new file mode 100644 index 0000000..2aece5d Binary files /dev/null and b/branding/screenshot_2.png differ diff --git a/src/core/application.cpp b/src/core/application.cpp index ae8a5d9..6239c36 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1,36 +1,41 @@ #include "application.h" -#include "rendering/renderer.h" +#include "rendering/render_api.h" // RendererAPI #include "settings_manager.h" #include "hdri_manager.h" -#include "states/simulation_state.h" -#include "states/config_state.h" -#include "states/world_builder_state.h" +#include "platform/opengl/opengl_device.h" +#include "platform/vulkan/vulkan_device.h" #include #include -#include -#include "platform/vulkan/vulkan_renderer.h" -#include "platform/opengl/opengl_renderer.h" - -#ifdef __APPLE__ -#include "platform/metal/metal_context.h" -#include "platform/vulkan/vulkan_context.h" -#endif +#include +#include +#include +#include namespace Donut { Application* Application::s_instance = nullptr; + // Scroll is delivered through a GLFW callback; accumulate it here and drain it + // once per frame in process_input (chaining ImGui's own scroll handler). + static double g_scroll_accum = 0.0; + static GLFWscrollfun g_prev_scroll = nullptr; + static void donut_scroll_callback(GLFWwindow* w, double x, double y) + { + if (g_prev_scroll) g_prev_scroll(w, x, y); + g_scroll_accum += y; + } + Application::Application(const std::string& name, int width, int height) : m_running(true), m_minimized(false) { s_instance = this; - // Select the render API before the window is created: the window is - // built differently for Vulkan (GLFW_NO_API) than for OpenGL. + // Select the render API before the window is created: the window is built + // differently for Vulkan (GLFW_NO_API) than for OpenGL. Logger::init(); SettingsManager::initialize(); { @@ -40,13 +45,10 @@ namespace Donut } if (RendererAPI::get_api() == RendererAPI::API::Vulkan) - vulkan_prepare_glfw(); // must precede glfwInit() inside the Window ctor + RHI::vulkan_prepare_glfw(); // must precede glfwInit() inside the Window ctor m_window = create_scope(name, width, height); - m_window->set_event_callback([this](Event& event) - { - on_event(event); - }); + m_window->set_event_callback([this](Event& event) { on_event(event); }); on_init(); } @@ -57,29 +59,227 @@ namespace Donut s_instance = nullptr; } + auto Application::on_init() -> void + { + int w = 0, h = 0; + glfwGetFramebufferSize((GLFWwindow*)m_window->get_native_window(), &w, &h); + + if (RendererAPI::get_api() == RendererAPI::API::Vulkan) + m_device = RHI::create_vulkan_device(); + else + m_device = RHI::create_opengl_device(); + + if (!m_device->init(m_window->get_native_window(), w, h)) + { + DONUT_ERROR("RHI device initialization failed"); + return; + } + + m_hdri_path = "assets/hdri/hdr_blue_nebulae_1.hdr"; + m_cubemap = m_device->create_cubemap_from_hdri(m_hdri_path); + + m_scene_renderer = create_scope(); + m_scene_renderer->init(*m_device); + m_black_hole_renderer = create_scope(); + m_black_hole_renderer->init(*m_device); + + // Black-hole camera: large-scale orbital viewer outside the disk. + m_camera.set_camera_mode(CameraMode::Orbital); + m_camera.set_orbital_target(glm::vec3(0.0f)); + m_camera.set_orbital_radius(4e11); // ~31 r_s: outside the 12 r_s disk + m_camera.set_orbital_limits(2.2e11, 1.5e12); + m_camera.set_orbital_speed(0.01f); + m_camera.set_zoom_speed(3e10); + m_camera.set_azimuth(0.0f); + m_camera.set_elevation(1.25f); + + // Scene camera: normal-scale orbital world-builder viewer. + m_scene_camera.set_camera_mode(CameraMode::Orbital); + m_scene_camera.set_orbital_target(glm::vec3(0.0f)); + m_scene_camera.set_orbital_radius(15.0); + m_scene_camera.set_orbital_limits(2.0, 200.0); + m_scene_camera.set_orbital_speed(0.01f); + m_scene_camera.set_zoom_speed(2.0); + m_scene_camera.set_azimuth(0.0f); + m_scene_camera.set_elevation((float)std::numbers::pi / 3.0f); + m_scene_camera.update_orbital(); + + m_device->init_imgui(); + // Load the UI font into the device-created ImGui context (backend-agnostic; + // the backend rebuilds the atlas on the next frame). + ImGuiIO& io = ImGui::GetIO(); + if (!io.Fonts->AddFontFromFileTTF("assets/fonts/inter/static/Inter_18pt-Regular.ttf", 16.0f)) + io.Fonts->AddFontDefault(); + + g_prev_scroll = glfwSetScrollCallback((GLFWwindow*)m_window->get_native_window(), donut_scroll_callback); + + m_start_time = glfwGetTime(); + DONUT_INFO("Application ready on {} backend", m_device->device_name()); + } + auto Application::run() -> void { while (m_running) { - if (m_renderer) + glfwPollEvents(); + if (!m_minimized && m_device) + render_frame(); + } + } + + auto Application::render_frame() -> void + { + RHI::CommandList* cmd = m_device->begin_frame(glm::vec4(0.05f, 0.06f, 0.10f, 1.0f)); + if (!cmd) return; // frame skipped (e.g. swapchain recreation) + + m_device->imgui_new_frame(); + build_ui(); + process_input(); + + const glm::vec4 clear(0.05f, 0.06f, 0.10f, 1.0f); + int w = 0, h = 0; + glfwGetFramebufferSize((GLFWwindow*)m_window->get_native_window(), &w, &h); + + if (m_scene_mode) + { + float aspect = (float)w / (float)std::max(h, 1); + m_scene_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f); + CameraView cv; + cv.view = m_scene_camera.get_view_matrix(); + cv.projection = m_scene_camera.get_projection_matrix(); + cv.position = m_scene_camera.get_orbital_position(); + cv.fb_width = w; cv.fb_height = h; + + cmd->begin_render_pass(nullptr, clear); + m_scene_renderer->render(*cmd, cv, m_scene_objects, m_selected_object, m_cubemap.get()); + m_device->imgui_render(*cmd); + cmd->end_render_pass(); + } + else + { + GeodesicView gv; + glm::vec3 pos, fwd; + if (m_camera.get_camera_mode() == CameraMode::FPS) { - glfwPollEvents(); // before the ImGui frame so input is current - if (!m_minimized) - m_renderer->draw_frame(glm::vec4(0.05f, 0.06f, 0.10f, 1.0f), - [this] { build_ui(); }); + pos = m_camera.get_position(); + fwd = m_camera.get_forward_direction(); } else { - if (!m_minimized) - { - on_update(); - on_render(); - } - m_window->on_update(); // polls events + swaps buffers (OpenGL) + pos = m_camera.get_orbital_position(); + fwd = glm::normalize(m_camera.get_orbital_target() - pos); + } + glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); + gv.position = pos; gv.right = right; gv.up = glm::cross(right, fwd); gv.forward = fwd; + gv.tan_half_fov = (float)tan(glm::radians(m_bh_params.fov_degrees * 0.5f)); + gv.aspect = (float)BlackHoleRenderer::GEO_HI_W / (float)BlackHoleRenderer::GEO_HI_H; + gv.moving = m_user_moving; + gv.time = (float)(glfwGetTime() - m_start_time); + + m_black_hole_renderer->render_geodesic(*cmd, gv, m_bh_params, m_cubemap.get()); + cmd->begin_render_pass(nullptr, clear); + m_black_hole_renderer->blit(*cmd, w, h); + m_device->imgui_render(*cmd); + cmd->end_render_pass(); + } + + m_device->end_frame(); + } + + auto Application::process_input() -> void + { + GLFWwindow* window = (GLFWwindow*)m_window->get_native_window(); + bool over_ui = ImGui::GetCurrentContext() && ImGui::GetIO().WantCaptureMouse; + + double now = glfwGetTime(); + float dt = m_last_frame_time > 0.0 ? (float)(now - m_last_frame_time) : 0.0f; + m_last_frame_time = now; + + double mx = 0, my = 0; + glfwGetCursorPos(window, &mx, &my); + bool left_down = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS; + + if (!m_scene_mode && m_camera.get_camera_mode() == CameraMode::FPS) + { + if (left_down && !m_left_was_down) { m_fps_last_x = mx; m_fps_last_y = my; } + if (left_down && !over_ui) + m_camera.on_mouse_move(float(mx - m_fps_last_x), float(m_fps_last_y - my)); + m_fps_last_x = mx; m_fps_last_y = my; + m_left_was_down = left_down; + + bool over_kb = ImGui::GetCurrentContext() && ImGui::GetIO().WantCaptureKeyboard; + bool moved = false; + if (!over_kb && dt > 0.0f) + { + float mdt = dt * (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS ? 4.0f : 1.0f); + if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) { m_camera.move_forward(mdt); moved = true; } + if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) { m_camera.move_backward(mdt); moved = true; } + if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) { m_camera.move_left(mdt); moved = true; } + if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) { m_camera.move_right(mdt); moved = true; } + if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) { m_camera.move_up(mdt); moved = true; } + if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) { m_camera.move_down(mdt); moved = true; } } + g_scroll_accum = 0.0; + m_user_moving = moved || (left_down && !over_ui); + } + else + { + Camera& cam = m_scene_mode ? m_scene_camera : m_camera; + if (left_down && !m_left_was_down && !over_ui) + cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, 0); + else if (!left_down && m_left_was_down) + cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0); + m_left_was_down = left_down; + + cam.process_orbital_mouse_move(mx, my); + + double scroll = g_scroll_accum; g_scroll_accum = 0.0; + if (scroll != 0.0 && !over_ui) + cam.process_orbital_scroll(0.0, scroll); + + m_user_moving = cam.is_dragging() || cam.is_panning(); + } + } + + auto Application::set_free_fly(bool enabled) -> void + { + const bool is_fps = m_camera.get_camera_mode() == CameraMode::FPS; + if (enabled == is_fps) return; + if (enabled) + { + // Seed the fly pose from the current orbital framing so the view is continuous. + glm::vec3 pos = m_camera.get_orbital_position(); + glm::vec3 fwd = glm::normalize(m_camera.get_orbital_target() - pos); + float pitch = glm::degrees(asin(glm::clamp(fwd.y, -1.0f, 1.0f))); + float yaw = glm::degrees(atan2(fwd.z, fwd.x)); + m_camera.set_camera_mode(CameraMode::FPS); + m_camera.set_movement_speed(2.0e10f); + m_camera.set_mouse_sensitivity(0.15f); + m_camera.set_position(pos); + m_camera.set_rotation(glm::vec3(pitch, yaw, 0.0f)); + } + else + { + m_camera.set_camera_mode(CameraMode::Orbital); // orbital state was left intact } } + auto Application::reset_camera() -> void + { + m_camera.set_camera_mode(CameraMode::Orbital); + m_camera.set_orbital_radius(4e11); + m_camera.set_azimuth(0.0f); + m_camera.set_elevation(1.25f); + } + + auto Application::set_hdri(const std::string& path) -> void + { + if (m_hdri_path == path || !m_device) return; + m_device->wait_idle(); + m_cubemap = m_device->create_cubemap_from_hdri(path); // old cube freed after idle + m_hdri_path = path; + } + // One-time ImGui theme: rounded, roomy, dark with a warm accretion-disk accent. static auto apply_donut_style() -> void { @@ -120,8 +320,7 @@ namespace Donut { static bool styled = false; if (!styled) { apply_donut_style(); styled = true; } - if (!m_renderer) return; - RendererBackend& r = *m_renderer; + if (!m_device) return; // Full-viewport dock space with a pass-through centre, so the panel can be // docked to any edge while the render shows through the middle. @@ -130,9 +329,8 @@ namespace Donut ImGui::SetNextWindowSize(ImVec2(340, 580), ImGuiCond_FirstUseEver); ImGui::Begin("Donut \xc2\xb7 Black Hole"); - // --- header: device + performance ----------------------------------- ImGuiIO& io = ImGui::GetIO(); - ImGui::TextDisabled("%s", r.device_name().empty() ? "GPU" : r.device_name().c_str()); + ImGui::TextDisabled("%s", m_device->device_name().empty() ? "GPU" : m_device->device_name().c_str()); ImGui::Text("%.0f FPS", io.Framerate); ImGui::SameLine(); ImGui::TextDisabled("%.1f ms/frame", io.Framerate > 0 ? 1000.0f / io.Framerate : 0.0f); ImGui::Spacing(); @@ -155,29 +353,28 @@ namespace Donut } ImGui::Spacing(); - bool scene = r.is_scene_mode(); ImGui::TextDisabled("VIEW"); - if (ImGui::RadioButton("Black hole", !scene)) r.set_scene_mode(false); + if (ImGui::RadioButton("Black hole", !m_scene_mode)) m_scene_mode = false; ImGui::SameLine(); - if (ImGui::RadioButton("Scene", scene)) r.set_scene_mode(true); + if (ImGui::RadioButton("Scene", m_scene_mode)) m_scene_mode = true; ImGui::Separator(); - if (scene) + if (m_scene_mode) { build_scene_ui(); } else { - BlackHoleParams& bh = r.black_hole_params(); + BlackHoleParams& bh = m_bh_params; if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) { - bool ff = r.is_free_fly(); - if (ImGui::RadioButton("Orbital", !ff)) r.set_free_fly(false); + bool ff = m_camera.get_camera_mode() == CameraMode::FPS; + if (ImGui::RadioButton("Orbital", !ff)) set_free_fly(false); ImGui::SameLine(); - if (ImGui::RadioButton("Free-fly", ff)) r.set_free_fly(true); + if (ImGui::RadioButton("Free-fly", ff)) set_free_fly(true); ImGui::SameLine(); - if (ImGui::Button("Reset view")) r.reset_camera(); + if (ImGui::Button("Reset view")) reset_camera(); ImGui::SliderFloat("FOV", &bh.fov_degrees, 20.0f, 90.0f, "%.0f\xc2\xb0"); ImGui::TextDisabled(ff ? "WASD move | Q/E down-up | Shift boost | drag look" : "Drag to orbit | scroll to zoom"); @@ -203,15 +400,14 @@ namespace Donut if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen)) { auto& hdri = HDRIManager::get(); - std::string current = r.current_hdri(); - std::string preview = hdri.get_hdri_name(current); + std::string preview = hdri.get_hdri_name(m_hdri_path); if (ImGui::BeginCombo("Starfield", preview.c_str())) { for (const auto& path : hdri.get_available_hdri()) { - bool selected = (path == current); + bool selected = (path == m_hdri_path); if (ImGui::Selectable(hdri.get_hdri_name(path).c_str(), selected)) - r.set_hdri(path); + set_hdri(path); if (selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); @@ -225,14 +421,12 @@ namespace Donut auto Application::build_scene_ui() -> void { - RendererBackend& r = *m_renderer; if (!ImGui::CollapsingHeader("Objects", ImGuiTreeNodeFlags_DefaultOpen)) return; ImGui::TextDisabled("Drag to orbit, scroll to zoom. HDRI skybox behind."); - auto& objs = r.scene_objects(); - static int sel = 0; - if (sel >= (int)objs.size()) sel = (int)objs.size() - 1; + auto& objs = m_scene_objects; + if (m_selected_object >= (int)objs.size()) m_selected_object = (int)objs.size() - 1; ImGui::Text("Spheres (%d)", (int)objs.size()); if (ImGui::Button("Add") && objs.size() < 64) @@ -242,27 +436,26 @@ namespace Donut o.radius = 1.5f; o.color = glm::vec3(0.35f + 0.12f * (objs.size() % 5), 0.55f, 0.9f - 0.12f * (objs.size() % 4)); objs.push_back(o); - sel = (int)objs.size() - 1; + m_selected_object = (int)objs.size() - 1; } ImGui::SameLine(); if (ImGui::Button("Delete") && !objs.empty()) { - objs.erase(objs.begin() + sel); - if (sel >= (int)objs.size()) sel = (int)objs.size() - 1; + objs.erase(objs.begin() + m_selected_object); + if (m_selected_object >= (int)objs.size()) m_selected_object = (int)objs.size() - 1; } ImGui::BeginChild("obj_list", ImVec2(0, 90), true); for (int i = 0; i < (int)objs.size(); ++i) { std::string label = "Sphere " + std::to_string(i); - if (ImGui::Selectable(label.c_str(), sel == i)) sel = i; + if (ImGui::Selectable(label.c_str(), m_selected_object == i)) m_selected_object = i; } ImGui::EndChild(); - r.set_selected_object(sel); - if (sel >= 0 && sel < (int)objs.size()) + if (m_selected_object >= 0 && m_selected_object < (int)objs.size()) { - SceneObject& o = objs[sel]; + SceneObject& o = objs[m_selected_object]; ImGui::DragFloat3("Position", &o.position.x, 0.1f); ImGui::DragFloat("Radius", &o.radius, 0.05f, 0.1f, 20.0f); ImGui::ColorEdit3("Color", &o.color.x); @@ -286,115 +479,30 @@ namespace Donut dispatcher.dispatch([this, &event](WindowResizeEvent& e) { - if (e.get_width() == 0 || e.get_height() == 0) - m_minimized = true; - else - m_minimized = false; - - if (m_renderer) - { - m_renderer->resize(e.get_width(), e.get_height()); - } - else - { - Renderer::on_window_resize(e.get_width(), e.get_height()); - if (m_engine) m_engine->set_window_dimensions(e.get_width(), e.get_height()); - } + m_minimized = (e.get_width() == 0 || e.get_height() == 0); + if (m_device) m_device->resize(e.get_width(), e.get_height()); event.handled = true; return true; }); - - dispatcher.dispatch([this, &event](KeyPressedEvent& e) - { - if (!m_state_manager) - return true; - if (e.get_key_code() == GLFW_KEY_1) - { - m_state_manager->switch_to_state("Config"); - event.handled = true; - return true; - } - else if (e.get_key_code() == GLFW_KEY_2) - { - m_state_manager->switch_to_state("Simulation"); - event.handled = true; - return true; - } - else if (e.get_key_code() == GLFW_KEY_3) - { - m_state_manager->switch_to_state("WorldBuilder"); - event.handled = true; - return true; - } - - return true; - }); - - if (m_state_manager) - m_state_manager->on_event(event); - } - - auto Application::on_init() -> void - { - // (Logger, settings and API selection happen in the constructor.) Both - // backends run the SAME application through the RendererBackend interface, - // so the app behaves identically regardless of the active graphics API. - int w = 0, h = 0; - glfwGetFramebufferSize((GLFWwindow*)m_window->get_native_window(), &w, &h); - - if (RendererAPI::get_api() == RendererAPI::API::Vulkan) - m_renderer = create_scope(); - else - m_renderer = create_scope(); - - if (!m_renderer->init(m_window->get_native_window(), w, h)) - DONUT_ERROR("Renderer initialization failed"); - else - m_renderer->init_ui(); } auto Application::on_shutdown() -> void { - if (m_renderer) - { - m_renderer->shutdown(); - m_renderer.reset(); - } - else + if (m_device) { - if (m_state_manager) - m_state_manager->shutdown(); - Renderer::shutdown(); + m_device->wait_idle(); + m_scene_renderer.reset(); + m_black_hole_renderer.reset(); + m_cubemap.reset(); + // Free HDRIManager's cached GPU textures now, while the render context + // is still alive. On OpenGL these are GL textures owned by a static + // singleton; letting them destruct at program exit would call + // glDeleteTextures after the GL context is gone and crash on close. + HDRIManager::get().clear_cache(); + m_device->shutdown(); + m_device.reset(); } SettingsManager::shutdown(); Logger::shutdown(); } - - auto Application::on_update() -> void - { - float current_frame = (float)glfwGetTime(); - m_delta_time = current_frame - m_last_frame; - m_last_frame = current_frame; - - m_state_manager->update(m_delta_time); - } - - auto Application::on_render() -> void - { - m_state_manager->render(); - m_window->begin_im_gui_frame(); - - ImGuizmo::BeginFrame(); - ImGuizmo::SetOrthographic(false); - - setup_docking_layout(); - m_state_manager->on_im_ui_render(); - m_window->end_im_gui_frame(); - } - - auto Application::setup_docking_layout() -> void - { - ImGuiViewport* viewport = ImGui::GetMainViewport(); - ImGui::DockSpaceOverViewport(0, viewport, ImGuiDockNodeFlags_PassthruCentralNode); - } }; diff --git a/src/core/application.h b/src/core/application.h index 4c377f0..4b67c84 100644 --- a/src/core/application.h +++ b/src/core/application.h @@ -1,16 +1,25 @@ #pragma once -#include "state_manager.h" #include "memory.h" #include "window.h" #include "event.h" #include "log.h" +#include "camera.h" -#include "engine/engine.h" -#include "rendering/renderer_backend.h" +#include "rendering/rhi.h" +#include "rendering/scene_renderer.h" +#include "rendering/black_hole_renderer.h" + +#include +#include namespace Donut { + // The application owns ONE RHI::Device (OpenGL or Vulkan, chosen at startup) + // and the two portable renderers written on top of it, plus all the shared + // state (cameras, scene objects, disk parameters, HDRI). Because the rendering + // lives in the RHI and the shared renderers, the app behaves identically no + // matter which backend is active. class Application { public: @@ -21,33 +30,52 @@ namespace Donut auto run() -> void; auto close() -> void; - auto get_window() -> Window& { return *m_window; } - auto get_state_manager() -> StateManager& { return *m_state_manager; } - auto get_engine() -> Engine& { return *m_engine; } - static auto get() -> Application& { return *s_instance; } + auto get_window() -> Window& { return *m_window; } + static auto get() -> Application& { return *s_instance; } private: auto on_init() -> void; auto on_shutdown() -> void; - auto on_update() -> void; - auto on_render() -> void; auto on_event(Event& event) -> void; - auto setup_docking_layout() -> void; - auto build_ui() -> void; // shared ImGui panel, drives the active RendererBackend - auto build_scene_ui() -> void; // scene-view (object list) section of the UI + + auto build_ui() -> void; // shared ImGui panel + auto build_scene_ui() -> void; // scene-view (object list) section + + auto process_input() -> void; // orbit / free-fly / zoom, was per-backend + auto set_free_fly(bool enabled) -> void; + auto reset_camera() -> void; + auto set_hdri(const std::string& path) -> void; + + auto render_frame() -> void; private: - Scope m_state_manager; Scope m_window; - Scope m_engine; - Scope m_renderer; // active backend (non-null once a backend runs the app) + + // The unified renderer: one device + the shared renderers on top of it. + Scope m_device; + Scope m_scene_renderer; + Scope m_black_hole_renderer; + Ref m_cubemap; // shared HDRI environment + + Camera m_camera{ 60.0f, 16.0f / 9.0f, 0.1f, 100.0f }; // black-hole view + Camera m_scene_camera{ 45.0f, 16.0f / 9.0f, 0.1f, 1000.0f }; // world-builder view + + BlackHoleParams m_bh_params; + std::vector m_scene_objects{ SceneObject{} }; + int m_selected_object = 0; + bool m_scene_mode = false; + std::string m_hdri_path; + + // input tracking (mirrors the old per-backend process_input) + bool m_left_was_down = false; + double m_fps_last_x = 0.0, m_fps_last_y = 0.0; + double m_last_frame_time = 0.0; + double m_start_time = 0.0; + bool m_user_moving = false; bool m_running; bool m_minimized; - float m_delta_time = 0.0f; - float m_last_frame = 0.0f; - static Application* s_instance; }; } diff --git a/src/core/hdri_manager.h b/src/core/hdri_manager.h index fa05330..d98830f 100644 --- a/src/core/hdri_manager.h +++ b/src/core/hdri_manager.h @@ -39,9 +39,9 @@ namespace Donut std::vector m_available_hdri = { - "assets/hdri/HDR_blue_nebulae-1.hdr", - "assets/hdri/HDR_subdued_blue_nebulae.hdr", - "assets/hdri/HDR_subdued_multi_nebulae.hdr", + "assets/hdri/hdr_blue_nebulae_1.hdr", + "assets/hdri/hdr_subdued_blue_nebulae.hdr", + "assets/hdri/hdr_subdued_multi_nebulae.hdr", "assets/hdri/night_sky.hdr" }; }; diff --git a/src/core/settings_manager.cpp b/src/core/settings_manager.cpp index ca4cac5..6909c60 100644 --- a/src/core/settings_manager.cpp +++ b/src/core/settings_manager.cpp @@ -1,7 +1,6 @@ #include "settings_manager.h" #include "log.h" #include "theme_manager.h" -#include "rendering/renderer.h" #include #include diff --git a/src/core/state.h b/src/core/state.h deleted file mode 100644 index 623f1c2..0000000 --- a/src/core/state.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "event.h" - -namespace Donut -{ - class State - { - public: - virtual ~State() = default; - - virtual auto on_enter() -> void = 0; - virtual auto on_exit() -> void = 0; - virtual auto on_update(float delta_time) -> void = 0; - virtual auto on_render() -> void = 0; - virtual auto on_im_ui_render() -> void = 0; - virtual auto on_event(Event& event) -> void = 0; - }; -}; diff --git a/src/core/state_manager.cpp b/src/core/state_manager.cpp deleted file mode 100644 index 8018401..0000000 --- a/src/core/state_manager.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include "state_manager.h" -#include "log.h" - -namespace Donut -{ - auto StateManager::shutdown() -> void - { - if (m_current_state) - m_current_state->on_exit(); - - destroy_states(); - DONUT_INFO("StateManager shutdown"); - } - - auto StateManager::destroy_states() -> void - { - m_states.clear(); - m_current_state = nullptr; - m_current_state_name = ""; - } - - auto StateManager::register_state(const std::string& state_name, Scope state) -> void - { - if (m_states.find(state_name) != m_states.end()) - DONUT_WARN("State '{}' already exists, overwriting", state_name); - m_states[state_name] = std::move(state); - DONUT_INFO("Registered state: {}", state_name); - } - - auto StateManager::switch_to_state(const std::string& state_name) -> void - { - auto it = m_states.find(state_name); - if (it == m_states.end()) - { - DONUT_ERROR("Attempted to switch to unknown state: {}", state_name); - return; - } - - State* new_state = it->second.get(); - if (new_state == m_current_state) - return; - - if (m_current_state) - m_current_state->on_exit(); - - m_current_state = new_state; - m_current_state_name = state_name; - m_current_state->on_enter(); - DONUT_INFO("Switched to state: {}", state_name); - } - - auto StateManager::get_state(const std::string& state_name) -> State* - { - auto it = m_states.find(state_name); - if (it != m_states.end()) - return it->second.get(); - return nullptr; - } - - auto StateManager::has_state(const std::string& state_name) const -> bool - { - return m_states.find(state_name) != m_states.end(); - } - - auto StateManager::update(float delta_time) -> void - { - if (m_current_state) - m_current_state->on_update(delta_time); - } - - auto StateManager::render() -> void - { - if (m_current_state) - m_current_state->on_render(); - } - - auto StateManager::on_im_ui_render() -> void - { - if (m_current_state) - m_current_state->on_im_ui_render(); - } - - auto StateManager::on_event(Event& event) -> void - { - if (m_current_state) - m_current_state->on_event(event); - } -} diff --git a/src/core/state_manager.h b/src/core/state_manager.h deleted file mode 100644 index 446841e..0000000 --- a/src/core/state_manager.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include "memory.h" -#include "state.h" - -#include -#include - -namespace Donut -{ - class StateManager - { - public: - ~StateManager() = default; - auto shutdown() -> void; - - auto update(float delta_time) -> void; - auto render() -> void; - auto on_im_ui_render() -> void; - auto on_event(Event& event) -> void; - - auto register_state(const std::string& state_name, Scope state) -> void; - auto switch_to_state(const std::string& state_name) -> void; - - auto get_current_state_name() const -> std::string { return m_current_state_name; } - auto get_current_state() const -> State* { return m_current_state; } - - auto get_state(const std::string& state_name) -> State*; - auto has_state(const std::string& state_name) const -> bool; - - private: - auto create_states() -> void; - auto destroy_states() -> void; - - private: - State* m_current_state = nullptr; - std::string m_current_state_name = ""; - - std::unordered_map> m_states; - }; -} diff --git a/src/core/window.cpp b/src/core/window.cpp index 047ef0d..dd5882c 100644 --- a/src/core/window.cpp +++ b/src/core/window.cpp @@ -1,14 +1,8 @@ -#include #include "window.h" -#include "theme_manager.h" -#include "rendering/renderer.h" // RendererAPI::get_api() +#include "rendering/render_api.h" // RendererAPI::get_api() #include -#include -#include -#include - namespace Donut { static bool s_glfw_initialized = false; @@ -94,8 +88,6 @@ namespace Donut { DONUT_INFO("Shutting down window: ", m_title); - shutdown_im_gui(); - glfwDestroyWindow(m_window); s_glfw_window_count--; @@ -133,101 +125,6 @@ namespace Donut GLFW_CURSOR_NORMAL : GLFW_CURSOR_HIDDEN); } - auto Window::init_im_gui() -> void - { - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - ImGuiIO& io = ImGui::GetIO(); - io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; - io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; -#ifndef __APPLE__ - // Multi-viewport (dragging ImGui panels out as separate OS windows) - // relies on a populated platform-monitor list and is unreliable on - // macOS, where it intermittently asserts (Monitors.Size > 0) and - // aborts. Docking stays enabled; panels just remain inside the window. - io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; -#endif - - setup_im_gui_fonts(); - ThemeManager::set_theme(Theme::Dark); - - ImGuiStyle& style = ImGui::GetStyle(); - if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) - { - style.WindowRounding = 0.0f; - style.Colors[ImGuiCol_WindowBg].w = 1.0f; - } - - ImGui_ImplGlfw_InitForOpenGL(m_window, true); -#ifdef __APPLE__ - // macOS uses a core-profile context, which rejects the legacy - // "#version 130" GLSL the ImGui backend defaults to. 150 is the - // minimum core-profile GLSL that macOS's OpenGL 4.1 accepts. - ImGui_ImplOpenGL3_Init("#version 150"); -#else - ImGui_ImplOpenGL3_Init("#version 130"); -#endif - - DONUT_INFO("ImGUI initialized successfully"); - } - - auto Window::setup_im_gui_fonts() -> void - { - ImGuiIO& io = ImGui::GetIO(); - io.Fonts->Clear(); - - m_main_font = io.Fonts->AddFontFromFileTTF("assets/fonts/inter/static/Inter_18pt-Regular.ttf", 16.0f); - if (!m_main_font) - { - DONUT_WARN("Failed to load Inter font, falling back to default"); - m_main_font = io.Fonts->AddFontDefault(); - } - else - DONUT_INFO("Successfully loaded Inter font"); - - m_large_font = io.Fonts->AddFontFromFileTTF("assets/fonts/inter/static/Inter_18pt-Bold.ttf", 20.0f); - if (!m_large_font) - m_large_font = m_main_font; - - m_small_font = io.Fonts->AddFontFromFileTTF("assets/fonts/inter/static/Inter_18pt-Light.ttf", 12.0f); - if (!m_small_font) - m_small_font = m_main_font; - - io.FontDefault = m_main_font; - DONUT_INFO("ImGUI fonts loaded successfully"); - } - - auto Window::shutdown_im_gui() -> void - { - ImGui_ImplOpenGL3_Shutdown(); - ImGui_ImplGlfw_Shutdown(); - ImGui::DestroyContext(); - - DONUT_INFO("ImGUI shutdown"); - } - - auto Window::begin_im_gui_frame() -> void - { - ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplGlfw_NewFrame(); - ImGui::NewFrame(); - } - - auto Window::end_im_gui_frame() -> void - { - ImGui::Render(); - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); - - ImGuiIO& io = ImGui::GetIO(); - if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) - { - GLFWwindow* backup_current_context = glfwGetCurrentContext(); - ImGui::UpdatePlatformWindows(); - ImGui::RenderPlatformWindowsDefault(); - glfwMakeContextCurrent(backup_current_context); - } - } - auto Window::glfw_error_callback(int error, const char* description) -> void { DONUT_ERROR("GLFW Error ({}): {}", error, description ? description : ""); diff --git a/src/core/window.h b/src/core/window.h index 9b7d7b9..b8b59c5 100644 --- a/src/core/window.h +++ b/src/core/window.h @@ -4,7 +4,6 @@ #include "log.h" #include -#include #include @@ -38,17 +37,10 @@ namespace Donut auto is_cursor_locked() const -> bool { return m_cursor_locked; } auto is_cursor_visible() const -> bool { return m_cursor_visible; } - auto init_im_gui() -> void; - auto begin_im_gui_frame() -> void; - auto end_im_gui_frame() -> void; - private: auto init() -> void; auto shutdown() -> void; - auto setup_im_gui_fonts() -> void; - auto shutdown_im_gui() -> void; - static auto glfw_error_callback(int error, const char* description) -> void; static auto glfw_window_close_callback(GLFWwindow* window) -> void; static auto glfw_window_size_callback(GLFWwindow* window, int width, int height) -> void; @@ -70,9 +62,5 @@ namespace Donut bool m_is_closed; bool m_cursor_locked = false; bool m_cursor_visible = true; - - ImFont* m_main_font = nullptr; - ImFont* m_small_font = nullptr; - ImFont* m_large_font = nullptr; }; } diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp deleted file mode 100644 index bf5a2b6..0000000 --- a/src/engine/engine.cpp +++ /dev/null @@ -1,625 +0,0 @@ -#include -#include -#include - -#include -#include - -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include "stb_image_write.h" - -#include "engine.h" -#include "core/log.h" -#include "core/hdri_manager.h" -#include "rendering/vertex_buffer.h" -#include "rendering/index_buffer.h" - -namespace Donut -{ - Engine::Engine() - : m_sag_a(glm::vec3(0.0f, 0.0f, 0.0f), 8.54e36f) - { - m_width = 1280; - m_height = 720; - - m_camera.set_camera_mode(CameraMode::Orbital); - m_camera.set_orbital_radius(1e11); - m_camera.set_orbital_limits(1e9, 1e13); - m_camera.set_orbital_speed(0.01f); - m_camera.set_zoom_speed(1e10f); - - m_objects = - { - { glm::vec4(0.00f, 0.00f, 0.00f, m_sag_a.m_rs), glm::vec4(0, 0, 0, 1), static_cast(m_sag_a.m_mass) } - }; - - // The geodesic ray tracer used to be a compute shader; it is now a - // fullscreen vertex+fragment pass (see dispatch_compute) so it runs on - // macOS OpenGL 4.1, which has no compute shaders. - m_compute_program = Ref(Shader::create("assets/shaders/Geodesic.glsl")); - m_shader_program = Ref(Shader::create("assets/shaders/TexturedQuad.glsl")); - m_blur_shader = Ref(Shader::create("assets/shaders/Blur.glsl")); - - auto& hdri_manager = HDRIManager::get(); - m_hdri_environment = hdri_manager.get_current_hdri(); - if (!m_hdri_environment) - { - hdri_manager.set_current_hdri("assets/hdri/HDR_blue_nebulae-1.hdr"); - m_hdri_environment = hdri_manager.get_current_hdri(); - if (!m_hdri_environment) - DONUT_WARN("Failed to load default HDRI, using fallback"); - } - - m_camera_ubo = UniformBuffer::create(128, 1); - m_disk_ubo = UniformBuffer::create(sizeof(float) * 6, 2); // r1,r2,turbulence,thickness,brightness,temperature - - uint32_t obj_ubo_size = sizeof(int) + 3 * sizeof(float) - + 16 * (sizeof(glm::vec4) + sizeof(glm::vec4)) - + 16 * sizeof(float); - m_objects_ubo = UniformBuffer::create(obj_ubo_size, 3); - - m_simulation_ubo = UniformBuffer::create(sizeof(int) * 2 + sizeof(float) * 2, 4); - - auto result = QuadVAO(); - m_quad_vao = result.first; - m_texture = result.second; - - // GLSL 4.10 forbids explicit binding qualifiers on uniform blocks, so - // associate the geodesic shader's blocks with their UBO binding points - // from the host side instead. - if (m_compute_program) - { - uint32_t prog = m_compute_program->get_renderer_id(); - struct { const char* name; uint32_t point; } blocks[] = - { - { "Camera", 1 }, { "Disk", 2 }, { "Objects", 3 }, { "Simulation", 4 } - }; - for (const auto& b : blocks) - { - uint32_t idx = glGetUniformBlockIndex(prog, b.name); - if (idx != GL_INVALID_INDEX) - glUniformBlockBinding(prog, idx, b.point); - } - } - - glGenFramebuffers(1, &m_geodesic_fbo); - } - - auto Engine::update_window_dimensions() -> void - { - update_compute_dimensions(); - } - - auto Engine::set_window_dimensions(int width, int height) -> void - { - int old_width = m_width; - int old_height = m_height; - int old_compute_height = m_compute_height; - - m_width = width; - m_height = height; - - if (old_width != m_width || - old_height != m_height || - old_compute_height != m_compute_height) - update_compute_dimensions(); - } - - auto Engine::update_performance(float delta_time) -> void - { - if (delta_time > 0.0f) - m_current_fps = 1.0f / delta_time; - } - - auto Engine::update_compute_dimensions() -> void - { - m_texture = Texture2D::create(get_compute_width(), m_compute_height); - } - - auto Engine::draw_full_screen_quad() -> void - { - RenderCommand::set_viewport(0, 0, m_width, m_height); - - m_shader_program->bind(); - m_quad_vao->bind(); - - m_texture->bind(0); - m_shader_program->set_int("u_ScreenTexture", 0); - - RenderCommand::disable_depth_test(); - RenderCommand::draw_arrays(6); - RenderCommand::enable_depth_test(); - } - - auto Engine::draw_blur_pass() -> void - { - RenderCommand::set_viewport(0, 0, m_width, m_height); - - m_blur_shader->bind(); - m_quad_vao->bind(); - - m_texture->bind(0); - m_blur_shader->set_int("u_ScreenTexture", 0); - m_blur_shader->set_float2("u_Resolution", glm::vec2(m_width, m_height)); - m_blur_shader->set_float("u_BlurStrength", m_blur_strength); - m_blur_shader->set_float("u_GlowIntensity", m_glow_intensity); - - RenderCommand::disable_depth_test(); - RenderCommand::draw_arrays(6); - RenderCommand::enable_depth_test(); - } - - auto Engine::draw_geodesic_pass(int cw, int ch) -> void - { - m_quad_vao->bind(); - RenderCommand::disable_depth_test(); - -#ifdef __APPLE__ - // macOS aborts any GPU submission that runs longer than a couple of - // seconds ("GPU Hang"). The geodesic ray-marcher can far exceed that in - // a single fullscreen draw, so render it in scissored tiles and flush - // after each, keeping every submission short enough to survive the - // watchdog. Compute-capable platforms draw it in one pass. - // Largest tile that keeps a tile's worst-case work (tile^2 * step_cap) - // inside the safe watchdog zone measured on this GPU (~25M pixel-steps - // per submission); bigger tiles mean fewer glFinish stalls. - const int tile = 64; - glEnable(GL_SCISSOR_TEST); - for (int y = 0; y < ch; y += tile) - { - int th = std::min(tile, ch - y); - for (int x = 0; x < cw; x += tile) - { - int tw = std::min(tile, cw - x); - glScissor(x, y, tw, th); - RenderCommand::draw_arrays(6); - glFinish(); - } - } - glDisable(GL_SCISSOR_TEST); -#else - RenderCommand::draw_arrays(6); -#endif - - RenderCommand::enable_depth_test(); - } - - auto Engine::dispatch_compute(const Camera& cam) -> void - { - auto& hdri_manager = HDRIManager::get(); - m_hdri_environment = hdri_manager.get_current_hdri(); - - int cw = get_compute_width(); - int ch = m_compute_height; - - // Render the geodesic pass into m_texture through an FBO. This replaces - // the old compute dispatch + image_store path, which relied on OpenGL - // 4.3 compute and 4.2 image load/store that macOS does not provide. - glBindFramebuffer(GL_FRAMEBUFFER, m_geodesic_fbo); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture->get_renderer_id(), 0); - glViewport(0, 0, cw, ch); - - m_compute_program->bind(); - upload_camera_ubo(cam); - upload_disk_ubo(); - upload_objects_ubo(m_objects); - upload_simulation_ubo(); - m_compute_program->set_float2("u_Resolution", glm::vec2(static_cast(cw), static_cast(ch))); - - if (m_hdri_environment) - { - m_hdri_environment->bind(5); - m_compute_program->set_int("u_HDRIEnvironment", 5); - } - - draw_geodesic_pass(cw, ch); - - glBindFramebuffer(GL_FRAMEBUFFER, 0); - } - - auto Engine::upload_camera_ubo(const Camera& cam) -> void - { - struct UBOData - { - glm::vec3 pos; float _pad0; - glm::vec3 right; float _pad1; - glm::vec3 up; float _pad2; - glm::vec3 forward; float _pad3; - float tan_half_fov; - float aspect; - bool moving; - int _pad4; - } data; - - glm::vec3 pos, fwd; - if (cam.get_camera_mode() == CameraMode::FPS) - { - pos = cam.get_position(); - fwd = cam.get_forward_direction(); - } - else - { - pos = cam.get_orbital_position(); - fwd = glm::normalize(cam.get_orbital_target() - pos); - } - glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); - glm::vec3 up = glm::cross(right, fwd); - - data.pos = pos; - data.right = right; - data.up = up; - data.forward = fwd; - data.tan_half_fov = static_cast(tan(glm::radians(m_bh.fov_degrees * 0.5f))); - data.aspect = static_cast(get_compute_width()) / static_cast(m_compute_height); - data.moving = cam.is_dragging() || cam.is_panning(); - - m_camera_ubo->set_data(&data, sizeof(UBOData)); - m_camera_ubo->bind(1); - } - - auto Engine::upload_objects_ubo(const std::vector& objs) -> void - { - struct UBOData - { - int num_objects; - float _pad0, _pad1, _pad2; - glm::vec4 pos_radius[16]; - glm::vec4 color[16]; - float mass[16]; - } data; - - size_t count = std::min(objs.size(), size_t(16)); - data.num_objects = static_cast(count); - - for (size_t i = 0; i < count; ++i) - { - data.pos_radius[i] = objs[i].m_pos_radius; - data.color[i] = objs[i].m_color; - data.mass[i] = objs[i].m_mass; - } - - m_objects_ubo->set_data(&data, sizeof(data)); - m_objects_ubo->bind(3); - } - - auto Engine::upload_disk_ubo() -> void - { - // Layout matches the shared Geodesic Disk struct (same as Vulkan). Radii - // are in Schwarzschild radii x the shader's SagA_rs constant (1.269e10). - const float rs = 1.269e10f; - float disk_data[6] = { - std::max(m_bh.disk_inner_rs, 3.0f) * rs, - std::max(m_bh.disk_outer_rs, m_bh.disk_inner_rs + 0.5f) * rs, - std::max(m_bh.turbulence, 0.0f), - rs * 0.1f, // slab half-thickness (fixed) - std::max(m_bh.brightness, 0.0f), - std::max(m_bh.temperature, 1000.0f), - }; - m_disk_ubo->set_data(disk_data, sizeof(disk_data)); - m_disk_ubo->bind(2); - } - - auto Engine::upload_simulation_ubo() -> void - { - struct UBOData - { - int max_steps_moving; - int max_steps_static; - float early_exit_distance; - float time; - } data; - - data.max_steps_moving = m_bh.quality_steps; - data.max_steps_static = m_bh.quality_steps; // resolution/tiling is the lever, not step count - data.early_exit_distance = m_early_exit_distance; - data.time = static_cast(glfwGetTime()) * m_rotation_speed; - -#ifdef __APPLE__ - // macOS has no compute shaders, so the geodesic pass runs as a tiled - // fragment shader under the OS GPU watchdog. Cap the step count so a - // single 64px tile stays under the safe per-submission budget (~25M - // pixel-steps); Windows/Linux keep the full count. - data.max_steps_moving = std::min(data.max_steps_moving, 6000); - data.max_steps_static = std::min(data.max_steps_static, 6000); -#endif - - m_simulation_ubo->set_data(&data, sizeof(data)); - m_simulation_ubo->bind(4); - } - - auto Engine::update_physics(float delta_time) -> void - { - for (auto& obj : m_objects) - { - for (auto& obj2 : m_objects) - { - if (&obj == &obj2) continue; - float dx = obj2.m_pos_radius.x - obj.m_pos_radius.x; - float dy = obj2.m_pos_radius.y - obj.m_pos_radius.y; - float dz = obj2.m_pos_radius.z - obj.m_pos_radius.z; - float distance = sqrt(dx * dx + dy * dy + dz * dz); - if (distance > 0) - { - std::vector direction = {dx / distance, dy / distance, dz / distance}; - double Gforce = (G * obj.m_mass * obj2.m_mass) / (distance * distance); - double acc1 = Gforce / obj.m_mass; - std::vector acc = {direction[0] * acc1, direction[1] * acc1, direction[2] * acc1}; - - if (m_gravity) - { - obj.m_velocity.x += static_cast(acc[0]); - obj.m_velocity.y += static_cast(acc[1]); - obj.m_velocity.z += static_cast(acc[2]); - - obj.m_pos_radius.x += static_cast(obj.m_velocity.x); - obj.m_pos_radius.y += static_cast(obj.m_velocity.y); - obj.m_pos_radius.z += static_cast(obj.m_velocity.z); - } - } - } - } - } - - auto Engine::render_scene() -> void - { - RenderCommand::clear(); - m_shader_program->bind(); - m_quad_vao->bind(); - m_texture->bind(0); - RenderCommand::draw_arrays(6); - } - - auto Engine::create_compute_program(const char* path) -> Ref - { - std::ifstream in(path); - if(!in.is_open()) - { - std::cerr << "Failed to open compute shader: " << path << "\n"; - return nullptr; - } - std::stringstream ss; - ss << in.rdbuf(); - std::string src_str = ss.str(); - return Ref(Shader::create_compute("ComputeShader", src_str)); - } - - auto Engine::QuadVAO() -> std::pair, Ref> - { - float quad_vertices[] = - { - // Positions // TexCoords - -1.0f, 1.0f, 0.0f, 1.0f, - -1.0f, -1.0f, 0.0f, 0.0f, - 1.0f, -1.0f, 1.0f, 0.0f, - -1.0f, 1.0f, 0.0f, 1.0f, - 1.0f, -1.0f, 1.0f, 0.0f, - 1.0f, 1.0f, 1.0f, 1.0f - }; - - auto vertex_buffer = Ref(VertexBuffer::create(quad_vertices, static_cast(sizeof(quad_vertices)))); - VertexBufferLayout layout; - layout.push(2); // Position (x, y) - layout.push(2); // TexCoord (u, v) - vertex_buffer->set_layout(layout); - - auto vertex_array = Ref(VertexArray::create()); - vertex_array->add_vertex_buffer(vertex_buffer); - auto texture = Texture2D::create(get_compute_width(), m_compute_height); - - return { vertex_array, texture }; - } - - auto Engine::load_objects_from_scene(const std::vector& objects) -> void - { - m_objects.clear(); - m_objects.push_back( - { - glm::vec4(0.00f, 0.00f, 0.00f, m_sag_a.m_rs), - glm::vec4(0, 0, 0, 1), - static_cast(m_sag_a.m_mass) - }); - - for (const auto& obj : objects) - { - ObjectData engine_obj; - - float scale_factor = 1e10f; - engine_obj.m_pos_radius = glm::vec4 - ( - obj.m_centre.x * scale_factor, - obj.m_centre.y * scale_factor, - obj.m_centre.z * scale_factor, - obj.m_radius * scale_factor - ); - - engine_obj.m_color = glm::vec4(obj.m_material.m_color, 1.0f); - - float volume = (4.0f / 3.0f) * 3.14159f * engine_obj.m_pos_radius.w * engine_obj.m_pos_radius.w * engine_obj.m_pos_radius.w; - float density = 1e12f; - engine_obj.m_mass = volume * density; - engine_obj.m_velocity = glm::vec3(0.0f, 0.0f, 0.0f); - - m_objects.push_back(engine_obj); - } - - DONUT_INFO("Loaded {} objects from WorldBuilder scene (scaled up by {})", objects.size(), 1e10f); - print_object_info(); - } - - auto Engine::print_object_info() const -> void - { - DONUT_INFO("=== Object Information ==="); - DONUT_INFO("Total objects: {}", m_objects.size()); - - for (size_t i = 0; i < m_objects.size(); ++i) - { - const auto& obj = m_objects[i]; - DONUT_INFO("Object {}: Pos=({}, {}, {}), Radius={}, Mass={}, Color=({}, {}, {})", - i, - obj.m_pos_radius.x, obj.m_pos_radius.y, obj.m_pos_radius.z, - obj.m_pos_radius.w, - obj.m_mass, - obj.m_color.x, obj.m_color.y, obj.m_color.z - ); - } - DONUT_INFO("Camera position: ({}, {}, {})", - m_camera.get_orbital_position().x, - m_camera.get_orbital_position().y, - m_camera.get_orbital_position().z - ); - DONUT_INFO("Camera radius: {}", m_camera.get_orbital_radius()); - DONUT_INFO("========================"); - } - - auto Engine::export_high_res_frame(const std::string& filename, int width, int height) -> void - { - DONUT_INFO("Exporting high-resolution frame: {}x{} to {}", width, height, filename); - - if (width <= 0 || height <= 0) - { - DONUT_ERROR("Invalid dimensions for export: {}x{}", width, height); - return; - } - - if (filename.empty()) - { - DONUT_ERROR("Invalid filename for export"); - return; - } - - int original_width = m_width; - int original_height = m_height; - int original_compute_height = m_compute_height; - - m_width = width; - m_height = height; - m_compute_height = height; - - int compute_height = height; - int compute_width = (width * compute_height) / height; - - if (compute_width <= 0 || compute_height <= 0) - { - DONUT_ERROR("Invalid compute dimensions: {}x{}", compute_width, compute_height); - return; - } - - FramebufferSpecification fb_spec; - fb_spec.Width = width; - fb_spec.Height = height; - fb_spec.attachments = { FramebufferTextureFormat::RGBA8 }; - - auto high_res_framebuffer = Framebuffer::create(fb_spec); - if (!high_res_framebuffer) - { - DONUT_ERROR("Failed to create high-resolution framebuffer"); - return; - } - - auto high_res_texture = Texture2D::create(compute_width, compute_height); - if (!high_res_texture) - { - DONUT_ERROR("Failed to create high-resolution texture"); - return; - } - - // Render the geodesic pass into high_res_texture through the geodesic FBO. - glBindFramebuffer(GL_FRAMEBUFFER, m_geodesic_fbo); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, high_res_texture->get_renderer_id(), 0); - glViewport(0, 0, compute_width, compute_height); - - m_compute_program->bind(); - - struct UBOData - { - glm::vec3 pos; float _pad0; - glm::vec3 right; float _pad1; - glm::vec3 up; float _pad2; - glm::vec3 forward; float _pad3; - float tan_half_fov; - float aspect; - bool moving; - int _pad4; - } data; - - glm::vec3 fwd = glm::normalize(m_camera.get_orbital_target() - m_camera.get_orbital_position()); - glm::vec3 up = glm::vec3(0, 1, 0); - glm::vec3 right = glm::normalize(glm::cross(fwd, up)); - up = glm::cross(right, fwd); - - data.pos = m_camera.get_orbital_position(); - data.right = right; - data.up = up; - data.forward = fwd; - data.tan_half_fov = static_cast(tan(glm::radians(60.0f * 0.5f))); - data.aspect = static_cast(compute_width) / static_cast(compute_height); - data.moving = m_camera.is_dragging() || m_camera.is_panning(); - - m_camera_ubo->set_data(&data, sizeof(UBOData)); - m_camera_ubo->bind(1); - - upload_disk_ubo(); - upload_objects_ubo(m_objects); - upload_simulation_ubo(); - m_compute_program->set_float2("u_Resolution", glm::vec2(static_cast(compute_width), static_cast(compute_height))); - - if (m_hdri_environment) - { - m_hdri_environment->bind(5); - m_compute_program->set_int("u_HDRIEnvironment", 5); - } - - draw_geodesic_pass(compute_width, compute_height); - - // Display the rendered frame into the high-res framebuffer for read-back. - high_res_framebuffer->bind(); - RenderCommand::set_viewport(0, 0, width, height); - RenderCommand::clear(); - - m_shader_program->bind(); - m_quad_vao->bind(); - - high_res_texture->bind(0); - m_shader_program->set_int("u_ScreenTexture", 0); - - RenderCommand::disable_depth_test(); - RenderCommand::draw_arrays(6); - RenderCommand::enable_depth_test(); - - std::vector pixels(width * height * 4); - DONUT_INFO("Reading {} pixels from framebuffer...", width * height); - RenderCommand::read_pixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); - - DONUT_INFO("Flipping image vertically..."); - std::vector flipped_pixels(width * height * 4); - for (int y = 0; y < height; ++y) - { - for (int x = 0; x < width; ++x) - { - int src_index = (y * width + x) * 4; - int dst_index = ((height - 1 - y) * width + x) * 4; - flipped_pixels[dst_index + 0] = pixels[src_index + 0]; // R - flipped_pixels[dst_index + 1] = pixels[src_index + 1]; // G - flipped_pixels[dst_index + 2] = pixels[src_index + 2]; // B - flipped_pixels[dst_index + 3] = pixels[src_index + 3]; // A - } - } - - DONUT_INFO("Saving PNG file: {}...", filename); - int result = stbi_write_png(filename.c_str(), width, height, 4, flipped_pixels.data(), width * 4); - - if (result) - DONUT_INFO("Successfully exported high-resolution frame to: {}", filename); - else - DONUT_ERROR("Failed to export high-resolution frame to: {}", filename); - - high_res_framebuffer->unbind(); - - m_width = original_width; - m_height = original_height; - m_compute_height = original_compute_height; - - RenderCommand::set_viewport(0, 0, original_width, original_height); - } - -} diff --git a/src/engine/engine.h b/src/engine/engine.h deleted file mode 100644 index fef8b57..0000000 --- a/src/engine/engine.h +++ /dev/null @@ -1,191 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "core/camera.h" -#include "object.h" - -#include "rendering/renderer.h" -#include "rendering/renderer_backend.h" -#include "rendering/shader.h" -#include "rendering/vertex_array.h" -#include "rendering/texture.h" -#include "rendering/uniform_buffer.h" -#include "rendering/texture_manager.h" - -#include -#include -#include -#include - -namespace Donut -{ - const double c = 299792458.0; - const double G = 6.67430e-11; - - struct BlackHole - { - glm::vec3 m_position; - double m_mass; - double m_radius; - double m_rs; - - BlackHole(glm::vec3 pos, float mass) - : m_position(pos), m_mass(mass) - { - m_rs = 2.0 * G * m_mass / (c * c); - } - - bool intercept(float px, float py, float pz) const - { - double dx = double(px) - double(m_position.x); - double dy = double(py) - double(m_position.y); - double dz = double(pz) - double(m_position.z); - double dist2 = dx * dx + dy * dy + dz * dz; - return dist2 < m_rs * m_rs; - } - }; - - struct ObjectData - { - glm::vec4 m_pos_radius; - glm::vec4 m_color; - float m_mass; - glm::vec3 m_velocity = glm::vec3(0.0f, 0.0f, 0.0f); - }; - - class Engine - { - public: - Engine(); - ~Engine() = default; - - auto draw_full_screen_quad() -> void; - auto draw_blur_pass() -> void; - auto dispatch_compute(const Camera& cam) -> void; - auto upload_camera_ubo(const Camera& cam) -> void; - auto upload_objects_ubo(const std::vector& objs) -> void; - auto upload_disk_ubo() -> void; - auto upload_simulation_ubo() -> void; - auto render_scene() -> void; - auto update_physics(float delta_time) -> void; - auto update_window_dimensions() -> void; - auto set_window_dimensions(int width, int height) -> void; - - auto get_width() const -> int { return m_width; } - auto get_height() const -> int { return m_height; } - - auto get_objects() -> std::vector& { return m_objects; } - auto get_sag_a() -> BlackHole& { return m_sag_a; } - auto get_camera() -> Camera& { return m_camera; } - auto get_gravity() -> bool& { return m_gravity; } - - auto update_performance(float delta_time) -> void; - auto set_target_fps(int fps) -> void { m_target_fps = fps; } - auto get_target_fps() const -> int { return m_target_fps; } - auto get_current_fps() const -> float { return m_current_fps; } - void set_compute_height(int height) - { -#ifdef __APPLE__ - // Without compute shaders the geodesic pass runs as a tiled - // fragment shader (see draw_geodesic_pass), so very high working - // resolutions make each frame take many seconds. Cap it on macOS. - if (height > 256) height = 256; -#endif - m_compute_height = height; - } - auto get_compute_height() const -> int { return m_compute_height; } - auto get_compute_width() const -> int { return (m_width * m_compute_height) / m_height; } - auto update_compute_dimensions() -> void; - - auto get_max_steps_moving() const -> int { return m_max_steps_moving; } - auto get_max_steps_static() const -> int { return m_max_steps_static; } - auto get_early_exit_distance() const -> float { return m_early_exit_distance; } - auto set_max_steps_moving(int steps) -> void { m_max_steps_moving = steps; } - auto set_max_steps_static(int steps) -> void { m_max_steps_static = steps; } - auto set_early_exit_distance(float distance) -> void { m_early_exit_distance = distance; } - - auto get_disk_thickness() const -> float { return m_disk_thickness; } - auto set_disk_thickness(float thickness) -> void { m_disk_thickness = thickness; } - - auto get_disk_density() const -> float { return m_disk_density; } - auto set_disk_density(float density) -> void { m_disk_density = density; } - - auto get_rotation_speed() const -> float { return m_rotation_speed; } - auto set_rotation_speed(float speed) -> void { m_rotation_speed = speed; } - - auto get_blur_strength() const -> float { return m_blur_strength; } - auto set_blur_strength(float strength) -> void { m_blur_strength = strength; } - - auto get_glow_intensity() const -> float { return m_glow_intensity; } - auto set_glow_intensity(float intensity) -> void { m_glow_intensity = intensity; } - - auto load_objects_from_scene(const std::vector& objects) -> void; - auto export_high_res_frame(const std::string& filename, int width = 4096, int height = 3072) -> void; - auto print_object_info() const -> void; - - auto set_hdri_environment(Ref hdri) -> void { m_hdri_environment = hdri; } - auto get_hdri_environment() const -> Ref { return m_hdri_environment; } - - // Shared live black-hole/disk parameters (drives the geodesic UBOs so the - // GL path renders identically to Vulkan). The OpenGL backend mutates this. - auto black_hole_params() -> BlackHoleParams& { return m_bh; } - private: - auto create_compute_program(const char* path) -> Ref; - std::pair, Ref> QuadVAO(); - - // Draws the bound geodesic shader over a cw x ch target. On macOS this - // is split into scissored tiles (with a flush each) so no single GPU - // submission trips the OS watchdog; elsewhere it is one fast draw. - auto draw_geodesic_pass(int cw, int ch) -> void; - private: - Ref m_quad_vao; - Ref m_texture; - Ref m_hdri_environment; - Ref m_shader_program; - Ref m_compute_program; - Ref m_blur_shader; - Ref m_camera_ubo; - Ref m_disk_ubo; - Ref m_objects_ubo; - Ref m_simulation_ubo; - - // FBO used to render the geodesic pass into m_texture. The geodesic - // shader is a fragment shader (macOS has no compute), so it draws a - // fullscreen quad into this framebuffer instead of dispatching compute. - uint32_t m_geodesic_fbo = 0; - - int m_width; - int m_height; - float m_width_f = 100.0f*1e10f; - float m_height_f = 75.0f*1e10f; - - int m_target_fps = 60; - float m_current_fps = 60.0f; - float m_last_frame_time = 0.0f; - int m_compute_height = 150; - - std::vector m_objects; - BlackHole m_sag_a; - Camera m_camera; - bool m_gravity = false; - - int m_max_steps_moving = 60000; - int m_max_steps_static = 30000; - float m_early_exit_distance = 5.0e11f; - - BlackHoleParams m_bh; // shared UI-tunable disk/camera params (GL geodesic driver) - - float m_disk_thickness = 0.1f; - float m_disk_density = 0.1f; - float m_rotation_speed = 1.0f; - float m_blur_strength = 2.0f; - float m_glow_intensity = 0.1f; - }; -}; diff --git a/src/engine/object.h b/src/engine/object.h deleted file mode 100644 index 8101540..0000000 --- a/src/engine/object.h +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once - -#include - -namespace Donut -{ - class Ray - { - public: - Ray(glm::vec3 o, glm::vec3 d) - : m_origin(o), - m_direction(glm::normalize(d)) { } - - public: - glm::vec3 m_direction; - glm::vec3 m_origin; - }; - - class Material - { - public: - Material() : m_color(1.0f, 1.0f, 1.0f), m_specular(0.5f), m_emission(0.0f) { } - Material(glm::vec3 c, float s, float e) - : m_color(c), - m_specular(s), - m_emission(e) { } - - public: - glm::vec3 m_color; - float m_specular; - float m_emission; - }; - - class Object - { - public: - Object() : m_centre(0.0f, 0.0f, 0.0f), m_radius(1.0f), m_material() { } - Object(glm::vec3 c, float r, Material m) - : m_centre(c), - m_radius(r), - m_material(m) { } - - auto intersect(Ray& ray, float& t) -> bool - { - glm::vec3 oc = ray.m_origin - m_centre; - float a = glm::dot(ray.m_direction, ray.m_direction); - float b = 2.0f * glm::dot(oc, ray.m_direction); - float c = glm::dot(oc, oc) - m_radius * m_radius; - float discriminant = static_cast(b*b - 4*a*c); - - if (discriminant < 0) - return false; - - float intercept = (-b - sqrt(discriminant)) / (2.0f*a); - if (intercept < 0) - { - intercept = (-b + sqrt(discriminant)) / (2.0f*a); - if (intercept < 0) - return false; - } - - t = intercept; - return true; - } - - auto get_normal(glm::vec3& point) const -> glm::vec3 - { - return glm::normalize(point - m_centre); - } - - public: - glm::vec3 m_centre; - float m_radius; - Material m_material; - }; -}; diff --git a/src/engine/scene.h b/src/engine/scene.h deleted file mode 100644 index 4b41203..0000000 --- a/src/engine/scene.h +++ /dev/null @@ -1,70 +0,0 @@ -#pragma once - -#include -#include - -#include "object.h" - -namespace Donut -{ - class Scene - { - public: - Scene() - : m_light_pos(5.0f, 5.0f, 5.0f) { } - - auto trace(Ray& ray) -> glm::vec3 - { - float closest = std::numeric_limits::infinity(); - const Object* hit_obj = nullptr; - - for (auto& obj : objs) - { - float t; - - if (obj.intersect(ray, t)) - if (t < closest) - { - closest = t; - hit_obj = &obj; - } - } - - if (hit_obj) - { - glm::vec3 hit_point = ray.m_origin + ray.m_direction * closest; - glm::vec3 normal = hit_obj->get_normal(hit_point); - glm::vec3 light_dir = glm::normalize(m_light_pos - hit_point); - - float diff = std::max(glm::dot(normal, light_dir), 0.0f); - - Ray shadow_ray(hit_point + normal * 0.001f, light_dir); - bool in_shadow = false; - - for (auto& obj : objs) - { - float t; - - if (obj.intersect(shadow_ray, t)) - { - in_shadow = true; - break; - } - } - - glm::vec3 color = hit_obj->m_material.m_color; - float ambient = 0.1f; - - if (in_shadow) - return color * ambient; - return color * (ambient + diff * 0.9f); - } - - return glm::vec3(0.0f, 0.0f, 0.1f); - } - - public: - std::vector objs; - glm::vec3 m_light_pos; - }; -}; diff --git a/src/platform/metal/metal_context.h b/src/platform/metal/metal_context.h deleted file mode 100644 index f1e376b..0000000 --- a/src/platform/metal/metal_context.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -// Pure-C++ interface to the Metal backend (no Objective-C leaks into the rest -// of the engine). Implemented in MetalContext.mm. -namespace Donut -{ - // Phase 1 bring-up: creates the default Metal device + command queue and - // logs its capabilities, proving the Metal toolchain and build integration - // work natively on Apple Silicon. This grows into the Metal compute island - // that runs the geodesic ray tracer in real time. - auto metal_probe() -> bool; - - // Compiles + dispatches a trivial compute kernel and validates the read-back, - // proving the Metal compute path the geodesic ray tracer will run on. - auto metal_compute_self_test() -> bool; -} diff --git a/src/platform/metal/metal_context.mm b/src/platform/metal/metal_context.mm deleted file mode 100644 index 71e8551..0000000 --- a/src/platform/metal/metal_context.mm +++ /dev/null @@ -1,124 +0,0 @@ -#import -#import - -#include "metal_context.h" -#include "core/log.h" - -#include - -namespace Donut -{ - bool metal_probe() - { - @autoreleasepool - { - id device = MTLCreateSystemDefaultDevice(); - if (device == nil) - { - DONUT_ERROR("Metal: no default device available"); - return false; - } - - id queue = [device newCommandQueue]; - const char* name = [[device name] UTF8String]; - - DONUT_INFO("Metal device: {}", name ? name : "(unknown)"); - DONUT_INFO("Metal: unified memory = {}, max threads/threadgroup = {}", - device.hasUnifiedMemory ? "yes" : "no", - (unsigned long)device.maxThreadsPerThreadgroup.width); - - if (queue == nil) - { - DONUT_WARN("Metal: failed to create command queue"); - return false; - } - - return true; - } - } - - // Compiles a trivial compute kernel, dispatches it over a small texture, and - // reads the result back to confirm the full Metal compute path works: source - // compilation, pipeline state, command encoding, dispatch, and shared-memory - // read-back. This is the mechanism the geodesic ray tracer will run on. - bool metal_compute_self_test() - { - @autoreleasepool - { - id device = MTLCreateSystemDefaultDevice(); - id queue = [device newCommandQueue]; - if (device == nil || queue == nil) - return false; - - NSString* src = - @"#include \n" - "using namespace metal;\n" - "kernel void selfTest(texture2d outTex [[texture(0)]],\n" - " uint2 gid [[thread_position_in_grid]])\n" - "{\n" - " uint w = outTex.get_width();\n" - " uint h = outTex.get_height();\n" - " if (gid.x >= w || gid.y >= h) return;\n" - " outTex.write(float4(float(gid.x) / float(w - 1),\n" - " float(gid.y) / float(h - 1), 0.5, 1.0), gid);\n" - "}\n"; - - NSError* err = nil; - id lib = [device newLibraryWithSource:src options:nil error:&err]; - if (lib == nil) - { - DONUT_ERROR("Metal self-test: kernel compile failed: {}", - err ? [[err localizedDescription] UTF8String] : "unknown"); - return false; - } - - id fn = [lib newFunctionWithName:@"selfTest"]; - id pipeline = - [device newComputePipelineStateWithFunction:fn error:&err]; - if (pipeline == nil) - { - DONUT_ERROR("Metal self-test: pipeline creation failed"); - return false; - } - - const uint32_t W = 64, H = 64; - MTLTextureDescriptor* desc = - [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm - width:W - height:H - mipmapped:NO]; - desc.usage = MTLTextureUsageShaderWrite | MTLTextureUsageShaderRead; - desc.storageMode = MTLStorageModeShared; - id tex = [device newTextureWithDescriptor:desc]; - - id cb = [queue commandBuffer]; - id enc = [cb computeCommandEncoder]; - [enc setComputePipelineState:pipeline]; - [enc setTexture:tex atIndex:0]; - - MTLSize tg = MTLSizeMake(16, 16, 1); - MTLSize grid = MTLSizeMake(W, H, 1); - [enc dispatchThreads:grid threadsPerThreadgroup:tg]; - [enc endEncoding]; - [cb commit]; - [cb waitUntilCompleted]; - - // Read back the far corner; the kernel writes (~1, ~1, 0.5, 1) there. - std::vector px(W * H * 4); - [tex getBytes:px.data() - bytesPerRow:W * 4 - fromRegion:MTLRegionMake2D(0, 0, W, H) - mipmapLevel:0]; - - size_t corner = ((size_t)(H - 1) * W + (W - 1)) * 4; - DONUT_INFO("Metal compute self-test: corner pixel RGBA = ({}, {}, {}, {})", - (int)px[corner + 0], (int)px[corner + 1], - (int)px[corner + 2], (int)px[corner + 3]); - - bool ok = px[corner + 0] > 250 && px[corner + 1] > 250 && - px[corner + 3] == 255; - DONUT_INFO("Metal compute self-test: {}", ok ? "PASS" : "FAIL"); - return ok; - } - } -} diff --git a/src/platform/opengl/opengl_device.cpp b/src/platform/opengl/opengl_device.cpp new file mode 100644 index 0000000..3d45d24 --- /dev/null +++ b/src/platform/opengl/opengl_device.cpp @@ -0,0 +1,279 @@ +#include "opengl_device.h" + +#include "core/log.h" +#include "core/hdri_manager.h" +#include "rendering/shader.h" +#include "rendering/texture.h" + +#include +#include +#include +#include +#include + +namespace Donut::RHI +{ + namespace + { + auto gl_topology(Topology t) -> GLenum { return t == Topology::Lines ? GL_LINES : GL_TRIANGLES; } + auto gl_compare(CompareOp o) -> GLenum { return o == CompareOp::Always ? GL_ALWAYS : o == CompareOp::LessEqual ? GL_LEQUAL : GL_LESS; } + auto gl_filter(Filter f) -> GLint { return f == Filter::Nearest ? GL_NEAREST : GL_LINEAR; } + auto gl_internal(Format f) -> GLint { return f == Format::RGBA16F ? GL_RGBA16F : f == Format::D32 ? GL_DEPTH_COMPONENT32F : GL_RGBA8; } + + // ---- Buffer ------------------------------------------------------- + class GLBuffer : public Buffer + { + public: + GLBuffer(BufferType type, size_t size, const void* data) + { + m_target = type == BufferType::Index ? GL_ELEMENT_ARRAY_BUFFER + : type == BufferType::Uniform ? GL_UNIFORM_BUFFER : GL_ARRAY_BUFFER; + glGenBuffers(1, &m_id); + glBindBuffer(m_target, m_id); + glBufferData(m_target, (GLsizeiptr)size, data, GL_DYNAMIC_DRAW); + } + ~GLBuffer() override { if (m_id) glDeleteBuffers(1, &m_id); } + auto update(const void* data, size_t size) -> void override + { + glBindBuffer(m_target, m_id); + glBufferSubData(m_target, 0, (GLsizeiptr)size, data); + } + GLuint m_id = 0; GLenum m_target = GL_ARRAY_BUFFER; + }; + + // ---- Texture ------------------------------------------------------ + class GLTexture : public Texture + { + public: + GLTexture(int w, int h, Format fmt, Filter filter, const void* data) + { + glGenTextures(1, &m_id); m_own = true; m_target = GL_TEXTURE_2D; + glBindTexture(GL_TEXTURE_2D, m_id); + GLenum ext = fmt == Format::D32 ? GL_DEPTH_COMPONENT : GL_RGBA; + GLenum type = fmt == Format::RGBA8 ? GL_UNSIGNED_BYTE : GL_FLOAT; + glTexImage2D(GL_TEXTURE_2D, 0, gl_internal(fmt), w, h, 0, ext, type, data); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, gl_filter(filter)); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, gl_filter(filter)); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + explicit GLTexture(Ref cube) + : m_cube(std::move(cube)) + { + m_target = GL_TEXTURE_CUBE_MAP; + m_id = m_cube ? m_cube->get_renderer_id() : 0; + } + ~GLTexture() override { if (m_own && m_id) glDeleteTextures(1, &m_id); } + GLuint m_id = 0; GLenum m_target = GL_TEXTURE_2D; bool m_own = false; + Ref m_cube; // keepalive for cubemaps + }; + + // ---- RenderTarget ------------------------------------------------- + class GLRenderTarget : public RenderTarget + { + public: + GLRenderTarget(int w, int h, Format color, bool depth, Filter filter) + : m_w(w), m_h(h) + { + m_color = create_scope(w, h, color, filter, nullptr); + glGenFramebuffers(1, &m_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_color->m_id, 0); + if (depth) + { + glGenRenderbuffers(1, &m_depth); + glBindRenderbuffer(GL_RENDERBUFFER, m_depth); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, w, h); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_depth); + } + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + DONUT_ERROR("GL RHI: render target incomplete"); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + ~GLRenderTarget() override + { + if (m_depth) glDeleteRenderbuffers(1, &m_depth); + if (m_fbo) glDeleteFramebuffers(1, &m_fbo); + } + auto width() const -> int override { return m_w; } + auto height() const -> int override { return m_h; } + auto color_texture() -> Texture* override { return m_color.get(); } + GLuint m_fbo = 0, m_depth = 0; int m_w, m_h; Scope m_color; + }; + + // ---- Pipeline ----------------------------------------------------- + class GLPipeline : public Pipeline + { + public: + explicit GLPipeline(const PipelineDesc& d) : m_desc(d) + { + std::string path = "assets/shaders/generated/" + d.shader + ".glsl"; + m_shader = Ref(Shader::create(path)); + m_prog = m_shader ? m_shader->get_renderer_id() : 0; + if (!m_prog) { DONUT_ERROR("GL RHI: shader '{}' failed", d.shader); return; } + + glUseProgram(m_prog); + for (const auto& r : d.resources) + { + if (r.kind == ResourceKind::UniformBuffer) + { + GLuint bi = glGetUniformBlockIndex(m_prog, r.name.c_str()); + if (bi != GL_INVALID_INDEX) glUniformBlockBinding(m_prog, bi, r.binding); + } + else // Texture: point the sampler at texture unit == binding + { + GLint loc = glGetUniformLocation(m_prog, r.name.c_str()); + if (loc >= 0) glUniform1i(loc, (GLint)r.binding); + } + } + glUseProgram(0); + } + PipelineDesc m_desc; Ref m_shader; GLuint m_prog = 0; + }; + + // ---- CommandList (immediate) -------------------------------------- + class GLCommandList : public CommandList + { + public: + explicit GLCommandList(GLuint vao) : m_vao(vao) {} + + auto begin_render_pass(RenderTarget* target, const glm::vec4& clear) -> void override + { + auto* rt = static_cast(target); + glBindFramebuffer(GL_FRAMEBUFFER, rt ? rt->m_fbo : 0); + int w = rt ? rt->m_w : m_default_w, h = rt ? rt->m_h : m_default_h; + glViewport(0, 0, w, h); + glDisable(GL_SCISSOR_TEST); + glDepthMask(GL_TRUE); + glClearColor(clear.r, clear.g, clear.b, clear.a); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + } + auto end_render_pass() -> void override {} + + auto bind_pipeline(Pipeline* p) -> void override + { + m_pipe = static_cast(p); + glUseProgram(m_pipe->m_prog); + const auto& d = m_pipe->m_desc; + if (d.depth_test) { glEnable(GL_DEPTH_TEST); glDepthFunc(gl_compare(d.depth_op)); } + else glDisable(GL_DEPTH_TEST); + glDepthMask(d.depth_write ? GL_TRUE : GL_FALSE); + if (d.cull == CullMode::None) glDisable(GL_CULL_FACE); + else { glEnable(GL_CULL_FACE); glCullFace(d.cull == CullMode::Back ? GL_BACK : GL_FRONT); } + if (d.blend == BlendMode::AlphaBlend) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } + else glDisable(GL_BLEND); + } + auto set_viewport(int x, int y, int w, int h, bool /*flip_y*/) -> void override { glViewport(x, y, w, h); } + auto bind_uniform(uint32_t binding, Buffer* ubo) -> void override + { + glBindBufferBase(GL_UNIFORM_BUFFER, binding, static_cast(ubo)->m_id); + } + auto bind_texture(uint32_t binding, Texture* tex) -> void override + { + auto* t = static_cast(tex); + glActiveTexture(GL_TEXTURE0 + binding); + glBindTexture(t->m_target, t->m_id); + } + auto bind_vertex_buffer(Buffer* vb) -> void override + { + glBindBuffer(GL_ARRAY_BUFFER, static_cast(vb)->m_id); + const auto& layout = m_pipe->m_desc.vertex_layout; + for (const auto& a : layout.attributes) + { + glEnableVertexAttribArray(a.location); + glVertexAttribPointer(a.location, (GLint)a.components, GL_FLOAT, GL_FALSE, + (GLsizei)layout.stride, (const void*)(uintptr_t)a.offset); + } + } + auto bind_index_buffer(Buffer* ib) -> void override + { + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, static_cast(ib)->m_id); + } + auto draw(uint32_t count) -> void override + { + glDrawArrays(gl_topology(m_pipe->m_desc.topology), 0, (GLsizei)count); + } + auto draw_indexed(uint32_t count) -> void override + { + glDrawElements(gl_topology(m_pipe->m_desc.topology), (GLsizei)count, GL_UNSIGNED_INT, nullptr); + } + + GLuint m_vao; GLPipeline* m_pipe = nullptr; int m_default_w = 1, m_default_h = 1; + }; + + // ---- Device ------------------------------------------------------- + class GLDevice : public Device + { + public: + auto init(void* window, int width, int height) -> bool override + { + m_window = window; + glfwMakeContextCurrent(static_cast(window)); + if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { DONUT_ERROR("GL RHI: GLAD load failed"); return false; } + m_width = width; m_height = height; + const char* name = (const char*)glGetString(GL_RENDERER); + m_name = name ? name : "OpenGL"; + glGenVertexArrays(1, &m_vao); glBindVertexArray(m_vao); // GL core needs one bound VAO + m_cmds = create_scope(m_vao); + DONUT_INFO("GL RHI device: {} ({}x{})", m_name, width, height); + return true; + } + auto shutdown() -> void override + { + if (m_imgui) { ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); m_imgui = false; } + if (m_vao) { glDeleteVertexArrays(1, &m_vao); m_vao = 0; } + } + auto resize(int w, int h) -> void override { m_width = w; m_height = h; } + auto wait_idle() -> void override { glFinish(); } + + auto create_buffer(BufferType t, size_t size, const void* data) -> Ref override + { return create_ref(t, size, data); } + auto create_texture(int w, int h, Format f, Filter fl, const void* data) -> Ref override + { return create_ref(w, h, f, fl, data); } + auto create_cubemap_from_hdri(const std::string& path) -> Ref override + { + HDRIManager::get().set_current_hdri(path); + return create_ref(HDRIManager::get().get_current_hdri()); + } + auto create_render_target(int w, int h, Format color, bool depth, Filter fl, int /*mips*/) -> Ref override + { return create_ref(w, h, color, depth, fl); } + auto create_pipeline(const PipelineDesc& d) -> Ref override + { return create_ref(d); } + + auto begin_frame(const glm::vec4& /*clear*/) -> CommandList* override + { + glfwGetFramebufferSize(static_cast(m_window), &m_width, &m_height); + m_cmds->m_default_w = m_width; m_cmds->m_default_h = m_height; + glBindVertexArray(m_vao); + return m_cmds.get(); + } + auto end_frame() -> void override { glfwSwapBuffers(static_cast(m_window)); } + + auto init_imgui() -> void override + { + IMGUI_CHECKVERSION(); ImGui::CreateContext(); + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable; + ImGui_ImplGlfw_InitForOpenGL(static_cast(m_window), true); + ImGui_ImplOpenGL3_Init("#version 410"); + m_imgui = true; + DONUT_INFO("GL RHI: ImGui backend initialized"); + } + auto imgui_new_frame() -> void override + { + ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); + } + auto imgui_render(CommandList&) -> void override + { + ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + } + auto device_name() const -> const std::string& override { return m_name; } + + private: + void* m_window = nullptr; std::string m_name; GLuint m_vao = 0; + int m_width = 0, m_height = 0; bool m_imgui = false; + Scope m_cmds; + }; + } + + auto create_opengl_device() -> Scope { return create_scope(); } +} diff --git a/src/platform/opengl/opengl_device.h b/src/platform/opengl/opengl_device.h new file mode 100644 index 0000000..4004396 --- /dev/null +++ b/src/platform/opengl/opengl_device.h @@ -0,0 +1,11 @@ +#pragma once + +#include "rendering/rhi.h" + +namespace Donut::RHI +{ + // OpenGL implementation of the RHI Device: emulates the explicit model with + // immediate-mode GL 4.1 (one global VAO, glBindBufferBase for UBOs, FBOs for + // render targets, per-draw attribute setup, bindings assigned by name). + auto create_opengl_device() -> Scope; +} diff --git a/src/platform/opengl/opengl_renderer.cpp b/src/platform/opengl/opengl_renderer.cpp deleted file mode 100644 index ae12f10..0000000 --- a/src/platform/opengl/opengl_renderer.cpp +++ /dev/null @@ -1,377 +0,0 @@ -#include "opengl_renderer.h" - -#include "engine/engine.h" -#include "core/log.h" -#include "core/camera.h" -#include "core/hdri_manager.h" -#include "rendering/renderer.h" // Renderer::init / RenderCommand - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/hdri_manager.h" -#include "rendering/texture.h" // CubemapTexture::bind - -namespace Donut -{ - // Scroll is accumulated through a chained GLFW callback (keeps ImGui's scroll - // handling intact), consumed once per frame for orbital zoom. - static double g_scroll_accum = 0.0; - static GLFWscrollfun g_prev_scroll = nullptr; - static void gl_scroll_cb(GLFWwindow* w, double x, double y) - { - if (g_prev_scroll) g_prev_scroll(w, x, y); - g_scroll_accum += y; - } - - OpenGLRenderer::OpenGLRenderer() = default; - OpenGLRenderer::~OpenGLRenderer() = default; - - auto OpenGLRenderer::init(void* glfwWindow, int width, int height) -> bool - { - m_window = glfwWindow; - auto* win = static_cast(glfwWindow); - glfwMakeContextCurrent(win); - - Renderer::init(); // GLAD load + default GL state - RenderCommand::set_face_culling(false); - - const GLubyte* name = glGetString(GL_RENDERER); - m_device_name = name ? reinterpret_cast(name) : "OpenGL"; - - m_engine = create_scope(); - m_engine->set_window_dimensions(width, height); -#ifdef __APPLE__ - m_engine->set_compute_height(256); // capped for the macOS watchdog (tiled fragment pass) -#else - m_engine->set_compute_height(540); -#endif - m_engine->update_compute_dimensions(); - - // Match the Vulkan framing exactly (camera sits outside the disk). - Camera& cam = m_engine->get_camera(); - cam.set_camera_mode(CameraMode::Orbital); - cam.set_orbital_target(glm::vec3(0.0f)); - cam.set_orbital_radius(4e11); - cam.set_orbital_limits(2.2e11, 1.5e12); - cam.set_orbital_speed(0.01f); - cam.set_zoom_speed(3e10); - cam.set_azimuth(0.0f); - cam.set_elevation(1.25f); - - m_hdri_path = "assets/hdri/HDR_blue_nebulae-1.hdr"; - HDRIManager::get().set_current_hdri(m_hdri_path); - - create_scene_resources(); - - DONUT_INFO("OpenGL renderer ready: {} ({}x{})", m_device_name, width, height); - return true; - } - - auto OpenGLRenderer::init_ui() -> bool - { - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable; - ImGui::StyleColorsDark(); - ImGui_ImplGlfw_InitForOpenGL(static_cast(m_window), true); - ImGui_ImplOpenGL3_Init("#version 410"); - g_prev_scroll = glfwSetScrollCallback(static_cast(m_window), gl_scroll_cb); - m_ui_ready = true; - DONUT_INFO("OpenGL: ImGui backend initialized"); - return true; - } - - auto OpenGLRenderer::shutdown() -> void - { - if (m_ui_ready) - { - ImGui_ImplOpenGL3_Shutdown(); - ImGui_ImplGlfw_Shutdown(); - ImGui::DestroyContext(); - m_ui_ready = false; - } - m_engine.reset(); - } - - auto OpenGLRenderer::resize(int width, int height) -> void - { - if (m_engine) m_engine->set_window_dimensions(width, height); - } - - auto OpenGLRenderer::process_input() -> void - { - auto* win = static_cast(m_window); - Camera& cam = m_scene_mode ? m_scene_camera : m_engine->get_camera(); - bool over_ui = m_ui_ready && ImGui::GetIO().WantCaptureMouse; - - double now = glfwGetTime(); - float dt = m_last_frame_time > 0.0 ? static_cast(now - m_last_frame_time) : 0.0f; - m_last_frame_time = now; - - double mx = 0, my = 0; glfwGetCursorPos(win, &mx, &my); - bool left_down = glfwGetMouseButton(win, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS; - - if (!m_scene_mode && cam.get_camera_mode() == CameraMode::FPS) - { - if (left_down && !m_left_was_down) { m_last_x = mx; m_last_y = my; } - if (left_down && !over_ui) - cam.on_mouse_move(static_cast(mx - m_last_x), static_cast(m_last_y - my)); - m_last_x = mx; m_last_y = my; m_left_was_down = left_down; - - bool over_kb = m_ui_ready && ImGui::GetIO().WantCaptureKeyboard; - if (!over_kb && dt > 0.0f) - { - float mdt = dt * (glfwGetKey(win, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS ? 4.0f : 1.0f); - if (glfwGetKey(win, GLFW_KEY_W) == GLFW_PRESS) cam.move_forward(mdt); - if (glfwGetKey(win, GLFW_KEY_S) == GLFW_PRESS) cam.move_backward(mdt); - if (glfwGetKey(win, GLFW_KEY_A) == GLFW_PRESS) cam.move_left(mdt); - if (glfwGetKey(win, GLFW_KEY_D) == GLFW_PRESS) cam.move_right(mdt); - if (glfwGetKey(win, GLFW_KEY_E) == GLFW_PRESS) cam.move_up(mdt); - if (glfwGetKey(win, GLFW_KEY_Q) == GLFW_PRESS) cam.move_down(mdt); - } - g_scroll_accum = 0.0; // scroll unused in free-fly - } - else - { - if (left_down && !m_left_was_down && !over_ui) - cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, 0); - else if (!left_down && m_left_was_down) - cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0); - m_left_was_down = left_down; - - cam.process_orbital_mouse_move(mx, my); - double scroll = g_scroll_accum; g_scroll_accum = 0.0; - if (scroll != 0.0 && !over_ui) cam.process_orbital_scroll(0.0, scroll); - } - } - - // 36-vertex unit cube for the skybox (positions only; the shader forces depth=1). - static const float SKYBOX_CUBE[] = { - -1, 1,-1, -1,-1,-1, 1,-1,-1, 1,-1,-1, 1, 1,-1, -1, 1,-1, - -1,-1, 1, -1,-1,-1, -1, 1,-1, -1, 1,-1, -1, 1, 1, -1,-1, 1, - 1,-1,-1, 1,-1, 1, 1, 1, 1, 1, 1, 1, 1, 1,-1, 1,-1,-1, - -1,-1, 1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 1,-1, 1, -1,-1, 1, - -1, 1,-1, 1, 1,-1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1, 1,-1, - -1,-1,-1, -1,-1, 1, 1,-1,-1, 1,-1,-1, -1,-1, 1, 1,-1, 1, - }; - - auto OpenGLRenderer::create_scene_resources() -> void - { - const float PI = 3.14159265358979f; - - m_grid_shader = Ref(Shader::create("assets/shaders/Grid.glsl")); - m_sphere_shader = Ref(Shader::create("assets/shaders/Sphere.glsl")); - m_skybox_shader = Ref(Shader::create("assets/shaders/Skybox.glsl")); - - m_scene_camera.set_camera_mode(CameraMode::Orbital); - m_scene_camera.set_orbital_target(glm::vec3(0.0f)); - m_scene_camera.set_orbital_radius(15.0); - m_scene_camera.set_orbital_limits(2.0, 200.0); - m_scene_camera.set_orbital_speed(0.01f); - m_scene_camera.set_zoom_speed(2.0); - m_scene_camera.set_azimuth(0.0f); - m_scene_camera.set_elevation(PI / 3.0f); - - // Grid: +/-50 lines on the XZ plane (Grid shader scales by u_GridSize/50). - std::vector lines; - for (int i = -50; i <= 50; ++i) - { - lines.push_back({ (float)i, 0.0f, -50.0f }); lines.push_back({ (float)i, 0.0f, 50.0f }); - lines.push_back({ -50.0f, 0.0f, (float)i }); lines.push_back({ 50.0f, 0.0f, (float)i }); - } - m_grid_vertex_count = (int)lines.size(); - glGenVertexArrays(1, &m_grid_vao); glGenBuffers(1, &m_grid_vbo); - glBindVertexArray(m_grid_vao); glBindBuffer(GL_ARRAY_BUFFER, m_grid_vbo); - glBufferData(GL_ARRAY_BUFFER, lines.size() * sizeof(glm::vec3), lines.data(), GL_STATIC_DRAW); - glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0); - - // Sphere: indexed UV sphere (position + normal), unit radius. - const int RINGS = 24, SECT = 48; - std::vector sv; std::vector si; - for (int r = 0; r <= RINGS; ++r) - { - float th = PI * r / RINGS; - for (int s = 0; s <= SECT; ++s) - { - float ph = 2.0f * PI * s / SECT; - glm::vec3 p(sinf(th) * cosf(ph), cosf(th), sinf(th) * sinf(ph)); - sv.insert(sv.end(), { p.x, p.y, p.z, p.x, p.y, p.z }); // pos, normal (unit sphere) - } - } - for (int r = 0; r < RINGS; ++r) - for (int s = 0; s < SECT; ++s) - { - int a = r * (SECT + 1) + s, b = a + SECT + 1; - si.insert(si.end(), { (unsigned)a, (unsigned)b, (unsigned)(a + 1), - (unsigned)(a + 1), (unsigned)b, (unsigned)(b + 1) }); - } - m_sphere_index_count = (int)si.size(); - glGenVertexArrays(1, &m_sphere_vao); glGenBuffers(1, &m_sphere_vbo); glGenBuffers(1, &m_sphere_ebo); - glBindVertexArray(m_sphere_vao); - glBindBuffer(GL_ARRAY_BUFFER, m_sphere_vbo); - glBufferData(GL_ARRAY_BUFFER, sv.size() * sizeof(float), sv.data(), GL_STATIC_DRAW); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_sphere_ebo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, si.size() * sizeof(unsigned), si.data(), GL_STATIC_DRAW); - glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); - glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); - - // Skybox cube. - glGenVertexArrays(1, &m_skybox_vao); glGenBuffers(1, &m_skybox_vbo); - glBindVertexArray(m_skybox_vao); glBindBuffer(GL_ARRAY_BUFFER, m_skybox_vbo); - glBufferData(GL_ARRAY_BUFFER, sizeof(SKYBOX_CUBE), SKYBOX_CUBE, GL_STATIC_DRAW); - glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); - - glBindVertexArray(0); - m_scene_ready = true; - } - - auto OpenGLRenderer::render_scene(int w, int h) -> void - { - if (!m_grid_shader || !m_sphere_shader || !m_skybox_shader) return; - glViewport(0, 0, w, h); - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - float aspect = h > 0 ? (float)w / (float)h : 1.0f; - m_scene_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f); - glm::mat4 proj = m_scene_camera.get_projection_matrix(); - glm::mat4 view = m_scene_camera.get_view_matrix(); - glm::mat4 vp = proj * view; - glm::vec3 cam = m_scene_camera.get_orbital_position(); - - // Skybox behind everything (depth test on, write off; shader forces depth=1). - glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); glDepthMask(GL_FALSE); - m_skybox_shader->bind(); - m_skybox_shader->set_mat4("u_Projection", proj); - m_skybox_shader->set_mat4("u_View", glm::mat4(glm::mat3(view))); // strip translation - auto hdri = HDRIManager::get().get_current_hdri(); - if (hdri) { hdri->bind(0); m_skybox_shader->set_int("u_Skybox", 0); } - glBindVertexArray(m_skybox_vao); glDrawArrays(GL_TRIANGLES, 0, 36); - - // Lit spheres. - glDepthFunc(GL_LESS); glDepthMask(GL_TRUE); - m_sphere_shader->bind(); glBindVertexArray(m_sphere_vao); - for (int i = 0; i < (int)m_scene_objects.size(); ++i) - { - const SceneObject& o = m_scene_objects[i]; - m_sphere_shader->set_mat4("u_ViewProjection", vp); - m_sphere_shader->set_mat4("u_Transform", - glm::translate(glm::mat4(1.0f), o.position) * glm::scale(glm::mat4(1.0f), glm::vec3(o.radius))); - m_sphere_shader->set_float3("u_Color", o.color); - m_sphere_shader->set_float("u_Specular", 0.6f); - m_sphere_shader->set_float("u_Emission", 0.0f); - m_sphere_shader->set_float3("u_LightPos", glm::vec3(10.0f, 20.0f, 10.0f)); - m_sphere_shader->set_float3("u_CameraPos", cam); - m_sphere_shader->set_int("u_IsSelected", i == m_selected_object ? 1 : 0); - m_sphere_shader->set_float3("u_OutlineColor", glm::vec3(1.0f, 1.0f, 0.0f)); - m_sphere_shader->set_float("u_OutlineWidth", 0.15f); - glDrawElements(GL_TRIANGLES, m_sphere_index_count, GL_UNSIGNED_INT, 0); - } - - // Transparent grid on top (test, no depth write). - glDepthMask(GL_FALSE); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - m_grid_shader->bind(); - m_grid_shader->set_mat4("u_ViewProjection", vp); - m_grid_shader->set_mat4("u_Transform", glm::mat4(1.0f)); - m_grid_shader->set_float("u_GridSize", 50.0f); - m_grid_shader->set_float3("u_GridColor", glm::vec3(0.55f, 0.55f, 0.6f)); - m_grid_shader->set_float("u_GridAlpha", 0.75f); - m_grid_shader->set_float3("u_CameraPos", cam); - glBindVertexArray(m_grid_vao); glDrawArrays(GL_LINES, 0, m_grid_vertex_count); - glDisable(GL_BLEND); glDepthMask(GL_TRUE); glBindVertexArray(0); - } - - auto OpenGLRenderer::draw_frame(const glm::vec4& clear_color, - const std::function& build_ui) -> void - { - auto* win = static_cast(m_window); - process_input(); - - int w = 0, h = 0; glfwGetFramebufferSize(win, &w, &h); - - if (m_scene_mode) - { - render_scene(w, h); - } - else - { - m_engine->set_window_dimensions(w, h); - // Geodesic pass -> low-res texture (watchdog-tiled on macOS), then - // upscale to the window. Disk/camera come from the shared BlackHoleParams. - m_engine->dispatch_compute(m_engine->get_camera()); - RenderCommand::set_viewport(0, 0, w, h); - RenderCommand::set_clear_color(clear_color); - RenderCommand::clear(); - m_engine->draw_full_screen_quad(); - } - - if (m_ui_ready) - { - ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplGlfw_NewFrame(); - ImGui::NewFrame(); - if (build_ui) build_ui(); - ImGui::Render(); - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); - } - glfwSwapBuffers(win); - } - - auto OpenGLRenderer::set_scene_mode(bool enabled) -> void { m_scene_mode = enabled; } - auto OpenGLRenderer::is_scene_mode() const -> bool { return m_scene_mode; } - - auto OpenGLRenderer::set_free_fly(bool enabled) -> void - { - Camera& cam = m_engine->get_camera(); - if (enabled && cam.get_camera_mode() != CameraMode::FPS) - { - glm::vec3 pos = cam.get_orbital_position(); - glm::vec3 fwd = glm::normalize(cam.get_orbital_target() - pos); - float pitch = glm::degrees(asinf(glm::clamp(fwd.y, -1.0f, 1.0f))); - float yaw = glm::degrees(atan2f(fwd.z, fwd.x)); - cam.set_position(pos); - cam.set_rotation(glm::vec3(pitch, yaw, 0.0f)); - cam.set_movement_speed(2.0e10f); - cam.set_mouse_sensitivity(0.15f); - cam.set_camera_mode(CameraMode::FPS); - } - else if (!enabled) - { - cam.set_camera_mode(CameraMode::Orbital); - } - } - - auto OpenGLRenderer::is_free_fly() const -> bool - { - return m_engine->get_camera().get_camera_mode() == CameraMode::FPS; - } - - auto OpenGLRenderer::reset_camera() -> void - { - Camera& cam = m_engine->get_camera(); - cam.set_camera_mode(CameraMode::Orbital); - cam.set_orbital_radius(4e11); - cam.set_azimuth(0.0f); - cam.set_elevation(1.25f); - } - - auto OpenGLRenderer::set_hdri(const std::string& path) -> void - { - HDRIManager::get().set_current_hdri(path); // the Engine re-reads it each frame - m_hdri_path = path; - } - - auto OpenGLRenderer::current_hdri() const -> const std::string& { return m_hdri_path; } - auto OpenGLRenderer::scene_objects() -> std::vector& { return m_scene_objects; } - auto OpenGLRenderer::set_selected_object(int index) -> void { m_selected_object = index; } - auto OpenGLRenderer::black_hole_params() -> BlackHoleParams& { return m_engine->black_hole_params(); } - auto OpenGLRenderer::device_name() const -> const std::string& { return m_device_name; } -} diff --git a/src/platform/opengl/opengl_renderer.h b/src/platform/opengl/opengl_renderer.h deleted file mode 100644 index 4258f40..0000000 --- a/src/platform/opengl/opengl_renderer.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include "rendering/renderer_backend.h" -#include "core/memory.h" -#include "core/camera.h" -#include "rendering/shader.h" - -// OpenGL implementation of the shared RendererBackend interface. It renders the -// SAME cross-compiled Geodesic shader the Vulkan backend does (driven by the -// same BlackHoleParams + camera), so the application behaves identically on -// either backend. Wraps the Engine, which owns the GL geodesic/present passes -// and the macOS watchdog-safe tiled rendering. -namespace Donut -{ - class Engine; - - class OpenGLRenderer : public RendererBackend - { - public: - OpenGLRenderer(); - ~OpenGLRenderer() override; - - auto init(void* glfwWindow, int width, int height) -> bool override; - auto init_ui() -> bool override; - auto shutdown() -> void override; - auto resize(int width, int height) -> void override; - auto draw_frame(const glm::vec4& clear_color, - const std::function& build_ui) -> void override; - - auto set_scene_mode(bool enabled) -> void override; - auto is_scene_mode() const -> bool override; - auto set_free_fly(bool enabled) -> void override; - auto is_free_fly() const -> bool override; - auto reset_camera() -> void override; - - auto set_hdri(const std::string& path) -> void override; - auto current_hdri() const -> const std::string& override; - auto scene_objects() -> std::vector& override; - auto set_selected_object(int index) -> void override; - auto black_hole_params() -> BlackHoleParams& override; - - auto device_name() const -> const std::string& override; - - private: - auto process_input() -> void; - auto create_scene_resources() -> void; // grid + sphere + skybox meshes/shaders - auto render_scene(int width, int height) -> void; - - void* m_window = nullptr; - Scope m_engine; - - // World-builder scene view (normal-scale): mirrors the Vulkan scene view - // with the same shared Grid/Sphere/Skybox shaders. - Camera m_scene_camera{ 45.0f, 16.0f / 9.0f, 0.1f, 1000.0f }; - Ref m_grid_shader, m_sphere_shader, m_skybox_shader; - unsigned int m_grid_vao = 0, m_grid_vbo = 0; - unsigned int m_sphere_vao = 0, m_sphere_vbo = 0, m_sphere_ebo = 0; - unsigned int m_skybox_vao = 0, m_skybox_vbo = 0; - int m_grid_vertex_count = 0, m_sphere_index_count = 0; - bool m_scene_ready = false; - std::vector m_scene_objects{ SceneObject{} }; - int m_selected_object = 0; - std::string m_hdri_path; - std::string m_device_name; - bool m_scene_mode = false; - bool m_ui_ready = false; - - double m_last_x = 0.0, m_last_y = 0.0; - bool m_left_was_down = false; - double m_last_frame_time = 0.0; - }; -} diff --git a/src/platform/vulkan/vulkan_context.cpp b/src/platform/vulkan/vulkan_context.cpp deleted file mode 100644 index ece7700..0000000 --- a/src/platform/vulkan/vulkan_context.cpp +++ /dev/null @@ -1,789 +0,0 @@ -#include "vulkan_context.h" -#include "core/log.h" - -#include - -#include -#include -#include "stb_image_write.h" - -#include -#include -#include -#include - -namespace Donut -{ - // Logs and returns false from the enclosing function on any non-success result. - #define VK_CHECK(expr) \ - do { \ - VkResult _r = (expr); \ - if (_r != VK_SUCCESS) { \ - DONUT_ERROR("Vulkan: {} failed ({})", #expr, (int)_r); \ - return false; \ - } \ - } while (0) - - struct VulkanContext::Impl - { - VkInstance instance = VK_NULL_HANDLE; - VkPhysicalDevice physical = VK_NULL_HANDLE; - VkDevice device = VK_NULL_HANDLE; - VkQueue graphics_queue = VK_NULL_HANDLE; - uint32_t graphics_family = 0; - VkPhysicalDeviceMemoryProperties mem_props{}; - - auto find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags flags) const -> uint32_t - { - for (uint32_t i = 0; i < mem_props.memoryTypeCount; ++i) - if ((type_filter & (1u << i)) && - (mem_props.memoryTypes[i].propertyFlags & flags) == flags) - return i; - return UINT32_MAX; - } - - bool create_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, - VkBuffer& buf, VkDeviceMemory& mem) const - { - VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; - bci.size = size; bci.usage = usage; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - if (vkCreateBuffer(device, &bci, nullptr, &buf) != VK_SUCCESS) return false; - VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(device, buf, &req); - VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - ai.allocationSize = req.size; - ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, props); - if (vkAllocateMemory(device, &ai, nullptr, &mem) != VK_SUCCESS) return false; - vkBindBufferMemory(device, buf, mem, 0); - return true; - } - }; - - VulkanContext::VulkanContext() { m_impl = new Impl(); } - VulkanContext::~VulkanContext() { shutdown(); delete m_impl; m_impl = nullptr; } - - auto VulkanContext::init() -> bool - { - Impl& v = *m_impl; - -#ifdef __APPLE__ - // The Homebrew Vulkan loader does not auto-discover MoltenVK or the - // validation layers; point it at both unless already configured. - if (!getenv("VK_ICD_FILENAMES")) - setenv("VK_ICD_FILENAMES", "/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json", 0); - if (!getenv("VK_LAYER_PATH")) - setenv("VK_LAYER_PATH", "/opt/homebrew/share/vulkan/explicit_layer.d", 0); -#endif - - // Instance - VkApplicationInfo app{ VK_STRUCTURE_TYPE_APPLICATION_INFO }; - app.pApplicationName = "Donut"; - app.apiVersion = VK_API_VERSION_1_2; - - // MoltenVK is a portability driver: without the portability-enumeration - // extension + flag, vkEnumeratePhysicalDevices returns zero devices. - std::vector exts = { - VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, - VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, - }; - - // Enable validation layers when they are installed (optional). - std::vector layers; - uint32_t layer_count = 0; - vkEnumerateInstanceLayerProperties(&layer_count, nullptr); - std::vector avail(layer_count); - vkEnumerateInstanceLayerProperties(&layer_count, avail.data()); - for (const auto& l : avail) - if (std::strcmp(l.layerName, "VK_LAYER_KHRONOS_validation") == 0) - layers.push_back("VK_LAYER_KHRONOS_validation"); - - VkInstanceCreateInfo ici{ VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO }; - ici.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; - ici.pApplicationInfo = &app; - ici.enabledExtensionCount = (uint32_t)exts.size(); - ici.ppEnabledExtensionNames = exts.data(); - ici.enabledLayerCount = (uint32_t)layers.size(); - ici.ppEnabledLayerNames = layers.data(); - VK_CHECK(vkCreateInstance(&ici, nullptr, &v.instance)); - - // Physical device - uint32_t device_count = 0; - vkEnumeratePhysicalDevices(v.instance, &device_count, nullptr); - if (device_count == 0) { DONUT_ERROR("Vulkan: no physical devices"); return false; } - std::vector devices(device_count); - vkEnumeratePhysicalDevices(v.instance, &device_count, devices.data()); - v.physical = devices[0]; - - VkPhysicalDeviceProperties props{}; - vkGetPhysicalDeviceProperties(v.physical, &props); - vkGetPhysicalDeviceMemoryProperties(v.physical, &v.mem_props); - - // Graphics queue family - uint32_t q_count = 0; - vkGetPhysicalDeviceQueueFamilyProperties(v.physical, &q_count, nullptr); - std::vector qfams(q_count); - vkGetPhysicalDeviceQueueFamilyProperties(v.physical, &q_count, qfams.data()); - bool found = false; - for (uint32_t i = 0; i < q_count; ++i) - if (qfams[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { v.graphics_family = i; found = true; break; } - if (!found) { DONUT_ERROR("Vulkan: no graphics queue family"); return false; } - - // Logical device - // MoltenVK requires VK_KHR_portability_subset to be enabled if present. - std::vector dev_exts; - uint32_t dev_ext_count = 0; - vkEnumerateDeviceExtensionProperties(v.physical, nullptr, &dev_ext_count, nullptr); - std::vector dev_ext_props(dev_ext_count); - vkEnumerateDeviceExtensionProperties(v.physical, nullptr, &dev_ext_count, dev_ext_props.data()); - for (const auto& e : dev_ext_props) - if (std::strcmp(e.extensionName, "VK_KHR_portability_subset") == 0) - dev_exts.push_back("VK_KHR_portability_subset"); - - float priority = 1.0f; - VkDeviceQueueCreateInfo qci{ VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO }; - qci.queueFamilyIndex = v.graphics_family; - qci.queueCount = 1; - qci.pQueuePriorities = &priority; - - VkDeviceCreateInfo dci{ VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO }; - dci.queueCreateInfoCount = 1; - dci.pQueueCreateInfos = &qci; - dci.enabledExtensionCount = (uint32_t)dev_exts.size(); - dci.ppEnabledExtensionNames = dev_exts.data(); - VK_CHECK(vkCreateDevice(v.physical, &dci, nullptr, &v.device)); - vkGetDeviceQueue(v.device, v.graphics_family, 0, &v.graphics_queue); - - DONUT_INFO("Vulkan device: {} (API {}.{}.{}, validation {})", - props.deviceName, - VK_API_VERSION_MAJOR(props.apiVersion), - VK_API_VERSION_MINOR(props.apiVersion), - VK_API_VERSION_PATCH(props.apiVersion), - layers.empty() ? "off" : "on"); - return true; - } - - auto VulkanContext::self_test_clear() -> bool - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE) return false; - - const uint32_t W = 64, H = 64; - const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM; - - // Offscreen colour image - VkImage image = VK_NULL_HANDLE; VkDeviceMemory image_mem = VK_NULL_HANDLE; - VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - ici.imageType = VK_IMAGE_TYPE_2D; - ici.format = fmt; - ici.extent = { W, H, 1 }; - ici.mipLevels = 1; - ici.arrayLayers = 1; - ici.samples = VK_SAMPLE_COUNT_1_BIT; - ici.tiling = VK_IMAGE_TILING_OPTIMAL; - ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; - ici.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - VK_CHECK(vkCreateImage(v.device, &ici, nullptr, &image)); - - VkMemoryRequirements im_req{}; - vkGetImageMemoryRequirements(v.device, image, &im_req); - VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - im_alloc.allocationSize = im_req.size; - im_alloc.memoryTypeIndex = v.find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK(vkAllocateMemory(v.device, &im_alloc, nullptr, &image_mem)); - VK_CHECK(vkBindImageMemory(v.device, image, image_mem, 0)); - - VkImageView view = VK_NULL_HANDLE; - VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - vci.image = image; - vci.viewType = VK_IMAGE_VIEW_TYPE_2D; - vci.format = fmt; - vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - VK_CHECK(vkCreateImageView(v.device, &vci, nullptr, &view)); - - // Render pass (clear -> store, leave in TRANSFER_SRC for readback) - VkAttachmentDescription color{}; - color.format = fmt; - color.samples = VK_SAMPLE_COUNT_1_BIT; - color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; - color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - color.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - - VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; - VkSubpassDescription subpass{}; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; - subpass.pColorAttachments = &color_ref; - - // Ensure colour writes finish before the read-back copy. - VkSubpassDependency dep{}; - dep.srcSubpass = 0; - dep.dstSubpass = VK_SUBPASS_EXTERNAL; - dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - dep.dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; - dep.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - - VkRenderPass render_pass = VK_NULL_HANDLE; - VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; - rpci.attachmentCount = 1; rpci.pAttachments = &color; - rpci.subpassCount = 1; rpci.pSubpasses = &subpass; - rpci.dependencyCount = 1; rpci.pDependencies = &dep; - VK_CHECK(vkCreateRenderPass(v.device, &rpci, nullptr, &render_pass)); - - VkFramebuffer fb = VK_NULL_HANDLE; - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = render_pass; - fbci.attachmentCount = 1; fbci.pAttachments = &view; - fbci.width = W; fbci.height = H; fbci.layers = 1; - VK_CHECK(vkCreateFramebuffer(v.device, &fbci, nullptr, &fb)); - - // Host-visible staging buffer for read-back - VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_mem = VK_NULL_HANDLE; - VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; - bci.size = (VkDeviceSize)W * H * 4; - bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; - bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - VK_CHECK(vkCreateBuffer(v.device, &bci, nullptr, &staging)); - VkMemoryRequirements b_req{}; - vkGetBufferMemoryRequirements(v.device, staging, &b_req); - VkMemoryAllocateInfo b_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - b_alloc.allocationSize = b_req.size; - b_alloc.memoryTypeIndex = v.find_memory_type(b_req.memoryTypeBits, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); - VK_CHECK(vkAllocateMemory(v.device, &b_alloc, nullptr, &staging_mem)); - VK_CHECK(vkBindBufferMemory(v.device, staging, staging_mem, 0)); - - // Command buffer: clear via render pass, then copy image -> buffer - VkCommandPool pool = VK_NULL_HANDLE; - VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; - pci.queueFamilyIndex = v.graphics_family; - VK_CHECK(vkCreateCommandPool(v.device, &pci, nullptr, &pool)); - - VkCommandBuffer cmd = VK_NULL_HANDLE; - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; - cbai.commandPool = pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; - VK_CHECK(vkAllocateCommandBuffers(v.device, &cbai, &cmd)); - - VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; - begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - VK_CHECK(vkBeginCommandBuffer(cmd, &begin)); - - VkClearValue clear{}; - clear.color = { { 0.2f, 0.4f, 0.8f, 1.0f } }; // -> RGBA8 (51, 102, 204, 255) - VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; - rpbi.renderPass = render_pass; rpbi.framebuffer = fb; - rpbi.renderArea = { { 0, 0 }, { W, H } }; - rpbi.clearValueCount = 1; rpbi.pClearValues = &clear; - vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); - vkCmdEndRenderPass(cmd); - - VkBufferImageCopy region{}; - region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; - region.imageExtent = { W, H, 1 }; - vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staging, 1, ®ion); - VK_CHECK(vkEndCommandBuffer(cmd)); - - VkFence fence = VK_NULL_HANDLE; - VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; - VK_CHECK(vkCreateFence(v.device, &fci, nullptr, &fence)); - VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; - submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd; - VK_CHECK(vkQueueSubmit(v.graphics_queue, 1, &submit, fence)); - VK_CHECK(vkWaitForFences(v.device, 1, &fence, VK_TRUE, UINT64_MAX)); - - // Read back + verify - void* mapped = nullptr; - VK_CHECK(vkMapMemory(v.device, staging_mem, 0, bci.size, 0, &mapped)); - const uint8_t* px = (const uint8_t*)mapped; - DONUT_INFO("Vulkan clear self-test: pixel RGBA = ({}, {}, {}, {})", - (int)px[0], (int)px[1], (int)px[2], (int)px[3]); - bool ok = px[0] > 45 && px[0] < 60 && px[1] > 95 && px[1] < 110 && - px[2] > 195 && px[2] < 210 && px[3] == 255; - vkUnmapMemory(v.device, staging_mem); - DONUT_INFO("Vulkan clear self-test: {}", ok ? "PASS" : "FAIL"); - - // Cleanup - vkDestroyFence(v.device, fence, nullptr); - vkDestroyCommandPool(v.device, pool, nullptr); - vkDestroyBuffer(v.device, staging, nullptr); - vkFreeMemory(v.device, staging_mem, nullptr); - vkDestroyFramebuffer(v.device, fb, nullptr); - vkDestroyRenderPass(v.device, render_pass, nullptr); - vkDestroyImageView(v.device, view, nullptr); - vkDestroyImage(v.device, image, nullptr); - vkFreeMemory(v.device, image_mem, nullptr); - return ok; - } - - static std::vector load_spirv(const std::string& path) - { - std::ifstream f(path, std::ios::binary | std::ios::ate); - if (!f) return {}; - size_t size = (size_t)f.tellg(); - std::vector data(size / 4); - f.seekg(0); - f.read((char*)data.data(), (std::streamsize)size); - return data; - } - - auto VulkanContext::self_test_triangle() -> bool - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE) return false; - - const uint32_t W = 64, H = 64; - const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM; - - // Offscreen image + view (as in the clear test) - VkImage image = VK_NULL_HANDLE; VkDeviceMemory image_mem = VK_NULL_HANDLE; - VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { W, H, 1 }; - ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT; - ici.tiling = VK_IMAGE_TILING_OPTIMAL; - ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; - VK_CHECK(vkCreateImage(v.device, &ici, nullptr, &image)); - VkMemoryRequirements im_req{}; vkGetImageMemoryRequirements(v.device, image, &im_req); - VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - im_alloc.allocationSize = im_req.size; - im_alloc.memoryTypeIndex = v.find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK(vkAllocateMemory(v.device, &im_alloc, nullptr, &image_mem)); - VK_CHECK(vkBindImageMemory(v.device, image, image_mem, 0)); - VkImageView view = VK_NULL_HANDLE; - VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - vci.image = image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt; - vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - VK_CHECK(vkCreateImageView(v.device, &vci, nullptr, &view)); - - // Render pass + framebuffer - VkAttachmentDescription color{}; - color.format = fmt; color.samples = VK_SAMPLE_COUNT_1_BIT; - color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; - color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; - VkSubpassDescription subpass{}; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref; - VkSubpassDependency dep{}; - dep.srcSubpass = 0; dep.dstSubpass = VK_SUBPASS_EXTERNAL; - dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - dep.dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; dep.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - VkRenderPass render_pass = VK_NULL_HANDLE; - VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; - rpci.attachmentCount = 1; rpci.pAttachments = &color; - rpci.subpassCount = 1; rpci.pSubpasses = &subpass; - rpci.dependencyCount = 1; rpci.pDependencies = &dep; - VK_CHECK(vkCreateRenderPass(v.device, &rpci, nullptr, &render_pass)); - VkFramebuffer fb = VK_NULL_HANDLE; - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = render_pass; fbci.attachmentCount = 1; fbci.pAttachments = &view; - fbci.width = W; fbci.height = H; fbci.layers = 1; - VK_CHECK(vkCreateFramebuffer(v.device, &fbci, nullptr, &fb)); - - // Shader modules from Slang SPIR-V - auto vspv = load_spirv("assets/shaders/generated/VkPipelineTest.vertexMain.spv"); - auto fspv = load_spirv("assets/shaders/generated/VkPipelineTest.fragmentMain.spv"); - if (vspv.empty() || fspv.empty()) { DONUT_ERROR("Vulkan: VkPipelineTest SPIR-V not found"); return false; } - VkShaderModule vmod = VK_NULL_HANDLE, fmod = VK_NULL_HANDLE; - VkShaderModuleCreateInfo smci{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO }; - smci.codeSize = vspv.size() * 4; smci.pCode = vspv.data(); - VK_CHECK(vkCreateShaderModule(v.device, &smci, nullptr, &vmod)); - smci.codeSize = fspv.size() * 4; smci.pCode = fspv.data(); - VK_CHECK(vkCreateShaderModule(v.device, &smci, nullptr, &fmod)); - - // Graphics pipeline - VkPipelineShaderStageCreateInfo stages[2]{}; - stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; - stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main"; - stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; - stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main"; - - VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; - ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - VkViewport vp{ 0, 0, (float)W, (float)H, 0, 1 }; - VkRect2D scissor{ { 0, 0 }, { W, H } }; - VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; - vps.viewportCount = 1; vps.pViewports = &vp; vps.scissorCount = 1; vps.pScissors = &scissor; - VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; - rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f; - VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; - ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - VkPipelineColorBlendAttachmentState cba{}; - cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; - cb.attachmentCount = 1; cb.pAttachments = &cba; - - VkPipelineLayout layout = VK_NULL_HANDLE; - VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; - VK_CHECK(vkCreatePipelineLayout(v.device, &plci, nullptr, &layout)); - - VkPipeline pipeline = VK_NULL_HANDLE; - VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; - gpci.stageCount = 2; gpci.pStages = stages; - gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; - gpci.pViewportState = &vps; gpci.pRasterizationState = &rs; - gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; - gpci.layout = layout; gpci.renderPass = render_pass; gpci.subpass = 0; - VK_CHECK(vkCreateGraphicsPipelines(v.device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline)); - - // Readback staging buffer - VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_mem = VK_NULL_HANDLE; - VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; - bci.size = (VkDeviceSize)W * H * 4; bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; - VK_CHECK(vkCreateBuffer(v.device, &bci, nullptr, &staging)); - VkMemoryRequirements b_req{}; vkGetBufferMemoryRequirements(v.device, staging, &b_req); - VkMemoryAllocateInfo b_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - b_alloc.allocationSize = b_req.size; - b_alloc.memoryTypeIndex = v.find_memory_type(b_req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); - VK_CHECK(vkAllocateMemory(v.device, &b_alloc, nullptr, &staging_mem)); - VK_CHECK(vkBindBufferMemory(v.device, staging, staging_mem, 0)); - - // Record + submit - VkCommandPool pool = VK_NULL_HANDLE; - VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; - pci.queueFamilyIndex = v.graphics_family; - VK_CHECK(vkCreateCommandPool(v.device, &pci, nullptr, &pool)); - VkCommandBuffer cmd = VK_NULL_HANDLE; - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; - cbai.commandPool = pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; - VK_CHECK(vkAllocateCommandBuffers(v.device, &cbai, &cmd)); - VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; - begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - VK_CHECK(vkBeginCommandBuffer(cmd, &begin)); - VkClearValue clear{}; clear.color = { { 0.0f, 0.0f, 0.0f, 1.0f } }; - VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; - rpbi.renderPass = render_pass; rpbi.framebuffer = fb; - rpbi.renderArea = { { 0, 0 }, { W, H } }; - rpbi.clearValueCount = 1; rpbi.pClearValues = &clear; - vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); - vkCmdDraw(cmd, 3, 1, 0, 0); - vkCmdEndRenderPass(cmd); - VkBufferImageCopy region{}; - region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; - region.imageExtent = { W, H, 1 }; - vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staging, 1, ®ion); - VK_CHECK(vkEndCommandBuffer(cmd)); - - VkFence fence = VK_NULL_HANDLE; - VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; - VK_CHECK(vkCreateFence(v.device, &fci, nullptr, &fence)); - VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; - submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd; - VK_CHECK(vkQueueSubmit(v.graphics_queue, 1, &submit, fence)); - VK_CHECK(vkWaitForFences(v.device, 1, &fence, VK_TRUE, UINT64_MAX)); - - // Verify: centre pixel should be the mid-gradient (not black) - void* mapped = nullptr; - VK_CHECK(vkMapMemory(v.device, staging_mem, 0, bci.size, 0, &mapped)); - const uint8_t* px = (const uint8_t*)mapped; - size_t c = ((size_t)(H / 2) * W + (W / 2)) * 4; - DONUT_INFO("Vulkan triangle self-test: centre pixel RGBA = ({}, {}, {}, {})", - (int)px[c + 0], (int)px[c + 1], (int)px[c + 2], (int)px[c + 3]); - bool ok = (px[c + 0] > 40 || px[c + 1] > 40) && px[c + 3] == 255; - vkUnmapMemory(v.device, staging_mem); - DONUT_INFO("Vulkan triangle self-test: {}", ok ? "PASS" : "FAIL"); - - // Cleanup - vkDestroyFence(v.device, fence, nullptr); - vkDestroyCommandPool(v.device, pool, nullptr); - vkDestroyBuffer(v.device, staging, nullptr); - vkFreeMemory(v.device, staging_mem, nullptr); - vkDestroyPipeline(v.device, pipeline, nullptr); - vkDestroyPipelineLayout(v.device, layout, nullptr); - vkDestroyShaderModule(v.device, vmod, nullptr); - vkDestroyShaderModule(v.device, fmod, nullptr); - vkDestroyFramebuffer(v.device, fb, nullptr); - vkDestroyRenderPass(v.device, render_pass, nullptr); - vkDestroyImageView(v.device, view, nullptr); - vkDestroyImage(v.device, image, nullptr); - vkFreeMemory(v.device, image_mem, nullptr); - return ok; - } - - auto VulkanContext::render_geodesic(const char* png_path) -> bool - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE) return false; - - const uint32_t W = 384, H = 216; - const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM; - const float SagA_rs = 1.269e10f; - - // Offscreen colour target - VkImage image = VK_NULL_HANDLE; VkDeviceMemory image_mem = VK_NULL_HANDLE; - VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { W, H, 1 }; - ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT; - ici.tiling = VK_IMAGE_TILING_OPTIMAL; - ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; - VK_CHECK(vkCreateImage(v.device, &ici, nullptr, &image)); - VkMemoryRequirements im_req{}; vkGetImageMemoryRequirements(v.device, image, &im_req); - VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - im_alloc.allocationSize = im_req.size; - im_alloc.memoryTypeIndex = v.find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK(vkAllocateMemory(v.device, &im_alloc, nullptr, &image_mem)); - VK_CHECK(vkBindImageMemory(v.device, image, image_mem, 0)); - VkImageView view = VK_NULL_HANDLE; - VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - vci.image = image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt; - vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - VK_CHECK(vkCreateImageView(v.device, &vci, nullptr, &view)); - - VkAttachmentDescription color{}; - color.format = fmt; color.samples = VK_SAMPLE_COUNT_1_BIT; - color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; - color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; - VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref; - VkSubpassDependency dep{}; - dep.srcSubpass = 0; dep.dstSubpass = VK_SUBPASS_EXTERNAL; - dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - dep.dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; dep.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - VkRenderPass render_pass = VK_NULL_HANDLE; - VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; - rpci.attachmentCount = 1; rpci.pAttachments = &color; - rpci.subpassCount = 1; rpci.pSubpasses = &subpass; - rpci.dependencyCount = 1; rpci.pDependencies = &dep; - VK_CHECK(vkCreateRenderPass(v.device, &rpci, nullptr, &render_pass)); - VkFramebuffer fb = VK_NULL_HANDLE; - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = render_pass; fbci.attachmentCount = 1; fbci.pAttachments = &view; - fbci.width = W; fbci.height = H; fbci.layers = 1; - VK_CHECK(vkCreateFramebuffer(v.device, &fbci, nullptr, &fb)); - - // Uniform buffers (host-visible), filled to match the shader's std140 layout - const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - VkBuffer cam_buf, disk_buf, obj_buf, sim_buf; - VkDeviceMemory cam_mem, disk_mem, obj_mem, sim_mem; - v.create_buffer(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, cam_buf, cam_mem); - v.create_buffer(32, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, disk_buf, disk_mem); - v.create_buffer(800, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, obj_buf, obj_mem); - v.create_buffer(16, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, sim_buf, sim_mem); - - struct CamUBO { - glm::vec3 pos; float p0; glm::vec3 right; float p1; - glm::vec3 up; float p2; glm::vec3 fwd; float p3; - float tan_half_fov; float aspect; uint32_t moving; int p4; - } cam{}; - glm::vec3 cam_pos(1e11f, 0.32e11f, 0.0f); - glm::vec3 fwd = glm::normalize(glm::vec3(0.0f) - cam_pos); - glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); - glm::vec3 up = glm::cross(right, fwd); - cam.pos = cam_pos; cam.right = right; cam.up = up; cam.fwd = fwd; - cam.tan_half_fov = 0.57735f; cam.aspect = (float)W / (float)H; cam.moving = 0; - void* p = nullptr; - vkMapMemory(v.device, cam_mem, 0, 128, 0, &p); memcpy(p, &cam, sizeof(cam)); vkUnmapMemory(v.device, cam_mem); - - float disk[8] = { SagA_rs * 2.2f, SagA_rs * 5.2f, 2.0f, SagA_rs * 0.1f, 0.1f, 0, 0, 0 }; - vkMapMemory(v.device, disk_mem, 0, 32, 0, &p); memcpy(p, disk, sizeof(disk)); vkUnmapMemory(v.device, disk_mem); - - std::vector obj_data(800, 0); - int num_objects = 1; memcpy(obj_data.data(), &num_objects, 4); - float pos_radius[4] = { 0, 0, 0, SagA_rs }; memcpy(obj_data.data() + 16, pos_radius, 16); - float obj_color[4] = { 0, 0, 0, 1 }; memcpy(obj_data.data() + 272, obj_color, 16); - vkMapMemory(v.device, obj_mem, 0, 800, 0, &p); memcpy(p, obj_data.data(), 800); vkUnmapMemory(v.device, obj_mem); - - struct SimUBO { int steps_moving; int steps_static; float early_exit; float time; } sim{ 6000, 6000, 5e12f, 0.0f }; - vkMapMemory(v.device, sim_mem, 0, 16, 0, &p); memcpy(p, &sim, sizeof(sim)); vkUnmapMemory(v.device, sim_mem); - - // Dark cubemap (stands in for the HDRI for now) - VkImage cube = VK_NULL_HANDLE; VkDeviceMemory cube_mem = VK_NULL_HANDLE; - VkImageCreateInfo cci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - cci.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; - cci.imageType = VK_IMAGE_TYPE_2D; cci.format = fmt; cci.extent = { 1, 1, 1 }; - cci.mipLevels = 1; cci.arrayLayers = 6; cci.samples = VK_SAMPLE_COUNT_1_BIT; - cci.tiling = VK_IMAGE_TILING_OPTIMAL; - cci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; - VK_CHECK(vkCreateImage(v.device, &cci, nullptr, &cube)); - VkMemoryRequirements cube_req{}; vkGetImageMemoryRequirements(v.device, cube, &cube_req); - VkMemoryAllocateInfo cube_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - cube_alloc.allocationSize = cube_req.size; - cube_alloc.memoryTypeIndex = v.find_memory_type(cube_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK(vkAllocateMemory(v.device, &cube_alloc, nullptr, &cube_mem)); - VK_CHECK(vkBindImageMemory(v.device, cube, cube_mem, 0)); - VkImageView cube_view = VK_NULL_HANDLE; - VkImageViewCreateInfo cvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - cvci.image = cube; cvci.viewType = VK_IMAGE_VIEW_TYPE_CUBE; cvci.format = fmt; - cvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 }; - VK_CHECK(vkCreateImageView(v.device, &cvci, nullptr, &cube_view)); - VkSampler sampler = VK_NULL_HANDLE; - VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; - smci.magFilter = VK_FILTER_LINEAR; smci.minFilter = VK_FILTER_LINEAR; - smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - VK_CHECK(vkCreateSampler(v.device, &smci, nullptr, &sampler)); - - VkBuffer cube_staging; VkDeviceMemory cube_staging_mem; - v.create_buffer(6 * 4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, host_vis, cube_staging, cube_staging_mem); - uint8_t cube_pixels[6 * 4]; - for (int i = 0; i < 6; ++i) { cube_pixels[i * 4 + 0] = 6; cube_pixels[i * 4 + 1] = 6; cube_pixels[i * 4 + 2] = 14; cube_pixels[i * 4 + 3] = 255; } - vkMapMemory(v.device, cube_staging_mem, 0, 24, 0, &p); memcpy(p, cube_pixels, 24); vkUnmapMemory(v.device, cube_staging_mem); - - // Descriptor set: 4 UBOs (bindings 0-3) + cubemap sampler (binding 4) - VkDescriptorSetLayoutBinding binds[5]{}; - for (int i = 0; i < 4; ++i) { binds[i].binding = i; binds[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[i].descriptorCount = 1; binds[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; } - binds[4].binding = 4; binds[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[4].descriptorCount = 1; binds[4].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - VkDescriptorSetLayout set_layout = VK_NULL_HANDLE; - VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; - dslci.bindingCount = 5; dslci.pBindings = binds; - VK_CHECK(vkCreateDescriptorSetLayout(v.device, &dslci, nullptr, &set_layout)); - VkDescriptorPoolSize psizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 4 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 } }; - VkDescriptorPool pool = VK_NULL_HANDLE; - VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; - dpci.maxSets = 1; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes; - VK_CHECK(vkCreateDescriptorPool(v.device, &dpci, nullptr, &pool)); - VkDescriptorSet set = VK_NULL_HANDLE; - VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; - dsai.descriptorPool = pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &set_layout; - VK_CHECK(vkAllocateDescriptorSets(v.device, &dsai, &set)); - - // load geodesic SPIR-V + build the pipeline - auto vspv = load_spirv("assets/shaders/generated/Geodesic.vertexMain.spv"); - auto fspv = load_spirv("assets/shaders/generated/Geodesic.fragmentMain.spv"); - if (vspv.empty() || fspv.empty()) { DONUT_ERROR("Vulkan: geodesic SPIR-V not found"); return false; } - VkShaderModule vmod, fmod; - VkShaderModuleCreateInfo smci2{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO }; - smci2.codeSize = vspv.size() * 4; smci2.pCode = vspv.data(); VK_CHECK(vkCreateShaderModule(v.device, &smci2, nullptr, &vmod)); - smci2.codeSize = fspv.size() * 4; smci2.pCode = fspv.data(); VK_CHECK(vkCreateShaderModule(v.device, &smci2, nullptr, &fmod)); - - VkPipelineLayout layout = VK_NULL_HANDLE; - VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; - plci.setLayoutCount = 1; plci.pSetLayouts = &set_layout; - VK_CHECK(vkCreatePipelineLayout(v.device, &plci, nullptr, &layout)); - - VkPipelineShaderStageCreateInfo stages[2]{}; - stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main"; - stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main"; - VkVertexInputBindingDescription vib{ 0, 16, VK_VERTEX_INPUT_RATE_VERTEX }; - VkVertexInputAttributeDescription via[2] = { { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32_SFLOAT, 8 } }; - VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; - vin.vertexAttributeDescriptionCount = 2; vin.pVertexAttributeDescriptions = via; - VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - VkViewport vp{ 0, 0, (float)W, (float)H, 0, 1 }; VkRect2D sc{ { 0, 0 }, { W, H } }; - VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.pViewports = &vp; vps.scissorCount = 1; vps.pScissors = ≻ - VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f; - VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - VkPipelineColorBlendAttachmentState cba{}; cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba; - VkPipeline pipeline = VK_NULL_HANDLE; - VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; - gpci.stageCount = 2; gpci.pStages = stages; - gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps; - gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; - gpci.layout = layout; gpci.renderPass = render_pass; gpci.subpass = 0; - VK_CHECK(vkCreateGraphicsPipelines(v.device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline)); - - // Fullscreen quad (position.xy, texcoord.uv) - float quad[] = { - -1.f, 1.f, 0.f, 1.f, -1.f, -1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 0.f, - -1.f, 1.f, 0.f, 1.f, 1.f, -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f, - }; - VkBuffer vbuf; VkDeviceMemory vbuf_mem; - v.create_buffer(sizeof(quad), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, vbuf, vbuf_mem); - vkMapMemory(v.device, vbuf_mem, 0, sizeof(quad), 0, &p); memcpy(p, quad, sizeof(quad)); vkUnmapMemory(v.device, vbuf_mem); - - // Write the descriptor set - VkDescriptorBufferInfo bi[4] = { - { cam_buf, 0, VK_WHOLE_SIZE }, { disk_buf, 0, VK_WHOLE_SIZE }, { obj_buf, 0, VK_WHOLE_SIZE }, { sim_buf, 0, VK_WHOLE_SIZE } }; - VkDescriptorImageInfo ii{ sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; - VkWriteDescriptorSet writes[5]{}; - for (int i = 0; i < 4; ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = set; writes[i].dstBinding = i; writes[i].descriptorCount = 1; writes[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; writes[i].pBufferInfo = &bi[i]; } - writes[4].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[4].dstSet = set; writes[4].dstBinding = 4; writes[4].descriptorCount = 1; writes[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[4].pImageInfo = ⅈ - vkUpdateDescriptorSets(v.device, 5, writes, 0, nullptr); - - VkBuffer readback; VkDeviceMemory readback_mem; - v.create_buffer((VkDeviceSize)W * H * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, host_vis, readback, readback_mem); - - // Record + submit - VkCommandPool cpool = VK_NULL_HANDLE; - VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; pci.queueFamilyIndex = v.graphics_family; - VK_CHECK(vkCreateCommandPool(v.device, &pci, nullptr, &cpool)); - VkCommandBuffer cmd = VK_NULL_HANDLE; - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; cbai.commandPool = cpool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; - VK_CHECK(vkAllocateCommandBuffers(v.device, &cbai, &cmd)); - VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - VK_CHECK(vkBeginCommandBuffer(cmd, &begin)); - - VkImageMemoryBarrier to_dst{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - to_dst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; to_dst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - to_dst.image = cube; to_dst.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 }; - to_dst.srcAccessMask = 0; to_dst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_dst); - VkBufferImageCopy cube_copy{}; cube_copy.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 6 }; cube_copy.imageExtent = { 1, 1, 1 }; - vkCmdCopyBufferToImage(cmd, cube_staging, cube, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &cube_copy); - VkImageMemoryBarrier to_read = to_dst; - to_read.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; to_read.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - to_read.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; to_read.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_read); - - VkClearValue clear{}; clear.color = { { 0, 0, 0, 1 } }; - VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; - rpbi.renderPass = render_pass; rpbi.framebuffer = fb; rpbi.renderArea = { { 0, 0 }, { W, H } }; - rpbi.clearValueCount = 1; rpbi.pClearValues = &clear; - vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &set, 0, nullptr); - VkDeviceSize voff = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &vbuf, &voff); - vkCmdDraw(cmd, 6, 1, 0, 0); - vkCmdEndRenderPass(cmd); - VkBufferImageCopy region{}; region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; region.imageExtent = { W, H, 1 }; - vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback, 1, ®ion); - VK_CHECK(vkEndCommandBuffer(cmd)); - - VkFence fence = VK_NULL_HANDLE; VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; - VK_CHECK(vkCreateFence(v.device, &fci, nullptr, &fence)); - VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd; - VK_CHECK(vkQueueSubmit(v.graphics_queue, 1, &submit, fence)); - VK_CHECK(vkWaitForFences(v.device, 1, &fence, VK_TRUE, UINT64_MAX)); - - vkMapMemory(v.device, readback_mem, 0, (VkDeviceSize)W * H * 4, 0, &p); - stbi_write_png(png_path, W, H, 4, p, W * 4); - vkUnmapMemory(v.device, readback_mem); - DONUT_INFO("Vulkan geodesic render written to {}", png_path); - - vkDestroyFence(v.device, fence, nullptr); - vkDestroyCommandPool(v.device, cpool, nullptr); - vkDestroyBuffer(v.device, readback, nullptr); vkFreeMemory(v.device, readback_mem, nullptr); - vkDestroyBuffer(v.device, vbuf, nullptr); vkFreeMemory(v.device, vbuf_mem, nullptr); - vkDestroyPipeline(v.device, pipeline, nullptr); vkDestroyPipelineLayout(v.device, layout, nullptr); - vkDestroyShaderModule(v.device, vmod, nullptr); vkDestroyShaderModule(v.device, fmod, nullptr); - vkDestroyDescriptorPool(v.device, pool, nullptr); vkDestroyDescriptorSetLayout(v.device, set_layout, nullptr); - vkDestroySampler(v.device, sampler, nullptr); vkDestroyImageView(v.device, cube_view, nullptr); - vkDestroyImage(v.device, cube, nullptr); vkFreeMemory(v.device, cube_mem, nullptr); - vkDestroyBuffer(v.device, cube_staging, nullptr); vkFreeMemory(v.device, cube_staging_mem, nullptr); - vkDestroyBuffer(v.device, cam_buf, nullptr); vkFreeMemory(v.device, cam_mem, nullptr); - vkDestroyBuffer(v.device, disk_buf, nullptr); vkFreeMemory(v.device, disk_mem, nullptr); - vkDestroyBuffer(v.device, obj_buf, nullptr); vkFreeMemory(v.device, obj_mem, nullptr); - vkDestroyBuffer(v.device, sim_buf, nullptr); vkFreeMemory(v.device, sim_mem, nullptr); - vkDestroyFramebuffer(v.device, fb, nullptr); vkDestroyRenderPass(v.device, render_pass, nullptr); - vkDestroyImageView(v.device, view, nullptr); vkDestroyImage(v.device, image, nullptr); vkFreeMemory(v.device, image_mem, nullptr); - return true; - } - - auto VulkanContext::shutdown() -> void - { - Impl& v = *m_impl; - if (v.device) { vkDestroyDevice(v.device, nullptr); v.device = VK_NULL_HANDLE; } - if (v.instance) { vkDestroyInstance(v.instance, nullptr); v.instance = VK_NULL_HANDLE; } - } - - auto vulkan_self_test() -> bool - { - VulkanContext ctx; - if (!ctx.init()) - { - DONUT_ERROR("Vulkan: initialization failed"); - return false; - } - bool ok = ctx.self_test_clear(); - ok = ctx.self_test_triangle() && ok; - ctx.shutdown(); - return ok; - } -} diff --git a/src/platform/vulkan/vulkan_context.h b/src/platform/vulkan/vulkan_context.h deleted file mode 100644 index de57c90..0000000 --- a/src/platform/vulkan/vulkan_context.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -// Pure-C++ interface to the Vulkan backend (no vulkan.h leaks into the rest of -// the engine; the implementation lives in VulkanContext.cpp). On macOS Vulkan -// runs through MoltenVK (Vulkan -> Metal). -namespace Donut -{ - class VulkanContext - { - public: - VulkanContext(); - ~VulkanContext(); - - // Creates the instance, picks a physical device, and creates the logical - // device + graphics queue. Returns false (and logs) on failure. - auto init() -> bool; - auto shutdown() -> void; - - // Phase 1 verification: renders a known clear colour into an offscreen - // image and reads it back, confirming instance -> device -> render pass - // -> command buffer -> submit -> read-back all work end to end. - auto self_test_clear() -> bool; - - // Phase 2/3 verification: builds a graphics pipeline from Slang-compiled - // SPIR-V and draws a full-screen gradient triangle into the offscreen - // image, confirming the SPIR-V -> pipeline -> draw path works. - auto self_test_triangle() -> bool; - - // B-3: renders the geodesic (black hole) fragment shader through Vulkan - // into an offscreen image and writes it to pngPath. Exercises UBOs, - // descriptor sets, a cubemap sampler and the geodesic pipeline. - auto render_geodesic(const char* pngPath) -> bool; - - private: - struct Impl; - Impl* m_impl = nullptr; - }; - - // Convenience one-shot: init() + self_test_clear() + shutdown(). Logs results. - auto vulkan_self_test() -> bool; -} diff --git a/src/platform/vulkan/vulkan_device.cpp b/src/platform/vulkan/vulkan_device.cpp new file mode 100644 index 0000000..4311d0c --- /dev/null +++ b/src/platform/vulkan/vulkan_device.cpp @@ -0,0 +1,1254 @@ +#include "vulkan_device.h" + +#include "core/log.h" + +#define GLFW_INCLUDE_VULKAN +#include + +#include +#include +#include + +#include +#include +#include "stb_image.h" + +#include +#include +#include +#include +#include +#include + +namespace Donut::RHI +{ + namespace + { + constexpr int MAX_FRAMES_IN_FLIGHT = 2; + constexpr uint32_t MAX_BINDINGS = 8; + + #define VKD_CHECK(expr) \ + do { \ + VkResult _r = (expr); \ + if (_r != VK_SUCCESS) { \ + DONUT_ERROR("Vulkan RHI: {} failed ({})", #expr, (int)_r); \ + return false; \ + } \ + } while (0) + + auto vk_format(Format f) -> VkFormat + { + switch (f) { + case Format::RGBA16F: return VK_FORMAT_R16G16B16A16_SFLOAT; + case Format::D32: return VK_FORMAT_D32_SFLOAT; + default: return VK_FORMAT_R8G8B8A8_UNORM; + } + } + auto vk_attr_format(uint32_t comps) -> VkFormat + { + switch (comps) { + case 1: return VK_FORMAT_R32_SFLOAT; + case 2: return VK_FORMAT_R32G32_SFLOAT; + case 3: return VK_FORMAT_R32G32B32_SFLOAT; + default: return VK_FORMAT_R32G32B32A32_SFLOAT; + } + } + auto vk_topology(Topology t) -> VkPrimitiveTopology + { return t == Topology::Lines ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; } + auto vk_compare(CompareOp o) -> VkCompareOp + { return o == CompareOp::Always ? VK_COMPARE_OP_ALWAYS : o == CompareOp::LessEqual ? VK_COMPARE_OP_LESS_OR_EQUAL : VK_COMPARE_OP_LESS; } + auto vk_filter(Filter f) -> VkFilter { return f == Filter::Nearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR; } + auto vk_cull(CullMode c) -> VkCullModeFlags + { return c == CullMode::None ? VK_CULL_MODE_NONE : c == CullMode::Back ? VK_CULL_MODE_BACK_BIT : VK_CULL_MODE_FRONT_BIT; } + + class VulkanDevice; + + // ---- Buffer: host-visible + coherent, persistently mapped ----------- + class VkBufferR : public Buffer + { + public: + VkBufferR(VkDevice d, VkBuffer b, VkDeviceMemory m, void* mapped, size_t size) + : m_device(d), m_buf(b), m_mem(m), m_mapped(mapped), m_size(size) {} + ~VkBufferR() override + { + if (m_mapped) vkUnmapMemory(m_device, m_mem); + if (m_buf) vkDestroyBuffer(m_device, m_buf, nullptr); + if (m_mem) vkFreeMemory(m_device, m_mem, nullptr); + } + auto update(const void* data, size_t size) -> void override + { if (m_mapped) std::memcpy(m_mapped, data, std::min(size, m_size)); } + + VkDevice m_device; VkBuffer m_buf; VkDeviceMemory m_mem; void* m_mapped; size_t m_size; + }; + + // ---- Texture: sampled image (2D or cube). Owns its handles unless it is + // a borrowed wrapper around a render-target view. ------------------ + class VkTextureR : public Texture + { + public: + VkTextureR() = default; + ~VkTextureR() override + { + if (!m_owns) return; + if (m_sampler) vkDestroySampler(m_device, m_sampler, nullptr); + if (m_view) vkDestroyImageView(m_device, m_view, nullptr); + if (m_image) vkDestroyImage(m_device, m_image, nullptr); + if (m_mem) vkFreeMemory(m_device, m_mem, nullptr); + } + VkDevice m_device = VK_NULL_HANDLE; + VkImage m_image = VK_NULL_HANDLE; + VkDeviceMemory m_mem = VK_NULL_HANDLE; + VkImageView m_view = VK_NULL_HANDLE; + VkSampler m_sampler = VK_NULL_HANDLE; + bool m_owns = true; + }; + + // ---- RenderTarget: off-screen colour image + framebuffer ------------ + class VkRenderTargetR : public RenderTarget + { + public: + ~VkRenderTargetR() override + { + if (m_fb) vkDestroyFramebuffer(m_device, m_fb, nullptr); + if (m_sampler) vkDestroySampler(m_device, m_sampler, nullptr); + if (m_view) vkDestroyImageView(m_device, m_view, nullptr); + if (m_image) vkDestroyImage(m_device, m_image, nullptr); + if (m_mem) vkFreeMemory(m_device, m_mem, nullptr); + } + auto width() const -> int override { return m_w; } + auto height() const -> int override { return m_h; } + auto color_texture() -> Texture* override { return &m_color; } + + VkDevice m_device = VK_NULL_HANDLE; + int m_w = 0, m_h = 0; + VkImage m_image = VK_NULL_HANDLE; + VkDeviceMemory m_mem = VK_NULL_HANDLE; + VkImageView m_view = VK_NULL_HANDLE; + VkSampler m_sampler = VK_NULL_HANDLE; + VkFramebuffer m_fb = VK_NULL_HANDLE; + VkTextureR m_color; // borrowed wrapper (view+sampler) for sampling + }; + + // ---- Pipeline ------------------------------------------------------- + class VkPipelineR : public Pipeline + { + public: + ~VkPipelineR() override + { + if (m_pipeline) vkDestroyPipeline(m_device, m_pipeline, nullptr); + if (m_layout) vkDestroyPipelineLayout(m_device, m_layout, nullptr); + if (m_set_layout) vkDestroyDescriptorSetLayout(m_device, m_set_layout, nullptr); + } + VkDevice m_device = VK_NULL_HANDLE; + VkPipeline m_pipeline = VK_NULL_HANDLE; + VkPipelineLayout m_layout = VK_NULL_HANDLE; + VkDescriptorSetLayout m_set_layout = VK_NULL_HANDLE; + std::vector m_resources; + }; + + // ---- CommandList ---------------------------------------------------- + class VkCommandListR : public CommandList + { + public: + auto begin_render_pass(RenderTarget* target, const glm::vec4& clear) -> void override + { + VkClearValue cvs[2]{}; + cvs[0].color = { { clear.r, clear.g, clear.b, clear.a } }; + cvs[1].depthStencil = { 1.0f, 0 }; + VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; + if (target) + { + auto* rt = static_cast(target); + rpbi.renderPass = m_offscreen_rp; rpbi.framebuffer = rt->m_fb; + rpbi.renderArea = { { 0, 0 }, { (uint32_t)rt->m_w, (uint32_t)rt->m_h } }; + rpbi.clearValueCount = 1; rpbi.pClearValues = cvs; + } + else + { + rpbi.renderPass = m_swapchain_rp; rpbi.framebuffer = m_swapchain_fb; + rpbi.renderArea = { { 0, 0 }, m_extent }; + rpbi.clearValueCount = 2; rpbi.pClearValues = cvs; + } + vkCmdBeginRenderPass(m_cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); + } + auto end_render_pass() -> void override { vkCmdEndRenderPass(m_cmd); } + + auto bind_pipeline(Pipeline* p) -> void override + { + m_pipe = static_cast(p); + vkCmdBindPipeline(m_cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipe->m_pipeline); + } + auto set_viewport(int x, int y, int w, int h, bool flip_y) -> void override + { + VkViewport vp{ (float)x, flip_y ? (float)(y + h) : (float)y, + (float)w, flip_y ? -(float)h : (float)h, 0.0f, 1.0f }; + VkRect2D sc{ { x, y }, { (uint32_t)w, (uint32_t)h } }; + vkCmdSetViewport(m_cmd, 0, 1, &vp); + vkCmdSetScissor(m_cmd, 0, 1, &sc); + } + auto bind_uniform(uint32_t binding, Buffer* ubo) -> void override + { + if (binding >= MAX_BINDINGS) return; + m_buf_info[binding] = { static_cast(ubo)->m_buf, 0, VK_WHOLE_SIZE }; + } + auto bind_texture(uint32_t binding, Texture* texture) -> void override + { + if (binding >= MAX_BINDINGS) return; + auto* t = static_cast(texture); + m_img_info[binding] = { t->m_sampler, t->m_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; + } + auto bind_vertex_buffer(Buffer* vb) -> void override + { + VkBuffer b = static_cast(vb)->m_buf; VkDeviceSize off = 0; + vkCmdBindVertexBuffers(m_cmd, 0, 1, &b, &off); + } + auto bind_index_buffer(Buffer* ib) -> void override + { vkCmdBindIndexBuffer(m_cmd, static_cast(ib)->m_buf, 0, VK_INDEX_TYPE_UINT32); } + auto draw(uint32_t vertex_count) -> void override + { flush_descriptors(); vkCmdDraw(m_cmd, vertex_count, 1, 0, 0); } + auto draw_indexed(uint32_t index_count) -> void override + { flush_descriptors(); vkCmdDrawIndexed(m_cmd, index_count, 1, 0, 0, 0); } + + // Allocate + write + bind a descriptor set for the current pipeline's + // declared resources, using whatever was bound since bind_pipeline. + auto flush_descriptors() -> void + { + if (!m_pipe || m_pipe->m_resources.empty()) return; + VkDescriptorSetAllocateInfo ai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; + ai.descriptorPool = m_frame_pool; ai.descriptorSetCount = 1; ai.pSetLayouts = &m_pipe->m_set_layout; + VkDescriptorSet set = VK_NULL_HANDLE; + if (vkAllocateDescriptorSets(m_device, &ai, &set) != VK_SUCCESS) + { DONUT_ERROR("Vulkan RHI: descriptor set allocation failed"); return; } + + std::array writes{}; + uint32_t n = 0; + for (const auto& r : m_pipe->m_resources) + { + VkWriteDescriptorSet& w = writes[n++]; + w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + w.dstSet = set; w.dstBinding = r.binding; w.descriptorCount = 1; + if (r.kind == ResourceKind::UniformBuffer) + { w.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; w.pBufferInfo = &m_buf_info[r.binding]; } + else + { w.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; w.pImageInfo = &m_img_info[r.binding]; } + } + vkUpdateDescriptorSets(m_device, n, writes.data(), 0, nullptr); + vkCmdBindDescriptorSets(m_cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipe->m_layout, 0, 1, &set, 0, nullptr); + } + + // Set by the device at begin_frame: + VkDevice m_device = VK_NULL_HANDLE; + VkCommandBuffer m_cmd = VK_NULL_HANDLE; + VkRenderPass m_swapchain_rp = VK_NULL_HANDLE; + VkRenderPass m_offscreen_rp = VK_NULL_HANDLE; + VkFramebuffer m_swapchain_fb = VK_NULL_HANDLE; + VkExtent2D m_extent{}; + VkDescriptorPool m_frame_pool = VK_NULL_HANDLE; + + VkPipelineR* m_pipe = nullptr; + VkDescriptorBufferInfo m_buf_info[MAX_BINDINGS]{}; + VkDescriptorImageInfo m_img_info[MAX_BINDINGS]{}; + }; + + // ---- Device --------------------------------------------------------- + class VulkanDevice : public Device + { + public: + auto init(void* glfwWindow, int width, int height) -> bool override; + auto shutdown() -> void override; + auto resize(int width, int height) -> void override { m_framebuffer_resized = true; m_width = width; m_height = height; } + auto wait_idle() -> void override { if (m_device) vkDeviceWaitIdle(m_device); } + + auto create_buffer(BufferType type, size_t size, const void* data) -> Ref override; + auto create_texture(int w, int h, Format format, Filter filter, const void* data) -> Ref override; + auto create_cubemap_from_hdri(const std::string& path) -> Ref override; + auto create_render_target(int w, int h, Format color, bool with_depth, Filter filter, int mips) -> Ref override; + auto create_pipeline(const PipelineDesc& desc) -> Ref override; + + auto begin_frame(const glm::vec4& clear) -> CommandList* override; + auto end_frame() -> void override; + + auto init_imgui() -> void override; + auto imgui_new_frame() -> void override; + auto imgui_render(CommandList& cmds) -> void override; + + auto device_name() const -> const std::string& override { return m_gpu_name; } + + // --- internals --- + auto find_memory_type(uint32_t filter, VkMemoryPropertyFlags flags) const -> uint32_t; + auto create_buffer_raw(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, VkBuffer& buf, VkDeviceMemory& mem) const -> bool; + auto load_spirv(const std::string& path) const -> std::vector; + auto create_shader_module(const std::string& path, VkShaderModule& out) const -> bool; + + auto create_instance() -> bool; + auto pick_physical_and_device() -> bool; + auto create_swapchain() -> bool; + auto create_image_views() -> bool; + auto create_swapchain_render_pass() -> bool; + auto create_offscreen_render_pass() -> bool; + auto create_depth_and_framebuffers() -> bool; + auto create_command_and_sync() -> bool; + auto recreate_swapchain() -> bool; + auto cleanup_swapchain() -> void; + + GLFWwindow* m_window = nullptr; + int m_width = 0, m_height = 0; + bool m_framebuffer_resized = false; + std::string m_gpu_name; + + VkInstance m_instance = VK_NULL_HANDLE; + VkSurfaceKHR m_surface = VK_NULL_HANDLE; + VkPhysicalDevice m_physical = VK_NULL_HANDLE; + VkDevice m_device = VK_NULL_HANDLE; + uint32_t m_graphics_family = 0, m_present_family = 0; + VkQueue m_graphics_queue = VK_NULL_HANDLE, m_present_queue = VK_NULL_HANDLE; + VkPhysicalDeviceMemoryProperties m_mem_props{}; + + VkSwapchainKHR m_swapchain = VK_NULL_HANDLE; + VkFormat m_swapchain_format = VK_FORMAT_B8G8R8A8_UNORM; + VkExtent2D m_extent{}; + std::vector m_images; + std::vector m_image_views; + VkRenderPass m_swapchain_rp = VK_NULL_HANDLE; + VkRenderPass m_offscreen_rp = VK_NULL_HANDLE; + std::vector m_framebuffers; + VkImage m_depth_image = VK_NULL_HANDLE; VkDeviceMemory m_depth_mem = VK_NULL_HANDLE; VkImageView m_depth_view = VK_NULL_HANDLE; + + VkCommandPool m_command_pool = VK_NULL_HANDLE; + std::vector m_command_buffers; + std::vector m_image_available; + std::vector m_render_finished; + std::vector m_in_flight; + std::vector m_images_in_flight; + VkFence m_geo_in_use = VK_NULL_HANDLE; + uint32_t m_current_frame = 0, m_image_index = 0; + + std::vector m_frame_pools; // one per frame in flight + VkDescriptorPool m_imgui_pool = VK_NULL_HANDLE; + bool m_imgui = false; + + VkCommandListR m_cmds; + }; + + // ================================================================== + auto VulkanDevice::find_memory_type(uint32_t filter, VkMemoryPropertyFlags flags) const -> uint32_t + { + for (uint32_t i = 0; i < m_mem_props.memoryTypeCount; ++i) + if ((filter & (1u << i)) && (m_mem_props.memoryTypes[i].propertyFlags & flags) == flags) + return i; + return UINT32_MAX; + } + + auto VulkanDevice::create_buffer_raw(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, + VkBuffer& buf, VkDeviceMemory& mem) const -> bool + { + VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; + bci.size = size; bci.usage = usage; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + if (vkCreateBuffer(m_device, &bci, nullptr, &buf) != VK_SUCCESS) return false; + VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(m_device, buf, &req); + VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + ai.allocationSize = req.size; ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, props); + if (vkAllocateMemory(m_device, &ai, nullptr, &mem) != VK_SUCCESS) return false; + vkBindBufferMemory(m_device, buf, mem, 0); + return true; + } + + auto VulkanDevice::load_spirv(const std::string& path) const -> std::vector + { + std::ifstream file(path, std::ios::ate | std::ios::binary); + if (!file.is_open()) return {}; + size_t size = (size_t)file.tellg(); + std::vector data(size / 4); + file.seekg(0); file.read(reinterpret_cast(data.data()), size); + return data; + } + + auto VulkanDevice::create_shader_module(const std::string& path, VkShaderModule& out) const -> bool + { + auto spv = load_spirv(path); + if (spv.empty()) { DONUT_ERROR("Vulkan RHI: failed to load SPIR-V {}", path); return false; } + VkShaderModuleCreateInfo ci{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO }; + ci.codeSize = spv.size() * 4; ci.pCode = spv.data(); + return vkCreateShaderModule(m_device, &ci, nullptr, &out) == VK_SUCCESS; + } + + auto VulkanDevice::create_instance() -> bool + { + VkApplicationInfo app{ VK_STRUCTURE_TYPE_APPLICATION_INFO }; + app.pApplicationName = "Donut"; app.apiVersion = VK_API_VERSION_1_2; + + uint32_t glfwExtCount = 0; + const char** glfwExts = glfwGetRequiredInstanceExtensions(&glfwExtCount); + if (!glfwExts) { DONUT_ERROR("Vulkan RHI: GLFW reports no surface support"); return false; } + std::vector exts(glfwExts, glfwExts + glfwExtCount); + exts.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); + exts.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME); + + std::vector layers; + uint32_t layer_count = 0; vkEnumerateInstanceLayerProperties(&layer_count, nullptr); + std::vector avail(layer_count); + vkEnumerateInstanceLayerProperties(&layer_count, avail.data()); + for (const auto& l : avail) + if (std::strcmp(l.layerName, "VK_LAYER_KHRONOS_validation") == 0) + layers.push_back("VK_LAYER_KHRONOS_validation"); + + VkInstanceCreateInfo ici{ VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO }; + ici.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + ici.pApplicationInfo = &app; + ici.enabledExtensionCount = (uint32_t)exts.size(); ici.ppEnabledExtensionNames = exts.data(); + ici.enabledLayerCount = (uint32_t)layers.size(); ici.ppEnabledLayerNames = layers.data(); + VkResult r = vkCreateInstance(&ici, nullptr, &m_instance); + if (r != VK_SUCCESS && !layers.empty()) + { + DONUT_WARN("Vulkan RHI: validation layer unavailable, continuing without it"); + ici.enabledLayerCount = 0; ici.ppEnabledLayerNames = nullptr; + r = vkCreateInstance(&ici, nullptr, &m_instance); + } + if (r != VK_SUCCESS) { DONUT_ERROR("Vulkan RHI: vkCreateInstance failed ({})", (int)r); return false; } + VKD_CHECK(glfwCreateWindowSurface(m_instance, m_window, nullptr, &m_surface)); + DONUT_INFO("Vulkan RHI: instance + surface created (validation {})", layers.empty() ? "off" : "on"); + return true; + } + + auto VulkanDevice::pick_physical_and_device() -> bool + { + uint32_t count = 0; vkEnumeratePhysicalDevices(m_instance, &count, nullptr); + if (count == 0) { DONUT_ERROR("Vulkan RHI: no physical devices"); return false; } + std::vector devices(count); + vkEnumeratePhysicalDevices(m_instance, &count, devices.data()); + m_physical = devices[0]; + + uint32_t q = 0; vkGetPhysicalDeviceQueueFamilyProperties(m_physical, &q, nullptr); + std::vector qfams(q); + vkGetPhysicalDeviceQueueFamilyProperties(m_physical, &q, qfams.data()); + bool fg = false, fp = false; + for (uint32_t i = 0; i < q; ++i) + { + if (!fg && (qfams[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) { m_graphics_family = i; fg = true; } + VkBool32 present = VK_FALSE; vkGetPhysicalDeviceSurfaceSupportKHR(m_physical, i, m_surface, &present); + if (!fp && present) { m_present_family = i; fp = true; } + } + if (!fg || !fp) { DONUT_ERROR("Vulkan RHI: no graphics/present queue"); return false; } + + std::vector dev_exts = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; + uint32_t dec = 0; vkEnumerateDeviceExtensionProperties(m_physical, nullptr, &dec, nullptr); + std::vector dep(dec); + vkEnumerateDeviceExtensionProperties(m_physical, nullptr, &dec, dep.data()); + for (const auto& e : dep) + if (std::strcmp(e.extensionName, "VK_KHR_portability_subset") == 0) + dev_exts.push_back("VK_KHR_portability_subset"); + + float priority = 1.0f; + std::vector qcis; + uint32_t families[2] = { m_graphics_family, m_present_family }; + for (uint32_t i = 0; i < (m_graphics_family == m_present_family ? 1u : 2u); ++i) + { + VkDeviceQueueCreateInfo qci{ VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO }; + qci.queueFamilyIndex = families[i]; qci.queueCount = 1; qci.pQueuePriorities = &priority; + qcis.push_back(qci); + } + VkDeviceCreateInfo dci{ VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO }; + dci.queueCreateInfoCount = (uint32_t)qcis.size(); dci.pQueueCreateInfos = qcis.data(); + dci.enabledExtensionCount = (uint32_t)dev_exts.size(); dci.ppEnabledExtensionNames = dev_exts.data(); + VKD_CHECK(vkCreateDevice(m_physical, &dci, nullptr, &m_device)); + vkGetDeviceQueue(m_device, m_graphics_family, 0, &m_graphics_queue); + vkGetDeviceQueue(m_device, m_present_family, 0, &m_present_queue); + + VkPhysicalDeviceProperties props{}; vkGetPhysicalDeviceProperties(m_physical, &props); + vkGetPhysicalDeviceMemoryProperties(m_physical, &m_mem_props); + m_gpu_name = props.deviceName; + DONUT_INFO("Vulkan RHI device: {}", m_gpu_name); + return true; + } + + auto VulkanDevice::create_swapchain() -> bool + { + VkSurfaceCapabilitiesKHR caps{}; + vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physical, m_surface, &caps); + uint32_t fc = 0; vkGetPhysicalDeviceSurfaceFormatsKHR(m_physical, m_surface, &fc, nullptr); + std::vector formats(fc); + vkGetPhysicalDeviceSurfaceFormatsKHR(m_physical, m_surface, &fc, formats.data()); + VkSurfaceFormatKHR chosen = formats[0]; + for (const auto& f : formats) + if (f.format == VK_FORMAT_B8G8R8A8_UNORM && f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) chosen = f; + m_swapchain_format = chosen.format; + + if (caps.currentExtent.width != UINT32_MAX) m_extent = caps.currentExtent; + else { + m_extent.width = std::clamp((uint32_t)m_width, caps.minImageExtent.width, caps.maxImageExtent.width); + m_extent.height = std::clamp((uint32_t)m_height, caps.minImageExtent.height, caps.maxImageExtent.height); + } + uint32_t image_count = caps.minImageCount + 1; + if (caps.maxImageCount > 0 && image_count > caps.maxImageCount) image_count = caps.maxImageCount; + + VkSwapchainCreateInfoKHR sci{ VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR }; + sci.surface = m_surface; sci.minImageCount = image_count; + sci.imageFormat = chosen.format; sci.imageColorSpace = chosen.colorSpace; + sci.imageExtent = m_extent; sci.imageArrayLayers = 1; + sci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + sci.preTransform = caps.currentTransform; sci.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + sci.presentMode = VK_PRESENT_MODE_FIFO_KHR; sci.clipped = VK_TRUE; + uint32_t fam[2] = { m_graphics_family, m_present_family }; + if (m_graphics_family != m_present_family) + { sci.imageSharingMode = VK_SHARING_MODE_CONCURRENT; sci.queueFamilyIndexCount = 2; sci.pQueueFamilyIndices = fam; } + else sci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + VKD_CHECK(vkCreateSwapchainKHR(m_device, &sci, nullptr, &m_swapchain)); + uint32_t n = 0; vkGetSwapchainImagesKHR(m_device, m_swapchain, &n, nullptr); + m_images.resize(n); vkGetSwapchainImagesKHR(m_device, m_swapchain, &n, m_images.data()); + return true; + } + + auto VulkanDevice::create_image_views() -> bool + { + m_image_views.resize(m_images.size()); + for (size_t i = 0; i < m_images.size(); ++i) + { + VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + vci.image = m_images[i]; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = m_swapchain_format; + vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + VKD_CHECK(vkCreateImageView(m_device, &vci, nullptr, &m_image_views[i])); + } + return true; + } + + // Swapchain pass: colour + depth. Scene geometry uses the depth; the + // black-hole present + ImGui simply don't test it. + auto VulkanDevice::create_swapchain_render_pass() -> bool + { + VkAttachmentDescription atts[2]{}; + atts[0].format = m_swapchain_format; atts[0].samples = VK_SAMPLE_COUNT_1_BIT; + atts[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + atts[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + atts[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + atts[1].format = VK_FORMAT_D32_SFLOAT; atts[1].samples = VK_SAMPLE_COUNT_1_BIT; + atts[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + atts[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + atts[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; + VkAttachmentReference depth_ref{ 1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL }; + VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref; subpass.pDepthStencilAttachment = &depth_ref; + VkSubpassDependency dep{}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; + rpci.attachmentCount = 2; rpci.pAttachments = atts; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; + rpci.dependencyCount = 1; rpci.pDependencies = &dep; + VKD_CHECK(vkCreateRenderPass(m_device, &rpci, nullptr, &m_swapchain_rp)); + return true; + } + + // Off-screen colour pass (RGBA8), leaving the image SHADER_READ_ONLY so the + // present pass can sample it. Shared by every render target. + auto VulkanDevice::create_offscreen_render_pass() -> bool + { + VkAttachmentDescription color{}; + color.format = VK_FORMAT_R8G8B8A8_UNORM; color.samples = VK_SAMPLE_COUNT_1_BIT; + color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; + VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &ref; + VkSubpassDependency deps[2]{}; + deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; deps[0].dstSubpass = 0; + deps[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + deps[1].srcSubpass = 0; deps[1].dstSubpass = VK_SUBPASS_EXTERNAL; + deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; + rpci.attachmentCount = 1; rpci.pAttachments = &color; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; + rpci.dependencyCount = 2; rpci.pDependencies = deps; + VKD_CHECK(vkCreateRenderPass(m_device, &rpci, nullptr, &m_offscreen_rp)); + return true; + } + + auto VulkanDevice::create_depth_and_framebuffers() -> bool + { + VkImageCreateInfo dici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + dici.imageType = VK_IMAGE_TYPE_2D; dici.format = VK_FORMAT_D32_SFLOAT; + dici.extent = { m_extent.width, m_extent.height, 1 }; + dici.mipLevels = 1; dici.arrayLayers = 1; dici.samples = VK_SAMPLE_COUNT_1_BIT; + dici.tiling = VK_IMAGE_TILING_OPTIMAL; dici.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + VKD_CHECK(vkCreateImage(m_device, &dici, nullptr, &m_depth_image)); + VkMemoryRequirements dreq{}; vkGetImageMemoryRequirements(m_device, m_depth_image, &dreq); + VkMemoryAllocateInfo dai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + dai.allocationSize = dreq.size; dai.memoryTypeIndex = find_memory_type(dreq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + VKD_CHECK(vkAllocateMemory(m_device, &dai, nullptr, &m_depth_mem)); + VKD_CHECK(vkBindImageMemory(m_device, m_depth_image, m_depth_mem, 0)); + VkImageViewCreateInfo dvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + dvci.image = m_depth_image; dvci.viewType = VK_IMAGE_VIEW_TYPE_2D; dvci.format = VK_FORMAT_D32_SFLOAT; + dvci.subresourceRange = { VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1 }; + VKD_CHECK(vkCreateImageView(m_device, &dvci, nullptr, &m_depth_view)); + + m_framebuffers.resize(m_image_views.size()); + for (size_t i = 0; i < m_image_views.size(); ++i) + { + VkImageView att[2] = { m_image_views[i], m_depth_view }; + VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; + fbci.renderPass = m_swapchain_rp; fbci.attachmentCount = 2; fbci.pAttachments = att; + fbci.width = m_extent.width; fbci.height = m_extent.height; fbci.layers = 1; + VKD_CHECK(vkCreateFramebuffer(m_device, &fbci, nullptr, &m_framebuffers[i])); + } + return true; + } + + auto VulkanDevice::create_command_and_sync() -> bool + { + VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; + pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pci.queueFamilyIndex = m_graphics_family; + VKD_CHECK(vkCreateCommandPool(m_device, &pci, nullptr, &m_command_pool)); + m_command_buffers.resize(MAX_FRAMES_IN_FLIGHT); + VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; + cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = MAX_FRAMES_IN_FLIGHT; + VKD_CHECK(vkAllocateCommandBuffers(m_device, &cbai, m_command_buffers.data())); + + m_image_available.resize(MAX_FRAMES_IN_FLIGHT); + m_in_flight.resize(MAX_FRAMES_IN_FLIGHT); + m_render_finished.resize(m_images.size()); + m_images_in_flight.assign(m_images.size(), VK_NULL_HANDLE); + VkSemaphoreCreateInfo sci{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO }; + VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; fci.flags = VK_FENCE_CREATE_SIGNALED_BIT; + for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) + { VKD_CHECK(vkCreateSemaphore(m_device, &sci, nullptr, &m_image_available[i])); VKD_CHECK(vkCreateFence(m_device, &fci, nullptr, &m_in_flight[i])); } + for (size_t i = 0; i < m_images.size(); ++i) + VKD_CHECK(vkCreateSemaphore(m_device, &sci, nullptr, &m_render_finished[i])); + + m_frame_pools.resize(MAX_FRAMES_IN_FLIGHT); + VkDescriptorPoolSize sizes[2] = { + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 512 }, + { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 256 }, + }; + for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) + { + VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; + dpci.maxSets = 256; dpci.poolSizeCount = 2; dpci.pPoolSizes = sizes; + VKD_CHECK(vkCreateDescriptorPool(m_device, &dpci, nullptr, &m_frame_pools[i])); + } + return true; + } + + auto VulkanDevice::init(void* glfwWindow, int width, int height) -> bool + { + m_window = (GLFWwindow*)glfwWindow; m_width = width; m_height = height; + if (!create_instance()) return false; + if (!pick_physical_and_device()) return false; + if (!create_swapchain()) return false; + if (!create_image_views()) return false; + if (!create_swapchain_render_pass()) return false; + if (!create_offscreen_render_pass()) return false; + if (!create_depth_and_framebuffers())return false; + if (!create_command_and_sync()) return false; + DONUT_INFO("Vulkan RHI device ready: {} swapchain images, {}x{}", (int)m_images.size(), m_extent.width, m_extent.height); + return true; + } + + auto VulkanDevice::cleanup_swapchain() -> void + { + for (auto fb : m_framebuffers) vkDestroyFramebuffer(m_device, fb, nullptr); + m_framebuffers.clear(); + if (m_depth_view) { vkDestroyImageView(m_device, m_depth_view, nullptr); m_depth_view = VK_NULL_HANDLE; } + if (m_depth_image) { vkDestroyImage(m_device, m_depth_image, nullptr); m_depth_image = VK_NULL_HANDLE; } + if (m_depth_mem) { vkFreeMemory(m_device, m_depth_mem, nullptr); m_depth_mem = VK_NULL_HANDLE; } + for (auto iv : m_image_views) vkDestroyImageView(m_device, iv, nullptr); + m_image_views.clear(); + if (m_swapchain) { vkDestroySwapchainKHR(m_device, m_swapchain, nullptr); m_swapchain = VK_NULL_HANDLE; } + } + + auto VulkanDevice::recreate_swapchain() -> bool + { + int w = 0, h = 0; glfwGetFramebufferSize(m_window, &w, &h); + while (w == 0 || h == 0) { glfwGetFramebufferSize(m_window, &w, &h); glfwWaitEvents(); } + m_width = w; m_height = h; + vkDeviceWaitIdle(m_device); + cleanup_swapchain(); + if (!create_swapchain()) return false; + if (!create_image_views()) return false; + if (!create_depth_and_framebuffers())return false; + m_images_in_flight.assign(m_images.size(), VK_NULL_HANDLE); + return true; + } + + auto VulkanDevice::create_buffer(BufferType type, size_t size, const void* data) -> Ref + { + VkBufferUsageFlags usage = type == BufferType::Index ? VK_BUFFER_USAGE_INDEX_BUFFER_BIT + : type == BufferType::Uniform ? VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT + : VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; + VkBuffer buf = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; + create_buffer_raw(size, usage, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, buf, mem); + void* mapped = nullptr; vkMapMemory(m_device, mem, 0, size, 0, &mapped); + if (data && mapped) std::memcpy(mapped, data, size); + return create_ref(m_device, buf, mem, mapped, size); + } + + auto VulkanDevice::create_texture(int w, int h, Format format, Filter filter, const void* data) -> Ref + { + auto tex = create_ref(); tex->m_device = m_device; + VkFormat fmt = vk_format(format); + VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { (uint32_t)w, (uint32_t)h, 1 }; + ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT; + ici.tiling = VK_IMAGE_TILING_OPTIMAL; ici.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + vkCreateImage(m_device, &ici, nullptr, &tex->m_image); + VkMemoryRequirements req{}; vkGetImageMemoryRequirements(m_device, tex->m_image, &req); + VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + ai.allocationSize = req.size; ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + vkAllocateMemory(m_device, &ai, nullptr, &tex->m_mem); + vkBindImageMemory(m_device, tex->m_image, tex->m_mem, 0); + + size_t bpp = format == Format::RGBA16F ? 8 : 4; + VkDeviceSize sz = (VkDeviceSize)w * h * bpp; + VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_mem = VK_NULL_HANDLE; + create_buffer_raw(sz, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, staging, staging_mem); + void* mp = nullptr; vkMapMemory(m_device, staging_mem, 0, sz, 0, &mp); + if (data) std::memcpy(mp, data, sz); else std::memset(mp, 0, sz); + vkUnmapMemory(m_device, staging_mem); + + VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; + cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; + VkCommandBuffer cmd; vkAllocateCommandBuffers(m_device, &cbai, &cmd); + VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cmd, &bi); + VkImageMemoryBarrier b{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + b.image = tex->m_image; b.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + b.srcAccessMask = 0; b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b); + VkBufferImageCopy copy{}; copy.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; copy.imageExtent = { (uint32_t)w, (uint32_t)h, 1 }; + vkCmdCopyBufferToImage(cmd, staging, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + VkImageMemoryBarrier r = b; r.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; r.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + r.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; r.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &r); + vkEndCommandBuffer(cmd); + VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; + vkQueueSubmit(m_graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(m_graphics_queue); + vkFreeCommandBuffers(m_device, m_command_pool, 1, &cmd); + vkDestroyBuffer(m_device, staging, nullptr); vkFreeMemory(m_device, staging_mem, nullptr); + + VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + vci.image = tex->m_image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt; + vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + vkCreateImageView(m_device, &vci, nullptr, &tex->m_view); + VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; + smci.magFilter = vk_filter(filter); smci.minFilter = vk_filter(filter); + smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + vkCreateSampler(m_device, &smci, nullptr, &tex->m_sampler); + return tex; + } + + auto VulkanDevice::create_render_target(int w, int h, Format color, bool /*with_depth*/, Filter filter, int /*mips*/) -> Ref + { + auto rt = create_ref(); + rt->m_device = m_device; rt->m_w = w; rt->m_h = h; + VkFormat fmt = vk_format(color); + VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { (uint32_t)w, (uint32_t)h, 1 }; + ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT; + ici.tiling = VK_IMAGE_TILING_OPTIMAL; ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + vkCreateImage(m_device, &ici, nullptr, &rt->m_image); + VkMemoryRequirements req{}; vkGetImageMemoryRequirements(m_device, rt->m_image, &req); + VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + ai.allocationSize = req.size; ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + vkAllocateMemory(m_device, &ai, nullptr, &rt->m_mem); + vkBindImageMemory(m_device, rt->m_image, rt->m_mem, 0); + VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + vci.image = rt->m_image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt; + vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + vkCreateImageView(m_device, &vci, nullptr, &rt->m_view); + VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; + smci.magFilter = vk_filter(filter); smci.minFilter = vk_filter(filter); + smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + vkCreateSampler(m_device, &smci, nullptr, &rt->m_sampler); + VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; + fbci.renderPass = m_offscreen_rp; fbci.attachmentCount = 1; fbci.pAttachments = &rt->m_view; + fbci.width = w; fbci.height = h; fbci.layers = 1; + vkCreateFramebuffer(m_device, &fbci, nullptr, &rt->m_fb); + + rt->m_color.m_device = m_device; rt->m_color.m_view = rt->m_view; rt->m_color.m_sampler = rt->m_sampler; rt->m_color.m_owns = false; + return rt; + } + + // Builds an environment cubemap from an equirectangular HDRI: render the 6 + // faces with the EquirectToCubemap pipeline, then a full mip chain by + // linear down-blits (so divergence-based LOD reads a blurred sky). Returns + // a Texture owning the cube image/view/sampler. + auto VulkanDevice::create_cubemap_from_hdri(const std::string& path) -> Ref + { + auto tex = create_ref(); tex->m_device = m_device; + const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + const uint32_t FACE = 1024; + const VkFormat cube_fmt = VK_FORMAT_R16G16B16A16_SFLOAT; + uint32_t CUBE_MIPS = 1; for (uint32_t s = FACE; s > 1; s >>= 1) ++CUBE_MIPS; + + VkImageCreateInfo cci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + cci.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; + cci.imageType = VK_IMAGE_TYPE_2D; cci.format = cube_fmt; cci.extent = { FACE, FACE, 1 }; + cci.mipLevels = CUBE_MIPS; cci.arrayLayers = 6; cci.samples = VK_SAMPLE_COUNT_1_BIT; + cci.tiling = VK_IMAGE_TILING_OPTIMAL; + cci.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT + | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + vkCreateImage(m_device, &cci, nullptr, &tex->m_image); + VkMemoryRequirements creq{}; vkGetImageMemoryRequirements(m_device, tex->m_image, &creq); + VkMemoryAllocateInfo cai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + cai.allocationSize = creq.size; cai.memoryTypeIndex = find_memory_type(creq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + vkAllocateMemory(m_device, &cai, nullptr, &tex->m_mem); + vkBindImageMemory(m_device, tex->m_image, tex->m_mem, 0); + VkImageViewCreateInfo cvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + cvci.image = tex->m_image; cvci.viewType = VK_IMAGE_VIEW_TYPE_CUBE; cvci.format = cube_fmt; + cvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; + vkCreateImageView(m_device, &cvci, nullptr, &tex->m_view); + VkSamplerCreateInfo csm{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; + csm.magFilter = VK_FILTER_LINEAR; csm.minFilter = VK_FILTER_LINEAR; + csm.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; csm.minLod = 0.0f; csm.maxLod = (float)CUBE_MIPS; + csm.addressModeU = csm.addressModeV = csm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + vkCreateSampler(m_device, &csm, nullptr, &tex->m_sampler); + + int w = 0, h = 0, ch = 0; + float* pixels = stbi_loadf(path.c_str(), &w, &h, &ch, 4); + if (!pixels) + { + DONUT_WARN("Vulkan RHI: HDRI '{}' could not be loaded; using a dark background", path); + VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; + cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; + VkCommandBuffer cmd; vkAllocateCommandBuffers(m_device, &cbai, &cmd); + VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cmd, &bi); + VkImageMemoryBarrier tb{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + tb.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; tb.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + tb.image = tex->m_image; tb.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; + tb.srcAccessMask = 0; tb.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &tb); + VkClearColorValue dark{}; dark.float32[0] = 0.02f; dark.float32[1] = 0.02f; dark.float32[2] = 0.05f; dark.float32[3] = 1.0f; + VkImageSubresourceRange rng{ VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; + vkCmdClearColorImage(cmd, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &dark, 1, &rng); + VkImageMemoryBarrier rb = tb; rb.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; rb.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + rb.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; rb.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &rb); + vkEndCommandBuffer(cmd); + VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; + vkQueueSubmit(m_graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(m_graphics_queue); + vkFreeCommandBuffers(m_device, m_command_pool, 1, &cmd); + return tex; + } + + const VkFormat eq_fmt = VK_FORMAT_R16G16B16A16_SFLOAT; + size_t texel_count = (size_t)w * h * 4; + VkDeviceSize eq_size = (VkDeviceSize)texel_count * sizeof(uint16_t); + VkBuffer eq_staging; VkDeviceMemory eq_staging_mem; + create_buffer_raw(eq_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, host_vis, eq_staging, eq_staging_mem); + void* mp = nullptr; vkMapMemory(m_device, eq_staging_mem, 0, eq_size, 0, &mp); + uint16_t* dst = (uint16_t*)mp; + for (size_t i = 0; i < texel_count; ++i) { __fp16 hf = (__fp16)pixels[i]; std::memcpy(&dst[i], &hf, sizeof(uint16_t)); } + vkUnmapMemory(m_device, eq_staging_mem); + stbi_image_free(pixels); + + VkImage eq_image; VkDeviceMemory eq_mem; + VkImageCreateInfo eci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + eci.imageType = VK_IMAGE_TYPE_2D; eci.format = eq_fmt; eci.extent = { (uint32_t)w, (uint32_t)h, 1 }; + eci.mipLevels = 1; eci.arrayLayers = 1; eci.samples = VK_SAMPLE_COUNT_1_BIT; + eci.tiling = VK_IMAGE_TILING_OPTIMAL; eci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + vkCreateImage(m_device, &eci, nullptr, &eq_image); + VkMemoryRequirements ereq{}; vkGetImageMemoryRequirements(m_device, eq_image, &ereq); + VkMemoryAllocateInfo eai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + eai.allocationSize = ereq.size; eai.memoryTypeIndex = find_memory_type(ereq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + vkAllocateMemory(m_device, &eai, nullptr, &eq_mem); + vkBindImageMemory(m_device, eq_image, eq_mem, 0); + VkImageView eq_view; + VkImageViewCreateInfo evci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + evci.image = eq_image; evci.viewType = VK_IMAGE_VIEW_TYPE_2D; evci.format = eq_fmt; + evci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + vkCreateImageView(m_device, &evci, nullptr, &eq_view); + VkSampler eq_sampler; + VkSamplerCreateInfo esm{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; + esm.magFilter = VK_FILTER_LINEAR; esm.minFilter = VK_FILTER_LINEAR; + esm.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; + esm.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + esm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + vkCreateSampler(m_device, &esm, nullptr, &eq_sampler); + + VkImageView face_views[6]; + for (uint32_t i = 0; i < 6; ++i) + { + VkImageViewCreateInfo fvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + fvci.image = tex->m_image; fvci.viewType = VK_IMAGE_VIEW_TYPE_2D; fvci.format = cube_fmt; + fvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, i, 1 }; + vkCreateImageView(m_device, &fvci, nullptr, &face_views[i]); + } + + VkAttachmentDescription color{}; + color.format = cube_fmt; color.samples = VK_SAMPLE_COUNT_1_BIT; + color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; + VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &ref; + VkSubpassDependency dep{}; dep.srcSubpass = 0; dep.dstSubpass = VK_SUBPASS_EXTERNAL; + dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + dep.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; dep.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + VkRenderPass rp; + VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; + rpci.attachmentCount = 1; rpci.pAttachments = &color; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; rpci.dependencyCount = 1; rpci.pDependencies = &dep; + vkCreateRenderPass(m_device, &rpci, nullptr, &rp); + VkFramebuffer face_fb[6]; + for (uint32_t i = 0; i < 6; ++i) + { + VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; + fbci.renderPass = rp; fbci.attachmentCount = 1; fbci.pAttachments = &face_views[i]; fbci.width = FACE; fbci.height = FACE; fbci.layers = 1; + vkCreateFramebuffer(m_device, &fbci, nullptr, &face_fb[i]); + } + + VkDescriptorSetLayoutBinding binds[2]{}; + binds[0].binding = 0; binds[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[0].descriptorCount = 1; binds[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + binds[1].binding = 1; binds[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[1].descriptorCount = 1; binds[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + VkDescriptorSetLayout set_layout; + VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; dslci.bindingCount = 2; dslci.pBindings = binds; + vkCreateDescriptorSetLayout(m_device, &dslci, nullptr, &set_layout); + VkDescriptorPoolSize psizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 6 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 6 } }; + VkDescriptorPool pool; + VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; dpci.maxSets = 6; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes; + vkCreateDescriptorPool(m_device, &dpci, nullptr, &pool); + + VkShaderModule vmod, fmod; + create_shader_module("assets/shaders/generated/equirect_to_cubemap.vertexMain.spv", vmod); + create_shader_module("assets/shaders/generated/equirect_to_cubemap.fragmentMain.spv", fmod); + VkPipelineLayout playout; + VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; plci.setLayoutCount = 1; plci.pSetLayouts = &set_layout; + vkCreatePipelineLayout(m_device, &plci, nullptr, &playout); + VkPipelineShaderStageCreateInfo stages[2]{}; + stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main"; + stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main"; + VkVertexInputBindingDescription vib{ 0, 12, VK_VERTEX_INPUT_RATE_VERTEX }; + VkVertexInputAttributeDescription via{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }; + VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; + vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; vin.vertexAttributeDescriptionCount = 1; vin.pVertexAttributeDescriptions = &via; + VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + VkViewport vp{ 0, 0, (float)FACE, (float)FACE, 0, 1 }; VkRect2D sc{ { 0, 0 }, { FACE, FACE } }; + VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.pViewports = &vp; vps.scissorCount = 1; vps.pScissors = ≻ + VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineColorBlendAttachmentState cba{}; cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba; + VkPipeline pipeline; + VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; + gpci.stageCount = 2; gpci.pStages = stages; gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps; + gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; gpci.layout = playout; gpci.renderPass = rp; gpci.subpass = 0; + vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline); + vkDestroyShaderModule(m_device, vmod, nullptr); vkDestroyShaderModule(m_device, fmod, nullptr); + + float cube_verts[] = { + -1,1,-1, -1,-1,-1, 1,-1,-1, 1,-1,-1, 1,1,-1, -1,1,-1, + -1,-1,1, -1,-1,-1, -1,1,-1, -1,1,-1, -1,1,1, -1,-1,1, + 1,-1,-1, 1,-1,1, 1,1,1, 1,1,1, 1,1,-1, 1,-1,-1, + -1,-1,1, -1,1,1, 1,1,1, 1,1,1, 1,-1,1, -1,-1,1, + -1,1,-1, 1,1,-1, 1,1,1, 1,1,1, -1,1,1, -1,1,-1, + -1,-1,-1, -1,-1,1, 1,-1,-1, 1,-1,-1, -1,-1,1, 1,-1,1, + }; + VkBuffer cube_vb; VkDeviceMemory cube_vb_mem; + create_buffer_raw(sizeof(cube_verts), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, cube_vb, cube_vb_mem); + vkMapMemory(m_device, cube_vb_mem, 0, sizeof(cube_verts), 0, &mp); std::memcpy(mp, cube_verts, sizeof(cube_verts)); vkUnmapMemory(m_device, cube_vb_mem); + + glm::mat4 proj = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f); + proj[1][1] *= -1.0f; + glm::mat4 views[6] = { + glm::lookAt(glm::vec3(0), glm::vec3( 1, 0, 0), glm::vec3(0, -1, 0)), + glm::lookAt(glm::vec3(0), glm::vec3(-1, 0, 0), glm::vec3(0, -1, 0)), + glm::lookAt(glm::vec3(0), glm::vec3( 0, 1, 0), glm::vec3(0, 0, 1)), + glm::lookAt(glm::vec3(0), glm::vec3( 0, -1, 0), glm::vec3(0, 0, -1)), + glm::lookAt(glm::vec3(0), glm::vec3( 0, 0, 1), glm::vec3(0, -1, 0)), + glm::lookAt(glm::vec3(0), glm::vec3( 0, 0, -1), glm::vec3(0, -1, 0)), + }; + VkBuffer ubo[6]; VkDeviceMemory ubo_mem[6]; VkDescriptorSet sets[6]; + for (uint32_t i = 0; i < 6; ++i) + { + create_buffer_raw(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, ubo[i], ubo_mem[i]); + glm::mat4 mats[2] = { glm::transpose(proj), glm::transpose(views[i]) }; + vkMapMemory(m_device, ubo_mem[i], 0, 128, 0, &mp); std::memcpy(mp, mats, 128); vkUnmapMemory(m_device, ubo_mem[i]); + VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; dsai.descriptorPool = pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &set_layout; + vkAllocateDescriptorSets(m_device, &dsai, &sets[i]); + VkDescriptorBufferInfo buf_info{ ubo[i], 0, VK_WHOLE_SIZE }; + VkDescriptorImageInfo img_info{ eq_sampler, eq_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; + VkWriteDescriptorSet ws[2]{}; + ws[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; ws[0].dstSet = sets[i]; ws[0].dstBinding = 0; ws[0].descriptorCount = 1; ws[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; ws[0].pBufferInfo = &buf_info; + ws[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; ws[1].dstSet = sets[i]; ws[1].dstBinding = 1; ws[1].descriptorCount = 1; ws[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; ws[1].pImageInfo = &img_info; + vkUpdateDescriptorSets(m_device, 2, ws, 0, nullptr); + } + + VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; + cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; + VkCommandBuffer cmd; vkAllocateCommandBuffers(m_device, &cbai, &cmd); + VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cmd, &bi); + VkImageMemoryBarrier to_dst{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + to_dst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; to_dst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + to_dst.image = eq_image; to_dst.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + to_dst.srcAccessMask = 0; to_dst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_dst); + VkBufferImageCopy copy{}; copy.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; copy.imageExtent = { (uint32_t)w, (uint32_t)h, 1 }; + vkCmdCopyBufferToImage(cmd, eq_staging, eq_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + VkImageMemoryBarrier to_read = to_dst; to_read.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; to_read.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + to_read.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; to_read.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_read); + + VkClearValue clear{}; clear.color = { { 0, 0, 0, 1 } }; + for (uint32_t i = 0; i < 6; ++i) + { + VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; + rpbi.renderPass = rp; rpbi.framebuffer = face_fb[i]; rpbi.renderArea = { { 0, 0 }, { FACE, FACE } }; rpbi.clearValueCount = 1; rpbi.pClearValues = &clear; + vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, playout, 0, 1, &sets[i], 0, nullptr); + VkDeviceSize off = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &cube_vb, &off); + vkCmdDraw(cmd, 36, 1, 0, 0); + vkCmdEndRenderPass(cmd); + } + + { + VkImageMemoryBarrier src0{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + src0.image = tex->m_image; src0.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 }; + src0.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; src0.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + src0.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; src0.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &src0); + int32_t mipW = (int32_t)FACE, mipH = (int32_t)FACE; + for (uint32_t m = 1; m < CUBE_MIPS; ++m) + { + int32_t nW = mipW > 1 ? mipW / 2 : 1, nH = mipH > 1 ? mipH / 2 : 1; + VkImageMemoryBarrier bd{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + bd.image = tex->m_image; bd.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, m, 1, 0, 6 }; + bd.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; bd.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + bd.srcAccessMask = 0; bd.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &bd); + VkImageBlit blit{}; + blit.srcOffsets[1] = { mipW, mipH, 1 }; blit.srcSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, m - 1, 0, 6 }; + blit.dstOffsets[1] = { nW, nH, 1 }; blit.dstSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, m, 0, 6 }; + vkCmdBlitImage(cmd, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); + VkImageMemoryBarrier bs{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + bs.image = tex->m_image; bs.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, m, 1, 0, 6 }; + bs.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; bs.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + bs.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; bs.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &bs); + mipW = nW; mipH = nH; + } + VkImageMemoryBarrier fin{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + fin.image = tex->m_image; fin.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; + fin.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; fin.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + fin.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; fin.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &fin); + } + + vkEndCommandBuffer(cmd); + VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; + vkQueueSubmit(m_graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(m_graphics_queue); + + vkFreeCommandBuffers(m_device, m_command_pool, 1, &cmd); + for (uint32_t i = 0; i < 6; ++i) { vkDestroyBuffer(m_device, ubo[i], nullptr); vkFreeMemory(m_device, ubo_mem[i], nullptr); vkDestroyFramebuffer(m_device, face_fb[i], nullptr); vkDestroyImageView(m_device, face_views[i], nullptr); } + vkDestroyBuffer(m_device, cube_vb, nullptr); vkFreeMemory(m_device, cube_vb_mem, nullptr); + vkDestroyPipeline(m_device, pipeline, nullptr); vkDestroyPipelineLayout(m_device, playout, nullptr); + vkDestroyDescriptorPool(m_device, pool, nullptr); vkDestroyDescriptorSetLayout(m_device, set_layout, nullptr); + vkDestroyRenderPass(m_device, rp, nullptr); + vkDestroySampler(m_device, eq_sampler, nullptr); vkDestroyImageView(m_device, eq_view, nullptr); + vkDestroyImage(m_device, eq_image, nullptr); vkFreeMemory(m_device, eq_mem, nullptr); + vkDestroyBuffer(m_device, eq_staging, nullptr); vkFreeMemory(m_device, eq_staging_mem, nullptr); + DONUT_INFO("Vulkan RHI: HDRI cubemap built from {} ({}x{} equirect -> {}^2 cube)", path, w, h, (int)FACE); + return tex; + } + + auto VulkanDevice::create_pipeline(const PipelineDesc& desc) -> Ref + { + auto p = create_ref(); p->m_device = m_device; p->m_resources = desc.resources; + + std::vector binds; + for (const auto& r : desc.resources) + { + VkDescriptorSetLayoutBinding b{}; + b.binding = r.binding; b.descriptorCount = 1; + b.descriptorType = r.kind == ResourceKind::UniformBuffer ? VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER : VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + b.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + binds.push_back(b); + } + VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; + dslci.bindingCount = (uint32_t)binds.size(); dslci.pBindings = binds.data(); + vkCreateDescriptorSetLayout(m_device, &dslci, nullptr, &p->m_set_layout); + VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; + plci.setLayoutCount = 1; plci.pSetLayouts = &p->m_set_layout; + vkCreatePipelineLayout(m_device, &plci, nullptr, &p->m_layout); + + VkShaderModule vmod = VK_NULL_HANDLE, fmod = VK_NULL_HANDLE; + if (!create_shader_module("assets/shaders/generated/" + desc.shader + ".vertexMain.spv", vmod) || + !create_shader_module("assets/shaders/generated/" + desc.shader + ".fragmentMain.spv", fmod)) + { DONUT_ERROR("Vulkan RHI: shader '{}' modules failed", desc.shader); return p; } + VkPipelineShaderStageCreateInfo stages[2]{}; + stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main"; + stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main"; + + VkVertexInputBindingDescription vib{ 0, desc.vertex_layout.stride, VK_VERTEX_INPUT_RATE_VERTEX }; + std::vector vias; + for (const auto& a : desc.vertex_layout.attributes) + vias.push_back({ a.location, 0, vk_attr_format(a.components), a.offset }); + VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; + vin.vertexBindingDescriptionCount = desc.vertex_layout.stride ? 1 : 0; vin.pVertexBindingDescriptions = &vib; + vin.vertexAttributeDescriptionCount = (uint32_t)vias.size(); vin.pVertexAttributeDescriptions = vias.data(); + + VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = vk_topology(desc.topology); + VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.scissorCount = 1; + VkDynamicState dyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; + VkPipelineDynamicStateCreateInfo dsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; dsci.dynamicStateCount = 2; dsci.pDynamicStates = dyn; + VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; + rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = vk_cull(desc.cull); rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineDepthStencilStateCreateInfo ds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO }; + ds.depthTestEnable = desc.depth_test ? VK_TRUE : VK_FALSE; ds.depthWriteEnable = desc.depth_write ? VK_TRUE : VK_FALSE; ds.depthCompareOp = vk_compare(desc.depth_op); + VkPipelineColorBlendAttachmentState cba{}; + cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + if (desc.blend == BlendMode::AlphaBlend) + { + cba.blendEnable = VK_TRUE; + cba.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; cba.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; cba.colorBlendOp = VK_BLEND_OP_ADD; + cba.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; cba.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; cba.alphaBlendOp = VK_BLEND_OP_ADD; + } + VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba; + + VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; + gpci.stageCount = 2; gpci.pStages = stages; + gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps; + gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; gpci.pDynamicState = &dsci; + if (desc.has_depth) gpci.pDepthStencilState = &ds; + gpci.layout = p->m_layout; + gpci.renderPass = desc.has_depth ? m_swapchain_rp : m_offscreen_rp; + gpci.subpass = 0; + VkResult pr = vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &gpci, nullptr, &p->m_pipeline); + vkDestroyShaderModule(m_device, vmod, nullptr); vkDestroyShaderModule(m_device, fmod, nullptr); + if (pr != VK_SUCCESS) DONUT_ERROR("Vulkan RHI: pipeline '{}' creation failed ({})", desc.shader, (int)pr); + return p; + } + + auto VulkanDevice::begin_frame(const glm::vec4&) -> CommandList* + { + if (m_device == VK_NULL_HANDLE) return nullptr; + vkWaitForFences(m_device, 1, &m_in_flight[m_current_frame], VK_TRUE, UINT64_MAX); + VkResult r = vkAcquireNextImageKHR(m_device, m_swapchain, UINT64_MAX, m_image_available[m_current_frame], VK_NULL_HANDLE, &m_image_index); + if (r == VK_ERROR_OUT_OF_DATE_KHR) { recreate_swapchain(); return nullptr; } + if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) { DONUT_ERROR("Vulkan RHI: acquire failed ({})", (int)r); return nullptr; } + if (m_images_in_flight[m_image_index] != VK_NULL_HANDLE) + vkWaitForFences(m_device, 1, &m_images_in_flight[m_image_index], VK_TRUE, UINT64_MAX); + m_images_in_flight[m_image_index] = m_in_flight[m_current_frame]; + if (m_geo_in_use != VK_NULL_HANDLE) + vkWaitForFences(m_device, 1, &m_geo_in_use, VK_TRUE, UINT64_MAX); + + vkResetDescriptorPool(m_device, m_frame_pools[m_current_frame], 0); + VkCommandBuffer cmd = m_command_buffers[m_current_frame]; + vkResetCommandBuffer(cmd, 0); + VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; + vkBeginCommandBuffer(cmd, &bi); + + m_cmds.m_device = m_device; m_cmds.m_cmd = cmd; + m_cmds.m_swapchain_rp = m_swapchain_rp; m_cmds.m_offscreen_rp = m_offscreen_rp; + m_cmds.m_swapchain_fb = m_framebuffers[m_image_index]; m_cmds.m_extent = m_extent; + m_cmds.m_frame_pool = m_frame_pools[m_current_frame]; m_cmds.m_pipe = nullptr; + return &m_cmds; + } + + auto VulkanDevice::end_frame() -> void + { + VkCommandBuffer cmd = m_command_buffers[m_current_frame]; + vkEndCommandBuffer(cmd); + VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; + submit.waitSemaphoreCount = 1; submit.pWaitSemaphores = &m_image_available[m_current_frame]; submit.pWaitDstStageMask = &wait_stage; + submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd; + submit.signalSemaphoreCount = 1; submit.pSignalSemaphores = &m_render_finished[m_image_index]; + vkResetFences(m_device, 1, &m_in_flight[m_current_frame]); + if (vkQueueSubmit(m_graphics_queue, 1, &submit, m_in_flight[m_current_frame]) != VK_SUCCESS) + { DONUT_ERROR("Vulkan RHI: queue submit failed"); return; } + m_geo_in_use = m_in_flight[m_current_frame]; + + VkPresentInfoKHR present{ VK_STRUCTURE_TYPE_PRESENT_INFO_KHR }; + present.waitSemaphoreCount = 1; present.pWaitSemaphores = &m_render_finished[m_image_index]; + present.swapchainCount = 1; present.pSwapchains = &m_swapchain; present.pImageIndices = &m_image_index; + VkResult r = vkQueuePresentKHR(m_present_queue, &present); + if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR || m_framebuffer_resized) + { m_framebuffer_resized = false; recreate_swapchain(); } + m_current_frame = (m_current_frame + 1) % MAX_FRAMES_IN_FLIGHT; + } + + auto VulkanDevice::init_imgui() -> void + { + VkDescriptorPoolSize pool_size{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 }; + VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; + dpci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; dpci.maxSets = 1000; + dpci.poolSizeCount = 1; dpci.pPoolSizes = &pool_size; + vkCreateDescriptorPool(m_device, &dpci, nullptr, &m_imgui_pool); + + IMGUI_CHECKVERSION(); ImGui::CreateContext(); + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable; + ImGui::StyleColorsDark(); + ImGui_ImplGlfw_InitForVulkan(m_window, true); + ImGui_ImplVulkan_InitInfo info{}; + info.ApiVersion = VK_API_VERSION_1_2; info.Instance = m_instance; info.PhysicalDevice = m_physical; + info.Device = m_device; info.QueueFamily = m_graphics_family; info.Queue = m_graphics_queue; + info.DescriptorPool = m_imgui_pool; info.RenderPass = m_swapchain_rp; + info.MinImageCount = 2; info.ImageCount = (uint32_t)m_images.size(); info.MSAASamples = VK_SAMPLE_COUNT_1_BIT; + if (!ImGui_ImplVulkan_Init(&info)) { DONUT_ERROR("Vulkan RHI: ImGui_ImplVulkan_Init failed"); return; } + m_imgui = true; + DONUT_INFO("Vulkan RHI: ImGui backend initialized"); + } + + auto VulkanDevice::imgui_new_frame() -> void + { + if (!m_imgui) return; + ImGui_ImplVulkan_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); + } + + auto VulkanDevice::imgui_render(CommandList& cmds) -> void + { + if (!m_imgui) return; + ImGui::Render(); + ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), static_cast(cmds).m_cmd); + } + + auto VulkanDevice::shutdown() -> void + { + if (m_device == VK_NULL_HANDLE) + { + if (m_instance && m_surface) { vkDestroySurfaceKHR(m_instance, m_surface, nullptr); m_surface = VK_NULL_HANDLE; } + if (m_instance) { vkDestroyInstance(m_instance, nullptr); m_instance = VK_NULL_HANDLE; } + return; + } + vkDeviceWaitIdle(m_device); + if (m_imgui) { ImGui_ImplVulkan_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); m_imgui = false; } + if (m_imgui_pool) vkDestroyDescriptorPool(m_device, m_imgui_pool, nullptr); + for (auto p : m_frame_pools) vkDestroyDescriptorPool(m_device, p, nullptr); + m_frame_pools.clear(); + for (auto s : m_render_finished) vkDestroySemaphore(m_device, s, nullptr); + for (auto s : m_image_available) vkDestroySemaphore(m_device, s, nullptr); + for (auto f : m_in_flight) vkDestroyFence(m_device, f, nullptr); + m_render_finished.clear(); m_image_available.clear(); m_in_flight.clear(); + if (m_command_pool) vkDestroyCommandPool(m_device, m_command_pool, nullptr); + if (m_offscreen_rp) vkDestroyRenderPass(m_device, m_offscreen_rp, nullptr); + if (m_swapchain_rp) vkDestroyRenderPass(m_device, m_swapchain_rp, nullptr); + cleanup_swapchain(); + vkDestroyDevice(m_device, nullptr); m_device = VK_NULL_HANDLE; + if (m_surface) vkDestroySurfaceKHR(m_instance, m_surface, nullptr); + if (m_instance) vkDestroyInstance(m_instance, nullptr); + m_surface = VK_NULL_HANDLE; m_instance = VK_NULL_HANDLE; + } + } + + auto create_vulkan_device() -> Scope { return create_scope(); } + + auto vulkan_prepare_glfw() -> void + { +#ifdef __APPLE__ + if (!getenv("VK_ICD_FILENAMES")) + setenv("VK_ICD_FILENAMES", "/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json", 0); + if (!getenv("VK_LAYER_PATH")) + setenv("VK_LAYER_PATH", "/opt/homebrew/share/vulkan/explicit_layer.d", 0); + if (!getenv("DYLD_LIBRARY_PATH")) + setenv("DYLD_LIBRARY_PATH", "/opt/homebrew/lib", 0); +#endif + glfwInitVulkanLoader(vkGetInstanceProcAddr); + } +} diff --git a/src/platform/vulkan/vulkan_device.h b/src/platform/vulkan/vulkan_device.h new file mode 100644 index 0000000..efc2433 --- /dev/null +++ b/src/platform/vulkan/vulkan_device.h @@ -0,0 +1,17 @@ +#pragma once + +#include "rendering/rhi.h" + +namespace Donut::RHI +{ + // Vulkan implementation of the RHI Device (MoltenVK on macOS). Owns the + // instance/device/swapchain and translates the RHI's baked pipelines + + // recorded command lists into native Vulkan; the geometry it draws is the + // shared SceneRenderer / BlackHoleRenderer, identical to every other backend. + auto create_vulkan_device() -> Scope; + + // Must run BEFORE glfwInit() when Vulkan is the selected API: points GLFW at + // the loader the app links against (its own dlopen fails on macOS/Homebrew) + // and configures the MoltenVK ICD / layer paths. + auto vulkan_prepare_glfw() -> void; +} diff --git a/src/platform/vulkan/vulkan_renderer.cpp b/src/platform/vulkan/vulkan_renderer.cpp deleted file mode 100644 index 8b0d0c0..0000000 --- a/src/platform/vulkan/vulkan_renderer.cpp +++ /dev/null @@ -1,2038 +0,0 @@ -#include "vulkan_renderer.h" -#include "core/log.h" -#include "core/camera.h" -#include "core/settings_manager.h" - -#define GLFW_INCLUDE_VULKAN -#include - -#include -#include -#include - -#include -#include -#include "stb_image.h" - -#include -#include -#include -#include -#include - -namespace Donut -{ - #define VK_CHECK(expr) \ - do { \ - VkResult _r = (expr); \ - if (_r != VK_SUCCESS) { \ - DONUT_ERROR("Vulkan: {} failed ({})", #expr, (int)_r); \ - return false; \ - } \ - } while (0) - - static constexpr int MAX_FRAMES_IN_FLIGHT = 2; - - auto vulkan_prepare_glfw() -> void - { -#ifdef __APPLE__ - if (!getenv("VK_ICD_FILENAMES")) - setenv("VK_ICD_FILENAMES", "/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json", 0); - if (!getenv("VK_LAYER_PATH")) - setenv("VK_LAYER_PATH", "/opt/homebrew/share/vulkan/explicit_layer.d", 0); - if (!getenv("DYLD_LIBRARY_PATH")) - setenv("DYLD_LIBRARY_PATH", "/opt/homebrew/lib", 0); -#endif - // GLFW dlopen's the Vulkan loader by bare name, which fails on - // mac_os/Homebrew; hand it the loader entry point we already link against. - glfwInitVulkanLoader(vkGetInstanceProcAddr); - } - - struct VulkanRenderer::Impl - { - GLFWwindow* window = nullptr; - int width = 0, height = 0; - bool framebuffer_resized = false; - - VkInstance instance = VK_NULL_HANDLE; - VkSurfaceKHR surface = VK_NULL_HANDLE; - VkPhysicalDevice physical = VK_NULL_HANDLE; - VkDevice device = VK_NULL_HANDLE; - uint32_t graphics_family = 0, present_family = 0; - VkQueue graphics_queue = VK_NULL_HANDLE, present_queue = VK_NULL_HANDLE; - - VkSwapchainKHR swapchain = VK_NULL_HANDLE; - VkFormat swapchain_format = VK_FORMAT_B8G8R8A8_UNORM; - VkExtent2D swapchain_extent{}; - std::vector images; - std::vector image_views; - VkRenderPass render_pass = VK_NULL_HANDLE; - std::vector framebuffers; - - VkCommandPool command_pool = VK_NULL_HANDLE; - std::vector command_buffers; // MAX_FRAMES_IN_FLIGHT - - std::vector image_available; // per frame in flight - std::vector render_finished; // per swapchain image - std::vector in_flight; // per frame in flight - std::vector images_in_flight; // per swapchain image - uint32_t current_frame = 0; - - VkDescriptorPool imgui_pool = VK_NULL_HANDLE; - bool imgui_init = false; - - VkPhysicalDeviceMemoryProperties mem_props{}; - - // The geodesic is rendered every frame into an offscreen image, then - // upscaled onto the swapchain by the present pass. Progressive resolution: - // low-res while the camera moves (keeps dragging responsive and each draw - // well under the Metal GPU watchdog) and high-res once it settles (resolves - // the photon ring cleanly). Both are 16:9 so the camera aspect is shared. - static constexpr uint32_t GEO_LO_W = 480, GEO_LO_H = 270; - static constexpr uint32_t GEO_HI_W = 960, GEO_HI_H = 540; - struct GeoTarget - { - uint32_t w = 0, h = 0; - VkImage image = VK_NULL_HANDLE; - VkDeviceMemory mem = VK_NULL_HANDLE; - VkImageView view = VK_NULL_HANDLE; - VkFramebuffer fb = VK_NULL_HANDLE; - VkDescriptorSet present_set = VK_NULL_HANDLE; // present pass samples this target - }; - GeoTarget geo_lo, geo_hi; - VkRenderPass geo_render_pass = VK_NULL_HANDLE; // shared (resolution-independent) - VkBuffer cam_buf = VK_NULL_HANDLE, disk_buf = VK_NULL_HANDLE, obj_buf = VK_NULL_HANDLE, sim_buf = VK_NULL_HANDLE; - VkDeviceMemory cam_mem = VK_NULL_HANDLE, disk_mem = VK_NULL_HANDLE, obj_mem = VK_NULL_HANDLE, sim_mem = VK_NULL_HANDLE; - void* cam_mapped = nullptr; - void* sim_mapped = nullptr; - void* disk_mapped = nullptr; // disk UBO, refilled from bh_params each frame - BlackHoleParams bh_params; // live UI-tunable disk/camera parameters - std::string gpu_name; // physical device name for the UI - Camera camera{ 60.0f, (float)GEO_HI_W / (float)GEO_HI_H, 0.1f, 100.0f }; - bool left_was_down = false; - double last_frame_time = 0.0; // for free-fly dt - double fps_last_x = 0.0, fps_last_y = 0.0; // cursor tracking for free-fly look - bool user_moving = false; // camera moved/looked this frame (drives step count) - VkImage cube_image = VK_NULL_HANDLE; VkDeviceMemory cube_mem = VK_NULL_HANDLE; - VkImageView cube_view = VK_NULL_HANDLE; VkSampler cube_sampler = VK_NULL_HANDLE; - std::string hdri_path; // path backing the current cubemap (UI display + no-op switch guard) - VkDescriptorSetLayout geo_set_layout = VK_NULL_HANDLE; - VkDescriptorPool geo_pool = VK_NULL_HANDLE; - VkDescriptorSet geo_set = VK_NULL_HANDLE; - VkPipelineLayout geo_pipeline_layout = VK_NULL_HANDLE; - VkPipeline geo_pipeline = VK_NULL_HANDLE; - VkBuffer quad_vb = VK_NULL_HANDLE; VkDeviceMemory quad_vb_mem = VK_NULL_HANDLE; - double start_time = 0.0; - VkFence geo_in_use = VK_NULL_HANDLE; // previous frame's fence; guards the shared geodesic image - - // Present pass: samples the geodesic image with a full-screen textured - // quad, drawn into the swapchain render pass just before the ImGui UI. - VkSampler present_sampler = VK_NULL_HANDLE; - VkDescriptorSetLayout present_set_layout = VK_NULL_HANDLE; - VkDescriptorPool present_pool = VK_NULL_HANDLE; - VkPipelineLayout present_pipeline_layout = VK_NULL_HANDLE; - VkPipeline present_pipeline = VK_NULL_HANDLE; - - // World-builder scene view (grid now; sphere/skybox next). Normal-scale - // orbital camera; geometry drawn straight into the swapchain render pass. - bool scene_mode = false; - Camera scene_camera{ 45.0f, (float)GEO_HI_W / (float)GEO_HI_H, 0.1f, 1000.0f }; - VkBuffer grid_vb = VK_NULL_HANDLE; VkDeviceMemory grid_vb_mem = VK_NULL_HANDLE; - uint32_t grid_vertex_count = 0; - VkBuffer grid_ubo = VK_NULL_HANDLE; VkDeviceMemory grid_ubo_mem = VK_NULL_HANDLE; - void* grid_ubo_mapped = nullptr; - VkDescriptorSetLayout grid_set_layout = VK_NULL_HANDLE; - VkDescriptorPool grid_pool = VK_NULL_HANDLE; - VkDescriptorSet grid_set = VK_NULL_HANDLE; - VkPipelineLayout grid_pipeline_layout = VK_NULL_HANDLE; - VkPipeline grid_pipeline = VK_NULL_HANDLE; - - // Scene color+depth pass into the swapchain (the grid is coplanar, but the - // sphere needs real depth). Depth image + framebuffers track the swapchain. - VkImage scene_depth_image = VK_NULL_HANDLE; VkDeviceMemory scene_depth_mem = VK_NULL_HANDLE; - VkImageView scene_depth_view = VK_NULL_HANDLE; - VkRenderPass scene_render_pass = VK_NULL_HANDLE; - std::vector scene_framebuffers; - - // Lit sphere (Sphere.slang): pos+normal indexed mesh; samples the HDRI - // cubemap for ambient at binding 1 (shares the geodesic cube). - VkBuffer sphere_vb = VK_NULL_HANDLE; VkDeviceMemory sphere_vb_mem = VK_NULL_HANDLE; - VkBuffer sphere_ib = VK_NULL_HANDLE; VkDeviceMemory sphere_ib_mem = VK_NULL_HANDLE; - uint32_t sphere_index_count = 0; - static constexpr uint32_t MAX_SCENE_OBJECTS = 64; - std::vector scene_objects{ SceneObject{} }; // one default sphere - int selected_object = 0; - VkDeviceSize sphere_ubo_stride = 0; // aligned per-object UBO slot (dynamic offset) - VkBuffer sphere_ubo = VK_NULL_HANDLE; VkDeviceMemory sphere_ubo_mem = VK_NULL_HANDLE; - void* sphere_ubo_mapped = nullptr; - VkDescriptorSetLayout sphere_set_layout = VK_NULL_HANDLE; - VkDescriptorPool sphere_pool = VK_NULL_HANDLE; - VkDescriptorSet sphere_set = VK_NULL_HANDLE; - VkPipelineLayout sphere_pipeline_layout = VK_NULL_HANDLE; - VkPipeline sphere_pipeline = VK_NULL_HANDLE; - - // Skybox (Skybox.slang): a cube sampling the HDRI cubemap at the far plane - // (pos.xyww -> depth 1), drawn first as the scene background. - VkBuffer skybox_vb = VK_NULL_HANDLE; VkDeviceMemory skybox_vb_mem = VK_NULL_HANDLE; - VkBuffer skybox_ubo = VK_NULL_HANDLE; VkDeviceMemory skybox_ubo_mem = VK_NULL_HANDLE; - void* skybox_ubo_mapped = nullptr; - VkDescriptorSetLayout skybox_set_layout = VK_NULL_HANDLE; - VkDescriptorPool skybox_pool = VK_NULL_HANDLE; - VkDescriptorSet skybox_set = VK_NULL_HANDLE; - VkPipelineLayout skybox_pipeline_layout = VK_NULL_HANDLE; - VkPipeline skybox_pipeline = VK_NULL_HANDLE; - - auto find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags flags) const -> uint32_t; - auto create_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, VkBuffer& buf, VkDeviceMemory& mem) const -> bool; - static auto load_spirv(const std::string& path) -> std::vector; - auto create_shader_module(const std::string& path, VkShaderModule& out) const -> bool; - - auto create_instance() -> bool; - auto pick_physical_and_device() -> bool; - auto create_swapchain() -> bool; - auto create_image_views() -> bool; - auto create_render_pass() -> bool; - auto create_framebuffers() -> bool; - auto create_command_buffers() -> bool; - auto create_sync_objects() -> bool; - auto create_geodesic_resources() -> bool; - auto create_geo_target(GeoTarget& target, uint32_t w, uint32_t h) -> bool; - auto create_hdri_cubemap(const char* path) -> bool; - auto rebuild_hdri_cubemap(const char* path) -> void; - auto create_present_resources() -> bool; - auto create_scene_resources() -> bool; - auto create_scene_targets() -> bool; - auto destroy_scene_targets() -> void; - auto process_input() -> void; - auto update_geodesic_uniforms() -> void; - auto update_scene_uniforms() -> void; - auto destroy_geodesic_resources() -> void; - auto recreate_swapchain() -> bool; - auto cleanup_swapchain() -> void; - auto record_command_buffer(VkCommandBuffer cmd, uint32_t image_index, const glm::vec4& clear, ImDrawData* draw_data) -> bool; - }; - - auto VulkanRenderer::Impl::create_instance() -> bool - { -#ifdef __APPLE__ - if (!getenv("VK_ICD_FILENAMES")) - setenv("VK_ICD_FILENAMES", "/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json", 0); - if (!getenv("VK_LAYER_PATH")) - setenv("VK_LAYER_PATH", "/opt/homebrew/share/vulkan/explicit_layer.d", 0); - // The Homebrew validation-layer manifest names the dylib without a path; - // let dlopen find it in the Homebrew lib dir. - if (!getenv("DYLD_LIBRARY_PATH")) - setenv("DYLD_LIBRARY_PATH", "/opt/homebrew/lib", 0); -#endif - VkApplicationInfo app{ VK_STRUCTURE_TYPE_APPLICATION_INFO }; - app.pApplicationName = "Donut"; - app.apiVersion = VK_API_VERSION_1_2; - - uint32_t glfwExtCount = 0; - const char** glfwExts = glfwGetRequiredInstanceExtensions(&glfwExtCount); - if (!glfwExts) { DONUT_ERROR("Vulkan: GLFW reports no surface support"); return false; } - std::vector exts; - for (uint32_t i = 0; i < glfwExtCount; ++i) exts.push_back(glfwExts[i]); - exts.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); - exts.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME); - - std::vector layers; - uint32_t layer_count = 0; - vkEnumerateInstanceLayerProperties(&layer_count, nullptr); - std::vector avail(layer_count); - vkEnumerateInstanceLayerProperties(&layer_count, avail.data()); - for (const auto& l : avail) - if (std::strcmp(l.layerName, "VK_LAYER_KHRONOS_validation") == 0) - layers.push_back("VK_LAYER_KHRONOS_validation"); - - VkInstanceCreateInfo ici{ VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO }; - ici.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; - ici.pApplicationInfo = &app; - ici.enabledExtensionCount = (uint32_t)exts.size(); - ici.ppEnabledExtensionNames = exts.data(); - ici.enabledLayerCount = (uint32_t)layers.size(); - ici.ppEnabledLayerNames = layers.data(); - - VkResult r = vkCreateInstance(&ici, nullptr, &instance); - if (r != VK_SUCCESS && !layers.empty()) - { - // The validation layer failed to load (its dylib isn't on the loader - // search path); it is optional, so retry without it. - DONUT_WARN("Vulkan: validation layer unavailable, continuing without it"); - layers.clear(); - ici.enabledLayerCount = 0; - ici.ppEnabledLayerNames = nullptr; - r = vkCreateInstance(&ici, nullptr, &instance); - } - if (r != VK_SUCCESS) { DONUT_ERROR("Vulkan: vkCreateInstance failed ({})", (int)r); return false; } - - VK_CHECK(glfwCreateWindowSurface(instance, window, nullptr, &surface)); - DONUT_INFO("Vulkan: instance + surface created (validation {})", layers.empty() ? "off" : "on"); - return true; - } - - auto VulkanRenderer::Impl::pick_physical_and_device() -> bool - { - uint32_t count = 0; - vkEnumeratePhysicalDevices(instance, &count, nullptr); - if (count == 0) { DONUT_ERROR("Vulkan: no physical devices"); return false; } - std::vector devices(count); - vkEnumeratePhysicalDevices(instance, &count, devices.data()); - physical = devices[0]; - - uint32_t q_count = 0; - vkGetPhysicalDeviceQueueFamilyProperties(physical, &q_count, nullptr); - std::vector qfams(q_count); - vkGetPhysicalDeviceQueueFamilyProperties(physical, &q_count, qfams.data()); - bool found_g = false, found_p = false; - for (uint32_t i = 0; i < q_count; ++i) - { - if (!found_g && (qfams[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) { graphics_family = i; found_g = true; } - VkBool32 present = VK_FALSE; - vkGetPhysicalDeviceSurfaceSupportKHR(physical, i, surface, &present); - if (!found_p && present) { present_family = i; found_p = true; } - } - if (!found_g || !found_p) { DONUT_ERROR("Vulkan: no graphics/present queue"); return false; } - - std::vector dev_exts = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; - uint32_t dev_ext_count = 0; - vkEnumerateDeviceExtensionProperties(physical, nullptr, &dev_ext_count, nullptr); - std::vector dev_ext_props(dev_ext_count); - vkEnumerateDeviceExtensionProperties(physical, nullptr, &dev_ext_count, dev_ext_props.data()); - for (const auto& e : dev_ext_props) - if (std::strcmp(e.extensionName, "VK_KHR_portability_subset") == 0) - dev_exts.push_back("VK_KHR_portability_subset"); - - float priority = 1.0f; - std::vector qcis; - uint32_t families[2] = { graphics_family, present_family }; - for (uint32_t i = 0; i < (graphics_family == present_family ? 1u : 2u); ++i) - { - VkDeviceQueueCreateInfo qci{ VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO }; - qci.queueFamilyIndex = families[i]; - qci.queueCount = 1; - qci.pQueuePriorities = &priority; - qcis.push_back(qci); - } - VkDeviceCreateInfo dci{ VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO }; - dci.queueCreateInfoCount = (uint32_t)qcis.size(); - dci.pQueueCreateInfos = qcis.data(); - dci.enabledExtensionCount = (uint32_t)dev_exts.size(); - dci.ppEnabledExtensionNames = dev_exts.data(); - VK_CHECK(vkCreateDevice(physical, &dci, nullptr, &device)); - vkGetDeviceQueue(device, graphics_family, 0, &graphics_queue); - vkGetDeviceQueue(device, present_family, 0, &present_queue); - - VkPhysicalDeviceProperties props{}; - vkGetPhysicalDeviceProperties(physical, &props); - vkGetPhysicalDeviceMemoryProperties(physical, &mem_props); - gpu_name = props.deviceName; - DONUT_INFO("Vulkan device: {}", gpu_name); - return true; - } - - auto VulkanRenderer::Impl::create_swapchain() -> bool - { - VkSurfaceCapabilitiesKHR caps{}; - vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical, surface, &caps); - - uint32_t fmt_count = 0; - vkGetPhysicalDeviceSurfaceFormatsKHR(physical, surface, &fmt_count, nullptr); - std::vector formats(fmt_count); - vkGetPhysicalDeviceSurfaceFormatsKHR(physical, surface, &fmt_count, formats.data()); - VkSurfaceFormatKHR chosen = formats[0]; - for (const auto& f : formats) - if (f.format == VK_FORMAT_B8G8R8A8_UNORM && f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) - chosen = f; - swapchain_format = chosen.format; - - if (caps.currentExtent.width != UINT32_MAX) - swapchain_extent = caps.currentExtent; - else - { - swapchain_extent.width = std::clamp((uint32_t)width, caps.minImageExtent.width, caps.maxImageExtent.width); - swapchain_extent.height = std::clamp((uint32_t)height, caps.minImageExtent.height, caps.maxImageExtent.height); - } - - uint32_t image_count = caps.minImageCount + 1; - if (caps.maxImageCount > 0 && image_count > caps.maxImageCount) - image_count = caps.maxImageCount; - - VkSwapchainCreateInfoKHR sci{ VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR }; - sci.surface = surface; - sci.minImageCount = image_count; - sci.imageFormat = chosen.format; - sci.imageColorSpace = chosen.colorSpace; - sci.imageExtent = swapchain_extent; - sci.imageArrayLayers = 1; - sci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; - sci.preTransform = caps.currentTransform; - sci.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; - sci.presentMode = VK_PRESENT_MODE_FIFO_KHR; // always supported, vsync - sci.clipped = VK_TRUE; - - uint32_t fam_idx[2] = { graphics_family, present_family }; - if (graphics_family != present_family) - { - sci.imageSharingMode = VK_SHARING_MODE_CONCURRENT; - sci.queueFamilyIndexCount = 2; - sci.pQueueFamilyIndices = fam_idx; - } - else - sci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; - - VK_CHECK(vkCreateSwapchainKHR(device, &sci, nullptr, &swapchain)); - uint32_t n = 0; - vkGetSwapchainImagesKHR(device, swapchain, &n, nullptr); - images.resize(n); - vkGetSwapchainImagesKHR(device, swapchain, &n, images.data()); - return true; - } - - auto VulkanRenderer::Impl::create_image_views() -> bool - { - image_views.resize(images.size()); - for (size_t i = 0; i < images.size(); ++i) - { - VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - vci.image = images[i]; - vci.viewType = VK_IMAGE_VIEW_TYPE_2D; - vci.format = swapchain_format; - vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - VK_CHECK(vkCreateImageView(device, &vci, nullptr, &image_views[i])); - } - return true; - } - - auto VulkanRenderer::Impl::create_render_pass() -> bool - { - VkAttachmentDescription color{}; - color.format = swapchain_format; - color.samples = VK_SAMPLE_COUNT_1_BIT; - color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; - color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - color.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; - - VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; - VkSubpassDescription subpass{}; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; - subpass.pColorAttachments = &ref; - - VkSubpassDependency dep{}; - dep.srcSubpass = VK_SUBPASS_EXTERNAL; - dep.dstSubpass = 0; - dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - - VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; - rpci.attachmentCount = 1; rpci.pAttachments = &color; - rpci.subpassCount = 1; rpci.pSubpasses = &subpass; - rpci.dependencyCount = 1; rpci.pDependencies = &dep; - VK_CHECK(vkCreateRenderPass(device, &rpci, nullptr, &render_pass)); - return true; - } - - auto VulkanRenderer::Impl::create_framebuffers() -> bool - { - framebuffers.resize(image_views.size()); - for (size_t i = 0; i < image_views.size(); ++i) - { - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = render_pass; - fbci.attachmentCount = 1; fbci.pAttachments = &image_views[i]; - fbci.width = swapchain_extent.width; fbci.height = swapchain_extent.height; fbci.layers = 1; - VK_CHECK(vkCreateFramebuffer(device, &fbci, nullptr, &framebuffers[i])); - } - return true; - } - - // Depth image + a color+depth render pass + per-image framebuffers for the - // scene view. Swapchain-sized, so recreated alongside the swapchain. - auto VulkanRenderer::Impl::create_scene_targets() -> bool - { - const VkFormat depth_fmt = VK_FORMAT_D32_SFLOAT; - - VkImageCreateInfo dici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - dici.imageType = VK_IMAGE_TYPE_2D; dici.format = depth_fmt; - dici.extent = { swapchain_extent.width, swapchain_extent.height, 1 }; - dici.mipLevels = 1; dici.arrayLayers = 1; dici.samples = VK_SAMPLE_COUNT_1_BIT; - dici.tiling = VK_IMAGE_TILING_OPTIMAL; dici.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; - VK_CHECK(vkCreateImage(device, &dici, nullptr, &scene_depth_image)); - VkMemoryRequirements dreq{}; vkGetImageMemoryRequirements(device, scene_depth_image, &dreq); - VkMemoryAllocateInfo dai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - dai.allocationSize = dreq.size; dai.memoryTypeIndex = find_memory_type(dreq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK(vkAllocateMemory(device, &dai, nullptr, &scene_depth_mem)); - VK_CHECK(vkBindImageMemory(device, scene_depth_image, scene_depth_mem, 0)); - VkImageViewCreateInfo dvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - dvci.image = scene_depth_image; dvci.viewType = VK_IMAGE_VIEW_TYPE_2D; dvci.format = depth_fmt; - dvci.subresourceRange = { VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1 }; - VK_CHECK(vkCreateImageView(device, &dvci, nullptr, &scene_depth_view)); - - VkAttachmentDescription atts[2]{}; - atts[0].format = swapchain_format; atts[0].samples = VK_SAMPLE_COUNT_1_BIT; - atts[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; - atts[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - atts[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; - atts[1].format = depth_fmt; atts[1].samples = VK_SAMPLE_COUNT_1_BIT; - atts[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - atts[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - atts[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; - VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; - VkAttachmentReference depth_ref{ 1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL }; - VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref; subpass.pDepthStencilAttachment = &depth_ref; - VkSubpassDependency dep{}; - dep.srcSubpass = VK_SUBPASS_EXTERNAL; dep.dstSubpass = 0; - dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; - dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; - dep.srcAccessMask = 0; - dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; - VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; - rpci.attachmentCount = 2; rpci.pAttachments = atts; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; - rpci.dependencyCount = 1; rpci.pDependencies = &dep; - VK_CHECK(vkCreateRenderPass(device, &rpci, nullptr, &scene_render_pass)); - - scene_framebuffers.resize(image_views.size()); - for (size_t i = 0; i < image_views.size(); ++i) - { - VkImageView att[2] = { image_views[i], scene_depth_view }; - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = scene_render_pass; fbci.attachmentCount = 2; fbci.pAttachments = att; - fbci.width = swapchain_extent.width; fbci.height = swapchain_extent.height; fbci.layers = 1; - VK_CHECK(vkCreateFramebuffer(device, &fbci, nullptr, &scene_framebuffers[i])); - } - return true; - } - - auto VulkanRenderer::Impl::destroy_scene_targets() -> void - { - for (auto fb : scene_framebuffers) vkDestroyFramebuffer(device, fb, nullptr); - scene_framebuffers.clear(); - if (scene_render_pass) { vkDestroyRenderPass(device, scene_render_pass, nullptr); scene_render_pass = VK_NULL_HANDLE; } - if (scene_depth_view) { vkDestroyImageView(device, scene_depth_view, nullptr); scene_depth_view = VK_NULL_HANDLE; } - if (scene_depth_image) { vkDestroyImage(device, scene_depth_image, nullptr); scene_depth_image = VK_NULL_HANDLE; } - if (scene_depth_mem) { vkFreeMemory(device, scene_depth_mem, nullptr); scene_depth_mem = VK_NULL_HANDLE; } - } - - auto VulkanRenderer::Impl::create_command_buffers() -> bool - { - VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; - pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; - pci.queueFamilyIndex = graphics_family; - VK_CHECK(vkCreateCommandPool(device, &pci, nullptr, &command_pool)); - - command_buffers.resize(MAX_FRAMES_IN_FLIGHT); - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; - cbai.commandPool = command_pool; - cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - cbai.commandBufferCount = MAX_FRAMES_IN_FLIGHT; - VK_CHECK(vkAllocateCommandBuffers(device, &cbai, command_buffers.data())); - return true; - } - - auto VulkanRenderer::Impl::create_sync_objects() -> bool - { - image_available.resize(MAX_FRAMES_IN_FLIGHT); - in_flight.resize(MAX_FRAMES_IN_FLIGHT); - render_finished.resize(images.size()); - images_in_flight.assign(images.size(), VK_NULL_HANDLE); - - VkSemaphoreCreateInfo sci{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO }; - VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; - fci.flags = VK_FENCE_CREATE_SIGNALED_BIT; - for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) - { - VK_CHECK(vkCreateSemaphore(device, &sci, nullptr, &image_available[i])); - VK_CHECK(vkCreateFence(device, &fci, nullptr, &in_flight[i])); - } - for (size_t i = 0; i < images.size(); ++i) - VK_CHECK(vkCreateSemaphore(device, &sci, nullptr, &render_finished[i])); - return true; - } - - auto VulkanRenderer::Impl::find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags flags) const -> uint32_t - { - for (uint32_t i = 0; i < mem_props.memoryTypeCount; ++i) - if ((type_filter & (1u << i)) && (mem_props.memoryTypes[i].propertyFlags & flags) == flags) - return i; - return UINT32_MAX; - } - - auto VulkanRenderer::Impl::create_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, - VkBuffer& buf, VkDeviceMemory& mem) const -> bool - { - VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; - bci.size = size; bci.usage = usage; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - if (vkCreateBuffer(device, &bci, nullptr, &buf) != VK_SUCCESS) return false; - VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(device, buf, &req); - VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - ai.allocationSize = req.size; - ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, props); - if (vkAllocateMemory(device, &ai, nullptr, &mem) != VK_SUCCESS) return false; - vkBindBufferMemory(device, buf, mem, 0); - return true; - } - - auto VulkanRenderer::Impl::load_spirv(const std::string& path) -> std::vector - { - std::ifstream file(path, std::ios::ate | std::ios::binary); - if (!file.is_open()) return {}; - size_t size = (size_t)file.tellg(); - std::vector data(size / 4); - file.seekg(0); - file.read(reinterpret_cast(data.data()), size); - return data; - } - - auto VulkanRenderer::Impl::create_shader_module(const std::string& path, VkShaderModule& out) const -> bool - { - auto spv = load_spirv(path); - if (spv.empty()) { DONUT_ERROR("Vulkan: failed to load SPIR-V {}", path); return false; } - VkShaderModuleCreateInfo ci{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO }; - ci.codeSize = spv.size() * 4; ci.pCode = spv.data(); - return vkCreateShaderModule(device, &ci, nullptr, &out) == VK_SUCCESS; - } - - // Creates one geodesic render target (image + memory + view + framebuffer) - // at w x h against the shared geo_render_pass. Used for the low- and high-res - // progressive targets. - auto VulkanRenderer::Impl::create_geo_target(GeoTarget& t, uint32_t w, uint32_t h) -> bool - { - const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM; - t.w = w; t.h = h; - - VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { w, h, 1 }; - ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT; - ici.tiling = VK_IMAGE_TILING_OPTIMAL; - ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; - VK_CHECK(vkCreateImage(device, &ici, nullptr, &t.image)); - VkMemoryRequirements im_req{}; vkGetImageMemoryRequirements(device, t.image, &im_req); - VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - im_alloc.allocationSize = im_req.size; - im_alloc.memoryTypeIndex = find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK(vkAllocateMemory(device, &im_alloc, nullptr, &t.mem)); - VK_CHECK(vkBindImageMemory(device, t.image, t.mem, 0)); - VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - vci.image = t.image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt; - vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - VK_CHECK(vkCreateImageView(device, &vci, nullptr, &t.view)); - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = geo_render_pass; fbci.attachmentCount = 1; fbci.pAttachments = &t.view; - fbci.width = w; fbci.height = h; fbci.layers = 1; - VK_CHECK(vkCreateFramebuffer(device, &fbci, nullptr, &t.fb)); - return true; - } - - auto VulkanRenderer::Impl::create_geodesic_resources() -> bool - { - const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM; - const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - const float SagA_rs = 1.269e10f; - - VkAttachmentDescription color{}; - color.format = fmt; color.samples = VK_SAMPLE_COUNT_1_BIT; - color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; - color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; - VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &ref; - VkSubpassDependency deps[2]{}; - deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; deps[0].dstSubpass = 0; - deps[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT; - deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - deps[1].srcSubpass = 0; deps[1].dstSubpass = VK_SUBPASS_EXTERNAL; - deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; - rpci.attachmentCount = 1; rpci.pAttachments = &color; - rpci.subpassCount = 1; rpci.pSubpasses = &subpass; - rpci.dependencyCount = 2; rpci.pDependencies = deps; - VK_CHECK(vkCreateRenderPass(device, &rpci, nullptr, &geo_render_pass)); - - if (!create_geo_target(geo_lo, GEO_LO_W, GEO_LO_H)) return false; - if (!create_geo_target(geo_hi, GEO_HI_W, GEO_HI_H)) return false; - - create_buffer(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, cam_buf, cam_mem); - create_buffer(32, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, disk_buf, disk_mem); - create_buffer(800, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, obj_buf, obj_mem); - create_buffer(16, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, sim_buf, sim_mem); - - camera.set_camera_mode(CameraMode::Orbital); - camera.set_orbital_target(glm::vec3(0.0f)); - camera.set_orbital_radius(4e11); // ~31 r_s: outside the 12 r_s disk - camera.set_orbital_limits(2.2e11, 1.5e12); - camera.set_orbital_speed(0.01f); - camera.set_zoom_speed(3e10); - camera.set_azimuth(0.0f); - camera.set_elevation(1.25f); - - void* p = nullptr; - vkMapMemory(device, cam_mem, 0, 128, 0, &cam_mapped); // camera UBO refilled every frame - vkMapMemory(device, disk_mem, 0, 32, 0, &disk_mapped); // disk UBO refilled from bh_params every frame - - std::vector obj_data(800, 0); - int num_objects = 1; memcpy(obj_data.data(), &num_objects, 4); - float pos_radius[4] = { 0, 0, 0, SagA_rs }; memcpy(obj_data.data() + 16, pos_radius, 16); - float obj_color[4] = { 0, 0, 0, 1 }; memcpy(obj_data.data() + 272, obj_color, 16); - vkMapMemory(device, obj_mem, 0, 800, 0, &p); memcpy(p, obj_data.data(), 800); vkUnmapMemory(device, obj_mem); - - vkMapMemory(device, sim_mem, 0, 16, 0, &sim_mapped); - - if (!create_hdri_cubemap("assets/hdri/HDR_blue_nebulae-1.hdr")) return false; - - VkDescriptorSetLayoutBinding binds[5]{}; - for (int i = 0; i < 4; ++i) { binds[i].binding = i; binds[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[i].descriptorCount = 1; binds[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; } - binds[4].binding = 4; binds[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[4].descriptorCount = 1; binds[4].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; - dslci.bindingCount = 5; dslci.pBindings = binds; - VK_CHECK(vkCreateDescriptorSetLayout(device, &dslci, nullptr, &geo_set_layout)); - VkDescriptorPoolSize psizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 4 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 } }; - VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; - dpci.maxSets = 1; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes; - VK_CHECK(vkCreateDescriptorPool(device, &dpci, nullptr, &geo_pool)); - VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; - dsai.descriptorPool = geo_pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &geo_set_layout; - VK_CHECK(vkAllocateDescriptorSets(device, &dsai, &geo_set)); - VkDescriptorBufferInfo bi[4] = { { cam_buf, 0, VK_WHOLE_SIZE }, { disk_buf, 0, VK_WHOLE_SIZE }, { obj_buf, 0, VK_WHOLE_SIZE }, { sim_buf, 0, VK_WHOLE_SIZE } }; - VkDescriptorImageInfo cube_info{ cube_sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; - VkWriteDescriptorSet writes[5]{}; - for (int i = 0; i < 4; ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = geo_set; writes[i].dstBinding = i; writes[i].descriptorCount = 1; writes[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; writes[i].pBufferInfo = &bi[i]; } - writes[4].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[4].dstSet = geo_set; writes[4].dstBinding = 4; writes[4].descriptorCount = 1; writes[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[4].pImageInfo = &cube_info; - vkUpdateDescriptorSets(device, 5, writes, 0, nullptr); - - float quad[] = { - -1.f, 1.f, 0.f, 1.f, -1.f, -1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 0.f, - -1.f, 1.f, 0.f, 1.f, 1.f, -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f, - }; - create_buffer(sizeof(quad), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, quad_vb, quad_vb_mem); - vkMapMemory(device, quad_vb_mem, 0, sizeof(quad), 0, &p); memcpy(p, quad, sizeof(quad)); vkUnmapMemory(device, quad_vb_mem); - - VkShaderModule vmod, fmod; - if (!create_shader_module("assets/shaders/generated/Geodesic.vertexMain.spv", vmod)) return false; - if (!create_shader_module("assets/shaders/generated/Geodesic.fragmentMain.spv", fmod)) return false; - VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; - plci.setLayoutCount = 1; plci.pSetLayouts = &geo_set_layout; - VK_CHECK(vkCreatePipelineLayout(device, &plci, nullptr, &geo_pipeline_layout)); - VkPipelineShaderStageCreateInfo stages[2]{}; - stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main"; - stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main"; - VkVertexInputBindingDescription vib{ 0, 16, VK_VERTEX_INPUT_RATE_VERTEX }; - VkVertexInputAttributeDescription via[2] = { { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32_SFLOAT, 8 } }; - VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; - vin.vertexAttributeDescriptionCount = 2; vin.pVertexAttributeDescriptions = via; - VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - // Dynamic viewport/scissor: the same pipeline renders into either the - // low- or high-res target, sized per frame in record_command_buffer. - VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.scissorCount = 1; - VkDynamicState dyn_states[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; - VkPipelineDynamicStateCreateInfo dyn{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; dyn.dynamicStateCount = 2; dyn.pDynamicStates = dyn_states; - VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f; - VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - VkPipelineColorBlendAttachmentState cba{}; cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba; - VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; - gpci.stageCount = 2; gpci.pStages = stages; - gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps; - gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; - gpci.pDynamicState = &dyn; - gpci.layout = geo_pipeline_layout; gpci.renderPass = geo_render_pass; gpci.subpass = 0; - VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &geo_pipeline); - vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr); - if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: geodesic pipeline creation failed ({})", (int)pr); return false; } - - start_time = glfwGetTime(); - update_geodesic_uniforms(); - DONUT_INFO("Vulkan: geodesic resources ready ({}x{} moving / {}x{} settled)", - (int)GEO_LO_W, (int)GEO_LO_H, (int)GEO_HI_W, (int)GEO_HI_H); - return true; - } - - auto VulkanRenderer::Impl::create_hdri_cubemap(const char* path) -> bool - { - hdri_path = path; - const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - const uint32_t FACE = 1024; - const VkFormat cube_fmt = VK_FORMAT_R16G16B16A16_SFLOAT; - uint32_t CUBE_MIPS = 1; for (uint32_t s = FACE; s > 1; s >>= 1) ++CUBE_MIPS; // full chain (11 for 1024) - - VkImageCreateInfo cci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - cci.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; - cci.imageType = VK_IMAGE_TYPE_2D; cci.format = cube_fmt; cci.extent = { FACE, FACE, 1 }; - cci.mipLevels = CUBE_MIPS; cci.arrayLayers = 6; cci.samples = VK_SAMPLE_COUNT_1_BIT; - cci.tiling = VK_IMAGE_TILING_OPTIMAL; - // COLOR_ATTACHMENT renders the 6 faces (mip 0); TRANSFER_SRC/DST build the - // mip chain by blitting; SAMPLED for shader reads (incl. mip-LOD sampling). - cci.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT - | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; - VK_CHECK(vkCreateImage(device, &cci, nullptr, &cube_image)); - VkMemoryRequirements creq{}; vkGetImageMemoryRequirements(device, cube_image, &creq); - VkMemoryAllocateInfo cai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - cai.allocationSize = creq.size; cai.memoryTypeIndex = find_memory_type(creq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK(vkAllocateMemory(device, &cai, nullptr, &cube_mem)); - VK_CHECK(vkBindImageMemory(device, cube_image, cube_mem, 0)); - VkImageViewCreateInfo cvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - cvci.image = cube_image; cvci.viewType = VK_IMAGE_VIEW_TYPE_CUBE; cvci.format = cube_fmt; - cvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; - VK_CHECK(vkCreateImageView(device, &cvci, nullptr, &cube_view)); - VkSamplerCreateInfo csm{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; - csm.magFilter = VK_FILTER_LINEAR; csm.minFilter = VK_FILTER_LINEAR; - csm.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; csm.minLod = 0.0f; csm.maxLod = (float)CUBE_MIPS; - csm.addressModeU = csm.addressModeV = csm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - VK_CHECK(vkCreateSampler(device, &csm, nullptr, &cube_sampler)); - - int w = 0, h = 0, ch = 0; - float* pixels = stbi_loadf(path, &w, &h, &ch, 4); - if (!pixels) - { - DONUT_WARN("Vulkan: HDRI '{}' could not be loaded; using a dark background", path); - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; - cbai.commandPool = command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; - VkCommandBuffer cmd; VK_CHECK(vkAllocateCommandBuffers(device, &cbai, &cmd)); - VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer(cmd, &bi); - VkImageMemoryBarrier tb{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - tb.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; tb.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - tb.image = cube_image; tb.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 }; - tb.srcAccessMask = 0; tb.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &tb); - VkClearColorValue dark{}; dark.float32[0] = 0.02f; dark.float32[1] = 0.02f; dark.float32[2] = 0.05f; dark.float32[3] = 1.0f; - VkImageSubresourceRange rng{ VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 }; - vkCmdClearColorImage(cmd, cube_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &dark, 1, &rng); - VkImageMemoryBarrier rb = tb; rb.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; rb.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - rb.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; rb.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &rb); - vkEndCommandBuffer(cmd); - VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; - vkQueueSubmit(graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(graphics_queue); - vkFreeCommandBuffers(device, command_pool, 1, &cmd); - return true; - } - - // Apple GPUs can't linearly filter RGBA32F, so store the equirect as - // RGBA16F (convert the loaded floats to half on the way into staging). - const VkFormat eq_fmt = VK_FORMAT_R16G16B16A16_SFLOAT; - size_t texel_count = (size_t)w * h * 4; - VkDeviceSize eq_size = (VkDeviceSize)texel_count * sizeof(uint16_t); - VkBuffer eq_staging; VkDeviceMemory eq_staging_mem; - if (!create_buffer(eq_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, host_vis, eq_staging, eq_staging_mem)) { stbi_image_free(pixels); return false; } - void* mp = nullptr; vkMapMemory(device, eq_staging_mem, 0, eq_size, 0, &mp); - uint16_t* dst = (uint16_t*)mp; - for (size_t i = 0; i < texel_count; ++i) { __fp16 hf = (__fp16)pixels[i]; memcpy(&dst[i], &hf, sizeof(uint16_t)); } - vkUnmapMemory(device, eq_staging_mem); - stbi_image_free(pixels); - - VkImage eq_image; VkDeviceMemory eq_mem; - VkImageCreateInfo eci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; - eci.imageType = VK_IMAGE_TYPE_2D; eci.format = eq_fmt; eci.extent = { (uint32_t)w, (uint32_t)h, 1 }; - eci.mipLevels = 1; eci.arrayLayers = 1; eci.samples = VK_SAMPLE_COUNT_1_BIT; - eci.tiling = VK_IMAGE_TILING_OPTIMAL; eci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; - VK_CHECK(vkCreateImage(device, &eci, nullptr, &eq_image)); - VkMemoryRequirements ereq{}; vkGetImageMemoryRequirements(device, eq_image, &ereq); - VkMemoryAllocateInfo eai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; - eai.allocationSize = ereq.size; eai.memoryTypeIndex = find_memory_type(ereq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK(vkAllocateMemory(device, &eai, nullptr, &eq_mem)); - VK_CHECK(vkBindImageMemory(device, eq_image, eq_mem, 0)); - VkImageView eq_view; - VkImageViewCreateInfo evci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - evci.image = eq_image; evci.viewType = VK_IMAGE_VIEW_TYPE_2D; evci.format = eq_fmt; - evci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - VK_CHECK(vkCreateImageView(device, &evci, nullptr, &eq_view)); - VkSampler eq_sampler; - VkSamplerCreateInfo esm{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; - esm.magFilter = VK_FILTER_LINEAR; esm.minFilter = VK_FILTER_LINEAR; - esm.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; // longitude wraps - esm.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; // latitude clamps - esm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - VK_CHECK(vkCreateSampler(device, &esm, nullptr, &eq_sampler)); - - VkImageView face_views[6]; - for (uint32_t i = 0; i < 6; ++i) - { - VkImageViewCreateInfo fvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; - fvci.image = cube_image; fvci.viewType = VK_IMAGE_VIEW_TYPE_2D; fvci.format = cube_fmt; - fvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, i, 1 }; - VK_CHECK(vkCreateImageView(device, &fvci, nullptr, &face_views[i])); - } - - VkAttachmentDescription color{}; - color.format = cube_fmt; color.samples = VK_SAMPLE_COUNT_1_BIT; - color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; - color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; - VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &ref; - VkSubpassDependency dep{}; dep.srcSubpass = 0; dep.dstSubpass = VK_SUBPASS_EXTERNAL; - dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - dep.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; dep.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - VkRenderPass rp; - VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; - rpci.attachmentCount = 1; rpci.pAttachments = &color; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; rpci.dependencyCount = 1; rpci.pDependencies = &dep; - VK_CHECK(vkCreateRenderPass(device, &rpci, nullptr, &rp)); - VkFramebuffer face_fb[6]; - for (uint32_t i = 0; i < 6; ++i) - { - VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; - fbci.renderPass = rp; fbci.attachmentCount = 1; fbci.pAttachments = &face_views[i]; fbci.width = FACE; fbci.height = FACE; fbci.layers = 1; - VK_CHECK(vkCreateFramebuffer(device, &fbci, nullptr, &face_fb[i])); - } - - VkDescriptorSetLayoutBinding binds[2]{}; - binds[0].binding = 0; binds[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[0].descriptorCount = 1; binds[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; - binds[1].binding = 1; binds[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[1].descriptorCount = 1; binds[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - VkDescriptorSetLayout set_layout; - VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; dslci.bindingCount = 2; dslci.pBindings = binds; - VK_CHECK(vkCreateDescriptorSetLayout(device, &dslci, nullptr, &set_layout)); - VkDescriptorPoolSize psizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 6 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 6 } }; - VkDescriptorPool pool; - VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; dpci.maxSets = 6; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes; - VK_CHECK(vkCreateDescriptorPool(device, &dpci, nullptr, &pool)); - - VkShaderModule vmod, fmod; - if (!create_shader_module("assets/shaders/generated/EquirectToCubemap.vertexMain.spv", vmod)) return false; - if (!create_shader_module("assets/shaders/generated/EquirectToCubemap.fragmentMain.spv", fmod)) return false; - VkPipelineLayout playout; - VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; plci.setLayoutCount = 1; plci.pSetLayouts = &set_layout; - VK_CHECK(vkCreatePipelineLayout(device, &plci, nullptr, &playout)); - VkPipelineShaderStageCreateInfo stages[2]{}; - stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main"; - stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main"; - VkVertexInputBindingDescription vib{ 0, 12, VK_VERTEX_INPUT_RATE_VERTEX }; - VkVertexInputAttributeDescription via{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }; - VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; vin.vertexAttributeDescriptionCount = 1; vin.pVertexAttributeDescriptions = &via; - VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - VkViewport vp{ 0, 0, (float)FACE, (float)FACE, 0, 1 }; VkRect2D sc{ { 0, 0 }, { FACE, FACE } }; - VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.pViewports = &vp; vps.scissorCount = 1; vps.pScissors = ≻ - VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f; - VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - VkPipelineColorBlendAttachmentState cba{}; cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba; - VkPipeline pipeline; - VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; - gpci.stageCount = 2; gpci.pStages = stages; gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps; - gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; gpci.layout = playout; gpci.renderPass = rp; gpci.subpass = 0; - VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline); - vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr); - if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: equirect pipeline failed ({})", (int)pr); return false; } - - float cube_verts[] = { - -1,1,-1, -1,-1,-1, 1,-1,-1, 1,-1,-1, 1,1,-1, -1,1,-1, - -1,-1,1, -1,-1,-1, -1,1,-1, -1,1,-1, -1,1,1, -1,-1,1, - 1,-1,-1, 1,-1,1, 1,1,1, 1,1,1, 1,1,-1, 1,-1,-1, - -1,-1,1, -1,1,1, 1,1,1, 1,1,1, 1,-1,1, -1,-1,1, - -1,1,-1, 1,1,-1, 1,1,1, 1,1,1, -1,1,1, -1,1,-1, - -1,-1,-1, -1,-1,1, 1,-1,-1, 1,-1,-1, -1,-1,1, 1,-1,1, - }; - VkBuffer cube_vb; VkDeviceMemory cube_vb_mem; - create_buffer(sizeof(cube_verts), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, cube_vb, cube_vb_mem); - vkMapMemory(device, cube_vb_mem, 0, sizeof(cube_verts), 0, &mp); memcpy(mp, cube_verts, sizeof(cube_verts)); vkUnmapMemory(device, cube_vb_mem); - - glm::mat4 proj = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f); - proj[1][1] *= -1.0f; // Vulkan clip space is Y-down vs OpenGL - glm::mat4 views[6] = { - glm::lookAt(glm::vec3(0), glm::vec3( 1, 0, 0), glm::vec3(0, -1, 0)), - glm::lookAt(glm::vec3(0), glm::vec3(-1, 0, 0), glm::vec3(0, -1, 0)), - glm::lookAt(glm::vec3(0), glm::vec3( 0, 1, 0), glm::vec3(0, 0, 1)), - glm::lookAt(glm::vec3(0), glm::vec3( 0, -1, 0), glm::vec3(0, 0, -1)), - glm::lookAt(glm::vec3(0), glm::vec3( 0, 0, 1), glm::vec3(0, -1, 0)), - glm::lookAt(glm::vec3(0), glm::vec3( 0, 0, -1), glm::vec3(0, -1, 0)), - }; - VkBuffer ubo[6]; VkDeviceMemory ubo_mem[6]; VkDescriptorSet sets[6]; - for (uint32_t i = 0; i < 6; ++i) - { - create_buffer(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, ubo[i], ubo_mem[i]); - glm::mat4 mats[2] = { glm::transpose(proj), glm::transpose(views[i]) }; // SPIR-V expects row-major - vkMapMemory(device, ubo_mem[i], 0, 128, 0, &mp); memcpy(mp, mats, 128); vkUnmapMemory(device, ubo_mem[i]); - VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; dsai.descriptorPool = pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &set_layout; - VK_CHECK(vkAllocateDescriptorSets(device, &dsai, &sets[i])); - VkDescriptorBufferInfo buf_info{ ubo[i], 0, VK_WHOLE_SIZE }; - VkDescriptorImageInfo img_info{ eq_sampler, eq_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; - VkWriteDescriptorSet ws[2]{}; - ws[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; ws[0].dstSet = sets[i]; ws[0].dstBinding = 0; ws[0].descriptorCount = 1; ws[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; ws[0].pBufferInfo = &buf_info; - ws[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; ws[1].dstSet = sets[i]; ws[1].dstBinding = 1; ws[1].descriptorCount = 1; ws[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; ws[1].pImageInfo = &img_info; - vkUpdateDescriptorSets(device, 2, ws, 0, nullptr); - } - - VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; - cbai.commandPool = command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; - VkCommandBuffer cmd; VK_CHECK(vkAllocateCommandBuffers(device, &cbai, &cmd)); - VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - VK_CHECK(vkBeginCommandBuffer(cmd, &bi)); - VkImageMemoryBarrier to_dst{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - to_dst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; to_dst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - to_dst.image = eq_image; to_dst.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; - to_dst.srcAccessMask = 0; to_dst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_dst); - VkBufferImageCopy copy{}; copy.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; copy.imageExtent = { (uint32_t)w, (uint32_t)h, 1 }; - vkCmdCopyBufferToImage(cmd, eq_staging, eq_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); - VkImageMemoryBarrier to_read = to_dst; to_read.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; to_read.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - to_read.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; to_read.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_read); - - VkClearValue clear{}; clear.color = { { 0, 0, 0, 1 } }; - for (uint32_t i = 0; i < 6; ++i) - { - VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; - rpbi.renderPass = rp; rpbi.framebuffer = face_fb[i]; rpbi.renderArea = { { 0, 0 }, { FACE, FACE } }; rpbi.clearValueCount = 1; rpbi.pClearValues = &clear; - vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, playout, 0, 1, &sets[i], 0, nullptr); - VkDeviceSize off = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &cube_vb, &off); - vkCmdDraw(cmd, 36, 1, 0, 0); - vkCmdEndRenderPass(cmd); - } - - // Build the mip chain (all 6 faces) by successive linear down-blits, so - // divergence-based LOD sampling can read a blurred sky for lensed rays. - // Mip 0 (every layer) is SHADER_READ_ONLY from the render passes above. - { - VkImageMemoryBarrier src0{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - src0.image = cube_image; src0.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 }; - src0.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; src0.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - src0.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; src0.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &src0); - - int32_t mipW = (int32_t)FACE, mipH = (int32_t)FACE; - for (uint32_t m = 1; m < CUBE_MIPS; ++m) - { - int32_t nW = mipW > 1 ? mipW / 2 : 1, nH = mipH > 1 ? mipH / 2 : 1; - VkImageMemoryBarrier bd{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - bd.image = cube_image; bd.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, m, 1, 0, 6 }; - bd.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; bd.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - bd.srcAccessMask = 0; bd.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &bd); - - VkImageBlit blit{}; - blit.srcOffsets[1] = { mipW, mipH, 1 }; blit.srcSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, m - 1, 0, 6 }; - blit.dstOffsets[1] = { nW, nH, 1 }; blit.dstSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, m, 0, 6 }; - vkCmdBlitImage(cmd, cube_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - cube_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); - - VkImageMemoryBarrier bs{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - bs.image = cube_image; bs.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, m, 1, 0, 6 }; - bs.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; bs.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - bs.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; bs.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &bs); - mipW = nW; mipH = nH; - } - VkImageMemoryBarrier fin{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; - fin.image = cube_image; fin.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; - fin.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; fin.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - fin.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; fin.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &fin); - } - - VK_CHECK(vkEndCommandBuffer(cmd)); - VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; - VK_CHECK(vkQueueSubmit(graphics_queue, 1, &si, VK_NULL_HANDLE)); - VK_CHECK(vkQueueWaitIdle(graphics_queue)); - - vkFreeCommandBuffers(device, command_pool, 1, &cmd); - for (uint32_t i = 0; i < 6; ++i) { vkDestroyBuffer(device, ubo[i], nullptr); vkFreeMemory(device, ubo_mem[i], nullptr); vkDestroyFramebuffer(device, face_fb[i], nullptr); vkDestroyImageView(device, face_views[i], nullptr); } - vkDestroyBuffer(device, cube_vb, nullptr); vkFreeMemory(device, cube_vb_mem, nullptr); - vkDestroyPipeline(device, pipeline, nullptr); vkDestroyPipelineLayout(device, playout, nullptr); - vkDestroyDescriptorPool(device, pool, nullptr); vkDestroyDescriptorSetLayout(device, set_layout, nullptr); - vkDestroyRenderPass(device, rp, nullptr); - vkDestroySampler(device, eq_sampler, nullptr); vkDestroyImageView(device, eq_view, nullptr); - vkDestroyImage(device, eq_image, nullptr); vkFreeMemory(device, eq_mem, nullptr); - vkDestroyBuffer(device, eq_staging, nullptr); vkFreeMemory(device, eq_staging_mem, nullptr); - DONUT_INFO("Vulkan: HDRI cubemap built from {} ({}x{} equirect -> {}^2 cube)", path, w, h, (int)FACE); - return true; - } - - // Runtime HDRI switch: drain the device, tear down the old cubemap, build the - // new one, and repoint the geodesic set's samplerCube (binding 4) at it. - auto VulkanRenderer::Impl::rebuild_hdri_cubemap(const char* path) -> void - { - vkDeviceWaitIdle(device); - - if (cube_sampler) vkDestroySampler(device, cube_sampler, nullptr); - if (cube_view) vkDestroyImageView(device, cube_view, nullptr); - if (cube_image) vkDestroyImage(device, cube_image, nullptr); - if (cube_mem) vkFreeMemory(device, cube_mem, nullptr); - cube_sampler = VK_NULL_HANDLE; cube_view = VK_NULL_HANDLE; - cube_image = VK_NULL_HANDLE; cube_mem = VK_NULL_HANDLE; - - create_hdri_cubemap(path); - - VkDescriptorImageInfo cube_info{ cube_sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; - VkWriteDescriptorSet write{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; - write.dstSet = geo_set; write.dstBinding = 4; write.descriptorCount = 1; - write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.pImageInfo = &cube_info; - vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); - - // The scene sphere samples the same cube (binding 1) — repoint it too. - if (sphere_set) - { - VkWriteDescriptorSet sw{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; - sw.dstSet = sphere_set; sw.dstBinding = 1; sw.descriptorCount = 1; - sw.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; sw.pImageInfo = &cube_info; - vkUpdateDescriptorSets(device, 1, &sw, 0, nullptr); - } - if (skybox_set) - { - VkWriteDescriptorSet kw{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; - kw.dstSet = skybox_set; kw.dstBinding = 1; kw.descriptorCount = 1; - kw.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; kw.pImageInfo = &cube_info; - vkUpdateDescriptorSets(device, 1, &kw, 0, nullptr); - } - } - - auto VulkanRenderer::Impl::create_present_resources() -> bool - { - VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; - smci.magFilter = VK_FILTER_LINEAR; smci.minFilter = VK_FILTER_LINEAR; - smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - VK_CHECK(vkCreateSampler(device, &smci, nullptr, &present_sampler)); - - VkDescriptorSetLayoutBinding bind{}; bind.binding = 0; bind.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; bind.descriptorCount = 1; bind.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; - dslci.bindingCount = 1; dslci.pBindings = &bind; - VK_CHECK(vkCreateDescriptorSetLayout(device, &dslci, nullptr, &present_set_layout)); - // One present set per geodesic target (low-/high-res), each sampling its - // own image; record_command_buffer binds whichever was rendered this frame. - VkDescriptorPoolSize psize{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2 }; - VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; - dpci.maxSets = 2; dpci.poolSizeCount = 1; dpci.pPoolSizes = &psize; - VK_CHECK(vkCreateDescriptorPool(device, &dpci, nullptr, &present_pool)); - for (GeoTarget* t : { &geo_lo, &geo_hi }) - { - VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; - dsai.descriptorPool = present_pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &present_set_layout; - VK_CHECK(vkAllocateDescriptorSets(device, &dsai, &t->present_set)); - VkDescriptorImageInfo ii{ present_sampler, t->view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; - VkWriteDescriptorSet write{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; - write.dstSet = t->present_set; write.dstBinding = 0; write.descriptorCount = 1; write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.pImageInfo = ⅈ - vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); - } - - VkShaderModule vmod, fmod; - if (!create_shader_module("assets/shaders/generated/TexturedQuad.vertexMain.spv", vmod)) return false; - if (!create_shader_module("assets/shaders/generated/TexturedQuad.fragmentMain.spv", fmod)) return false; - VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; - plci.setLayoutCount = 1; plci.pSetLayouts = &present_set_layout; - VK_CHECK(vkCreatePipelineLayout(device, &plci, nullptr, &present_pipeline_layout)); - VkPipelineShaderStageCreateInfo stages[2]{}; - stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main"; - stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main"; - VkVertexInputBindingDescription vib{ 0, 16, VK_VERTEX_INPUT_RATE_VERTEX }; - VkVertexInputAttributeDescription via[2] = { { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32_SFLOAT, 8 } }; - VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; - vin.vertexAttributeDescriptionCount = 2; vin.pVertexAttributeDescriptions = via; - VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.scissorCount = 1; - VkDynamicState dyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; - VkPipelineDynamicStateCreateInfo dsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; dsci.dynamicStateCount = 2; dsci.pDynamicStates = dyn; - VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f; - VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - VkPipelineColorBlendAttachmentState cba{}; cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba; - VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; - gpci.stageCount = 2; gpci.pStages = stages; - gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps; - gpci.pDynamicState = &dsci; - gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; - gpci.layout = present_pipeline_layout; gpci.renderPass = render_pass; gpci.subpass = 0; - VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &present_pipeline); - vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr); - if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: present pipeline creation failed ({})", (int)pr); return false; } - DONUT_INFO("Vulkan: present pipeline ready"); - return true; - } - - auto VulkanRenderer::Impl::create_scene_resources() -> bool - { - const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - - // Scene camera: normal-scale orbital viewer (matches the OpenGL world-builder). - scene_camera.set_camera_mode(CameraMode::Orbital); - scene_camera.set_orbital_target(glm::vec3(0.0f)); - scene_camera.set_orbital_radius(15.0); - scene_camera.set_orbital_limits(2.0, 200.0); - scene_camera.set_orbital_speed(0.01f); - scene_camera.set_zoom_speed(2.0); - scene_camera.set_azimuth(0.0f); - scene_camera.set_elevation((float)std::numbers::pi / 3.0f); - scene_camera.update_orbital(); - - // Line grid on the XZ plane (+/-50 units, 1-unit cells). Grid.slang scales - // the position by u_GridSize/50, so u_GridSize = 50 keeps it 1:1. - std::vector lines; - const int N = 50; - for (int i = -N; i <= N; ++i) - { - lines.push_back({ (float)i, 0.0f, (float)-N }); - lines.push_back({ (float)i, 0.0f, (float) N }); - lines.push_back({ (float)-N, 0.0f, (float)i }); - lines.push_back({ (float) N, 0.0f, (float)i }); - } - grid_vertex_count = (uint32_t)lines.size(); - VkDeviceSize vbsize = lines.size() * sizeof(glm::vec3); - if (!create_buffer(vbsize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, grid_vb, grid_vb_mem)) return false; - void* mp = nullptr; vkMapMemory(device, grid_vb_mem, 0, vbsize, 0, &mp); memcpy(mp, lines.data(), vbsize); vkUnmapMemory(device, grid_vb_mem); - - // $Globals UBO (set 0, binding 0), std140, 176 bytes; refilled each frame. - if (!create_buffer(176, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, grid_ubo, grid_ubo_mem)) return false; - vkMapMemory(device, grid_ubo_mem, 0, 176, 0, &grid_ubo_mapped); - - VkDescriptorSetLayoutBinding b{}; b.binding = 0; b.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; b.descriptorCount = 1; b.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; - VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; dslci.bindingCount = 1; dslci.pBindings = &b; - VK_CHECK(vkCreateDescriptorSetLayout(device, &dslci, nullptr, &grid_set_layout)); - VkDescriptorPoolSize psize{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1 }; - VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; dpci.maxSets = 1; dpci.poolSizeCount = 1; dpci.pPoolSizes = &psize; - VK_CHECK(vkCreateDescriptorPool(device, &dpci, nullptr, &grid_pool)); - VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; dsai.descriptorPool = grid_pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &grid_set_layout; - VK_CHECK(vkAllocateDescriptorSets(device, &dsai, &grid_set)); - VkDescriptorBufferInfo bi{ grid_ubo, 0, VK_WHOLE_SIZE }; - VkWriteDescriptorSet w{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; w.dstSet = grid_set; w.dstBinding = 0; w.descriptorCount = 1; w.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; w.pBufferInfo = &bi; - vkUpdateDescriptorSets(device, 1, &w, 0, nullptr); - - VkShaderModule vmod, fmod; - if (!create_shader_module("assets/shaders/generated/Grid.vertexMain.spv", vmod)) return false; - if (!create_shader_module("assets/shaders/generated/Grid.fragmentMain.spv", fmod)) return false; - VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; plci.setLayoutCount = 1; plci.pSetLayouts = &grid_set_layout; - VK_CHECK(vkCreatePipelineLayout(device, &plci, nullptr, &grid_pipeline_layout)); - VkPipelineShaderStageCreateInfo stages[2]{}; - stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vmod; stages[0].pName = "main"; - stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fmod; stages[1].pName = "main"; - VkVertexInputBindingDescription vib{ 0, sizeof(glm::vec3), VK_VERTEX_INPUT_RATE_VERTEX }; - VkVertexInputAttributeDescription via{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }; - VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; - vin.vertexAttributeDescriptionCount = 1; vin.pVertexAttributeDescriptions = &via; - VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST; - VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.scissorCount = 1; - VkDynamicState dyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; - VkPipelineDynamicStateCreateInfo dsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; dsci.dynamicStateCount = 2; dsci.pDynamicStates = dyn; - VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f; - VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - VkPipelineDepthStencilStateCreateInfo ds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO }; - ds.depthTestEnable = VK_TRUE; ds.depthWriteEnable = VK_FALSE; ds.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; // transparent lines: test but don't write - VkPipelineColorBlendAttachmentState cba{}; - cba.blendEnable = VK_TRUE; - cba.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; cba.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; cba.colorBlendOp = VK_BLEND_OP_ADD; - cba.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; cba.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; cba.alphaBlendOp = VK_BLEND_OP_ADD; - cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba; - VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; - gpci.stageCount = 2; gpci.pStages = stages; - gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps; - gpci.pDynamicState = &dsci; - gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; gpci.pDepthStencilState = &ds; - gpci.layout = grid_pipeline_layout; gpci.renderPass = scene_render_pass; gpci.subpass = 0; - VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &grid_pipeline); - vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr); - if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: grid pipeline creation failed ({})", (int)pr); return false; } - - // Lit sphere: unit UV-sphere mesh (pos+normal, stride 24), placed/scaled by - // u_Transform; samples the geodesic HDRI cubemap for ambient (binding 1). - { - std::vector sv; std::vector si; - const int RINGS = 24, SECTORS = 48; - for (int r = 0; r <= RINGS; ++r) - { - float phi = (float)std::numbers::pi * r / RINGS; - for (int s = 0; s <= SECTORS; ++s) - { - float theta = 2.0f * (float)std::numbers::pi * s / SECTORS; - float x = sinf(phi) * cosf(theta), y = cosf(phi), z = sinf(phi) * sinf(theta); - sv.push_back(x); sv.push_back(y); sv.push_back(z); // position (unit) - sv.push_back(x); sv.push_back(y); sv.push_back(z); // normal == position - } - } - for (int r = 0; r < RINGS; ++r) - for (int s = 0; s < SECTORS; ++s) - { - uint32_t a = r * (SECTORS + 1) + s, b = a + SECTORS + 1; - si.push_back(a); si.push_back(b); si.push_back(a + 1); - si.push_back(b); si.push_back(b + 1); si.push_back(a + 1); - } - sphere_index_count = (uint32_t)si.size(); - VkDeviceSize svsz = sv.size() * sizeof(float), sisz = si.size() * sizeof(uint32_t); - if (!create_buffer(svsz, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, sphere_vb, sphere_vb_mem)) return false; - if (!create_buffer(sisz, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, host_vis, sphere_ib, sphere_ib_mem)) return false; - void* sp = nullptr; - vkMapMemory(device, sphere_vb_mem, 0, svsz, 0, &sp); memcpy(sp, sv.data(), svsz); vkUnmapMemory(device, sphere_vb_mem); - vkMapMemory(device, sphere_ib_mem, 0, sisz, 0, &sp); memcpy(sp, si.data(), sisz); vkUnmapMemory(device, sphere_ib_mem); - - VkPhysicalDeviceProperties pdp{}; vkGetPhysicalDeviceProperties(physical, &pdp); - VkDeviceSize min_align = pdp.limits.minUniformBufferOffsetAlignment; - sphere_ubo_stride = ((208 + min_align - 1) / min_align) * min_align; // per-object slot - VkDeviceSize sphere_ubo_size = sphere_ubo_stride * MAX_SCENE_OBJECTS; - if (!create_buffer(sphere_ubo_size, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, sphere_ubo, sphere_ubo_mem)) return false; - vkMapMemory(device, sphere_ubo_mem, 0, sphere_ubo_size, 0, &sphere_ubo_mapped); - - VkDescriptorSetLayoutBinding sb[2]{}; - sb[0].binding = 0; sb[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; sb[0].descriptorCount = 1; sb[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; - sb[1].binding = 1; sb[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; sb[1].descriptorCount = 1; sb[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - VkDescriptorSetLayoutCreateInfo sdslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; sdslci.bindingCount = 2; sdslci.pBindings = sb; - VK_CHECK(vkCreateDescriptorSetLayout(device, &sdslci, nullptr, &sphere_set_layout)); - VkDescriptorPoolSize sps[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 } }; - VkDescriptorPoolCreateInfo sdpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; sdpci.maxSets = 1; sdpci.poolSizeCount = 2; sdpci.pPoolSizes = sps; - VK_CHECK(vkCreateDescriptorPool(device, &sdpci, nullptr, &sphere_pool)); - VkDescriptorSetAllocateInfo sdsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; sdsai.descriptorPool = sphere_pool; sdsai.descriptorSetCount = 1; sdsai.pSetLayouts = &sphere_set_layout; - VK_CHECK(vkAllocateDescriptorSets(device, &sdsai, &sphere_set)); - VkDescriptorBufferInfo sbi{ sphere_ubo, 0, 208 }; // per-draw size; the dynamic offset selects the object slot - VkDescriptorImageInfo sii{ cube_sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; - VkWriteDescriptorSet sw[2]{}; - sw[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; sw[0].dstSet = sphere_set; sw[0].dstBinding = 0; sw[0].descriptorCount = 1; sw[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; sw[0].pBufferInfo = &sbi; - sw[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; sw[1].dstSet = sphere_set; sw[1].dstBinding = 1; sw[1].descriptorCount = 1; sw[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; sw[1].pImageInfo = &sii; - vkUpdateDescriptorSets(device, 2, sw, 0, nullptr); - - VkShaderModule svmod, sfmod; - if (!create_shader_module("assets/shaders/generated/Sphere.vertexMain.spv", svmod)) return false; - if (!create_shader_module("assets/shaders/generated/Sphere.fragmentMain.spv", sfmod)) return false; - VkPipelineLayoutCreateInfo splci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; splci.setLayoutCount = 1; splci.pSetLayouts = &sphere_set_layout; - VK_CHECK(vkCreatePipelineLayout(device, &splci, nullptr, &sphere_pipeline_layout)); - VkPipelineShaderStageCreateInfo sstages[2]{}; - sstages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; sstages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; sstages[0].module = svmod; sstages[0].pName = "main"; - sstages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; sstages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; sstages[1].module = sfmod; sstages[1].pName = "main"; - VkVertexInputBindingDescription svib{ 0, 6 * (uint32_t)sizeof(float), VK_VERTEX_INPUT_RATE_VERTEX }; - VkVertexInputAttributeDescription svia[2] = { { 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32B32_SFLOAT, 3 * (uint32_t)sizeof(float) } }; - VkPipelineVertexInputStateCreateInfo svin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - svin.vertexBindingDescriptionCount = 1; svin.pVertexBindingDescriptions = &svib; - svin.vertexAttributeDescriptionCount = 2; svin.pVertexAttributeDescriptions = svia; - VkPipelineInputAssemblyStateCreateInfo sia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; sia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - VkPipelineViewportStateCreateInfo svps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; svps.viewportCount = 1; svps.scissorCount = 1; - VkDynamicState sdyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; - VkPipelineDynamicStateCreateInfo sdsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; sdsci.dynamicStateCount = 2; sdsci.pDynamicStates = sdyn; - VkPipelineRasterizationStateCreateInfo srs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; srs.polygonMode = VK_POLYGON_MODE_FILL; srs.cullMode = VK_CULL_MODE_NONE; srs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; srs.lineWidth = 1.0f; - VkPipelineMultisampleStateCreateInfo sms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; sms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - VkPipelineDepthStencilStateCreateInfo sds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO }; - sds.depthTestEnable = VK_TRUE; sds.depthWriteEnable = VK_TRUE; sds.depthCompareOp = VK_COMPARE_OP_LESS; - VkPipelineColorBlendAttachmentState scba{}; scba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - VkPipelineColorBlendStateCreateInfo scb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; scb.attachmentCount = 1; scb.pAttachments = &scba; - VkGraphicsPipelineCreateInfo sgpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; - sgpci.stageCount = 2; sgpci.pStages = sstages; - sgpci.pVertexInputState = &svin; sgpci.pInputAssemblyState = &sia; sgpci.pViewportState = &svps; - sgpci.pDynamicState = &sdsci; - sgpci.pRasterizationState = &srs; sgpci.pMultisampleState = &sms; sgpci.pColorBlendState = &scb; sgpci.pDepthStencilState = &sds; - sgpci.layout = sphere_pipeline_layout; sgpci.renderPass = scene_render_pass; sgpci.subpass = 0; - VkResult spr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &sgpci, nullptr, &sphere_pipeline); - vkDestroyShaderModule(device, svmod, nullptr); vkDestroyShaderModule(device, sfmod, nullptr); - if (spr != VK_SUCCESS) { DONUT_ERROR("Vulkan: sphere pipeline creation failed ({})", (int)spr); return false; } - } - - // Skybox: a unit cube (36 verts) sampling the HDRI cubemap; the vertex - // shader forces depth 1 (pos.xyww) so it sits behind all scene geometry. - { - const float cube[] = { - -1,-1,-1, 1,-1,-1, 1, 1,-1, 1, 1,-1, -1, 1,-1, -1,-1,-1, - -1,-1, 1, 1,-1, 1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1,-1, 1, - -1, 1, 1, -1, 1,-1, -1,-1,-1, -1,-1,-1, -1,-1, 1, -1, 1, 1, - 1, 1, 1, 1, 1,-1, 1,-1,-1, 1,-1,-1, 1,-1, 1, 1, 1, 1, - -1,-1,-1, 1,-1,-1, 1,-1, 1, 1,-1, 1, -1,-1, 1, -1,-1,-1, - -1, 1,-1, 1, 1,-1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1, 1,-1 - }; - if (!create_buffer(sizeof(cube), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, skybox_vb, skybox_vb_mem)) return false; - void* kp = nullptr; vkMapMemory(device, skybox_vb_mem, 0, sizeof(cube), 0, &kp); memcpy(kp, cube, sizeof(cube)); vkUnmapMemory(device, skybox_vb_mem); - - // $Globals UBO (128 B: u_Projection@0, u_View@64); u_Skybox at binding 1. - if (!create_buffer(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, skybox_ubo, skybox_ubo_mem)) return false; - vkMapMemory(device, skybox_ubo_mem, 0, 128, 0, &skybox_ubo_mapped); - - VkDescriptorSetLayoutBinding kb[2]{}; - kb[0].binding = 0; kb[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; kb[0].descriptorCount = 1; kb[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; - kb[1].binding = 1; kb[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; kb[1].descriptorCount = 1; kb[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - VkDescriptorSetLayoutCreateInfo kdslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; kdslci.bindingCount = 2; kdslci.pBindings = kb; - VK_CHECK(vkCreateDescriptorSetLayout(device, &kdslci, nullptr, &skybox_set_layout)); - VkDescriptorPoolSize kps[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 } }; - VkDescriptorPoolCreateInfo kdpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; kdpci.maxSets = 1; kdpci.poolSizeCount = 2; kdpci.pPoolSizes = kps; - VK_CHECK(vkCreateDescriptorPool(device, &kdpci, nullptr, &skybox_pool)); - VkDescriptorSetAllocateInfo kdsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; kdsai.descriptorPool = skybox_pool; kdsai.descriptorSetCount = 1; kdsai.pSetLayouts = &skybox_set_layout; - VK_CHECK(vkAllocateDescriptorSets(device, &kdsai, &skybox_set)); - VkDescriptorBufferInfo kbi{ skybox_ubo, 0, VK_WHOLE_SIZE }; - VkDescriptorImageInfo kii{ cube_sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; - VkWriteDescriptorSet kw[2]{}; - kw[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; kw[0].dstSet = skybox_set; kw[0].dstBinding = 0; kw[0].descriptorCount = 1; kw[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; kw[0].pBufferInfo = &kbi; - kw[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; kw[1].dstSet = skybox_set; kw[1].dstBinding = 1; kw[1].descriptorCount = 1; kw[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; kw[1].pImageInfo = &kii; - vkUpdateDescriptorSets(device, 2, kw, 0, nullptr); - - VkShaderModule kvmod, kfmod; - if (!create_shader_module("assets/shaders/generated/Skybox.vertexMain.spv", kvmod)) return false; - if (!create_shader_module("assets/shaders/generated/Skybox.fragmentMain.spv", kfmod)) return false; - VkPipelineLayoutCreateInfo kplci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; kplci.setLayoutCount = 1; kplci.pSetLayouts = &skybox_set_layout; - VK_CHECK(vkCreatePipelineLayout(device, &kplci, nullptr, &skybox_pipeline_layout)); - VkPipelineShaderStageCreateInfo kstages[2]{}; - kstages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; kstages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; kstages[0].module = kvmod; kstages[0].pName = "main"; - kstages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; kstages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; kstages[1].module = kfmod; kstages[1].pName = "main"; - VkVertexInputBindingDescription kvib{ 0, 3 * (uint32_t)sizeof(float), VK_VERTEX_INPUT_RATE_VERTEX }; - VkVertexInputAttributeDescription kvia{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }; - VkPipelineVertexInputStateCreateInfo kvin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; - kvin.vertexBindingDescriptionCount = 1; kvin.pVertexBindingDescriptions = &kvib; - kvin.vertexAttributeDescriptionCount = 1; kvin.pVertexAttributeDescriptions = &kvia; - VkPipelineInputAssemblyStateCreateInfo kia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; kia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - VkPipelineViewportStateCreateInfo kvps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; kvps.viewportCount = 1; kvps.scissorCount = 1; - VkDynamicState kdyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; - VkPipelineDynamicStateCreateInfo kdsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; kdsci.dynamicStateCount = 2; kdsci.pDynamicStates = kdyn; - VkPipelineRasterizationStateCreateInfo krs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; krs.polygonMode = VK_POLYGON_MODE_FILL; krs.cullMode = VK_CULL_MODE_NONE; krs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; krs.lineWidth = 1.0f; - VkPipelineMultisampleStateCreateInfo kms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; kms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - VkPipelineDepthStencilStateCreateInfo kds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO }; - kds.depthTestEnable = VK_FALSE; kds.depthWriteEnable = VK_FALSE; // background: neither tests nor writes depth - VkPipelineColorBlendAttachmentState kcba{}; kcba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - VkPipelineColorBlendStateCreateInfo kcb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; kcb.attachmentCount = 1; kcb.pAttachments = &kcba; - VkGraphicsPipelineCreateInfo kgpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; - kgpci.stageCount = 2; kgpci.pStages = kstages; - kgpci.pVertexInputState = &kvin; kgpci.pInputAssemblyState = &kia; kgpci.pViewportState = &kvps; - kgpci.pDynamicState = &kdsci; - kgpci.pRasterizationState = &krs; kgpci.pMultisampleState = &kms; kgpci.pColorBlendState = &kcb; kgpci.pDepthStencilState = &kds; - kgpci.layout = skybox_pipeline_layout; kgpci.renderPass = scene_render_pass; kgpci.subpass = 0; - VkResult kpr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &kgpci, nullptr, &skybox_pipeline); - vkDestroyShaderModule(device, kvmod, nullptr); vkDestroyShaderModule(device, kfmod, nullptr); - if (kpr != VK_SUCCESS) { DONUT_ERROR("Vulkan: skybox pipeline creation failed ({})", (int)kpr); return false; } - } - - DONUT_INFO("Vulkan: scene resources ready ({} grid verts, {} sphere indices, skybox)", grid_vertex_count, sphere_index_count); - return true; - } - - auto VulkanRenderer::Impl::update_scene_uniforms() -> void - { - struct GridUBO { - glm::mat4 view_projection; // 0 (SPIR-V RowMajor -> upload transposed) - glm::mat4 transform; // 64 - float grid_size; float p0[3]; // 128 - glm::vec3 grid_color; // 144 - float grid_alpha; // 156 - glm::vec3 camera_pos; // 160 - float p1; // 172 - } g{}; - static_assert(sizeof(GridUBO) == 176, "GridUBO std140 layout mismatch"); - - uint32_t h = swapchain_extent.height ? swapchain_extent.height : 1; - float aspect = (float)swapchain_extent.width / (float)h; - scene_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f); - glm::mat4 vp = scene_camera.get_projection_matrix() * scene_camera.get_view_matrix(); - - // Slang's mul(M,v) + the SPIR-V RowMajor decoration means glm's column-major - // matrices upload directly here (no transpose) to read as the intended M. - g.view_projection = vp; - g.transform = glm::mat4(1.0f); - g.grid_size = 50.0f; - g.grid_color = glm::vec3(0.55f, 0.55f, 0.6f); - g.grid_alpha = 0.75f; - g.camera_pos = scene_camera.get_orbital_position(); - if (grid_ubo_mapped) memcpy(grid_ubo_mapped, &g, sizeof(g)); - - struct SphereUBO { - glm::mat4 view_projection; // 0 - glm::mat4 transform; // 64 - glm::vec3 color; float specular; // 128, 140 - float emission; float p0[3]; // 144 - glm::vec3 light_pos; float p1; // 160, 172 - glm::vec3 camera_pos; int is_selected; // 176, 188 - glm::vec3 outline_color; float outline_width; // 192, 204 - }; - static_assert(sizeof(SphereUBO) == 208, "SphereUBO std140 layout mismatch"); - glm::vec3 cam_pos = scene_camera.get_orbital_position(); - uint32_t obj_count = (uint32_t)std::min(scene_objects.size(), (size_t)MAX_SCENE_OBJECTS); - for (uint32_t i = 0; i < obj_count; ++i) - { - const SceneObject& o = scene_objects[i]; - SphereUBO s{}; - s.view_projection = vp; // same no-transpose rule as the grid - s.transform = glm::translate(glm::mat4(1.0f), o.position) * glm::scale(glm::mat4(1.0f), glm::vec3(o.radius)); - s.color = o.color; - s.specular = 0.6f; - s.emission = 0.0f; - s.light_pos = glm::vec3(10.0f, 20.0f, 10.0f); - s.camera_pos = cam_pos; - s.is_selected = ((int)i == selected_object) ? 1 : 0; - s.outline_color = glm::vec3(1.0f, 1.0f, 0.0f); - s.outline_width = 0.15f; - if (sphere_ubo_mapped) memcpy((char*)sphere_ubo_mapped + (VkDeviceSize)i * sphere_ubo_stride, &s, sizeof(s)); - } - - struct SkyboxUBO { glm::mat4 projection; glm::mat4 view; } sky{}; - static_assert(sizeof(SkyboxUBO) == 128, "SkyboxUBO std140 layout mismatch"); - sky.projection = scene_camera.get_projection_matrix(); // no transpose (mul(M,v)) - sky.view = glm::mat4(glm::mat3(scene_camera.get_view_matrix())); // strip translation so the sky stays centered - if (skybox_ubo_mapped) memcpy(skybox_ubo_mapped, &sky, sizeof(sky)); - } - - auto VulkanRenderer::Impl::update_geodesic_uniforms() -> void - { - struct CamUBO { - glm::vec3 pos; float p0; glm::vec3 right; float p1; - glm::vec3 up; float p2; glm::vec3 fwd; float p3; - float tan_half_fov; float aspect; uint32_t moving; int p4; - } cam_data{}; - glm::vec3 pos, fwd; - if (camera.get_camera_mode() == CameraMode::FPS) - { - pos = camera.get_position(); - fwd = camera.get_forward_direction(); - } - else - { - pos = camera.get_orbital_position(); - fwd = glm::normalize(camera.get_orbital_target() - pos); - } - glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); - cam_data.pos = pos; cam_data.right = right; cam_data.up = glm::cross(right, fwd); cam_data.fwd = fwd; - cam_data.tan_half_fov = (float)tan(glm::radians(bh_params.fov_degrees * 0.5f)); - cam_data.aspect = (float)GEO_HI_W / (float)GEO_HI_H; // 16:9, same for both targets - cam_data.moving = user_moving ? 1u : 0u; - if (cam_mapped) memcpy(cam_mapped, &cam_data, sizeof(cam_data)); - - // Integration budget drives how deep the geodesics are traced: too few - // steps and grazing rays exhaust the loop mid-lensing, so the shader - // paints them with the HDRI (Geodesic.slang) and the hole looks sliced - // off "up to a certain depth" -- and near face-on it vanishes entirely. - // Measured: at ~8000 steps the disk disappears at steep poses, ~15000 is - // needed for it to resolve, INDEPENDENT of resolution. So we DON'T drop - // steps while moving (that would make the hole flicker out mid-drag); - // responsiveness comes from the lower-res target instead (see record). - // Sourced from settings.toml (max_steps_static), capped GPU-safe. - constexpr int kStepCeil = 15000; - struct SimUBO { int steps_moving; int steps_static; float early_exit; float time; } sim; - sim.steps_static = std::clamp(bh_params.quality_steps, 1000, kStepCeil); - sim.steps_moving = sim.steps_static; // same budget both frames; resolution is the lever - sim.early_exit = SettingsManager::get_early_exit_distance(); - sim.time = (float)(glfwGetTime() - start_time); - if (sim_mapped) memcpy(sim_mapped, &sim, sizeof(sim)); - - // Disk UBO, refilled live from the UI-tunable parameters. Layout matches - // the shader Disk struct: r1, r2, turbulence, slab thickness, brightness, - // temperature. Radii are in Schwarzschild radii (x SagA_rs). - const float SagA_rs = 1.269e10f; - float disk_data[8] = { - std::max(bh_params.disk_inner_rs, 3.0f) * SagA_rs, - std::max(bh_params.disk_outer_rs, bh_params.disk_inner_rs + 0.5f) * SagA_rs, - std::max(bh_params.turbulence, 0.0f), - SagA_rs * 0.1f, // slab half-thickness (fixed) - std::max(bh_params.brightness, 0.0f), - std::max(bh_params.temperature, 1000.0f), - 0.0f, 0.0f, - }; - if (disk_mapped) memcpy(disk_mapped, disk_data, sizeof(disk_data)); - } - - static double g_ScrollAccum = 0.0; - static GLFWscrollfun g_PrevScroll = nullptr; - static void donut_vk_scroll_callback(GLFWwindow* w, double x, double y) - { - if (g_PrevScroll) g_PrevScroll(w, x, y); // keep ImGui's scroll handling intact - g_ScrollAccum += y; - } - - auto VulkanRenderer::Impl::process_input() -> void - { - bool over_ui = imgui_init && ImGui::GetIO().WantCaptureMouse; - - double now = glfwGetTime(); - float dt = last_frame_time > 0.0 ? (float)(now - last_frame_time) : 0.0f; - last_frame_time = now; - - double mx = 0, my = 0; - glfwGetCursorPos(window, &mx, &my); - bool left_down = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS; - - if (!scene_mode && camera.get_camera_mode() == CameraMode::FPS) - { - // Left-drag looks around (screen-up looks up); WASD/QE move. - if (left_down && !left_was_down) { fps_last_x = mx; fps_last_y = my; } - if (left_down && !over_ui) - camera.on_mouse_move(float(mx - fps_last_x), float(fps_last_y - my)); - fps_last_x = mx; fps_last_y = my; - left_was_down = left_down; - - bool over_kb = imgui_init && ImGui::GetIO().WantCaptureKeyboard; - bool moved = false; - if (!over_kb && dt > 0.0f) - { - float mdt = dt * (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS ? 4.0f : 1.0f); - if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) { camera.move_forward(mdt); moved = true; } - if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) { camera.move_backward(mdt); moved = true; } - if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) { camera.move_left(mdt); moved = true; } - if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) { camera.move_right(mdt); moved = true; } - if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) { camera.move_up(mdt); moved = true; } - if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) { camera.move_down(mdt); moved = true; } - } - g_ScrollAccum = 0.0; // scroll unused in free-fly - user_moving = moved || (left_down && !over_ui); - } - else - { - Camera& cam = scene_mode ? scene_camera : camera; - if (left_down && !left_was_down && !over_ui) - cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, 0); - else if (!left_down && left_was_down) - cam.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0); - left_was_down = left_down; - - cam.process_orbital_mouse_move(mx, my); // orbits only while dragging - - double scroll = g_ScrollAccum; g_ScrollAccum = 0.0; - if (scroll != 0.0 && !over_ui) - cam.process_orbital_scroll(0.0, scroll); - - user_moving = cam.is_dragging() || cam.is_panning(); - } - } - - auto VulkanRenderer::Impl::destroy_geodesic_resources() -> void - { - if (grid_pipeline) vkDestroyPipeline(device, grid_pipeline, nullptr); - if (grid_pipeline_layout) vkDestroyPipelineLayout(device, grid_pipeline_layout, nullptr); - if (grid_pool) vkDestroyDescriptorPool(device, grid_pool, nullptr); - if (grid_set_layout) vkDestroyDescriptorSetLayout(device, grid_set_layout, nullptr); - if (grid_ubo_mapped) { vkUnmapMemory(device, grid_ubo_mem); grid_ubo_mapped = nullptr; } - if (grid_ubo) vkDestroyBuffer(device, grid_ubo, nullptr); - if (grid_ubo_mem) vkFreeMemory(device, grid_ubo_mem, nullptr); - if (grid_vb) vkDestroyBuffer(device, grid_vb, nullptr); - if (grid_vb_mem) vkFreeMemory(device, grid_vb_mem, nullptr); - - if (sphere_pipeline) vkDestroyPipeline(device, sphere_pipeline, nullptr); - if (sphere_pipeline_layout) vkDestroyPipelineLayout(device, sphere_pipeline_layout, nullptr); - if (sphere_pool) vkDestroyDescriptorPool(device, sphere_pool, nullptr); - if (sphere_set_layout) vkDestroyDescriptorSetLayout(device, sphere_set_layout, nullptr); - if (sphere_ubo_mapped) { vkUnmapMemory(device, sphere_ubo_mem); sphere_ubo_mapped = nullptr; } - if (sphere_ubo) vkDestroyBuffer(device, sphere_ubo, nullptr); - if (sphere_ubo_mem) vkFreeMemory(device, sphere_ubo_mem, nullptr); - if (sphere_ib) vkDestroyBuffer(device, sphere_ib, nullptr); - if (sphere_ib_mem) vkFreeMemory(device, sphere_ib_mem, nullptr); - if (sphere_vb) vkDestroyBuffer(device, sphere_vb, nullptr); - if (sphere_vb_mem) vkFreeMemory(device, sphere_vb_mem, nullptr); - - if (skybox_pipeline) vkDestroyPipeline(device, skybox_pipeline, nullptr); - if (skybox_pipeline_layout) vkDestroyPipelineLayout(device, skybox_pipeline_layout, nullptr); - if (skybox_pool) vkDestroyDescriptorPool(device, skybox_pool, nullptr); - if (skybox_set_layout) vkDestroyDescriptorSetLayout(device, skybox_set_layout, nullptr); - if (skybox_ubo_mapped) { vkUnmapMemory(device, skybox_ubo_mem); skybox_ubo_mapped = nullptr; } - if (skybox_ubo) vkDestroyBuffer(device, skybox_ubo, nullptr); - if (skybox_ubo_mem) vkFreeMemory(device, skybox_ubo_mem, nullptr); - if (skybox_vb) vkDestroyBuffer(device, skybox_vb, nullptr); - if (skybox_vb_mem) vkFreeMemory(device, skybox_vb_mem, nullptr); - - if (present_pipeline) vkDestroyPipeline(device, present_pipeline, nullptr); - if (present_pipeline_layout) vkDestroyPipelineLayout(device, present_pipeline_layout, nullptr); - if (present_pool) vkDestroyDescriptorPool(device, present_pool, nullptr); - if (present_set_layout) vkDestroyDescriptorSetLayout(device, present_set_layout, nullptr); - if (present_sampler) vkDestroySampler(device, present_sampler, nullptr); - - if (geo_pipeline) vkDestroyPipeline(device, geo_pipeline, nullptr); - if (geo_pipeline_layout) vkDestroyPipelineLayout(device, geo_pipeline_layout, nullptr); - if (geo_pool) vkDestroyDescriptorPool(device, geo_pool, nullptr); - if (geo_set_layout) vkDestroyDescriptorSetLayout(device, geo_set_layout, nullptr); - if (quad_vb) vkDestroyBuffer(device, quad_vb, nullptr); - if (quad_vb_mem) vkFreeMemory(device, quad_vb_mem, nullptr); - if (cube_sampler) vkDestroySampler(device, cube_sampler, nullptr); - if (cube_view) vkDestroyImageView(device, cube_view, nullptr); - if (cube_image) vkDestroyImage(device, cube_image, nullptr); - if (cube_mem) vkFreeMemory(device, cube_mem, nullptr); - if (cam_mapped) { vkUnmapMemory(device, cam_mem); cam_mapped = nullptr; } - if (sim_mapped) { vkUnmapMemory(device, sim_mem); sim_mapped = nullptr; } - if (disk_mapped) { vkUnmapMemory(device, disk_mem); disk_mapped = nullptr; } - VkBuffer ubos[4] = { cam_buf, disk_buf, obj_buf, sim_buf }; - VkDeviceMemory umem[4] = { cam_mem, disk_mem, obj_mem, sim_mem }; - for (int i = 0; i < 4; ++i) { if (ubos[i]) vkDestroyBuffer(device, ubos[i], nullptr); if (umem[i]) vkFreeMemory(device, umem[i], nullptr); } - for (GeoTarget* t : { &geo_lo, &geo_hi }) - { - if (t->fb) vkDestroyFramebuffer(device, t->fb, nullptr); - if (t->view) vkDestroyImageView(device, t->view, nullptr); - if (t->image) vkDestroyImage(device, t->image, nullptr); - if (t->mem) vkFreeMemory(device, t->mem, nullptr); - } - if (geo_render_pass) vkDestroyRenderPass(device, geo_render_pass, nullptr); - } - - auto VulkanRenderer::Impl::record_command_buffer(VkCommandBuffer cmd, uint32_t image_index, const glm::vec4& clear, ImDrawData* draw_data) -> bool - { - VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; - VK_CHECK(vkBeginCommandBuffer(cmd, &begin)); - - if (scene_mode) - { - // Scene view: opaque sphere (writes depth) then the transparent grid on - // top, into a color+depth swapchain pass, then ImGui. - VkClearValue cvs[2]{}; - cvs[0].color = { { clear.r, clear.g, clear.b, clear.a } }; - cvs[1].depthStencil = { 1.0f, 0 }; - VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; - rpbi.renderPass = scene_render_pass; rpbi.framebuffer = scene_framebuffers[image_index]; - rpbi.renderArea = { { 0, 0 }, swapchain_extent }; - rpbi.clearValueCount = 2; rpbi.pClearValues = cvs; - vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); - - // Negative-height viewport flips Y so the scene reads like the GL path. - VkViewport gvp{ 0, (float)swapchain_extent.height, (float)swapchain_extent.width, -(float)swapchain_extent.height, 0, 1 }; - VkRect2D gsc{ { 0, 0 }, swapchain_extent }; - vkCmdSetViewport(cmd, 0, 1, &gvp); - vkCmdSetScissor(cmd, 0, 1, &gsc); - - // Skybox background first (forces depth 1; no depth test/write). - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, skybox_pipeline); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, skybox_pipeline_layout, 0, 1, &skybox_set, 0, nullptr); - VkDeviceSize skoff = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &skybox_vb, &skoff); - vkCmdDraw(cmd, 36, 1, 0, 0); - - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sphere_pipeline); - VkDeviceSize soff = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &sphere_vb, &soff); - vkCmdBindIndexBuffer(cmd, sphere_ib, 0, VK_INDEX_TYPE_UINT32); - uint32_t obj_count = (uint32_t)std::min(scene_objects.size(), (size_t)MAX_SCENE_OBJECTS); - for (uint32_t i = 0; i < obj_count; ++i) - { - uint32_t dyn = (uint32_t)((VkDeviceSize)i * sphere_ubo_stride); // select this object's UBO slot - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sphere_pipeline_layout, 0, 1, &sphere_set, 1, &dyn); - vkCmdDrawIndexed(cmd, sphere_index_count, 1, 0, 0, 0); - } - - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, grid_pipeline); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, grid_pipeline_layout, 0, 1, &grid_set, 0, nullptr); - VkDeviceSize goff = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &grid_vb, &goff); - vkCmdDraw(cmd, grid_vertex_count, 1, 0, 0); - - if (draw_data) - ImGui_ImplVulkan_RenderDrawData(draw_data, cmd); - vkCmdEndRenderPass(cmd); - VK_CHECK(vkEndCommandBuffer(cmd)); - return true; - } - - // Geodesic offscreen pass. Progressive resolution: low-res while the - // camera moves (4x fewer pixels -> responsive), high-res once it settles - // (resolves the photon ring). Step count is the same for both (the disk - // vanishes below ~15000 steps at steep poses), so resolution is the lever. - GeoTarget& gt = user_moving ? geo_lo : geo_hi; - VkClearValue geo_clear{}; geo_clear.color = { { 0, 0, 0, 1 } }; - VkRenderPassBeginInfo grp{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; - grp.renderPass = geo_render_pass; grp.framebuffer = gt.fb; - grp.renderArea = { { 0, 0 }, { gt.w, gt.h } }; - grp.clearValueCount = 1; grp.pClearValues = &geo_clear; - vkCmdBeginRenderPass(cmd, &grp, VK_SUBPASS_CONTENTS_INLINE); - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, geo_pipeline); - VkViewport gvp{ 0, 0, (float)gt.w, (float)gt.h, 0, 1 }; VkRect2D gsc{ { 0, 0 }, { gt.w, gt.h } }; - vkCmdSetViewport(cmd, 0, 1, &gvp); vkCmdSetScissor(cmd, 0, 1, &gsc); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, geo_pipeline_layout, 0, 1, &geo_set, 0, nullptr); - VkDeviceSize off = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &quad_vb, &off); - vkCmdDraw(cmd, 6, 1, 0, 0); - vkCmdEndRenderPass(cmd); - - // Swapchain pass: upscale the geodesic image, then the ImGui UI on top - VkClearValue cv{}; cv.color = { { clear.r, clear.g, clear.b, clear.a } }; - VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; - rpbi.renderPass = render_pass; - rpbi.framebuffer = framebuffers[image_index]; - rpbi.renderArea = { { 0, 0 }, swapchain_extent }; - rpbi.clearValueCount = 1; rpbi.pClearValues = &cv; - vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); - vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, present_pipeline); - // Negative-height viewport flips the geodesic image vertically so the scene - // reads the same as the OpenGL path (Vulkan's clip space is Y-down). Only - // this draw is affected; ImGui sets its own viewport. - VkViewport vp{ 0, (float)swapchain_extent.height, (float)swapchain_extent.width, -(float)swapchain_extent.height, 0, 1 }; - VkRect2D scissor{ { 0, 0 }, swapchain_extent }; - vkCmdSetViewport(cmd, 0, 1, &vp); - vkCmdSetScissor(cmd, 0, 1, &scissor); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, present_pipeline_layout, 0, 1, >.present_set, 0, nullptr); - vkCmdBindVertexBuffers(cmd, 0, 1, &quad_vb, &off); - vkCmdDraw(cmd, 6, 1, 0, 0); - if (draw_data) - ImGui_ImplVulkan_RenderDrawData(draw_data, cmd); - vkCmdEndRenderPass(cmd); - - VK_CHECK(vkEndCommandBuffer(cmd)); - return true; - } - - auto VulkanRenderer::Impl::cleanup_swapchain() -> void - { - destroy_scene_targets(); - for (auto fb : framebuffers) vkDestroyFramebuffer(device, fb, nullptr); - framebuffers.clear(); - for (auto iv : image_views) vkDestroyImageView(device, iv, nullptr); - image_views.clear(); - if (render_pass) { vkDestroyRenderPass(device, render_pass, nullptr); render_pass = VK_NULL_HANDLE; } - if (swapchain) { vkDestroySwapchainKHR(device, swapchain, nullptr); swapchain = VK_NULL_HANDLE; } - } - - auto VulkanRenderer::Impl::recreate_swapchain() -> bool - { - // Wait until the window has a non-zero size (e.g. after un-minimizing). - int w = 0, h = 0; - glfwGetFramebufferSize(window, &w, &h); - while (w == 0 || h == 0) - { - glfwGetFramebufferSize(window, &w, &h); - glfwWaitEvents(); - } - width = w; height = h; - vkDeviceWaitIdle(device); - - cleanup_swapchain(); - // render_finished are tied to image count; recreate below via sync if it changed. - if (!create_swapchain()) return false; - if (!create_image_views()) return false; - if (!create_render_pass()) return false; - if (!create_framebuffers())return false; - if (!create_scene_targets())return false; - images_in_flight.assign(images.size(), VK_NULL_HANDLE); - return true; - } - - VulkanRenderer::VulkanRenderer() { m_impl = new Impl(); } - VulkanRenderer::~VulkanRenderer() { shutdown(); delete m_impl; m_impl = nullptr; } - - auto VulkanRenderer::init(void* glfwWindow, int width, int height) -> bool - { - Impl& v = *m_impl; - v.window = (GLFWwindow*)glfwWindow; - v.width = width; v.height = height; - - if (!v.create_instance()) return false; - if (!v.pick_physical_and_device()) return false; - if (!v.create_swapchain()) return false; - if (!v.create_image_views()) return false; - if (!v.create_render_pass()) return false; - if (!v.create_framebuffers()) return false; - if (!v.create_scene_targets()) return false; - if (!v.create_command_buffers()) return false; - if (!v.create_sync_objects()) return false; - if (!v.create_geodesic_resources()) return false; - if (!v.create_present_resources()) return false; - if (!v.create_scene_resources()) return false; - - DONUT_INFO("Vulkan renderer ready: {} swapchain images, {}x{}", - (int)v.images.size(), v.swapchain_extent.width, v.swapchain_extent.height); - return true; - } - - auto VulkanRenderer::init_ui() -> bool - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE) return false; - - VkDescriptorPoolSize pool_size{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 }; - VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; - dpci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; - dpci.maxSets = 1000; - dpci.poolSizeCount = 1; dpci.pPoolSizes = &pool_size; - VK_CHECK(vkCreateDescriptorPool(v.device, &dpci, nullptr, &v.imgui_pool)); - - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - ImGuiIO& io = ImGui::GetIO(); - io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; - io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; - ImGui::StyleColorsDark(); - - ImGui_ImplGlfw_InitForVulkan(v.window, true); - g_PrevScroll = glfwSetScrollCallback(v.window, donut_vk_scroll_callback); // chain ImGui + camera zoom - ImGui_ImplVulkan_InitInfo info{}; - info.ApiVersion = VK_API_VERSION_1_2; - info.Instance = v.instance; - info.PhysicalDevice = v.physical; - info.Device = v.device; - info.QueueFamily = v.graphics_family; - info.Queue = v.graphics_queue; - info.DescriptorPool = v.imgui_pool; - info.RenderPass = v.render_pass; - info.MinImageCount = 2; - info.ImageCount = (uint32_t)v.images.size(); - info.MSAASamples = VK_SAMPLE_COUNT_1_BIT; - if (!ImGui_ImplVulkan_Init(&info)) - { - DONUT_ERROR("Vulkan: ImGui_ImplVulkan_Init failed"); - return false; - } - - v.imgui_init = true; - DONUT_INFO("Vulkan: ImGui backend initialized"); - return true; - } - - auto VulkanRenderer::resize(int width, int height) -> void - { - m_impl->framebuffer_resized = true; - m_impl->width = width; m_impl->height = height; - } - - auto VulkanRenderer::set_hdri(const std::string& path) -> void - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE || v.geo_set == VK_NULL_HANDLE) return; - if (v.hdri_path == path) return; - v.rebuild_hdri_cubemap(path.c_str()); - } - - auto VulkanRenderer::current_hdri() const -> const std::string& - { - return m_impl->hdri_path; - } - - auto VulkanRenderer::set_free_fly(bool enabled) -> void - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE) return; - const bool is_fps = v.camera.get_camera_mode() == CameraMode::FPS; - if (enabled == is_fps) return; - - if (enabled) - { - // Seed the fly pose from the current orbital framing so the view is continuous. - glm::vec3 pos = v.camera.get_orbital_position(); - glm::vec3 fwd = glm::normalize(v.camera.get_orbital_target() - pos); - float pitch = glm::degrees(asin(glm::clamp(fwd.y, -1.0f, 1.0f))); - float yaw = glm::degrees(atan2(fwd.z, fwd.x)); - v.camera.set_camera_mode(CameraMode::FPS); - v.camera.set_movement_speed(2.0e10f); // scene spans ~1e11 units - v.camera.set_mouse_sensitivity(0.15f); - v.camera.set_position(pos); - v.camera.set_rotation(glm::vec3(pitch, yaw, 0.0f)); - } - else - { - v.camera.set_camera_mode(CameraMode::Orbital); // orbital state was left intact - } - } - - auto VulkanRenderer::is_free_fly() const -> bool - { - return m_impl->camera.get_camera_mode() == CameraMode::FPS; - } - - auto VulkanRenderer::set_scene_mode(bool enabled) -> void - { - if (m_impl) m_impl->scene_mode = enabled; - } - - auto VulkanRenderer::is_scene_mode() const -> bool - { - return m_impl && m_impl->scene_mode; - } - - auto VulkanRenderer::scene_objects() -> std::vector& - { - return m_impl->scene_objects; - } - - auto VulkanRenderer::set_selected_object(int index) -> void - { - if (m_impl) m_impl->selected_object = index; - } - - auto VulkanRenderer::black_hole_params() -> BlackHoleParams& - { - return m_impl->bh_params; - } - - auto VulkanRenderer::reset_camera() -> void - { - if (!m_impl) return; - Camera& cam = m_impl->camera; - cam.set_camera_mode(CameraMode::Orbital); // also flips is_free_fly() back - cam.set_orbital_radius(4e11); - cam.set_azimuth(0.0f); - cam.set_elevation(1.25f); - } - - auto VulkanRenderer::device_name() const -> const std::string& - { - return m_impl->gpu_name; - } - - auto VulkanRenderer::draw_frame(const glm::vec4& clear_color, const std::function& build_ui) -> void - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE) return; - - ImDrawData* draw_data = nullptr; - if (v.imgui_init) - { - ImGui_ImplVulkan_NewFrame(); - ImGui_ImplGlfw_NewFrame(); - ImGui::NewFrame(); - if (build_ui) build_ui(); - ImGui::Render(); - draw_data = ImGui::GetDrawData(); - } - - vkWaitForFences(v.device, 1, &v.in_flight[v.current_frame], VK_TRUE, UINT64_MAX); - - uint32_t image_index = 0; - VkResult r = vkAcquireNextImageKHR(v.device, v.swapchain, UINT64_MAX, - v.image_available[v.current_frame], VK_NULL_HANDLE, &image_index); - if (r == VK_ERROR_OUT_OF_DATE_KHR) { v.recreate_swapchain(); return; } - if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) { DONUT_ERROR("Vulkan: acquire failed ({})", (int)r); return; } - - if (v.images_in_flight[image_index] != VK_NULL_HANDLE) - vkWaitForFences(v.device, 1, &v.images_in_flight[image_index], VK_TRUE, UINT64_MAX); - v.images_in_flight[image_index] = v.in_flight[v.current_frame]; - - // The geodesic offscreen image is shared across frames in flight; wait for - // the previous frame to finish reading it before overwriting it this frame. - if (v.geo_in_use != VK_NULL_HANDLE) - vkWaitForFences(v.device, 1, &v.geo_in_use, VK_TRUE, UINT64_MAX); - v.process_input(); - if (v.scene_mode) v.update_scene_uniforms(); else v.update_geodesic_uniforms(); - - vkResetCommandBuffer(v.command_buffers[v.current_frame], 0); - if (!v.record_command_buffer(v.command_buffers[v.current_frame], image_index, clear_color, draw_data)) return; - - VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; - submit.waitSemaphoreCount = 1; - submit.pWaitSemaphores = &v.image_available[v.current_frame]; - submit.pWaitDstStageMask = &wait_stage; - submit.commandBufferCount = 1; - submit.pCommandBuffers = &v.command_buffers[v.current_frame]; - submit.signalSemaphoreCount = 1; - submit.pSignalSemaphores = &v.render_finished[image_index]; - - vkResetFences(v.device, 1, &v.in_flight[v.current_frame]); - if (vkQueueSubmit(v.graphics_queue, 1, &submit, v.in_flight[v.current_frame]) != VK_SUCCESS) - { DONUT_ERROR("Vulkan: queue submit failed"); return; } - v.geo_in_use = v.in_flight[v.current_frame]; - - VkPresentInfoKHR present{ VK_STRUCTURE_TYPE_PRESENT_INFO_KHR }; - present.waitSemaphoreCount = 1; - present.pWaitSemaphores = &v.render_finished[image_index]; - present.swapchainCount = 1; - present.pSwapchains = &v.swapchain; - present.pImageIndices = &image_index; - r = vkQueuePresentKHR(v.present_queue, &present); - if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR || v.framebuffer_resized) - { - v.framebuffer_resized = false; - v.recreate_swapchain(); - } - - v.current_frame = (v.current_frame + 1) % MAX_FRAMES_IN_FLIGHT; - } - - auto VulkanRenderer::shutdown() -> void - { - Impl& v = *m_impl; - if (v.device == VK_NULL_HANDLE) { if (v.instance && v.surface) { vkDestroySurfaceKHR(v.instance, v.surface, nullptr); v.surface = VK_NULL_HANDLE; } if (v.instance) { vkDestroyInstance(v.instance, nullptr); v.instance = VK_NULL_HANDLE; } return; } - - vkDeviceWaitIdle(v.device); - v.destroy_geodesic_resources(); - if (v.imgui_init) - { - ImGui_ImplVulkan_Shutdown(); - ImGui_ImplGlfw_Shutdown(); - ImGui::DestroyContext(); - v.imgui_init = false; - } - if (v.imgui_pool) { vkDestroyDescriptorPool(v.device, v.imgui_pool, nullptr); v.imgui_pool = VK_NULL_HANDLE; } - for (auto s : v.render_finished) vkDestroySemaphore(v.device, s, nullptr); - for (auto s : v.image_available) vkDestroySemaphore(v.device, s, nullptr); - for (auto f : v.in_flight) vkDestroyFence(v.device, f, nullptr); - v.render_finished.clear(); v.image_available.clear(); v.in_flight.clear(); - if (v.command_pool) { vkDestroyCommandPool(v.device, v.command_pool, nullptr); v.command_pool = VK_NULL_HANDLE; } - v.cleanup_swapchain(); - vkDestroyDevice(v.device, nullptr); v.device = VK_NULL_HANDLE; - if (v.surface) { vkDestroySurfaceKHR(v.instance, v.surface, nullptr); v.surface = VK_NULL_HANDLE; } - if (v.instance) { vkDestroyInstance(v.instance, nullptr); v.instance = VK_NULL_HANDLE; } - } -} diff --git a/src/platform/vulkan/vulkan_renderer.h b/src/platform/vulkan/vulkan_renderer.h deleted file mode 100644 index d583f07..0000000 --- a/src/platform/vulkan/vulkan_renderer.h +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include "rendering/renderer_backend.h" - -// Live-window Vulkan backend: owns the instance, surface, device, swapchain, -// render pass, framebuffers and per-frame synchronization, and drives the -// acquire -> record -> submit -> present loop. Implements the shared -// RendererBackend interface (SceneObject / BlackHoleParams live there). Pure-C++ -// header (no vulkan.h / glfw leak); the GLFW window is passed as an opaque handle. -namespace Donut -{ - // Must be called BEFORE glfwInit() when the Vulkan API is selected: points - // GLFW at the loader the app links against (GLFW's own dlopen fails on - // macOS/Homebrew) and configures the MoltenVK ICD / layer paths. - auto vulkan_prepare_glfw() -> void; - - class VulkanRenderer : public RendererBackend - { - public: - VulkanRenderer(); - ~VulkanRenderer() override; - - // glfwWindow must be a GLFW window created with GLFW_NO_API. - auto init(void* glfwWindow, int width, int height) -> bool override; - auto init_ui() -> bool override; - auto shutdown() -> void override; - auto resize(int width, int height) -> void override; - - auto draw_frame(const glm::vec4& clear_color, - const std::function& build_ui) -> void override; - - auto set_scene_mode(bool enabled) -> void override; - auto is_scene_mode() const -> bool override; - auto set_free_fly(bool enabled) -> void override; // seeds the fly pose from the orbital framing - auto is_free_fly() const -> bool override; - auto reset_camera() -> void override; - - auto set_hdri(const std::string& path) -> void override; // rebuilds the cubemap (device-idle) - auto current_hdri() const -> const std::string& override; - auto scene_objects() -> std::vector& override; - auto set_selected_object(int index) -> void override; - auto black_hole_params() -> BlackHoleParams& override; - - auto device_name() const -> const std::string& override; - - private: - struct Impl; - Impl* m_impl = nullptr; - }; -} diff --git a/src/rendering/black_hole_renderer.cpp b/src/rendering/black_hole_renderer.cpp new file mode 100644 index 0000000..46b002c --- /dev/null +++ b/src/rendering/black_hole_renderer.cpp @@ -0,0 +1,144 @@ +#include "black_hole_renderer.h" + +#include "core/log.h" +#include "core/settings_manager.h" + +#include +#include +#include + +namespace Donut +{ + using namespace RHI; + + namespace + { + constexpr float SagA_rs = 1.269e10f; // Schwarzschild radius of the modelled hole + + struct CamUBO { + glm::vec3 pos; float p0; glm::vec3 right; float p1; + glm::vec3 up; float p2; glm::vec3 fwd; float p3; + float tan_half_fov; float aspect; uint32_t moving; int p4; + }; + struct SimUBO { int steps_moving; int steps_static; float early_exit; float time; }; + } + + auto BlackHoleRenderer::init(RHI::Device& device) -> bool + { + m_device = &device; + + m_geo_lo = device.create_render_target(GEO_LO_W, GEO_LO_H, Format::RGBA8, false, Filter::Linear); + m_geo_hi = device.create_render_target(GEO_HI_W, GEO_HI_H, Format::RGBA8, false, Filter::Linear); + + // Fullscreen quad shared by the geodesic and present passes: pos.xy + uv. + const float quad[] = { + -1.f, 1.f, 0.f, 1.f, -1.f, -1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 0.f, + -1.f, 1.f, 0.f, 1.f, 1.f, -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f, + }; + m_quad_vb = device.create_buffer(BufferType::Vertex, sizeof(quad), quad); + + m_cam_ubo = device.create_buffer(BufferType::Uniform, 128); + m_disk_ubo = device.create_buffer(BufferType::Uniform, 32); + m_obj_ubo = device.create_buffer(BufferType::Uniform, 800); + m_sim_ubo = device.create_buffer(BufferType::Uniform, 16); + + // The single "object" is the black hole itself (position 0, radius = r_s). + std::vector obj(800, 0); + int num_objects = 1; std::memcpy(obj.data() + 0, &num_objects, 4); + float pos_radius[4] = { 0, 0, 0, SagA_rs }; std::memcpy(obj.data() + 16, pos_radius, 16); + float color[4] = { 0, 0, 0, 1 }; std::memcpy(obj.data() + 272, color, 16); + m_obj_ubo->update(obj.data(), obj.size()); + + { + PipelineDesc d; + d.shader = "geodesic"; + d.vertex_layout = { 16, { { 0, 2, 0 }, { 1, 2, 8 } } }; + d.resources = { + { ResourceKind::UniformBuffer, 0, "Camera" }, + { ResourceKind::UniformBuffer, 1, "Disk" }, + { ResourceKind::UniformBuffer, 2, "Objects" }, + { ResourceKind::UniformBuffer, 3, "Simulation" }, + { ResourceKind::Texture, 4, "u_HDRIEnvironment" }, + }; + d.topology = Topology::Triangles; + d.has_depth = false; // color-only off-screen target + m_geo_pipeline = device.create_pipeline(d); + } + { + PipelineDesc d; + d.shader = "textured_quad"; + d.vertex_layout = { 16, { { 0, 2, 0 }, { 1, 2, 8 } } }; + d.resources = { { ResourceKind::Texture, 0, "u_ScreenTexture" } }; + d.topology = Topology::Triangles; + d.has_depth = true; // swapchain target carries depth (unused here) + m_present_pipeline = device.create_pipeline(d); + } + + DONUT_INFO("BlackHoleRenderer ready ({}x{} moving / {}x{} settled)", + GEO_LO_W, GEO_LO_H, GEO_HI_W, GEO_HI_H); + return true; + } + + auto BlackHoleRenderer::render_geodesic(RHI::CommandList& cmd, const GeodesicView& view, + const BlackHoleParams& params, RHI::Texture* cubemap) -> void + { + CamUBO cam{}; + cam.pos = view.position; cam.right = view.right; cam.up = view.up; cam.fwd = view.forward; + cam.tan_half_fov = view.tan_half_fov; + cam.aspect = view.aspect; + cam.moving = view.moving ? 1u : 0u; + m_cam_ubo->update(&cam, sizeof(cam)); + + // Same integration budget whether moving or settled (the disk vanishes at + // steep poses below ~15000 steps); responsiveness comes from the lower-res + // target instead. Sourced from the UI, clamped GPU-safe. + constexpr int kStepCeil = 15000; + SimUBO sim{}; + sim.steps_static = std::clamp(params.quality_steps, 1000, kStepCeil); + sim.steps_moving = sim.steps_static; + sim.early_exit = SettingsManager::get_early_exit_distance(); + sim.time = view.time; + m_sim_ubo->update(&sim, sizeof(sim)); + + // Disk struct: r_in, r_out, turbulence, slab half-thickness, brightness, + // temperature (radii in Schwarzschild radii). + float disk[8] = { + std::max(params.disk_inner_rs, 3.0f) * SagA_rs, + std::max(params.disk_outer_rs, params.disk_inner_rs + 0.5f) * SagA_rs, + std::max(params.turbulence, 0.0f), + SagA_rs * 0.1f, + std::max(params.brightness, 0.0f), + std::max(params.temperature, 1000.0f), + 0.0f, 0.0f, + }; + m_disk_ubo->update(disk, sizeof(disk)); + + // Progressive resolution: small target while moving, large once settled. + RenderTarget* target = view.moving ? m_geo_lo.get() : m_geo_hi.get(); + m_last_target = target; + + cmd.begin_render_pass(target, glm::vec4(0, 0, 0, 1)); + cmd.set_viewport(0, 0, target->width(), target->height(), false); + cmd.bind_pipeline(m_geo_pipeline.get()); + cmd.bind_uniform(0, m_cam_ubo.get()); + cmd.bind_uniform(1, m_disk_ubo.get()); + cmd.bind_uniform(2, m_obj_ubo.get()); + cmd.bind_uniform(3, m_sim_ubo.get()); + cmd.bind_texture(4, cubemap); + cmd.bind_vertex_buffer(m_quad_vb.get()); + cmd.draw(6); + cmd.end_render_pass(); + } + + auto BlackHoleRenderer::blit(RHI::CommandList& cmd, int fb_width, int fb_height) -> void + { + if (!m_last_target) return; + // flip_y matches the shared top-left orientation (Vulkan flips via a + // negative-height viewport; GL is a no-op). + cmd.set_viewport(0, 0, fb_width, fb_height, true); + cmd.bind_pipeline(m_present_pipeline.get()); + cmd.bind_texture(0, m_last_target->color_texture()); + cmd.bind_vertex_buffer(m_quad_vb.get()); + cmd.draw(6); + } +} diff --git a/src/rendering/black_hole_renderer.h b/src/rendering/black_hole_renderer.h new file mode 100644 index 0000000..b9c158b --- /dev/null +++ b/src/rendering/black_hole_renderer.h @@ -0,0 +1,55 @@ +#pragma once + +#include "rhi.h" +#include "renderer_backend.h" // BlackHoleParams +#include + +namespace Donut +{ + // The black-hole view, written ONCE against the RHI. Each frame it ray-traces + // the geodesic shader into an off-screen target (progressive resolution: a + // small target while the camera moves, a larger one once it settles) and then + // upscales that target onto the swapchain. The Application owns the camera and + // the HDRI cubemap and passes them in. + struct GeodesicView + { + glm::vec3 position, right, up, forward; + float tan_half_fov = 1.0f; + float aspect = 16.0f / 9.0f; + bool moving = false; + float time = 0.0f; + }; + + class BlackHoleRenderer + { + public: + static constexpr int GEO_LO_W = 480, GEO_LO_H = 270; + static constexpr int GEO_HI_W = 960, GEO_HI_H = 540; + + auto init(RHI::Device& device) -> bool; + + // Off-screen geodesic pass (opens/closes its own render pass). Call before + // the swapchain pass; `cubemap` is the shared HDRI environment. + auto render_geodesic(RHI::CommandList& cmd, const GeodesicView& view, + const BlackHoleParams& params, RHI::Texture* cubemap) -> void; + + // Upscales the geodesic target onto the swapchain (call inside the open + // swapchain render pass, before ImGui). + auto blit(RHI::CommandList& cmd, int fb_width, int fb_height) -> void; + + private: + RHI::Device* m_device = nullptr; + + Ref m_geo_pipeline; + Ref m_geo_lo; + Ref m_geo_hi; + Ref m_quad_vb; + Ref m_cam_ubo; + Ref m_disk_ubo; + Ref m_obj_ubo; + Ref m_sim_ubo; + + Ref m_present_pipeline; + RHI::RenderTarget* m_last_target = nullptr; // target chosen this frame + }; +} diff --git a/src/rendering/framebuffer.cpp b/src/rendering/framebuffer.cpp deleted file mode 100644 index 3da4b48..0000000 --- a/src/rendering/framebuffer.cpp +++ /dev/null @@ -1,251 +0,0 @@ -#include "framebuffer.h" -#include "core/log.h" - -#include - -namespace Donut -{ - namespace Utils - { - static GLenum TextureTarget(bool multisampled) - { - return multisampled ? GL_TEXTURE_2D_MULTISAMPLE : GL_TEXTURE_2D; - } - - static void bind_texture(bool multisampled, uint32_t id) - { - glBindTexture(TextureTarget(multisampled), id); - } - - static void CreateTextures(bool multisampled, uint32_t* out_id, uint32_t count) - { - // glCreateTextures is 4.5 DSA; macOS caps at 4.1. Callers bind each - // texture (with the correct target) before use. - glGenTextures(count, out_id); - } - - static void AttachColorTexture(uint32_t id, int samples, GLenum internal_format, GLenum format, uint32_t width, uint32_t height, int index) - { - bool multisampled = samples > 1; - if (multisampled) - { - glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, internal_format, width, height, GL_FALSE); - } - else - { - // Integer color formats require an integer pixel type even when - // data is null, or macOS's strict core profile rejects the call. - GLenum type = (format == GL_RED_INTEGER) ? GL_INT : GL_UNSIGNED_BYTE; - glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width, height, 0, format, type, nullptr); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - } - - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + index, TextureTarget(multisampled), id, 0); - } - - static void AttachDepthTexture(uint32_t id, int samples, GLenum format, GLenum attachment_type, uint32_t width, uint32_t height) - { - bool multisampled = samples > 1; - if (multisampled) - { - glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, format, width, height, GL_FALSE); - } - else - { - // glTexStorage2D is 4.2; use mutable storage for macOS (4.1). - GLenum depth_format = (format == GL_DEPTH24_STENCIL8) ? GL_DEPTH_STENCIL : GL_DEPTH_COMPONENT; - GLenum depth_type = (format == GL_DEPTH24_STENCIL8) ? GL_UNSIGNED_INT_24_8 : GL_FLOAT; - glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, depth_format, depth_type, nullptr); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - } - - glFramebufferTexture2D(GL_FRAMEBUFFER, attachment_type, TextureTarget(multisampled), id, 0); - } - - static bool IsDepthFormat(FramebufferTextureFormat format) - { - switch (format) - { - case FramebufferTextureFormat::DEPTH24STENCIL8: return true; - } - return false; - } - - static GLenum DonutFBTextureFormatToGL(FramebufferTextureFormat format) - { - switch (format) - { - case FramebufferTextureFormat::RGBA8: return GL_RGBA8; - case FramebufferTextureFormat::RED_INTEGER: return GL_RED_INTEGER; - } - - return 0; - } - } - - Framebuffer::Framebuffer(const FramebufferSpecification& spec) - : m_specification(spec) - { - for (auto spec : m_specification.attachments.attachments) - { - if (!Utils::IsDepthFormat(spec.texture_format)) - m_color_attachment_specifications.emplace_back(spec); - else - m_depth_attachment_specification = spec; - } - - invalidate(); - } - - Framebuffer::~Framebuffer() - { - glDeleteFramebuffers(1, &m_renderer_id); - glDeleteTextures(static_cast(m_color_attachments.size()), m_color_attachments.data()); - glDeleteTextures(1, &m_depth_attachment); - } - - auto Framebuffer::invalidate() -> void - { - if (m_renderer_id) - { - glDeleteFramebuffers(1, &m_renderer_id); - glDeleteTextures(static_cast(m_color_attachments.size()), m_color_attachments.data()); - glDeleteTextures(1, &m_depth_attachment); - - m_color_attachments.clear(); - m_depth_attachment = 0; - } - - glGenFramebuffers(1, &m_renderer_id); // glCreateFramebuffers is 4.5 DSA; unavailable on macOS 4.1 - glBindFramebuffer(GL_FRAMEBUFFER, m_renderer_id); - - bool multisample = m_specification.Samples > 1; - - if (m_color_attachment_specifications.size()) - { - m_color_attachments.resize(m_color_attachment_specifications.size()); - Utils::CreateTextures(multisample, m_color_attachments.data(), static_cast(m_color_attachments.size())); - - for (size_t i = 0; i < m_color_attachments.size(); i++) - { - Utils::bind_texture(multisample, m_color_attachments[i]); - switch (m_color_attachment_specifications[i].texture_format) - { - case FramebufferTextureFormat::RGBA8: - Utils::AttachColorTexture(m_color_attachments[i], m_specification.Samples, GL_RGBA8, GL_RGBA, m_specification.Width, m_specification.Height, static_cast(i)); - break; - case FramebufferTextureFormat::RED_INTEGER: - Utils::AttachColorTexture(m_color_attachments[i], m_specification.Samples, GL_R32I, GL_RED_INTEGER, m_specification.Width, m_specification.Height, static_cast(i)); - break; - } - } - } - - if (m_depth_attachment_specification.texture_format != FramebufferTextureFormat::None) - { - Utils::CreateTextures(multisample, &m_depth_attachment, 1); - Utils::bind_texture(multisample, m_depth_attachment); - switch (m_depth_attachment_specification.texture_format) - { - case FramebufferTextureFormat::DEPTH24STENCIL8: - Utils::AttachDepthTexture(m_depth_attachment, m_specification.Samples, GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL_ATTACHMENT, m_specification.Width, m_specification.Height); - break; - } - } - - if (m_color_attachments.size() > 1) - { - GLenum buffers[4] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 }; - glDrawBuffers(static_cast(m_color_attachments.size()), buffers); - } - else if (m_color_attachments.empty()) - glDrawBuffer(GL_NONE); - - if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) - { - GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - switch (status) - { - case GL_FRAMEBUFFER_UNDEFINED: - DONUT_ERROR("Framebuffer is undefined"); - break; - case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: - DONUT_ERROR("Framebuffer has incomplete attachment"); - break; - case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: - DONUT_ERROR("Framebuffer is missing attachment"); - break; - case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: - DONUT_ERROR("Framebuffer has incomplete draw buffer"); - break; - case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: - DONUT_ERROR("Framebuffer has incomplete read buffer"); - break; - case GL_FRAMEBUFFER_UNSUPPORTED: - DONUT_ERROR("Framebuffer format is unsupported"); - break; - case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: - DONUT_ERROR("Framebuffer has incomplete multisample"); - break; - case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS: - DONUT_ERROR("Framebuffer has incomplete layer targets"); - break; - default: - DONUT_ERROR("Framebuffer is incomplete (unknown error: {})", status); - break; - } - } - - glBindFramebuffer(GL_FRAMEBUFFER, 0); - } - - auto Framebuffer::bind() -> void - { - glBindFramebuffer(GL_FRAMEBUFFER, m_renderer_id); - glViewport(0, 0, m_specification.Width, m_specification.Height); - } - - auto Framebuffer::unbind() -> void - { - glBindFramebuffer(GL_FRAMEBUFFER, 0); - } - - auto Framebuffer::resize(uint32_t width, uint32_t height) -> void - { - m_specification.Width = width; - m_specification.Height = height; - - invalidate(); - } - - auto Framebuffer::read_pixel(uint32_t attachment_index, int x, int y) -> int - { - glReadBuffer(GL_COLOR_ATTACHMENT0 + attachment_index); - int pixel_data; - glReadPixels(x, y, 1, 1, GL_RED_INTEGER, GL_INT, &pixel_data); - return pixel_data; - } - - auto Framebuffer::clear_attachment(uint32_t attachment_index, int value) -> void - { - // glClearTexImage is 4.4 and unavailable on macOS. clear the integer - // attachment by binding this framebuffer and clearing its draw buffer. - glBindFramebuffer(GL_FRAMEBUFFER, m_renderer_id); - glClearBufferiv(GL_COLOR, static_cast(attachment_index), &value); - } - - auto Framebuffer::create(const FramebufferSpecification& spec) -> Ref - { - return create_ref(spec); - } -}; diff --git a/src/rendering/framebuffer.h b/src/rendering/framebuffer.h deleted file mode 100644 index cf1fbef..0000000 --- a/src/rendering/framebuffer.h +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -#include "core/memory.h" -#include "texture.h" - -#include - -namespace Donut -{ - enum class FramebufferTextureFormat - { - None = 0, - - RGBA8, - RED_INTEGER, - - DEPTH24STENCIL8, - - Depth = DEPTH24STENCIL8 - }; - - struct FramebufferTextureSpecification - { - FramebufferTextureSpecification() = default; - FramebufferTextureSpecification(FramebufferTextureFormat format) - : texture_format(format) {} - - FramebufferTextureFormat texture_format = FramebufferTextureFormat::None; - }; - - struct FramebufferAttachmentSpecification - { - FramebufferAttachmentSpecification() = default; - FramebufferAttachmentSpecification(std::initializer_list attachments) - : attachments(attachments) {} - - std::vector attachments; - }; - - struct FramebufferSpecification - { - uint32_t Width = 0, Height = 0; - FramebufferAttachmentSpecification attachments; - uint32_t Samples = 1; - - bool SwapChainTarget = false; - }; - - class Framebuffer - { - public: - Framebuffer(const FramebufferSpecification& spec); - ~Framebuffer(); - - auto bind() -> void; - auto unbind() -> void; - - auto resize(uint32_t width, uint32_t height) -> void; - auto read_pixel(uint32_t attachment_index, int x, int y) -> int; - - auto clear_attachment(uint32_t attachment_index, int value) -> void; - auto get_color_attachment_renderer_id(uint32_t index = 0) const -> uint32_t { return m_color_attachments[index]; } - - auto get_specification() const -> const FramebufferSpecification& { return m_specification; } - - static auto create(const FramebufferSpecification& spec) -> Ref; - - private: - auto invalidate() -> void; - - uint32_t m_renderer_id = 0; - FramebufferSpecification m_specification; - - std::vector m_color_attachment_specifications; - FramebufferTextureSpecification m_depth_attachment_specification = FramebufferTextureFormat::None; - - std::vector m_color_attachments; - uint32_t m_depth_attachment = 0; - }; -}; diff --git a/src/rendering/index_buffer.cpp b/src/rendering/index_buffer.cpp deleted file mode 100644 index 2c933e0..0000000 --- a/src/rendering/index_buffer.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "index_buffer.h" - -#include - -namespace Donut -{ - IndexBuffer::IndexBuffer(const uint32_t* indices, uint32_t count) - : m_count(count) - { - glGenBuffers(1, &m_renderer_id); // glCreateBuffers is 4.5 DSA; unavailable on macOS 4.1 - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_renderer_id); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(uint32_t), indices, GL_STATIC_DRAW); - } - - IndexBuffer::~IndexBuffer() - { - glDeleteBuffers(1, &m_renderer_id); - } - - auto IndexBuffer::bind() const -> void - { - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_renderer_id); - } - - auto IndexBuffer::unbind() const -> void - { - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - } - - auto IndexBuffer::create(const uint32_t* indices, uint32_t count) -> IndexBuffer* - { - return new IndexBuffer(indices, count); - } -}; diff --git a/src/rendering/index_buffer.h b/src/rendering/index_buffer.h deleted file mode 100644 index 0b1b2b6..0000000 --- a/src/rendering/index_buffer.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include - -namespace Donut -{ - class IndexBuffer - { - public: - IndexBuffer(const uint32_t* indices, uint32_t count); - ~IndexBuffer(); - - auto bind() const -> void; - auto unbind() const -> void; - auto get_count() const -> uint32_t { return m_count; } - - static auto create(const uint32_t* indices, uint32_t count) -> IndexBuffer*; - - private: - uint32_t m_renderer_id = 0; - uint32_t m_count = 0; - }; -}; diff --git a/src/rendering/render_api.h b/src/rendering/render_api.h new file mode 100644 index 0000000..62742c9 --- /dev/null +++ b/src/rendering/render_api.h @@ -0,0 +1,25 @@ +#pragma once + +namespace Donut +{ + // Which graphics backend the app runs on, selected once at startup (from + // settings) before the window is created. The rendering itself goes through + // the RHI (rendering/rhi.h) and the shared renderers; this is only the + // backend selector the window + application branch on. + class RendererAPI + { + public: + enum class API + { + None = 0, + OpenGL = 1, + Vulkan = 2, + }; + + static auto get_api() -> API { return s_api; } + static auto set_api(API api) -> void { s_api = api; } + + private: + inline static API s_api = API::None; + }; +} diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp deleted file mode 100644 index 1c82447..0000000 --- a/src/rendering/renderer.cpp +++ /dev/null @@ -1,168 +0,0 @@ -#include "renderer.h" - -#include "core/log.h" - -#include -#include -#include - -namespace Donut -{ - RendererAPI::API RendererAPI::s_api = RendererAPI::API::OpenGL; - - auto RendererAPI::create() -> Scope - { - return create_scope(); - } - - auto RendererAPI::init() -> void - { - if (!glfwGetCurrentContext()) - { - DONUT_ERROR("No OpenGL context is current! Cannot initialize GLAD."); - return; - } - - if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) - { - DONUT_ERROR("Failed to initialize GLAD!"); - return; - } - - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_LESS); - - glEnable(GL_CULL_FACE); - glCullFace(GL_BACK); - glFrontFace(GL_CCW); - } - - auto RendererAPI::set_viewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) -> void - { - glViewport(x, y, width, height); - } - - auto RendererAPI::set_clear_color(const glm::vec4& color) -> void - { - glClearColor(color.r, color.g, color.b, color.a); - } - - auto RendererAPI::clear() -> void - { - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - } - - auto RendererAPI::enable_depth_test() -> void - { - glEnable(GL_DEPTH_TEST); - } - - auto RendererAPI::disable_depth_test() -> void - { - glDisable(GL_DEPTH_TEST); - } - - auto RendererAPI::set_face_culling(bool enabled) -> void - { - if (enabled) - { - glEnable(GL_CULL_FACE); - glCullFace(GL_BACK); - glFrontFace(GL_CCW); - } - else - glDisable(GL_CULL_FACE); - } - - auto RendererAPI::enable_blending() -> void - { - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - } - - auto RendererAPI::disable_blending() -> void - { - glDisable(GL_BLEND); - } - - auto RendererAPI::draw_indexed(const Ref& vertex_array, uint32_t index_count) -> void - { - uint32_t count = index_count ? index_count : vertex_array->get_index_buffer()->get_count(); - glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, nullptr); - glBindTexture(GL_TEXTURE_2D, 0); - } - - auto RendererAPI::draw_arrays(uint32_t vertex_count, uint32_t first) -> void - { - glDrawArrays(GL_TRIANGLES, first, vertex_count); - } - - auto RendererAPI::draw_lines(const Ref& vertex_array, uint32_t index_count) -> void - { - uint32_t count = index_count ? index_count : vertex_array->get_index_buffer()->get_count(); - glDrawElements(GL_LINES, count, GL_UNSIGNED_INT, nullptr); - } - - auto RendererAPI::bind_texture(uint32_t texture_id, uint32_t slot) -> void - { - glActiveTexture(GL_TEXTURE0 + slot); - glBindTexture(GL_TEXTURE_2D, texture_id); - } - - auto RendererAPI::bind_image_texture(uint32_t texture_id, uint32_t slot, bool read_only) -> void - { - // Image load/store is OpenGL 4.2; the pointer is null on macOS (4.1). - if (glBindImageTexture == nullptr) - return; - glBindImageTexture(slot, texture_id, 0, GL_FALSE, 0, - read_only ? GL_READ_ONLY : GL_WRITE_ONLY, GL_RGBA8); - } - - auto RendererAPI::read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height, - uint32_t format, uint32_t type, void* pixels) -> void - { - glReadPixels(x, y, width, height, format, type, pixels); - } - - auto Renderer::init() -> void - { - RenderCommand::init(); - RenderCommand::enable_depth_test(); - } - - auto Renderer::shutdown() -> void - { - } - - auto Renderer::on_window_resize(uint32_t width, uint32_t height) -> void - { - RenderCommand::set_viewport(0, 0, width, height); - } - - auto Renderer::submit(const Ref& shader, - const Ref& vertex_array, - const glm::mat4& transform, - const glm::mat4& view_projection) -> void - { - shader->bind(); - shader->set_mat4("u_ViewProjection", view_projection); - shader->set_mat4("u_Transform", transform); - - vertex_array->bind(); - RenderCommand::draw_indexed(vertex_array); - } - - Scope RenderCommand::s_renderer_api = RendererAPI::create(); - - auto Renderer::set_clear_color(const glm::vec4& color) -> void - { - RenderCommand::set_clear_color(color); - } - - auto Renderer::clear() -> void - { - RenderCommand::clear(); - } -}; diff --git a/src/rendering/renderer.h b/src/rendering/renderer.h deleted file mode 100644 index d7855a4..0000000 --- a/src/rendering/renderer.h +++ /dev/null @@ -1,155 +0,0 @@ -#pragma once - -#include "core/memory.h" -#include "vertex_array.h" -#include "shader.h" -#include "framebuffer.h" - -#include - -namespace Donut -{ - // Single concrete backend: the OpenGL renderer. (Vulkan runs through its own - // dedicated VulkanRenderer, not this immediate-mode command layer.) - class RendererAPI - { - public: - enum class API - { - None = 0, - OpenGL = 1, - Vulkan = 2, - }; - - public: - RendererAPI() = default; - ~RendererAPI() = default; - - auto init() -> void; - auto set_viewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) -> void; - auto set_clear_color(const glm::vec4& color) -> void; - auto clear() -> void; - auto enable_depth_test() -> void; - auto disable_depth_test() -> void; - auto set_face_culling(bool enabled) -> void; - auto enable_blending() -> void; - auto disable_blending() -> void; - - auto draw_indexed(const Ref& vertex_array, uint32_t index_count = 0) -> void; - auto draw_arrays(uint32_t vertex_count, uint32_t first = 0) -> void; - auto draw_lines(const Ref& vertex_array, uint32_t index_count = 0) -> void; - auto bind_texture(uint32_t texture_id, uint32_t slot = 0) -> void; - auto bind_image_texture(uint32_t texture_id, uint32_t slot = 0, bool read_only = false) -> void; - auto read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height, - uint32_t format, uint32_t type, void* pixels) -> void; - - inline static auto get_api() -> API { return s_api; } - inline static auto set_api(API api) -> void { s_api = api; } - static auto create() -> Scope; - - private: - static API s_api; - }; - - class RenderCommand - { - public: - inline static auto init() -> void - { - s_renderer_api->init(); - } - - inline static auto set_viewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) -> void - { - s_renderer_api->set_viewport(x, y, width, height); - } - - inline static auto set_clear_color(const glm::vec4& color) -> void - { - s_renderer_api->set_clear_color(color); - } - - inline static auto clear() -> void - { - s_renderer_api->clear(); - } - - inline static auto enable_depth_test() -> void - { - s_renderer_api->enable_depth_test(); - } - - inline static auto disable_depth_test() -> void - { - s_renderer_api->disable_depth_test(); - } - - inline static auto set_face_culling(bool enabled) -> void - { - s_renderer_api->set_face_culling(enabled); - } - - inline static auto enable_blending() -> void - { - s_renderer_api->enable_blending(); - } - - inline static auto disable_blending() -> void - { - s_renderer_api->disable_blending(); - } - - inline static auto draw_indexed(const Ref& vertex_array, uint32_t index_count = 0) -> void - { - s_renderer_api->draw_indexed(vertex_array, index_count); - } - - inline static auto draw_arrays(uint32_t vertex_count, uint32_t first = 0) -> void - { - s_renderer_api->draw_arrays(vertex_count, first); - } - - inline static auto draw_lines(const Ref& vertex_array, uint32_t index_count = 0) -> void - { - s_renderer_api->draw_lines(vertex_array, index_count); - } - - inline static auto bind_texture(uint32_t texture_id, uint32_t slot = 0) -> void - { - s_renderer_api->bind_texture(texture_id, slot); - } - - inline static auto bind_image_texture(uint32_t texture_id, uint32_t slot = 0, bool read_only = false) -> void - { - s_renderer_api->bind_image_texture(texture_id, slot, read_only); - } - - inline static auto read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height, - uint32_t format, uint32_t type, void* pixels) -> void - { - s_renderer_api->read_pixels(x, y, width, height, format, type, pixels); - } - - private: - static Scope s_renderer_api; - }; - - class Renderer - { - public: - static auto init() -> void; - static auto shutdown() -> void; - - static auto on_window_resize(uint32_t width, uint32_t height) -> void; - - static auto submit(const Ref& shader, - const Ref& vertex_array, - const glm::mat4& transform, - const glm::mat4& view_projection) -> void; - - static auto set_clear_color(const glm::vec4& color) -> void; - static auto clear() -> void; - - inline static auto get_api() -> RendererAPI::API { return RendererAPI::get_api(); } - }; -}; diff --git a/src/rendering/renderer_backend.h b/src/rendering/renderer_backend.h index 6a4f1af..d34983f 100644 --- a/src/rendering/renderer_backend.h +++ b/src/rendering/renderer_backend.h @@ -1,14 +1,15 @@ #pragma once #include -#include -#include -#include namespace Donut { - // A placeable/editable sphere in the world-builder scene view. Backend- - // agnostic: the app owns the list, every backend renders it the same way. + // Backend-agnostic scene/black-hole data shared by the Application and the + // portable renderers (SceneRenderer, BlackHoleRenderer). The rendering itself + // is written once on the RHI (rendering/rhi.h); these are just the inputs. + + // A placeable/editable sphere in the world-builder scene view. The app owns + // the list; SceneRenderer draws it the same way on every backend. struct SceneObject { glm::vec3 position = { 0.0f, 2.0f, 0.0f }; @@ -28,46 +29,4 @@ namespace Donut int quality_steps = 15000; // geodesic integration steps float fov_degrees = 60.0f; // camera field of view }; - - // The swappable high-level renderer. Each graphics backend (OpenGL, Vulkan, - // and later Metal / D3D12) implements this one interface; the Application, - // states and UI drive it identically, so the app behaves the same no matter - // which backend is active. Backends own their own device/swapchain/pipelines - // and translate these high-level calls into their native API. - class RendererBackend - { - public: - virtual ~RendererBackend() = default; - - // glfwWindow is an opaque GLFW handle (created NO_API for Vulkan/Metal, - // with a GL context for OpenGL). Returns false on failure. - virtual auto init(void* glfwWindow, int width, int height) -> bool = 0; - - // Creates the ImGui context + this backend's ImGui platform/render impl. - virtual auto init_ui() -> bool = 0; - virtual auto shutdown() -> void = 0; - virtual auto resize(int width, int height) -> void = 0; - - // Renders one frame: draws the current view (black hole or scene) from - // the shared state, then invokes build_ui to draw the ImGui panel on top. - virtual auto draw_frame(const glm::vec4& clear_color, - const std::function& build_ui) -> void = 0; - - // --- shared view / camera state ------------------------------------ - virtual auto set_scene_mode(bool enabled) -> void = 0; // black hole vs scene view - virtual auto is_scene_mode() const -> bool = 0; - virtual auto set_free_fly(bool enabled) -> void = 0; // free-fly vs orbital camera - virtual auto is_free_fly() const -> bool = 0; - virtual auto reset_camera() -> void = 0; - - // --- shared content ------------------------------------------------ - virtual auto set_hdri(const std::string& path) -> void = 0; - virtual auto current_hdri() const -> const std::string& = 0; - virtual auto scene_objects() -> std::vector& = 0; - virtual auto set_selected_object(int index) -> void = 0; - virtual auto black_hole_params() -> BlackHoleParams& = 0; - - // --- introspection for the UI -------------------------------------- - virtual auto device_name() const -> const std::string& = 0; - }; } diff --git a/src/rendering/rhi.h b/src/rendering/rhi.h new file mode 100644 index 0000000..65f10b5 --- /dev/null +++ b/src/rendering/rhi.h @@ -0,0 +1,132 @@ +#pragma once + +// Donut RHI (Render Hardware Interface): a small, portable GPU abstraction that +// OpenGL, Vulkan (and later Metal / D3D12) implement behind ONE interface, so the +// app's rendering — the black hole, the scene — is written ONCE on top and runs +// on any backend. The shape is modelled on the explicit APIs (baked pipelines, +// recorded command lists, explicit render targets); OpenGL emulates that, which +// is easy, whereas the reverse (making Vulkan speak GL's immediate mode) is not. + +#include "core/memory.h" +#include +#include +#include +#include +#include + +namespace Donut::RHI +{ + enum class Format + { + RGBA8, // 8-bit unorm colour (swapchain / LDR targets) + RGBA16F, // half-float colour (HDR / cubemap) + D32, // 32-bit depth + }; + + enum class BufferType { Vertex, Index, Uniform }; + enum class Topology { Triangles, Lines }; + enum class CullMode { None, Back, Front }; + enum class BlendMode { Opaque, AlphaBlend }; + enum class CompareOp { Always, Less, LessEqual }; + enum class Filter { Nearest, Linear }; + + // One vertex attribute; offsets/stride are in bytes. `components` is 1..4 floats. + struct VertexAttribute { uint32_t location; uint32_t components; uint32_t offset; }; + struct VertexLayout { uint32_t stride = 0; std::vector attributes; }; + + // A shader resource slot the pipeline exposes. `binding` is the set-0 binding + // index used by Vulkan; `name` is the GLSL block/sampler identifier used by the + // OpenGL backend (GL 4.1 has no binding qualifier, so it binds by name). The app + // declares both when building a pipeline (it knows its own shader). + enum class ResourceKind { UniformBuffer, Texture }; + struct ResourceSlot { ResourceKind kind; uint32_t binding; std::string name; }; + + // --- opaque GPU resources (backends subclass) -------------------------- + class Buffer { public: virtual ~Buffer() = default; virtual auto update(const void* data, size_t size) -> void = 0; }; + class Texture { public: virtual ~Texture() = default; }; + class Pipeline{ public: virtual ~Pipeline() = default; }; + + // An off-screen target (colour, optional depth). The swapchain is the implicit + // default target, addressed by passing nullptr to begin_render_pass. + class RenderTarget + { + public: + virtual ~RenderTarget() = default; + virtual auto width() const -> int = 0; + virtual auto height() const -> int = 0; + virtual auto color_texture() -> Texture* = 0; // to sample this target's colour + }; + + struct PipelineDesc + { + std::string shader; // base name; backend loads .spv or .glsl + VertexLayout vertex_layout; + std::vector resources; // UBO/texture slots the shader reads + Topology topology = Topology::Triangles; + CullMode cull = CullMode::None; + BlendMode blend = BlendMode::Opaque; + bool depth_test = false; + bool depth_write = false; + CompareOp depth_op = CompareOp::Less; + Format color_format = Format::RGBA8; // format of the target it renders into + bool has_depth = false; // target has a depth attachment + }; + + // Records draws for one frame. Obtained from Device::begin_frame (targets the + // swapchain) or created transiently for off-screen passes via the Device. + class CommandList + { + public: + virtual ~CommandList() = default; + + // target == nullptr renders to the swapchain; otherwise to the RenderTarget. + virtual auto begin_render_pass(RenderTarget* target, const glm::vec4& clear) -> void = 0; + virtual auto end_render_pass() -> void = 0; + + virtual auto bind_pipeline(Pipeline* pipeline) -> void = 0; + virtual auto set_viewport(int x, int y, int w, int h, bool flip_y = false) -> void = 0; + + virtual auto bind_uniform(uint32_t binding, Buffer* ubo) -> void = 0; + virtual auto bind_texture(uint32_t binding, Texture* texture) -> void = 0; + + virtual auto bind_vertex_buffer(Buffer* vb) -> void = 0; + virtual auto bind_index_buffer(Buffer* ib) -> void = 0; // 32-bit indices + + virtual auto draw(uint32_t vertex_count) -> void = 0; + virtual auto draw_indexed(uint32_t index_count) -> void = 0; + }; + + // The backend root: owns the device/swapchain and creates every resource. + class Device + { + public: + virtual ~Device() = default; + + // glfwWindow: opaque GLFW handle (GL context for OpenGL, NO_API otherwise). + virtual auto init(void* glfwWindow, int width, int height) -> bool = 0; + virtual auto shutdown() -> void = 0; + virtual auto resize(int width, int height) -> void = 0; + virtual auto wait_idle() -> void = 0; + + virtual auto create_buffer(BufferType type, size_t size, const void* data = nullptr) -> Ref = 0; + virtual auto create_texture(int width, int height, Format format, Filter filter, const void* data = nullptr) -> Ref = 0; + virtual auto create_cubemap_from_hdri(const std::string& equirect_path) -> Ref = 0; + virtual auto create_render_target(int width, int height, Format color, bool with_depth, + Filter sample_filter = Filter::Linear, int mip_levels = 1) -> Ref = 0; + virtual auto create_pipeline(const PipelineDesc& desc) -> Ref = 0; + + // Frame loop: begin_frame returns the frame's command list (or nullptr if + // the frame is skipped, e.g. minimised); record one or more render passes + // into it — off-screen passes into RenderTargets first, then the swapchain + // pass (begin_render_pass(nullptr, ...)) — then end_frame submits + presents. + virtual auto begin_frame(const glm::vec4& clear) -> CommandList* = 0; + virtual auto end_frame() -> void = 0; + + // ImGui lives above the RHI but its platform/render backend is per-device. + virtual auto init_imgui() -> void = 0; + virtual auto imgui_new_frame() -> void = 0; + virtual auto imgui_render(CommandList& swapchain_cmds) -> void = 0; + + virtual auto device_name() const -> const std::string& = 0; + }; +} diff --git a/src/rendering/rhi/rhi.h b/src/rendering/rhi/rhi.h deleted file mode 100644 index f64252c..0000000 --- a/src/rendering/rhi/rhi.h +++ /dev/null @@ -1,131 +0,0 @@ -#pragma once - -// Donut RHI (Render Hardware Interface): a small, portable GPU abstraction that -// OpenGL, Vulkan (and later Metal / D3D12) implement behind ONE interface, so the -// app's rendering — the black hole, the scene — is written ONCE on top and runs -// on any backend. The shape is modelled on the explicit APIs (baked pipelines, -// recorded command lists, explicit render targets); OpenGL emulates that, which -// is easy, whereas the reverse (making Vulkan speak GL's immediate mode) is not. - -#include "core/memory.h" -#include -#include -#include -#include -#include - -namespace Donut::rhi -{ - enum class Format - { - RGBA8, // 8-bit unorm colour (swapchain / LDR targets) - RGBA16F, // half-float colour (HDR / cubemap) - D32, // 32-bit depth - }; - - enum class BufferType { Vertex, Index, Uniform }; - enum class Topology { Triangles, Lines }; - enum class CullMode { None, Back, Front }; - enum class BlendMode { Opaque, AlphaBlend }; - enum class CompareOp { Always, Less, LessEqual }; - enum class Filter { Nearest, Linear }; - - // One vertex attribute; offsets/stride are in bytes. `components` is 1..4 floats. - struct VertexAttribute { uint32_t location; uint32_t components; uint32_t offset; }; - struct VertexLayout { uint32_t stride = 0; std::vector attributes; }; - - // A shader resource slot the pipeline exposes. `binding` is the set-0 binding - // index shared by the GLSL/SPIR-V (the app binds resources to these before a draw). - enum class ResourceKind { UniformBuffer, Texture }; - struct ResourceSlot { ResourceKind kind; uint32_t binding; }; - - // --- opaque GPU resources (backends subclass) -------------------------- - class Buffer { public: virtual ~Buffer() = default; virtual auto update(const void* data, size_t size) -> void = 0; }; - class Texture { public: virtual ~Texture() = default; }; - class Pipeline{ public: virtual ~Pipeline() = default; }; - - // An off-screen target (colour, optional depth). The swapchain is the implicit - // default target, addressed by passing nullptr to begin_render_pass. - class RenderTarget - { - public: - virtual ~RenderTarget() = default; - virtual auto width() const -> int = 0; - virtual auto height() const -> int = 0; - virtual auto color_texture() -> Texture* = 0; // to sample this target's colour - }; - - struct PipelineDesc - { - std::string shader; // base name; backend loads .spv or .glsl - VertexLayout vertex_layout; - std::vector resources; // UBO/texture slots the shader reads - Topology topology = Topology::Triangles; - CullMode cull = CullMode::None; - BlendMode blend = BlendMode::Opaque; - bool depth_test = false; - bool depth_write = false; - CompareOp depth_op = CompareOp::Less; - Format color_format = Format::RGBA8; // format of the target it renders into - bool has_depth = false; // target has a depth attachment - }; - - // Records draws for one frame. Obtained from Device::begin_frame (targets the - // swapchain) or created transiently for off-screen passes via the Device. - class CommandList - { - public: - virtual ~CommandList() = default; - - // target == nullptr renders to the swapchain; otherwise to the RenderTarget. - virtual auto begin_render_pass(RenderTarget* target, const glm::vec4& clear) -> void = 0; - virtual auto end_render_pass() -> void = 0; - - virtual auto bind_pipeline(Pipeline* pipeline) -> void = 0; - virtual auto set_viewport(int x, int y, int w, int h, bool flip_y = false) -> void = 0; - - virtual auto bind_uniform(uint32_t binding, Buffer* ubo) -> void = 0; - virtual auto bind_texture(uint32_t binding, Texture* texture) -> void = 0; - - virtual auto bind_vertex_buffer(Buffer* vb) -> void = 0; - virtual auto bind_index_buffer(Buffer* ib) -> void = 0; // 32-bit indices - - virtual auto draw(uint32_t vertex_count) -> void = 0; - virtual auto draw_indexed(uint32_t index_count) -> void = 0; - }; - - // The backend root: owns the device/swapchain and creates every resource. - class Device - { - public: - virtual ~Device() = default; - - // glfwWindow: opaque GLFW handle (GL context for OpenGL, NO_API otherwise). - virtual auto init(void* glfwWindow, int width, int height) -> bool = 0; - virtual auto shutdown() -> void = 0; - virtual auto resize(int width, int height) -> void = 0; - virtual auto wait_idle() -> void = 0; - - virtual auto create_buffer(BufferType type, size_t size, const void* data = nullptr) -> Ref = 0; - virtual auto create_texture(int width, int height, Format format, Filter filter, const void* data = nullptr) -> Ref = 0; - virtual auto create_cubemap_from_hdri(const std::string& equirect_path) -> Ref = 0; - virtual auto create_render_target(int width, int height, Format color, bool with_depth, - Filter sample_filter = Filter::Linear, int mip_levels = 1) -> Ref = 0; - virtual auto create_pipeline(const PipelineDesc& desc) -> Ref = 0; - - // Frame loop: begin_frame returns the swapchain command list (or nullptr if - // the frame is skipped, e.g. minimised); record into it, then end_frame - // submits + presents. record_offscreen runs a self-contained pass whose - // command list targets a RenderTarget (used before the swapchain frame). - virtual auto begin_frame(const glm::vec4& clear) -> CommandList* = 0; - virtual auto end_frame() -> void = 0; - virtual auto record_offscreen(const std::function& record) -> void = 0; - - // ImGui lives above the RHI but its platform/render backend is per-device. - virtual auto init_imgui() -> void = 0; - virtual auto imgui_new_frame() -> void = 0; - virtual auto imgui_render(CommandList& swapchain_cmds) -> void = 0; - - virtual auto device_name() const -> const std::string& = 0; - }; -} diff --git a/src/rendering/scene_renderer.cpp b/src/rendering/scene_renderer.cpp new file mode 100644 index 0000000..e276074 --- /dev/null +++ b/src/rendering/scene_renderer.cpp @@ -0,0 +1,217 @@ +#include "scene_renderer.h" + +#include "core/log.h" + +#include +#include +#include +#include +#include + +namespace Donut +{ + using namespace RHI; + + namespace + { + // std140 layouts, matching the Slang ConstantBuffer structs exactly. glm's + // column-major matrices upload directly (the shaders decorate the members + // row_major and use mul(M,v), so the same bytes read correctly on every + // backend, with no transpose). + struct GridUBO { + glm::mat4 view_projection; // 0 + glm::mat4 transform; // 64 + float grid_size; float p0[3]; // 128 + glm::vec3 grid_color; // 144 + float grid_alpha; // 156 + glm::vec3 camera_pos; // 160 + float p1; // 172 + }; + static_assert(sizeof(GridUBO) == 176, "GridUBO std140 layout mismatch"); + + struct SphereUBO { + glm::mat4 view_projection; // 0 + glm::mat4 transform; // 64 + glm::vec3 color; float specular; // 128, 140 + float emission; float p0[3]; // 144 + glm::vec3 light_pos; float p1; // 160, 172 + glm::vec3 camera_pos; int is_selected; // 176, 188 + glm::vec3 outline_color; float outline_width; // 192, 204 + }; + static_assert(sizeof(SphereUBO) == 208, "SphereUBO std140 layout mismatch"); + + struct SkyboxUBO { glm::mat4 projection; glm::mat4 view; }; + static_assert(sizeof(SkyboxUBO) == 128, "SkyboxUBO std140 layout mismatch"); + } + + auto SceneRenderer::init(RHI::Device& device) -> bool + { + m_device = &device; + + // Reference grid: line list on the XZ plane (+/-50, 1-unit cells). Grid.slang + // scales by u_GridSize/50, so u_GridSize = 50 keeps it 1:1. + { + std::vector lines; + const int N = 50; + for (int i = -N; i <= N; ++i) + { + lines.push_back({ (float)i, 0.0f, (float)-N }); + lines.push_back({ (float)i, 0.0f, (float) N }); + lines.push_back({ (float)-N, 0.0f, (float)i }); + lines.push_back({ (float) N, 0.0f, (float)i }); + } + m_grid_vertex_count = (int)lines.size(); + m_grid_vb = device.create_buffer(BufferType::Vertex, lines.size() * sizeof(glm::vec3), lines.data()); + m_grid_ubo = device.create_buffer(BufferType::Uniform, sizeof(GridUBO)); + + PipelineDesc d; + d.shader = "grid"; + d.vertex_layout = { sizeof(glm::vec3), { { 0, 3, 0 } } }; + d.resources = { { ResourceKind::UniformBuffer, 0, "GridU" } }; + d.topology = Topology::Lines; + d.blend = BlendMode::AlphaBlend; + d.depth_test = true; d.depth_write = false; d.depth_op = CompareOp::LessEqual; + d.has_depth = true; + m_grid_pipeline = device.create_pipeline(d); + } + + // Lit sphere: unit UV-sphere (pos + normal), placed/scaled per object. + { + std::vector sv; std::vector si; + const int RINGS = 24, SECTORS = 48; + for (int r = 0; r <= RINGS; ++r) + { + float phi = (float)std::numbers::pi * r / RINGS; + for (int s = 0; s <= SECTORS; ++s) + { + float theta = 2.0f * (float)std::numbers::pi * s / SECTORS; + float x = sinf(phi) * cosf(theta), y = cosf(phi), z = sinf(phi) * sinf(theta); + sv.push_back(x); sv.push_back(y); sv.push_back(z); // position (unit) + sv.push_back(x); sv.push_back(y); sv.push_back(z); // normal == position + } + } + for (int r = 0; r < RINGS; ++r) + for (int s = 0; s < SECTORS; ++s) + { + uint32_t a = r * (SECTORS + 1) + s, b = a + SECTORS + 1; + si.push_back(a); si.push_back(b); si.push_back(a + 1); + si.push_back(b); si.push_back(b + 1); si.push_back(a + 1); + } + m_sphere_index_count = (int)si.size(); + m_sphere_vb = device.create_buffer(BufferType::Vertex, sv.size() * sizeof(float), sv.data()); + m_sphere_ib = device.create_buffer(BufferType::Index, si.size() * sizeof(uint32_t), si.data()); + + PipelineDesc d; + d.shader = "sphere"; + d.vertex_layout = { 6 * sizeof(float), { { 0, 3, 0 }, { 1, 3, 3 * sizeof(float) } } }; + d.resources = { { ResourceKind::UniformBuffer, 0, "SphereU" }, + { ResourceKind::Texture, 1, "u_HDRIEnvironment" } }; + d.topology = Topology::Triangles; + d.depth_test = true; d.depth_write = true; d.depth_op = CompareOp::Less; + d.has_depth = true; + m_sphere_pipeline = device.create_pipeline(d); + } + + // Skybox: a unit cube (36 verts) sampling the HDRI cubemap; the vertex + // shader forces depth 1 (pos.xyww) so it sits behind all scene geometry. + { + const float cube[] = { + -1,-1,-1, 1,-1,-1, 1, 1,-1, 1, 1,-1, -1, 1,-1, -1,-1,-1, + -1,-1, 1, 1,-1, 1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1,-1, 1, + -1, 1, 1, -1, 1,-1, -1,-1,-1, -1,-1,-1, -1,-1, 1, -1, 1, 1, + 1, 1, 1, 1, 1,-1, 1,-1,-1, 1,-1,-1, 1,-1, 1, 1, 1, 1, + -1,-1,-1, 1,-1,-1, 1,-1, 1, 1,-1, 1, -1,-1, 1, -1,-1,-1, + -1, 1,-1, 1, 1,-1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1, 1,-1 + }; + m_skybox_vb = device.create_buffer(BufferType::Vertex, sizeof(cube), cube); + m_skybox_ubo = device.create_buffer(BufferType::Uniform, sizeof(SkyboxUBO)); + + PipelineDesc d; + d.shader = "skybox"; + d.vertex_layout = { 3 * sizeof(float), { { 0, 3, 0 } } }; + d.resources = { { ResourceKind::UniformBuffer, 0, "SkyboxU" }, + { ResourceKind::Texture, 1, "u_Skybox" } }; + d.topology = Topology::Triangles; + d.depth_test = false; d.depth_write = false; + d.has_depth = true; + m_skybox_pipeline = device.create_pipeline(d); + } + + DONUT_INFO("SceneRenderer ready ({} grid verts, {} sphere indices, skybox)", + m_grid_vertex_count, m_sphere_index_count); + return true; + } + + auto SceneRenderer::render(RHI::CommandList& cmd, const CameraView& cam, + const std::vector& objects, int selected, + RHI::Texture* cubemap) -> void + { + // Full-window viewport; flip_y lets the backend match the shared top-left + // orientation (Vulkan flips via a negative-height viewport, GL is a no-op). + cmd.set_viewport(0, 0, cam.fb_width, cam.fb_height, true); + + glm::mat4 vp = cam.projection * cam.view; + + // Skybox background first (depth 1, no depth test/write). + { + SkyboxUBO sky{}; + sky.projection = cam.projection; + sky.view = glm::mat4(glm::mat3(cam.view)); // strip translation + m_skybox_ubo->update(&sky, sizeof(sky)); + cmd.bind_pipeline(m_skybox_pipeline.get()); + cmd.bind_uniform(0, m_skybox_ubo.get()); + cmd.bind_texture(1, cubemap); + cmd.bind_vertex_buffer(m_skybox_vb.get()); + cmd.draw(36); + } + + // Opaque spheres (write depth). One UBO per object slot avoids aliasing the + // per-draw uniforms across the deferred command stream. + { + int count = std::min((int)objects.size(), MAX_OBJECTS); + while ((int)m_sphere_ubos.size() < count) + m_sphere_ubos.push_back(m_device->create_buffer(BufferType::Uniform, sizeof(SphereUBO))); + + cmd.bind_pipeline(m_sphere_pipeline.get()); + for (int i = 0; i < count; ++i) + { + const SceneObject& o = objects[i]; + SphereUBO s{}; + s.view_projection = vp; + s.transform = glm::translate(glm::mat4(1.0f), o.position) + * glm::scale(glm::mat4(1.0f), glm::vec3(o.radius)); + s.color = o.color; + s.specular = 0.6f; + s.emission = 0.0f; + s.light_pos = glm::vec3(10.0f, 20.0f, 10.0f); + s.camera_pos = cam.position; + s.is_selected = (i == selected) ? 1 : 0; + s.outline_color = glm::vec3(1.0f, 1.0f, 0.0f); + s.outline_width = 0.15f; + m_sphere_ubos[i]->update(&s, sizeof(s)); + + cmd.bind_uniform(0, m_sphere_ubos[i].get()); + cmd.bind_texture(1, cubemap); + cmd.bind_vertex_buffer(m_sphere_vb.get()); + cmd.bind_index_buffer(m_sphere_ib.get()); + cmd.draw_indexed(m_sphere_index_count); + } + } + + // Transparent grid on top (tests depth, doesn't write). + { + GridUBO g{}; + g.view_projection = vp; + g.transform = glm::mat4(1.0f); + g.grid_size = 50.0f; + g.grid_color = glm::vec3(0.55f, 0.55f, 0.6f); + g.grid_alpha = 0.75f; + g.camera_pos = cam.position; + m_grid_ubo->update(&g, sizeof(g)); + cmd.bind_pipeline(m_grid_pipeline.get()); + cmd.bind_uniform(0, m_grid_ubo.get()); + cmd.bind_vertex_buffer(m_grid_vb.get()); + cmd.draw(m_grid_vertex_count); + } + } +} diff --git a/src/rendering/scene_renderer.h b/src/rendering/scene_renderer.h new file mode 100644 index 0000000..eafd56e --- /dev/null +++ b/src/rendering/scene_renderer.h @@ -0,0 +1,56 @@ +#pragma once + +#include "rhi.h" +#include "renderer_backend.h" // SceneObject +#include +#include + +namespace Donut +{ + // The world-builder scene view (skybox + lit spheres + reference grid), + // written ONCE against the RHI so it renders identically on every backend. + // The Application owns the camera, the object list and the HDRI cubemap and + // passes them in each frame; this class owns only the GPU resources (meshes, + // pipelines, per-object uniform buffers) and records the draws. + struct CameraView + { + glm::mat4 view; + glm::mat4 projection; + glm::vec3 position; + int fb_width = 1; + int fb_height = 1; + }; + + class SceneRenderer + { + public: + // Builds the grid/sphere/skybox meshes and pipelines on the given device. + auto init(RHI::Device& device) -> bool; + + // Records the scene into the currently-open swapchain render pass. `cubemap` + // is the shared HDRI environment (sphere ambient + skybox background). + auto render(RHI::CommandList& cmd, const CameraView& cam, + const std::vector& objects, int selected, + RHI::Texture* cubemap) -> void; + + private: + static constexpr int MAX_OBJECTS = 64; + + RHI::Device* m_device = nullptr; + + Ref m_grid_pipeline; + Ref m_grid_vb; + Ref m_grid_ubo; + int m_grid_vertex_count = 0; + + Ref m_sphere_pipeline; + Ref m_sphere_vb; + Ref m_sphere_ib; + int m_sphere_index_count = 0; + std::vector> m_sphere_ubos; // one per object slot + + Ref m_skybox_pipeline; + Ref m_skybox_vb; + Ref m_skybox_ubo; + }; +} diff --git a/src/rendering/texture_manager.cpp b/src/rendering/texture_manager.cpp deleted file mode 100644 index 3afba0d..0000000 --- a/src/rendering/texture_manager.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "texture_manager.h" -#include "renderer.h" - -#include - -namespace Donut -{ - auto TextureManager::create_texture(uint32_t width, uint32_t height) -> Ref - { - return create_ref(width, height); - } - - auto TextureManager::bind_texture(uint32_t texture_id, uint32_t slot) -> void - { - RenderCommand::bind_texture(texture_id, slot); - } - - auto TextureManager::bind_image_texture(uint32_t texture_id, uint32_t slot, bool read_only) -> void - { - RenderCommand::bind_image_texture(texture_id, slot, read_only); - } - - auto TextureManager::set_texture_data(uint32_t texture_id, void* data, uint32_t width, uint32_t height) -> void - { - glBindTexture(GL_TEXTURE_2D, texture_id); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data); - } - - auto TextureManager::resize_texture(uint32_t texture_id, uint32_t width, uint32_t height) -> void - { - glBindTexture(GL_TEXTURE_2D, texture_id); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); - } -}; diff --git a/src/rendering/texture_manager.h b/src/rendering/texture_manager.h deleted file mode 100644 index 02c32b1..0000000 --- a/src/rendering/texture_manager.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "core/memory.h" -#include "rendering/texture.h" -#include - -namespace Donut -{ - class TextureManager - { - public: - static auto create_texture(uint32_t width, uint32_t height) -> Ref; - - static auto bind_texture(uint32_t texture_id, uint32_t slot = 0) -> void; - static auto bind_image_texture(uint32_t texture_id, uint32_t slot = 0, bool read_only = false) -> void; - static auto set_texture_data(uint32_t texture_id, void* data, uint32_t width, uint32_t height) -> void; - static auto resize_texture(uint32_t texture_id, uint32_t width, uint32_t height) -> void; - }; -}; diff --git a/src/rendering/uniform_buffer.cpp b/src/rendering/uniform_buffer.cpp deleted file mode 100644 index c4118b8..0000000 --- a/src/rendering/uniform_buffer.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "uniform_buffer.h" - -#include - -namespace Donut -{ - UniformBuffer::UniformBuffer(uint32_t size, uint32_t binding) - : m_size(size), m_binding(binding) - { - glGenBuffers(1, &m_renderer_id); - glBindBuffer(GL_UNIFORM_BUFFER, m_renderer_id); - glBufferData(GL_UNIFORM_BUFFER, size, nullptr, GL_DYNAMIC_DRAW); - glBindBufferBase(GL_UNIFORM_BUFFER, binding, m_renderer_id); - } - - UniformBuffer::~UniformBuffer() - { - glDeleteBuffers(1, &m_renderer_id); - } - - auto UniformBuffer::set_data(const void* data, uint32_t size, uint32_t offset) -> void - { - glBindBuffer(GL_UNIFORM_BUFFER, m_renderer_id); - glBufferSubData(GL_UNIFORM_BUFFER, offset, size, data); - } - - auto UniformBuffer::bind(uint32_t binding) -> void - { - glBindBufferBase(GL_UNIFORM_BUFFER, binding, m_renderer_id); - } - - auto UniformBuffer::create(uint32_t size, uint32_t binding) -> Ref - { - return create_ref(size, binding); - } -}; diff --git a/src/rendering/uniform_buffer.h b/src/rendering/uniform_buffer.h deleted file mode 100644 index b5617b9..0000000 --- a/src/rendering/uniform_buffer.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "core/memory.h" -#include - -namespace Donut -{ - class UniformBuffer - { - public: - UniformBuffer(uint32_t size, uint32_t binding); - ~UniformBuffer(); - - auto set_data(const void* data, uint32_t size, uint32_t offset = 0) -> void; - auto bind(uint32_t binding) -> void; - - static auto create(uint32_t size, uint32_t binding) -> Ref; - - private: - uint32_t m_renderer_id = 0; - uint32_t m_size = 0; - uint32_t m_binding = 0; - }; -}; diff --git a/src/rendering/vertex_array.cpp b/src/rendering/vertex_array.cpp deleted file mode 100644 index cddcf35..0000000 --- a/src/rendering/vertex_array.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include - -#include "vertex_array.h" - -namespace Donut -{ - VertexArray::VertexArray() - { - // glCreateVertexArrays is 4.5 DSA; macOS caps at 4.1. glGenVertexArrays - // reserves the name and the VAO is created on first bind (done below). - glGenVertexArrays(1, &m_renderer_id); - } - - VertexArray::~VertexArray() - { - glDeleteVertexArrays(1, &m_renderer_id); - } - - auto VertexArray::bind() const -> void - { - glBindVertexArray(m_renderer_id); - } - - auto VertexArray::unbind() const -> void - { - glBindVertexArray(0); - } - - auto VertexArray::add_vertex_buffer(const Ref& vertex_buffer) -> void - { - glBindVertexArray(m_renderer_id); - vertex_buffer->bind(); - - const auto& layout = vertex_buffer->get_layout(); - for (const auto& element : layout.get_elements()) - { - glEnableVertexAttribArray(m_vertex_buffer_index); - glVertexAttribPointer(m_vertex_buffer_index, - element.count, - element.type, - element.normalized ? GL_TRUE : GL_FALSE, - layout.get_stride(), - reinterpret_cast(static_cast(element.offset))); - m_vertex_buffer_index++; - } - - m_vertex_buffers.push_back(vertex_buffer); - } - - auto VertexArray::set_index_buffer(const Ref& index_buffer) -> void - { - glBindVertexArray(m_renderer_id); - index_buffer->bind(); - m_index_buffer = index_buffer; - } - - auto VertexArray::create() -> VertexArray* - { - return new VertexArray(); - } -}; diff --git a/src/rendering/vertex_array.h b/src/rendering/vertex_array.h deleted file mode 100644 index 1f06758..0000000 --- a/src/rendering/vertex_array.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include "core/memory.h" - -#include "vertex_buffer.h" -#include "index_buffer.h" - -#include - -namespace Donut -{ - class VertexArray - { - public: - VertexArray(); - ~VertexArray(); - - auto bind() const -> void; - auto unbind() const -> void; - - auto add_vertex_buffer(const Ref& vertex_buffer) -> void; - auto set_index_buffer(const Ref& index_buffer) -> void; - - auto get_vertex_buffers() const -> const std::vector>& { return m_vertex_buffers; } - auto get_index_buffer() const -> const Ref& { return m_index_buffer; } - - static auto create() -> VertexArray*; - - private: - uint32_t m_renderer_id = 0; - uint32_t m_vertex_buffer_index = 0; - std::vector> m_vertex_buffers; - Ref m_index_buffer; - }; -}; diff --git a/src/rendering/vertex_buffer.cpp b/src/rendering/vertex_buffer.cpp deleted file mode 100644 index 1bc471c..0000000 --- a/src/rendering/vertex_buffer.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "vertex_buffer.h" - -#include - -namespace Donut -{ - auto VertexBufferElement::get_size_of_type(uint32_t type) -> uint32_t - { - switch (type) - { - case 0x1406: return 4; // GL_FLOAT - case 0x1405: return 4; // GL_UNSIGNED_INT - case 0x1401: return 1; // GL_UNSIGNED_BYTE - default: return 0; - } - } - - VertexBuffer::VertexBuffer(const void* data, uint32_t size) - { - glGenBuffers(1, &m_renderer_id); // glCreateBuffers is 4.5 DSA; unavailable on macOS 4.1 - glBindBuffer(GL_ARRAY_BUFFER, m_renderer_id); - glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW); - } - - VertexBuffer::~VertexBuffer() - { - glDeleteBuffers(1, &m_renderer_id); - } - - auto VertexBuffer::bind() const -> void - { - glBindBuffer(GL_ARRAY_BUFFER, m_renderer_id); - } - - auto VertexBuffer::unbind() const -> void - { - glBindBuffer(GL_ARRAY_BUFFER, 0); - } - - auto VertexBuffer::set_data(const void* data, uint32_t size) -> void - { - glBindBuffer(GL_ARRAY_BUFFER, m_renderer_id); - glBufferSubData(GL_ARRAY_BUFFER, 0, size, data); - } - - auto VertexBuffer::create(const void* data, uint32_t size) -> VertexBuffer* - { - return new VertexBuffer(data, size); - } -}; diff --git a/src/rendering/vertex_buffer.h b/src/rendering/vertex_buffer.h deleted file mode 100644 index 4e61700..0000000 --- a/src/rendering/vertex_buffer.h +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once - -#include -#include - -namespace Donut -{ - struct VertexBufferElement - { - uint32_t type; - uint32_t count; - uint8_t normalized; - uint32_t offset; - - static auto get_size_of_type(uint32_t type) -> uint32_t; - }; - - class VertexBufferLayout - { - public: - VertexBufferLayout() = default; - ~VertexBufferLayout() = default; - - template - auto push(uint32_t count) -> void - { - static_assert(false); - } - - template<> - void push(uint32_t count) - { - m_elements.push_back({ 0x1406, count, 0, m_stride }); // GL_FLOAT - m_stride += count * VertexBufferElement::get_size_of_type(0x1406); - } - - template<> - void push(uint32_t count) - { - m_elements.push_back({ 0x1405, count, 0, m_stride }); // GL_UNSIGNED_INT - m_stride += count * VertexBufferElement::get_size_of_type(0x1405); - } - - template<> - void push(uint32_t count) - { - m_elements.push_back({ 0x1401, count, 1, m_stride }); // GL_UNSIGNED_BYTE - m_stride += count * VertexBufferElement::get_size_of_type(0x1401); - } - - inline auto get_elements() const -> const std::vector& { return m_elements; } - inline auto get_stride() const -> uint32_t { return m_stride; } - - private: - std::vector m_elements; - uint32_t m_stride = 0; - }; - - class VertexBuffer - { - public: - VertexBuffer(const void* data, uint32_t size); - ~VertexBuffer(); - - auto bind() const -> void; - auto unbind() const -> void; - auto set_data(const void* data, uint32_t size) -> void; - - auto get_layout() const -> const VertexBufferLayout& { return m_layout; } - auto set_layout(const VertexBufferLayout& layout) -> void { m_layout = layout; } - - static auto create(const void* data, uint32_t size) -> VertexBuffer*; - - private: - uint32_t m_renderer_id = 0; - VertexBufferLayout m_layout; - }; -}; diff --git a/src/states/config_state.cpp b/src/states/config_state.cpp deleted file mode 100644 index d5fca87..0000000 --- a/src/states/config_state.cpp +++ /dev/null @@ -1,307 +0,0 @@ -#include "config_state.h" -#include "rendering/renderer.h" -#include "core/application.h" -#include "core/theme_manager.h" -#include "core/settings_manager.h" - -#include -#include -#include - -namespace Donut -{ - auto ConfigState::on_enter() -> void - { - DONUT_INFO("Entering Config State"); - - const auto& settings = SettingsManager::get_settings_const(); - - m_selected_api = (settings.graphics.render_api == "Vulkan") ? RendererAPI::API::Vulkan : RendererAPI::API::OpenGL; - m_selected_theme = (settings.graphics.selected_theme == "Light") ? 1 : - (settings.graphics.selected_theme == "Blue") ? 2 : 0; - m_target_fps = settings.simulation.target_fps; - m_compute_height = settings.simulation.compute_height; - m_max_steps_moving = settings.simulation.max_steps_moving; - m_max_steps_static = settings.simulation.max_steps_static; - m_early_exit_distance = settings.simulation.early_exit_distance; - m_gravity_enabled = settings.simulation.gravity_enabled; - m_v_sync_enabled = settings.graphics.v_sync_enabled; - m_show_fps = settings.graphics.show_fps; - m_show_performance_metrics = settings.graphics.show_performance_metrics; - m_show_debug_info = settings.graphics.show_debug_info; - m_enable_anti_aliasing = settings.graphics.enable_anti_aliasing; - } - - auto ConfigState::on_exit() -> void - { - DONUT_INFO("Exiting Config State"); - } - - auto ConfigState::on_update(float delta_time) -> void - { - if (m_show_restart_message) - { - m_restart_message_timer += delta_time; - if (m_restart_message_timer > 3.0f) - { - m_show_restart_message = false; - m_restart_message_timer = 0.0f; - } - } - } - - auto ConfigState::on_render() -> void - { - Renderer::set_clear_color({ 0.1f, 0.1f, 0.1f, 1.0f }); - Renderer::clear(); - } - - auto ConfigState::on_event(Event& event) -> void - { - } - - auto ConfigState::on_im_ui_render() -> void - { - ImGui::SetNextWindowSize(ImVec2(900, 700), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f, ImGui::GetIO().DisplaySize.y * 0.5f), - ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); - - ImGui::Begin("Donut Configuration", nullptr, ImGuiWindowFlags_NoCollapse); - - ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); - ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "Donut Configuration"); - ImGui::PopFont(); - ImGui::Separator(); - - ImGui::Columns(2, "ConfigColumns", true); - - float available_height = ImGui::GetWindowHeight() - 140; - - ImGui::BeginChild("GraphicsSettings", ImVec2(0, available_height), true, ImGuiWindowFlags_AlwaysVerticalScrollbar); - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Graphics Settings"); - ImGui::Separator(); - - ImGui::Text("Render API:"); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "(requires restart)"); - - const char* api_names[] = { "OpenGL", "Vulkan" }; - static int current_api = (int)m_selected_api - 1; - - if (ImGui::BeginCombo("##RenderAPI", api_names[current_api])) - { - for (int i = 0; i < IM_ARRAYSIZE(api_names); i++) - { - const bool is_selected = (current_api == i); - if (ImGui::Selectable(api_names[i], is_selected)) - { - current_api = i; - m_selected_api = (RendererAPI::API)(i + 1); - m_show_restart_message = true; - m_restart_message_timer = 0.0f; - } - - if (is_selected) - ImGui::SetItemDefaultFocus(); - } - - ImGui::EndCombo(); - } - - ImGui::Text("Current API: "); - ImGui::SameLine(); - const char* current_api_name = (Renderer::get_api() == RendererAPI::API::OpenGL) ? "OpenGL" : "Vulkan"; - ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), current_api_name); - - ImGui::Spacing(); - - ImGui::Checkbox("Enable VSync", &m_v_sync_enabled); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "(recommended)"); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Display"); - ImGui::Separator(); - - ImGui::Text("Window Size: 1280x720"); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Fullscreen: Not implemented yet"); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "System Info"); - ImGui::Separator(); - - ImGui::Text("OpenGL Version: %s", glGetString(GL_VERSION)); - ImGui::Text("GPU: %s", glGetString(GL_RENDERER)); - ImGui::Text("Vendor: %s", glGetString(GL_VENDOR)); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Performance"); - ImGui::Separator(); - - ImGui::SliderInt("Target FPS", &m_target_fps, 30, 120, "%d FPS"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Target frame rate for the simulation"); - - ImGui::Checkbox("Show FPS Counter", &m_show_fps); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Display current FPS in the simulation"); - - ImGui::Checkbox("Show Performance Metrics", &m_show_performance_metrics); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Show detailed performance information"); - - ImGui::Checkbox("Show Debug Info", &m_show_debug_info); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Display debug information and statistics"); - - ImGui::Checkbox("Enable Anti-Aliasing", &m_enable_anti_aliasing); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Enable anti-aliasing for smoother rendering"); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Theme"); - ImGui::Separator(); - - const char* theme_names[] = { "Dark", "Light", "Blue" }; - if (ImGui::Combo("UI Theme", &m_selected_theme, theme_names, IM_ARRAYSIZE(theme_names))) - ThemeManager::set_theme(static_cast(m_selected_theme)); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Choose the application theme"); - - ImGui::EndChild(); - - ImGui::NextColumn(); - - ImGui::BeginChild("SimulationSettings", ImVec2(0, available_height), true, ImGuiWindowFlags_AlwaysVerticalScrollbar); - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Simulation Settings"); - ImGui::Separator(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Quality"); - ImGui::Separator(); - - ImGui::SliderInt("Compute Height", &m_compute_height, 64, 2048, "%d px"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Resolution of the compute shader. Higher values give better quality but lower performance."); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Higher = better quality, lower performance"); - - ImGui::SliderInt("Max Steps (Moving)", &m_max_steps_moving, 1000, 60000, "%d"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Maximum ray marching steps when camera is moving"); - - ImGui::SliderInt("Max Steps (Static)", &m_max_steps_static, 1000, 30000, "%d"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Maximum ray marching steps when camera is stationary"); - - ImGui::SliderFloat("Early Exit Distance", &m_early_exit_distance, 1e11f, 1e13f, "%.2e"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Distance at which ray marching stops to improve performance"); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Distance at which ray marching stops"); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Physics"); - ImGui::Separator(); - - ImGui::Checkbox("Enable Gravity", &m_gravity_enabled); - - ImGui::EndChild(); - - ImGui::Columns(1); - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - float button_width = (ImGui::GetWindowWidth() - 120) / 5.0f; - - if (ImGui::Button("World Builder", ImVec2(button_width, 35))) - { - apply_settings(); - Application::get().get_state_manager().switch_to_state("WorldBuilder"); - } - - ImGui::SameLine(); - if (ImGui::Button("Reset to Defaults", ImVec2(button_width, 35))) - reset_to_defaults(); - - ImGui::SameLine(); - if (ImGui::Button("Apply Settings", ImVec2(button_width, 35))) - apply_settings(); - - ImGui::SameLine(); - if (ImGui::Button("Save Settings", ImVec2(button_width, 35))) - { - apply_settings(); - DONUT_INFO("Settings saved manually"); - } - - ImGui::SameLine(); - if (ImGui::Button("Exit", ImVec2(button_width, 35))) - Application::get().close(); - - if (m_show_restart_message) - { - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 10); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); - ImGui::TextWrapped("Warning: Render API changed! Please restart the application for changes to take effect."); - ImGui::PopStyleColor(); - } - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Donut Engine v1.0.0 | Black Hole Simulation"); - - ImGui::End(); - } - - auto ConfigState::apply_settings() -> void - { - SimulationSettings sim_settings; - sim_settings.target_fps = m_target_fps; - sim_settings.compute_height = m_compute_height; - sim_settings.max_steps_moving = m_max_steps_moving; - sim_settings.max_steps_static = m_max_steps_static; - sim_settings.early_exit_distance = m_early_exit_distance; - sim_settings.gravity_enabled = m_gravity_enabled; - - GraphicsSettings gfx_settings; - gfx_settings.render_api = (m_selected_api == RendererAPI::API::Vulkan) ? "Vulkan" : "OpenGL"; - gfx_settings.v_sync_enabled = m_v_sync_enabled; - gfx_settings.show_fps = m_show_fps; - gfx_settings.show_performance_metrics = m_show_performance_metrics; - gfx_settings.show_debug_info = m_show_debug_info; - gfx_settings.enable_anti_aliasing = m_enable_anti_aliasing; - gfx_settings.selected_theme = (m_selected_theme == 1) ? "Light" : - (m_selected_theme == 2) ? "Blue" : "Dark"; - - SettingsManager::set_simulation_settings(sim_settings); - SettingsManager::set_graphics_settings(gfx_settings); - - RendererAPI::set_api(m_selected_api); - DONUT_INFO("Settings applied and saved"); - } - - auto ConfigState::reset_to_defaults() -> void - { - m_selected_api = RendererAPI::API::OpenGL; - m_target_fps = 60; - m_compute_height = 512; - m_max_steps_moving = 30000; - m_max_steps_static = 15000; - m_early_exit_distance = 5e12f; - m_gravity_enabled = true; - m_v_sync_enabled = true; - m_show_fps = true; - m_show_performance_metrics = true; - m_show_debug_info = false; - m_enable_anti_aliasing = true; - m_selected_theme = 0; - - apply_settings(); - DONUT_INFO("Settings reset to defaults and saved"); - } -}; diff --git a/src/states/config_state.h b/src/states/config_state.h deleted file mode 100644 index e64889b..0000000 --- a/src/states/config_state.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include "core/state.h" -#include "core/event.h" -#include "core/log.h" -#include "rendering/renderer.h" - -namespace Donut -{ - class ConfigState - : public State - { - public: - ~ConfigState() = default; - - auto on_enter() -> void override; - auto on_exit() -> void override; - auto on_update(float delta_time) -> void override; - auto on_render() -> void override; - auto on_im_ui_render() -> void override; - auto on_event(Event& event) -> void override; - - private: - auto apply_settings() -> void; - auto reset_to_defaults() -> void; - - private: - RendererAPI::API m_selected_api = RendererAPI::API::OpenGL; - bool m_show_restart_message = false; - float m_restart_message_timer = 0.0f; - - int m_target_fps = 60; - int m_compute_height = 512; - int m_max_steps_moving = 30000; - int m_max_steps_static = 15000; - float m_early_exit_distance = 5e12f; - bool m_gravity_enabled = true; - - bool m_v_sync_enabled = true; - bool m_show_fps = true; - bool m_show_performance_metrics = true; - bool m_show_debug_info = false; - bool m_enable_anti_aliasing = true; - - int m_selected_theme = 0; // 0=Dark, 1=Light, 2=Blue - }; -}; \ No newline at end of file diff --git a/src/states/simulation_state.cpp b/src/states/simulation_state.cpp deleted file mode 100644 index 06955ff..0000000 --- a/src/states/simulation_state.cpp +++ /dev/null @@ -1,386 +0,0 @@ -#include "simulation_state.h" -#include "rendering/renderer.h" -#include "core/application.h" -#include "core/hdri_manager.h" -#include "core/window.h" -#include "core/event.h" -#include "core/settings_manager.h" - -#include -#include -#include -#include -#include -#include - -namespace Donut -{ - auto SimulationState::on_enter() -> void - { - DONUT_INFO("Entering Simulation State"); - - const auto& settings = SettingsManager::get_settings_const(); - auto& engine = Application::get().get_engine(); - - engine.set_target_fps(settings.simulation.target_fps); - engine.set_compute_height(settings.simulation.compute_height); - engine.set_max_steps_moving(settings.simulation.max_steps_moving); - engine.set_max_steps_static(settings.simulation.max_steps_static); - engine.set_early_exit_distance(settings.simulation.early_exit_distance); - engine.get_gravity() = settings.simulation.gravity_enabled; - - engine.set_disk_thickness(settings.simulation.disk_thickness); - engine.set_disk_density(settings.simulation.disk_density); - engine.set_rotation_speed(settings.simulation.rotation_speed); - engine.set_blur_strength(settings.simulation.blur_strength); - engine.set_glow_intensity(settings.simulation.glow_intensity); - - engine.update_compute_dimensions(); - m_initialized = true; - } - - auto SimulationState::on_exit() -> void - { - DONUT_INFO("Exiting Simulation State"); - } - - auto SimulationState::on_update(float delta_time) -> void - { - auto& engine = Application::get().get_engine(); - engine.update_performance(delta_time); - engine.update_window_dimensions(); - engine.update_physics(delta_time); - - if (engine.get_camera().is_dragging()) - { - GLFWwindow* window = static_cast(Application::get().get_window().get_native_window()); - double xpos, ypos; - glfwGetCursorPos(window, &xpos, &ypos); - engine.get_camera().process_orbital_mouse_move(xpos, ypos); - } - } - - auto SimulationState::on_render() -> void - { - auto& engine = Application::get().get_engine(); - RenderCommand::set_clear_color(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); - RenderCommand::clear(); - RenderCommand::set_viewport(0, 0, static_cast(engine.get_width()), static_cast(engine.get_height())); - - engine.dispatch_compute(engine.get_camera()); - engine.draw_blur_pass(); - } - - auto SimulationState::on_event(Event& event) -> void - { - auto& engine = Application::get().get_engine(); - - if (event.get_event_type() == EventType::MouseButtonPressed) - { - MouseButtonPressedEvent& e = (MouseButtonPressedEvent&)event; - int button = e.get_mouse_button(); - - GLFWwindow* window = static_cast(Application::get().get_window().get_native_window()); - Camera& camera = engine.get_camera(); - double last_x = camera.get_last_x(); - double last_y = camera.get_last_y(); - glfwGetCursorPos(window, &last_x, &last_y); - - camera.process_orbital_mouse_button(button, GLFW_PRESS, 0); - } - else if (event.get_event_type() == EventType::MouseButtonReleased) - { - MouseButtonReleasedEvent& e = (MouseButtonReleasedEvent&)event; - int button = e.get_mouse_button(); - engine.get_camera().process_orbital_mouse_button(button, GLFW_RELEASE, 0); - } - - if (event.get_event_type() == EventType::MouseScrolled) - { - MouseScrolledEvent& e = (MouseScrolledEvent&)event; - engine.get_camera().process_orbital_scroll(e.get_x_offset(), e.get_y_offset()); - } - - if (event.get_event_type() == EventType::KeyPressed) - { - KeyPressedEvent& e = (KeyPressedEvent&)event; - if (e.get_key_code() == GLFW_KEY_G) - { - engine.get_gravity() = !engine.get_gravity(); - DONUT_INFO("Gravity turned {}", engine.get_gravity() ? "ON" : "OFF"); - } - } - } - - auto SimulationState::on_im_ui_render() -> void - { - auto& engine = Application::get().get_engine(); - - ImGui::SetNextWindowSize(ImVec2(400, 600), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - 420, 20), ImGuiCond_FirstUseEver); - - ImGui::Begin("Simulation Controls", nullptr, ImGuiWindowFlags_NoCollapse); - - ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "Black Hole Simulation"); - ImGui::SameLine(); - if (ImGui::Button("Back to Config")) - Application::get().get_state_manager().switch_to_state("Config"); - ImGui::SameLine(); - if (ImGui::Button("Save Settings")) - { - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.target_fps = engine.get_target_fps(); - settings.compute_height = engine.get_compute_height(); - settings.max_steps_moving = engine.get_max_steps_moving(); - settings.max_steps_static = engine.get_max_steps_static(); - settings.early_exit_distance = engine.get_early_exit_distance(); - settings.gravity_enabled = engine.get_gravity(); - SettingsManager::set_simulation_settings(settings); - DONUT_INFO("Simulation settings saved manually"); - } - - ImGui::Separator(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Performance"); - ImGui::Separator(); - - ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate); - ImGui::Text("Frame Time: %.3f ms", 1000.0f / ImGui::GetIO().Framerate); - ImGui::Text("Engine FPS: %.1f", engine.get_current_fps()); - - int target_fps = engine.get_target_fps(); - if (ImGui::SliderInt("Target FPS", &target_fps, 30, 120)) - { - engine.set_target_fps(target_fps); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.target_fps = target_fps; - SettingsManager::set_simulation_settings(settings); - } - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Simulation Info"); - ImGui::Separator(); - - ImGui::Text("Resolution: %dx%d", engine.get_width(), engine.get_height()); - ImGui::Text("Compute Resolution: %dx%d", engine.get_compute_width(), engine.get_compute_height()); - ImGui::Text("Objects: %zu", engine.get_objects().size()); - - if (ImGui::Button("Print Object Info")) - engine.print_object_info(); - - int compute_height = engine.get_compute_height(); - if (ImGui::SliderInt("Compute Height", &compute_height, 64, 2048)) - { - engine.set_compute_height(compute_height); - engine.update_compute_dimensions(); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.compute_height = compute_height; - SettingsManager::set_simulation_settings(settings); - } - - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Compute Width: %d (auto-calculated)", engine.get_compute_width()); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Quality Settings"); - ImGui::Separator(); - - int max_steps_moving = engine.get_max_steps_moving(); - if (ImGui::SliderInt("Max Steps (Moving)", &max_steps_moving, 1000, 60000)) - { - engine.set_max_steps_moving(max_steps_moving); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.max_steps_moving = max_steps_moving; - SettingsManager::set_simulation_settings(settings); - } - int max_steps_static = engine.get_max_steps_static(); - if (ImGui::SliderInt("Max Steps (Static)", &max_steps_static, 1000, 30000)) - { - engine.set_max_steps_static(max_steps_static); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.max_steps_static = max_steps_static; - SettingsManager::set_simulation_settings(settings); - } - float early_exit_distance = engine.get_early_exit_distance(); - if (ImGui::SliderFloat("Early Exit Distance", &early_exit_distance, 1e11f, 1e13f, "%.2e")) - { - engine.set_early_exit_distance(early_exit_distance); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.early_exit_distance = early_exit_distance; - SettingsManager::set_simulation_settings(settings); - } - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Physics"); - ImGui::Separator(); - - bool& gravity = engine.get_gravity(); - if (ImGui::Checkbox("Gravity Enabled", &gravity)) - { - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.gravity_enabled = gravity; - SettingsManager::set_simulation_settings(settings); - } - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Accretion Disk"); - ImGui::Separator(); - - float disk_thickness = engine.get_disk_thickness(); - if (ImGui::SliderFloat("Cloud Thickness", &disk_thickness, 0.1f, 2.0f, "%.2f")) - { - engine.set_disk_thickness(disk_thickness); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.disk_thickness = disk_thickness; - SettingsManager::set_simulation_settings(settings); - } - ImGui::TextDisabled("Thickness relative to Schwarzschild radius"); - - float disk_density = engine.get_disk_density(); - if (ImGui::SliderFloat("Cloud Density", &disk_density, 0.1f, 3.0f, "%.2f")) - { - engine.set_disk_density(disk_density); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.disk_density = disk_density; - SettingsManager::set_simulation_settings(settings); - } - ImGui::TextDisabled("Overall density multiplier"); - - float rotation_speed = engine.get_rotation_speed(); - if (ImGui::SliderFloat("Rotation Speed", &rotation_speed, 0.0f, 3.0f, "%.2f")) - { - engine.set_rotation_speed(rotation_speed); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.rotation_speed = rotation_speed; - SettingsManager::set_simulation_settings(settings); - } - ImGui::TextDisabled("Rotation speed multiplier (0 = no rotation)"); - - float blur_strength = engine.get_blur_strength(); - if (ImGui::SliderFloat("Blur Strength", &blur_strength, 0.5f, 5.0f, "%.2f")) - { - engine.set_blur_strength(blur_strength); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.blur_strength = blur_strength; - SettingsManager::set_simulation_settings(settings); - } - ImGui::TextDisabled("Blur radius for glow effect"); - - float glow_intensity = engine.get_glow_intensity(); - if (ImGui::SliderFloat("Glow Intensity", &glow_intensity, 0.1f, 3.0f, "%.2f")) - { - engine.set_glow_intensity(glow_intensity); - SimulationSettings settings = SettingsManager::get_settings_const().simulation; - settings.glow_intensity = glow_intensity; - SettingsManager::set_simulation_settings(settings); - } - ImGui::TextDisabled("Intensity of the glow effect"); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "HDRI Environment"); - ImGui::Separator(); - - static int selected_hdri = 0; - ImGui::PushID("SimulationHDRI"); - auto& hdri_manager = HDRIManager::get(); - const auto& available_hdri = hdri_manager.get_available_hdri(); - - static std::vector hdri_option_names; - static std::vector hdri_options; - - if (hdri_option_names.size() != available_hdri.size()) - { - hdri_option_names.clear(); - hdri_options.clear(); - - for (const auto& path : available_hdri) - { - hdri_option_names.push_back(hdri_manager.get_hdri_name(path)); - hdri_options.push_back(hdri_option_names.back().c_str()); - } - } - - if (ImGui::Combo("HDRI Environment", &selected_hdri, hdri_options.data(), static_cast(hdri_options.size()))) - { - auto& hdri_manager = HDRIManager::get(); - hdri_manager.set_current_hdri(available_hdri[selected_hdri]); - engine.set_hdri_environment(hdri_manager.get_current_hdri()); - } - ImGui::TextDisabled("HDRI provides background and lighting for the simulation"); - ImGui::PopID(); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Camera"); - ImGui::Separator(); - - ImGui::Text("Position: (%.2e, %.2e, %.2e)", - engine.get_camera().get_orbital_position().x, - engine.get_camera().get_orbital_position().y, - engine.get_camera().get_orbital_position().z); - ImGui::Text("Radius: %.2e", engine.get_camera().get_orbital_radius()); - ImGui::Text("Azimuth: %.2f", engine.get_camera().get_azimuth()); - ImGui::Text("Elevation: %.2f", engine.get_camera().get_elevation()); - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Export"); - ImGui::Separator(); - - if (ImGui::Button("Export Frame (1080p)", ImVec2(-1, 30))) - { - auto& engine = Application::get().get_engine(); - auto now = std::chrono::system_clock::now(); - auto time_t = std::chrono::system_clock::to_time_t(now); - std::stringstream ss; - ss << "frame_1080p_" << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H-%M-%S") << ".png"; - engine.export_high_res_frame(ss.str(), 1920, 1080); - } - - if (ImGui::Button("Export High-Res Frame (4K)", ImVec2(-1, 30))) - { - auto& engine = Application::get().get_engine(); - auto now = std::chrono::system_clock::now(); - auto time_t = std::chrono::system_clock::to_time_t(now); - std::stringstream ss; - ss << "high_res_frame_4k_" << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H-%M-%S") << ".png"; - engine.export_high_res_frame(ss.str(), 4096, 3072); - } - - if (ImGui::Button("Export High-Res Frame (8K)", ImVec2(-1, 30))) - { - auto& engine = Application::get().get_engine(); - auto now = std::chrono::system_clock::now(); - auto time_t = std::chrono::system_clock::to_time_t(now); - std::stringstream ss; - ss << "high_res_frame_8k_" << std::put_time(std::localtime(&time_t), "%Y-%m-%d_%H-%M-%S") << ".png"; - engine.export_high_res_frame(ss.str(), 8192, 6144); - } - - if (ImGui::Button("Export Ultra High-Res Frame (16K)", ImVec2(-1, 30))) - { - auto& engine = Application::get().get_engine(); - auto now = std::chrono::system_clock::now(); - auto time_t = std::chrono::system_clock::to_time_t(now); - std::stringstream ss; - ss << "high_res_frame_16k_" << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S") << ".png"; - engine.export_high_res_frame(ss.str(), 16384, 12288); - } - - ImGui::Spacing(); - - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Controls"); - ImGui::Separator(); - - ImGui::Text("Left Mouse: Orbit camera"); - ImGui::Text("Scroll: Zoom in/out"); - ImGui::Text("G: Toggle gravity"); - ImGui::Text("Right Mouse: Enable gravity (hold)"); - - ImGui::End(); - } -}; diff --git a/src/states/simulation_state.h b/src/states/simulation_state.h deleted file mode 100644 index 625d19e..0000000 --- a/src/states/simulation_state.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include "core/state.h" -#include "core/event.h" -#include "core/log.h" -#include "engine/engine.h" -#include - -namespace Donut -{ - class SimulationState - : public State - { - public: - ~SimulationState() = default; - - auto on_enter() -> void override; - auto on_exit() -> void override; - auto on_update(float delta_time) -> void override; - auto on_render() -> void override; - auto on_im_ui_render() -> void override; - auto on_event(Event& event) -> void override; - - private: - bool m_initialized = false; - }; -}; diff --git a/src/states/world_builder_state.cpp b/src/states/world_builder_state.cpp deleted file mode 100644 index 09283c3..0000000 --- a/src/states/world_builder_state.cpp +++ /dev/null @@ -1,1113 +0,0 @@ -#include "world_builder_state.h" - -#include "core/application.h" -#include "core/window.h" -#include "core/hdri_manager.h" - -#include "rendering/renderer.h" -#include "rendering/shader.h" -#include "rendering/vertex_array.h" -#include "rendering/vertex_buffer.h" -#include "rendering/index_buffer.h" -#include "rendering/texture.h" - -#include -#include -#include - -#include -#include - -#include - -#include -#include -#include -#include -#include - -namespace Donut -{ - auto WorldBuilderState::on_enter() -> void - { - DONUT_INFO("Entering World Builder State"); - - ImGuizmo::Enable(true); - - m_camera.set_camera_mode(CameraMode::Orbital); - m_camera.set_orbital_target(glm::vec3(0.0f, 0.0f, 0.0f)); - m_camera.set_orbital_radius(15.0f); - m_camera.set_orbital_limits(2.0f, 200.0f); - m_camera.set_orbital_speed(0.01f); - m_camera.set_zoom_speed(2.0f); - m_camera.set_azimuth(0.0f); - m_camera.set_elevation(static_cast(std::numbers::pi) / 3.0f); - m_camera.update_orbital(); - - m_sphere_shader = Ref(Shader::create("assets/shaders/Sphere.glsl")); - m_skybox_shader = Ref(Shader::create("assets/shaders/Skybox.glsl")); - m_grid_shader = Ref(Shader::create("assets/shaders/Grid.glsl")); - - if (!m_sphere_shader) - DONUT_ERROR("Failed to create sphere shader"); - if (!m_skybox_shader) - DONUT_ERROR("Failed to create skybox shader"); - if (!m_grid_shader) - DONUT_ERROR("Failed to create grid shader"); - - initialize_sphere_geometry(); - initialize_skybox_geometry(); - initialize_grid_geometry(); - - auto& hdri_manager = HDRIManager::get(); - m_hdri_environment = hdri_manager.get_current_hdri(); - if (!m_hdri_environment) - { - hdri_manager.set_current_hdri("assets/hdri/HDR_blue_nebulae-1.hdr"); - m_hdri_environment = hdri_manager.get_current_hdri(); - if (!m_hdri_environment) - DONUT_WARN("Failed to load default HDRI for WorldBuilder, using fallback"); - } - - Material black_hole_material(glm::vec3(0.0f, 0.0f, 0.0f), 0.0f, 0.0f); - m_black_hole = Object(glm::vec3(0.0f, 0.0f, 0.0f), 2.0f, black_hole_material); - m_black_hole_initialized = true; - - // Set default grid size - m_grid_size = 10.0f; - - m_initialized = true; - } - - auto WorldBuilderState::on_exit() -> void - { - DONUT_INFO("Exiting World Builder State"); - } - - auto WorldBuilderState::on_update(float delta_time) -> void - { - if (m_camera_dragging && - !ImGuizmo::IsUsing()) - { - GLFWwindow* window = static_cast(Application::get().get_window().get_native_window()); - double xpos, ypos; - glfwGetCursorPos(window, &xpos, &ypos); - - glm::vec2 current_mouse_pos(xpos, ypos); - glm::vec2 delta = current_mouse_pos - m_last_mouse_pos; - - float sensitivity = 0.005f; - float azimuth_delta = delta.x * sensitivity; - float elevation_delta = -delta.y * sensitivity; - - float new_azimuth = m_camera.get_azimuth() + azimuth_delta; - float new_elevation = m_camera.get_elevation() + elevation_delta; - - new_elevation = glm::clamp(new_elevation, 0.01f, static_cast(std::numbers::pi) - 0.01f); - - m_camera.set_azimuth(new_azimuth); - m_camera.set_elevation(new_elevation); - m_camera.update_orbital(); - - m_last_mouse_pos = current_mouse_pos; - } - } - - auto WorldBuilderState::on_render() -> void - { - auto& hdri_manager = HDRIManager::get(); - m_hdri_environment = hdri_manager.get_current_hdri(); - - RenderCommand::set_clear_color(glm::vec4(0.1f, 0.1f, 0.1f, 1.0f)); - RenderCommand::clear(); - - if (m_hdri_environment) - render_skybox(); - - if (m_show_grid) - render_grid(); - - render_scene(); - } - - auto WorldBuilderState::on_event(Event& event) -> void - { - EventDispatcher dispatcher(event); - - dispatcher.dispatch([this](MouseButtonPressedEvent& e) - { - if (e.get_mouse_button() == GLFW_MOUSE_BUTTON_LEFT) - { - if (ImGuizmo::IsUsing() || - ImGuizmo::IsOver()) - return false; - - GLFWwindow* window = static_cast(Application::get().get_window().get_native_window()); - double xpos, ypos; - glfwGetCursorPos(window, &xpos, &ypos); - - int width, height; - glfwGetFramebufferSize(window, &width, &height); - - float ndc_x = (2.0f * static_cast(xpos)) / static_cast(width) - 1.0f; - float ndc_y = 1.0f - (2.0f * static_cast(ypos)) / static_cast(height); - - glm::vec4 rayStart_NDC(ndc_x, ndc_y, -1.0f, 1.0f); - glm::vec4 rayEnd_NDC(ndc_x, ndc_y, 0.0f, 1.0f); - - glm::mat4 inv_vp = glm::inverse(m_camera.get_projection_matrix() * m_camera.get_view_matrix()); - glm::vec4 rayStart_World = inv_vp * rayStart_NDC; - glm::vec4 rayEnd_World = inv_vp * rayEnd_NDC; - - rayStart_World /= rayStart_World.w; - rayEnd_World /= rayEnd_World.w; - - glm::vec3 ray_dir = glm::normalize(glm::vec3(rayEnd_World - rayStart_World)); - glm::vec3 ray_origin = glm::vec3(rayStart_World); - - float closest_distance = std::numeric_limits::max(); - int closest_object_index = -1; - - if (m_black_hole_initialized) - { - glm::vec3 oc = ray_origin - m_black_hole.m_centre; - float a = glm::dot(ray_dir, ray_dir); - float b = 2.0f * glm::dot(oc, ray_dir); - float c = glm::dot(oc, oc) - m_black_hole.m_radius * m_black_hole.m_radius; - float discriminant = b * b - 4 * a * c; - - if (discriminant > 0) - { - float t1 = (-b - sqrt(discriminant)) / (2.0f * a); - float t2 = (-b + sqrt(discriminant)) / (2.0f * a); - - if (t1 > 0 && t1 < closest_distance) - { - closest_distance = t1; - closest_object_index = -2; - } - else if (t2 > 0 && t2 < closest_distance) - { - closest_distance = t2; - closest_object_index = -2; - } - } - } - - for (size_t i = 0; i < m_scene.objs.size(); ++i) - { - const Object& obj = m_scene.objs[i]; - - glm::vec3 oc = ray_origin - obj.m_centre; - float a = glm::dot(ray_dir, ray_dir); - float b = 2.0f * glm::dot(oc, ray_dir); - float c = glm::dot(oc, oc) - obj.m_radius * obj.m_radius; - float discriminant = b * b - 4 * a * c; - - if (discriminant > 0) - { - float t1 = (-b - sqrt(discriminant)) / (2.0f * a); - float t2 = (-b + sqrt(discriminant)) / (2.0f * a); - - if (t1 > 0 && t1 < closest_distance) - { - closest_distance = t1; - closest_object_index = static_cast(i); - } - else if (t2 > 0 && t2 < closest_distance) - { - closest_distance = t2; - closest_object_index = static_cast(i); - } - } - } - - if (closest_object_index >= 0) - { - m_selected_object_index = closest_object_index; - DONUT_INFO("Selected object {}", closest_object_index); - return true; - } - else if (closest_object_index == -2) - { - m_selected_object_index = -1; - DONUT_INFO("Black hole clicked (not selectable)"); - return true; - } - else - { - m_selected_object_index = -1; - m_camera_dragging = true; - m_last_mouse_pos = glm::vec2(xpos, ypos); - } - - return true; - } - return false; - }); - - dispatcher.dispatch([this](MouseButtonReleasedEvent& e) - { - if (e.get_mouse_button() == GLFW_MOUSE_BUTTON_LEFT) - { - if (!ImGuizmo::IsUsing()) - m_camera_dragging = false; - return true; - } - - return false; - }); - - dispatcher.dispatch([this](WindowResizeEvent& e) - { - int new_width = e.get_width(); - int new_height = e.get_height(); - if (new_width > 0 && new_height > 0) - { - float aspect = static_cast(new_width) / static_cast(new_height); - m_camera.set_projection(45.0f, aspect, 0.1f, 1000.0f); - } - return false; - }); - - dispatcher.dispatch([this](MouseScrolledEvent& e) - { - float zoom_speed = 0.1f; - float zoom_delta = e.get_y_offset() * zoom_speed; - - double current_radius = m_camera.get_orbital_radius(); - double new_radius = current_radius - zoom_delta * current_radius * 0.1f; - - double min_radius = 2.0f; - double max_radius = 200.0f; - new_radius = glm::clamp(new_radius, min_radius, max_radius); - - m_camera.set_orbital_radius(new_radius); - m_camera.update_orbital(); - - return true; - }); - - dispatcher.dispatch([this](KeyPressedEvent& e) - { - if (m_selected_object_index >= 0 && - m_selected_object_index < static_cast(m_scene.objs.size())) - { - switch (e.get_key_code()) - { - case GLFW_KEY_M: - m_gizmo_operation = ImGuizmo::TRANSLATE; - return true; - case GLFW_KEY_R: - m_gizmo_operation = ImGuizmo::ROTATE; - return true; - case GLFW_KEY_S: - m_gizmo_operation = ImGuizmo::SCALE; - return true; - } - } - return false; - }); - } - - auto WorldBuilderState::on_im_ui_render() -> void - { - GLFWwindow* window = static_cast(Application::get().get_window().get_native_window()); - int width, height; - glfwGetFramebufferSize(window, &width, &height); - - ImGuiViewport* vp = ImGui::GetMainViewport(); - ImGuizmo::SetRect(vp->Pos.x, vp->Pos.y, vp->Size.x, vp->Size.y); - - ImGui::SetNextWindowSize(ImVec2(400, 600), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_FirstUseEver); - - ImGui::Begin("World Builder", nullptr, ImGuiWindowFlags_NoCollapse); - - ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); - ImGui::TextColored(ImVec4(0.8f, 0.8f, 1.0f, 1.0f), "World Builder"); - ImGui::PopFont(); - ImGui::Separator(); - - if (ImGui::CollapsingHeader("Scene Info")) - { - ImGui::Text("Objects in scene: %zu/16 (+ 1 black hole)", m_scene.objs.size()); - - ImGui::Separator(); - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Camera Info:"); - - glm::vec3 camera_pos = m_camera.get_orbital_position(); - glm::vec3 target = m_camera.get_orbital_target(); - - ImGui::Text("Position: (%.2f, %.2f, %.2f)", camera_pos.x, camera_pos.y, camera_pos.z); - ImGui::Text("Target: (%.2f, %.2f, %.2f)", target.x, target.y, target.z); - ImGui::Text("Distance: %.2f", m_camera.get_orbital_radius()); - ImGui::Text("Azimuth: %.1f°", glm::degrees(m_camera.get_azimuth())); - ImGui::Text("Elevation: %.1f°", glm::degrees(m_camera.get_elevation())); - - ImGui::Separator(); - - if (ImGui::Button("Reset Camera")) - { - m_camera.set_orbital_radius(15.0f); - m_camera.set_azimuth(0.0f); - m_camera.set_elevation(static_cast(std::numbers::pi) / 3.0f); - m_camera.set_orbital_target(glm::vec3(0.0f, 0.0f, 0.0f)); - m_camera.update_orbital(); - } - - ImGui::SameLine(); - if (ImGui::Button("Clear Scene")) - clear_scene(); - - ImGui::SameLine(); - if (ImGui::Button("Focus on Objects")) - { - if (!m_scene.objs.empty()) - { - glm::vec3 center(0.0f); - for (const auto& obj : m_scene.objs) - center += obj.m_centre; - center /= static_cast(m_scene.objs.size()); - m_camera.set_orbital_target(center); - m_camera.update_orbital(); - } - } - } - - ImGui::Spacing(); - - if (ImGui::CollapsingHeader("create Object")) - { - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "New Sphere"); - ImGui::Separator(); - - ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "Objects: %zu/16", m_scene.objs.size()); - - ImGui::Text("Position:"); - ImGui::DragFloat3("##Position", &m_new_object_position.x, 0.1f); - - ImGui::Text("Radius:"); - ImGui::DragFloat("##Radius", &m_new_object_radius, 0.1f, 0.1f, 10.0f); - - ImGui::Text("Color:"); - ImGui::ColorEdit3("##Color", &m_new_object_color.x); - - ImGui::Text("Specular:"); - ImGui::SliderFloat("##Specular", &m_new_object_specular, 0.0f, 1.0f); - - ImGui::Text("Emission:"); - ImGui::SliderFloat("##Emission", &m_new_object_emission, 0.0f, 1.0f); - - ImGui::Spacing(); - - if (m_scene.objs.size() >= 16) - { - ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); - ImGui::Button("add Sphere (Limit Reached)", ImVec2(ImGui::GetWindowWidth() - 20, 30)); - ImGui::PopStyleVar(); - } - else - { - if (ImGui::Button("add Sphere", ImVec2(ImGui::GetWindowWidth() - 20, 30))) - add_sphere(); - } - } - - ImGui::Spacing(); - - if (ImGui::CollapsingHeader("Scene Objects")) - { - if (m_black_hole_initialized) - { - ImGui::PushID(-1); - - ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), "Black Hole (Center)"); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Position: (0.00, 0.00, 0.00)"); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Radius: %.2f", m_black_hole.m_radius); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Color: Black (with white outline)"); - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Cannot be removed or modified"); - - ImGui::PopID(); - ImGui::Separator(); - } - - if (m_scene.objs.empty()) - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No additional objects in scene"); - else - { - for (size_t i = 0; i < m_scene.objs.size(); ++i) - { - Object& obj = m_scene.objs[i]; - - ImGui::PushID(static_cast(i)); - - bool is_selected = (m_selected_object_index == static_cast(i)); - if (ImGui::Selectable(("Sphere " + std::to_string(i)).c_str(), is_selected)) - m_selected_object_index = static_cast(i); - - if (is_selected) - { - ImGui::SameLine(); - if (ImGui::Button("Remove")) - { - remove_selected_object(); - } - - ImGui::Text("Position: (%.2f, %.2f, %.2f)", - obj.m_centre.x, obj.m_centre.y, obj.m_centre.z); - ImGui::Text("Radius: %.2f", obj.m_radius); - ImGui::Text("Color: (%.2f, %.2f, %.2f)", - obj.m_material.m_color.x, obj.m_material.m_color.y, obj.m_material.m_color.z); - } - - ImGui::PopID(); - } - } - } - - ImGui::Spacing(); - - if (ImGui::CollapsingHeader("Gizmo Controls")) - { - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Gizmo Operation:"); - - if (ImGui::RadioButton("Translate", m_gizmo_operation == ImGuizmo::TRANSLATE)) - m_gizmo_operation = ImGuizmo::TRANSLATE; - ImGui::SameLine(); - if (ImGui::RadioButton("Rotate", m_gizmo_operation == ImGuizmo::ROTATE)) - m_gizmo_operation = ImGuizmo::ROTATE; - ImGui::SameLine(); - if (ImGui::RadioButton("Scale", m_gizmo_operation == ImGuizmo::SCALE)) - m_gizmo_operation = ImGuizmo::SCALE; - - ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Hotkeys:"); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "M = Move, R = Rotate, S = Scale"); - } - - ImGui::Spacing(); - - if (ImGui::CollapsingHeader("Selection Outline")) - { - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Outline Settings:"); - - ImGui::Text("Outline Color:"); - ImGui::ColorEdit3("##OutlineColor", &m_outline_color.x); - - ImGui::Text("Outline Width:"); - ImGui::SliderFloat("##OutlineWidth", &m_outline_width, 0.01f, 0.9f, "%.2f"); - - ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "White rim around sphere silhouette"); - } - - ImGui::Spacing(); - - if (ImGui::CollapsingHeader("Grid Settings")) - { - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Grid Settings:"); - - ImGui::Text("Show Grid:"); - ImGui::SameLine(); - ImGui::Checkbox("##ShowGrid", &m_show_grid); - - ImGui::Text("Grid Color:"); - ImGui::ColorEdit3("##GridColor", &m_grid_color.x); - - ImGui::Text("Grid Alpha:"); - ImGui::SliderFloat("##GridAlpha", &m_grid_alpha, 0.0f, 1.0f, "%.2f"); - - ImGui::Text("Grid Size:"); - ImGui::SliderFloat("##GridSize", &m_grid_size, 1.0f, 500.0f, "%.1f"); - - ImGui::Spacing(); - if (ImGui::Button("Regenerate Grid", ImVec2(ImGui::GetWindowWidth() - 20, 25))) - initialize_grid_geometry(); - - ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Reference grid for spatial orientation"); - } - - ImGui::Spacing(); - - if (ImGui::CollapsingHeader("HDRI Environment", nullptr)) - { - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "HDRI Settings:"); - - static int selected_hdri = 0; - ImGui::PushID("WorldBuilderHDRI"); - auto& hdri_manager = HDRIManager::get(); - const auto& available_hdri = hdri_manager.get_available_hdri(); - - static std::vector hdri_option_names; - static std::vector hdri_options; - - if (hdri_option_names.size() != available_hdri.size()) - { - hdri_option_names.clear(); - hdri_options.clear(); - - for (const auto& path : available_hdri) - { - hdri_option_names.push_back(hdri_manager.get_hdri_name(path)); - hdri_options.push_back(hdri_option_names.back().c_str()); - } - } - - if (ImGui::Combo("HDRI Environment", &selected_hdri, hdri_options.data(), static_cast(hdri_options.size()))) - { - auto& hdri_manager = HDRIManager::get(); - hdri_manager.set_current_hdri(available_hdri[selected_hdri]); - m_hdri_environment = hdri_manager.get_current_hdri(); - } - - ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "HDRI provides background skybox and lighting for the scene"); - ImGui::PopID(); - } - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - float button_width = (ImGui::GetWindowWidth() - 30) / 2.0f; - - if (ImGui::Button("Save Scene", ImVec2(button_width, 30))) - save_scene(); - - ImGui::SameLine(); - if (ImGui::Button("load Scene", ImVec2(button_width, 30))) - load_scene(); - - ImGui::Spacing(); - - if (ImGui::Button("Start Simulation", ImVec2(ImGui::GetWindowWidth() - 20, 30))) - { - auto& engine = Application::get().get_engine(); - engine.load_objects_from_scene(m_scene.objs); - Application::get().get_state_manager().switch_to_state("Simulation"); - } - - ImGui::Spacing(); - - if (ImGui::Button("Back to Config", ImVec2(ImGui::GetWindowWidth() - 20, 30))) - Application::get().get_state_manager().switch_to_state("Config"); - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Camera: Left mouse = rotate, Scroll = zoom"); - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Gizmo: M=Move, R=Rotate, S=Scale"); - - ImGui::End(); - - if (m_selected_object_index >= 0 && - m_selected_object_index < static_cast(m_scene.objs.size()) && - m_selected_object_index != -2) - { - glm::mat4 view = m_camera.get_view_matrix(); - glm::mat4 projection = m_camera.get_projection_matrix(); - - Object& selected_obj = m_scene.objs[m_selected_object_index]; - - glm::mat4 object_transform = glm::mat4(1.0f); - object_transform = glm::translate(object_transform, selected_obj.m_centre); - object_transform = glm::scale(object_transform, glm::vec3(selected_obj.m_radius)); - - DONUT_INFO("Object position: ({}, {}, {})", selected_obj.m_centre.x, selected_obj.m_centre.y, selected_obj.m_centre.z); - glm::vec3 camera_pos = m_camera.get_orbital_position(); - DONUT_INFO("Camera position: ({}, {}, {})", camera_pos.x, camera_pos.y, camera_pos.z); - - glm::vec3 view_translation = glm::vec3(view[3][0], view[3][1], view[3][2]); - DONUT_INFO("View translation: ({}, {}, {})", view_translation.x, view_translation.y, view_translation.z); - - ImGuizmo::SetDrawlist(ImGui::GetForegroundDrawList()); - if (ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection), - m_gizmo_operation, m_gizmo_mode, - glm::value_ptr(object_transform), - nullptr, nullptr)) - { - glm::vec3 new_position = glm::vec3(object_transform[3][0], object_transform[3][1], object_transform[3][2]); - selected_obj.m_centre = new_position; - - glm::vec3 scale = glm::vec3 - ( - glm::length(glm::vec3(object_transform[0])), - glm::length(glm::vec3(object_transform[1])), - glm::length(glm::vec3(object_transform[2])) - ); - selected_obj.m_radius = (scale.x + scale.y + scale.z) / 3.0f; - - DONUT_INFO("Matrix [3]: ({}, {}, {}, {})", object_transform[3][0], object_transform[3][1], object_transform[3][2], object_transform[3][3]); - DONUT_INFO("New position: ({}, {}, {})", new_position.x, new_position.y, new_position.z); - } - } - } - - auto WorldBuilderState::add_sphere() -> void - { - if (m_scene.objs.size() >= 16) - { - DONUT_WARN("Cannot add more objects. Maximum of 16 objects reached."); - return; - } - - Material material(m_new_object_color, m_new_object_specular, m_new_object_emission); - Object sphere(m_new_object_position, m_new_object_radius, material); - m_scene.objs.push_back(sphere); - - m_selected_object_index = static_cast(m_scene.objs.size() - 1); - - m_new_object_position = glm::vec3(0.0f, 0.0f, 0.0f); - m_new_object_radius = 1.0f; - m_new_object_color = glm::vec3(1.0f, 1.0f, 1.0f); - m_new_object_specular = 0.5f; - m_new_object_emission = 0.0f; - - DONUT_INFO("Added sphere to scene and selected it"); - } - - auto WorldBuilderState::remove_selected_object() -> void - { - if (m_selected_object_index >= 0 && m_selected_object_index < static_cast(m_scene.objs.size())) - { - m_scene.objs.erase(m_scene.objs.begin() + m_selected_object_index); - m_selected_object_index = -1; - DONUT_INFO("Removed object from scene"); - } - } - - auto WorldBuilderState::clear_scene() -> void - { - m_scene.objs.clear(); - m_selected_object_index = -1; - DONUT_INFO("Cleared scene (black hole remains at center)"); - } - - auto WorldBuilderState::save_scene() -> void - { - nlohmann::json scene_data; - scene_data["objects"] = nlohmann::json::array(); - - for (const auto& obj : m_scene.objs) - { - nlohmann::json sphere_data; - sphere_data["position"] = - { - obj.m_centre.x, - obj.m_centre.y, - obj.m_centre.z - }; - - sphere_data["color"] = - { - obj.m_material.m_color.x, - obj.m_material.m_color.y, - obj.m_material.m_color.z - }; - - sphere_data["radius"] = obj.m_radius; - sphere_data["specular"] = obj.m_material.m_specular; - sphere_data["emission"] = obj.m_material.m_emission; - scene_data["objects"].push_back(sphere_data); - } - - std::ofstream file("Scene.json"); - if (file.is_open()) - { - file << scene_data.dump(4); - file.close(); - DONUT_INFO("Scene saved to Scene.json (black hole always present at center)"); - } - else - DONUT_ERROR("Failed to save scene"); - } - - auto WorldBuilderState::load_scene() -> void - { - std::ifstream file("Scene.json"); - if (file.is_open()) - { - m_scene.objs.clear(); - m_selected_object_index = -1; - - nlohmann::json scene_data; - file >> scene_data; - - if (scene_data.contains("objects")) - { - for (const auto& sphere_data : scene_data["objects"]) - { - glm::vec3 position; - glm::vec3 color; - float radius; - float specular; - float emission; - - auto pos_array = sphere_data["position"]; - position.x = pos_array[0].get(); - position.y = pos_array[1].get(); - position.z = pos_array[2].get(); - - auto color_array = sphere_data["color"]; - color.x = color_array[0].get(); - color.y = color_array[1].get(); - color.z = color_array[2].get(); - - radius = sphere_data["radius"].get(); - specular = sphere_data["specular"].get(); - emission = sphere_data["emission"].get(); - - Material material(color, specular, emission); - Object sphere(position, radius, material); - m_scene.objs.push_back(sphere); - } - } - - file.close(); - DONUT_INFO("Scene loaded from Scene.json (black hole remains at center)"); - } - else - DONUT_ERROR("Failed to load scene"); - } - - auto WorldBuilderState::initialize_sphere_geometry() -> void - { - std::vector vertices; - std::vector indices; - - const int segments = 32; - const int rings = 16; - - for (int ring = 0; ring <= rings; ++ring) - { - float phi = static_cast(std::numbers::pi) * ring / rings; - float sin_phi = sin(phi); - float cos_phi = cos(phi); - - for (int segment = 0; segment <= segments; ++segment) - { - float theta = 2.0f * static_cast(std::numbers::pi) * segment / segments; - float sin_theta = sin(theta); - float cos_theta = cos(theta); - - float x = cos_theta * sin_phi; - float y = cos_phi; - float z = sin_theta * sin_phi; - - float nx = x; - float ny = y; - float nz = z; - - vertices.push_back(x); - vertices.push_back(y); - vertices.push_back(z); - vertices.push_back(nx); - vertices.push_back(ny); - vertices.push_back(nz); - } - } - - for (int ring = 0; ring < rings; ++ring) - { - for (int segment = 0; segment < segments; ++segment) - { - uint32_t first = ring * (segments + 1) + segment; - uint32_t second = first + segments + 1; - - indices.push_back(first); - indices.push_back(second); - indices.push_back(first + 1); - - indices.push_back(second); - indices.push_back(second + 1); - indices.push_back(first + 1); - } - } - - auto vertex_buffer = Ref(VertexBuffer::create(vertices.data(), static_cast(vertices.size() * sizeof(float)))); - VertexBufferLayout layout; - layout.push(3); - layout.push(3); - vertex_buffer->set_layout(layout); - - auto index_buffer = Ref(IndexBuffer::create(indices.data(), static_cast(indices.size()))); - - m_sphere_vao = Ref(VertexArray::create()); - m_sphere_vao->add_vertex_buffer(vertex_buffer); - m_sphere_vao->set_index_buffer(index_buffer); - } - - auto WorldBuilderState::render_scene() -> void - { - GLFWwindow* window = static_cast(Application::get().get_window().get_native_window()); - int width, height; - glfwGetFramebufferSize(window, &width, &height); - - RenderCommand::set_viewport(0, 0, width, height); - RenderCommand::enable_depth_test(); - - glm::mat4 view = m_camera.get_view_matrix(); - glm::mat4 projection = m_camera.get_projection_matrix(); - glm::mat4 view_projection = projection * view; - - glm::vec3 light_pos = m_scene.m_light_pos; - glm::vec3 camera_pos = m_camera.get_orbital_position(); - - m_sphere_shader->bind(); - m_sphere_shader->set_mat4("u_ViewProjection", view_projection); - m_sphere_shader->set_float3("u_LightPos", light_pos); - m_sphere_shader->set_float3("u_CameraPos", camera_pos); - - m_sphere_shader->set_float3("u_OutlineColor", m_outline_color); - m_sphere_shader->set_float("u_OutlineWidth", m_outline_width); - - if (m_hdri_environment) - { - m_hdri_environment->bind(1); - m_sphere_shader->set_int("u_HDRIEnvironment", 1); - } - - if (m_black_hole_initialized) - { - glm::mat4 black_hole_transform = glm::translate(glm::mat4(1.0f), m_black_hole.m_centre); - black_hole_transform = glm::scale(black_hole_transform, glm::vec3(m_black_hole.m_radius)); - - m_sphere_shader->set_mat4("u_Transform", black_hole_transform); - m_sphere_shader->set_int("u_IsSelected", 1); - m_sphere_shader->set_float3("u_Color", m_black_hole.m_material.m_color); - m_sphere_shader->set_float("u_Emission", m_black_hole.m_material.m_emission); - m_sphere_shader->set_float("u_Specular", m_black_hole.m_material.m_specular); - m_sphere_shader->set_float("u_OutlineWidth", 0.3f); - - m_sphere_vao->bind(); - RenderCommand::draw_indexed(m_sphere_vao); - - m_sphere_shader->set_float("u_OutlineWidth", m_outline_width); - } - - for (size_t i = 0; i < m_scene.objs.size(); ++i) - { - const auto& obj = m_scene.objs[i]; - - glm::mat4 transform = glm::translate(glm::mat4(1.0f), obj.m_centre); - transform = glm::scale(transform, glm::vec3(obj.m_radius)); - - m_sphere_shader->set_mat4("u_Transform", transform); - - bool is_selected = (m_selected_object_index == static_cast(i)); - m_sphere_shader->set_int("u_IsSelected", is_selected ? 1 : 0); - if (is_selected) - { - glm::vec3 highlight_color = obj.m_material.m_color * 1.5f; - highlight_color = glm::clamp(highlight_color, 0.0f, 1.0f); - m_sphere_shader->set_float3("u_Color", highlight_color); - m_sphere_shader->set_float("u_Emission", 0.2f); - } - else - { - m_sphere_shader->set_float3("u_Color", obj.m_material.m_color); - m_sphere_shader->set_float("u_Emission", obj.m_material.m_emission); - } - - m_sphere_shader->set_float("u_Specular", obj.m_material.m_specular); - - m_sphere_vao->bind(); - RenderCommand::draw_indexed(m_sphere_vao); - } - - RenderCommand::disable_depth_test(); - } - - - - auto WorldBuilderState::initialize_skybox_geometry() -> void - { - float skybox_vertices[] = - { - -1.0f, 1.0f, -1.0f, - -1.0f, -1.0f, -1.0f, - 1.0f, -1.0f, -1.0f, - 1.0f, -1.0f, -1.0f, - 1.0f, 1.0f, -1.0f, - -1.0f, 1.0f, -1.0f, - - -1.0f, -1.0f, 1.0f, - -1.0f, -1.0f, -1.0f, - -1.0f, 1.0f, -1.0f, - -1.0f, 1.0f, -1.0f, - -1.0f, 1.0f, 1.0f, - -1.0f, -1.0f, 1.0f, - - 1.0f, -1.0f, -1.0f, - 1.0f, -1.0f, 1.0f, - 1.0f, 1.0f, 1.0f, - 1.0f, 1.0f, 1.0f, - 1.0f, 1.0f, -1.0f, - 1.0f, -1.0f, -1.0f, - - -1.0f, -1.0f, 1.0f, - -1.0f, 1.0f, 1.0f, - 1.0f, 1.0f, 1.0f, - 1.0f, 1.0f, 1.0f, - 1.0f, -1.0f, 1.0f, - -1.0f, -1.0f, 1.0f, - - -1.0f, 1.0f, -1.0f, - 1.0f, 1.0f, -1.0f, - 1.0f, 1.0f, 1.0f, - 1.0f, 1.0f, 1.0f, - -1.0f, 1.0f, 1.0f, - -1.0f, 1.0f, -1.0f, - - -1.0f, -1.0f, -1.0f, - -1.0f, -1.0f, 1.0f, - 1.0f, -1.0f, -1.0f, - 1.0f, -1.0f, -1.0f, - -1.0f, -1.0f, 1.0f, - 1.0f, -1.0f, 1.0f - }; - - auto vertex_buffer = Ref(VertexBuffer::create(skybox_vertices, sizeof(skybox_vertices))); - VertexBufferLayout layout; - layout.push(3); - vertex_buffer->set_layout(layout); - - m_skybox_vao = Ref(VertexArray::create()); - m_skybox_vao->add_vertex_buffer(vertex_buffer); - } - - auto WorldBuilderState::render_skybox() -> void - { - if (!m_skybox_shader || !m_skybox_vao || !m_hdri_environment) - return; - - GLFWwindow* window = static_cast(Application::get().get_window().get_native_window()); - int width, height; - glfwGetFramebufferSize(window, &width, &height); - - RenderCommand::set_viewport(0, 0, width, height); - RenderCommand::disable_depth_test(); - - glm::mat4 view = m_camera.get_view_matrix(); - glm::mat4 projection = m_camera.get_projection_matrix(); - - view = glm::mat4(glm::mat3(view)); - - m_skybox_shader->bind(); - m_skybox_shader->set_mat4("u_View", view); - m_skybox_shader->set_mat4("u_Projection", projection); - - m_hdri_environment->bind(0); - m_skybox_shader->set_int("u_Skybox", 0); - - m_skybox_vao->bind(); - RenderCommand::draw_arrays(36); - - RenderCommand::enable_depth_test(); - } - - auto WorldBuilderState::initialize_grid_geometry() -> void - { - if (m_grid_vao) - { - m_grid_vao->unbind(); - m_grid_vao.reset(); - } - - const float grid_size = 50.0f; - const int grid_lines = 101; - const float half_size = grid_size * 0.5f; - const float step = grid_size / (grid_lines - 1); - - std::vector vertices; - std::vector indices; - - for (int i = 0; i < grid_lines; ++i) - { - float z = -half_size + i * step; - - vertices.push_back(-half_size); - vertices.push_back(0.0f); - vertices.push_back(z); - - vertices.push_back(half_size); - vertices.push_back(0.0f); - vertices.push_back(z); - - uint32_t base_index = static_cast(vertices.size() / 3) - 2; - indices.push_back(base_index); - indices.push_back(base_index + 1); - } - - for (int i = 0; i < grid_lines; ++i) - { - float x = -half_size + i * step; - - vertices.push_back(x); - vertices.push_back(0.0f); - vertices.push_back(-half_size); - - vertices.push_back(x); - vertices.push_back(0.0f); - vertices.push_back(half_size); - - uint32_t base_index = static_cast(vertices.size() / 3) - 2; - indices.push_back(base_index); - indices.push_back(base_index + 1); - } - - auto vertex_buffer = Ref(VertexBuffer::create(vertices.data(), static_cast(vertices.size() * sizeof(float)))); - VertexBufferLayout layout; - layout.push(3); - vertex_buffer->set_layout(layout); - - auto index_buffer = Ref(IndexBuffer::create(indices.data(), static_cast(indices.size()))); - - m_grid_vao = Ref(VertexArray::create()); - m_grid_vao->add_vertex_buffer(vertex_buffer); - m_grid_vao->set_index_buffer(index_buffer); - - m_grid_vao->unbind(); - } - - auto WorldBuilderState::render_grid() -> void - { - if (!m_grid_shader || !m_grid_vao) - return; - - GLFWwindow* window = static_cast(Application::get().get_window().get_native_window()); - int width, height; - glfwGetFramebufferSize(window, &width, &height); - - RenderCommand::set_viewport(0, 0, width, height); - RenderCommand::enable_depth_test(); - RenderCommand::enable_blending(); - - glm::mat4 view = m_camera.get_view_matrix(); - glm::mat4 projection = m_camera.get_projection_matrix(); - glm::mat4 view_projection = projection * view; - glm::mat4 transform = glm::mat4(1.0f); - - glm::vec3 camera_pos = m_camera.get_orbital_position(); - - m_grid_shader->bind(); - m_grid_shader->set_mat4("u_ViewProjection", view_projection); - m_grid_shader->set_mat4("u_Transform", transform); - m_grid_shader->set_float3("u_GridColor", m_grid_color); - m_grid_shader->set_float("u_GridAlpha", m_grid_alpha); - m_grid_shader->set_float("u_GridSize", m_grid_size); - m_grid_shader->set_float3("u_CameraPos", camera_pos); - - m_grid_vao->bind(); - RenderCommand::draw_lines(m_grid_vao); - - // Clean up state to prevent conflicts with scene rendering - m_grid_vao->unbind(); - m_grid_shader->unbind(); - RenderCommand::disable_blending(); - - // Ensure depth test is still enabled for scene rendering - RenderCommand::enable_depth_test(); - } -}; diff --git a/src/states/world_builder_state.h b/src/states/world_builder_state.h deleted file mode 100644 index 8b4678a..0000000 --- a/src/states/world_builder_state.h +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once - -#include "core/camera.h" -#include "core/state.h" -#include "core/event.h" -#include "core/log.h" - -#include "engine/scene.h" -#include "engine/object.h" - -#include "rendering/renderer.h" -#include "rendering/shader.h" -#include "rendering/vertex_array.h" - -#include -#include - -#include -#include - -namespace Donut -{ - class WorldBuilderState - : public State - { - public: - ~WorldBuilderState() = default; - - auto on_enter() -> void override; - auto on_exit() -> void override; - auto on_update(float delta_time) -> void override; - auto on_render() -> void override; - auto on_im_ui_render() -> void override; - auto on_event(Event& event) -> void override; - private: - auto add_sphere() -> void; - auto remove_selected_object() -> void; - auto clear_scene() -> void; - auto save_scene() -> void; - auto load_scene() -> void; - auto render_scene() -> void; - auto initialize_sphere_geometry() -> void; - auto initialize_grid_geometry() -> void; - auto render_grid() -> void; - private: - Scene m_scene; - Camera m_camera; - bool m_initialized = false; - - Ref m_sphere_shader; - Ref m_sphere_vao; - Ref m_hdri_environment; - - Ref m_skybox_shader; - Ref m_skybox_vao; - - Ref m_grid_shader; - Ref m_grid_vao; - - glm::vec3 m_new_object_position = glm::vec3(0.0f, 0.0f, 0.0f); - float m_new_object_radius = 1.0f; - glm::vec3 m_new_object_color = glm::vec3(1.0f, 1.0f, 1.0f); - float m_new_object_specular = 0.5f; - float m_new_object_emission = 0.0f; - - int m_selected_object_index = -1; - bool m_camera_dragging = false; - glm::vec2 m_last_mouse_pos = glm::vec2(0.0f, 0.0f); - - ImGuizmo::OPERATION m_gizmo_operation = ImGuizmo::TRANSLATE; - ImGuizmo::MODE m_gizmo_mode = ImGuizmo::LOCAL; - - bool m_show_object_list = true; - bool m_show_object_creator = true; - bool m_show_scene_info = true; - bool m_show_gizmo_controls = true; - bool m_show_outline_controls = true; - bool m_show_grid = true; - - glm::vec3 m_outline_color = glm::vec3(1.0f, 1.0f, 1.0f); - float m_outline_width = 0.25f; - - glm::vec3 m_grid_color = glm::vec3(0.5f, 0.5f, 0.5f); - float m_grid_alpha = 0.5f; - float m_grid_size = 50.0f; - - Object m_black_hole; - bool m_black_hole_initialized = false; - - auto set_hdri_environment(Ref hdri) -> void { m_hdri_environment = hdri; } - auto get_hdri_environment() const -> Ref { return m_hdri_environment; } - - auto initialize_skybox_geometry() -> void; - auto render_skybox() -> void; - }; -}; diff --git a/tools/compile-shaders.sh b/tools/compile-shaders.sh index 1f58c2a..5da5d9a 100755 --- a/tools/compile-shaders.sh +++ b/tools/compile-shaders.sh @@ -16,7 +16,7 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" SLANGC="${SLANGC:-$ROOT/tools/slang/bin/slangc}" SPIRV_CROSS="${SPIRV_CROSS:-$(command -v spirv-cross || echo spirv-cross)}" -SRC="$ROOT/assets/Shaders" +SRC="$ROOT/assets/shaders" OUT="$SRC/generated" [ -x "$SLANGC" ] || { echo "compile-shaders: slangc not found at $SLANGC (run tools/fetch-slang.sh)"; exit 1; } -- cgit v1.3