diff options
Diffstat (limited to 'docs/architecture.md')
| -rw-r--r-- | docs/architecture.md | 348 |
1 files changed, 0 insertions, 348 deletions
diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index 1e6560b..0000000 --- a/docs/architecture.md +++ /dev/null @@ -1,348 +0,0 @@ -# the architecture of donut - -how donut is put together as a program: how the code is layered, how the same -rendering runs on two graphics apis, how a frame is drawn, and how the editor, the -live simulation, and the data export fit together. - -for the physics behind the image itself, see [`physics.md`](physics.md). - -## contents - -- [overview](#overview) -- [the rhi: one interface, two backends](#the-rhi-one-interface-two-backends) -- [a frame, end to end](#a-frame-end-to-end) -- [the two renderers](#the-two-renderers) -- [scene and simulation: one world](#scene-and-simulation-one-world) -- [the rendering pipeline](#the-rendering-pipeline) - - [progressive resolution and supersampling](#progressive-resolution-and-supersampling) -- [the workspace: tabs](#the-workspace-tabs) -- [the export pipeline](#the-export-pipeline) -- [the build system](#the-build-system) - -## overview - -donut is split into a few pieces with distinct jobs, so the physics, the platform, -and the interface can change independently. - -```mermaid -flowchart TD - App["Application<br/>thin shell + main loop"] - App --> Scene["Scene<br/>the document/world:<br/>objects, black hole, cameras"] - App --> UI["UILayer<br/>the docking UI shell"] - App --> RP["RenderPath<br/>device-side rendering"] - App --> Dev["RHI::Device<br/>the GPU, abstracted"] - - RP --> SR["SceneRenderer<br/>raster world editor"] - RP --> BHR["BlackHoleRenderer<br/>geodesic ray tracer"] - RP --> Dev - - Dev -.implemented by.-> GL["OpenGLDevice"] - Dev -.implemented by.-> VK["VulkanDevice<br/>MoltenVK"] - - UI -.reads/writes.-> Scene - SR -.reads.-> Scene - BHR -.reads.-> Scene -``` - -| component | file | responsibility | -| --- | --- | --- | -| `Application` | [`src/core/application.cpp`](../src/core/application.cpp) | owns everything; runs the main loop; handles input, resize, vsync, fullscreen; exposes actions to the ui | -| `Scene` | [`src/scene/scene.h`](../src/scene/scene.h) | the world as plain data — placed objects, black-hole/disk parameters, the editor and simulation cameras, the hdri path | -| `UILayer` | [`src/ui/ui_layer.cpp`](../src/ui/ui_layer.cpp) | the docking shell: menu bar, workspace tabs, dockable panels with a layout per workspace; returns which view is live and drives the scene through `AppActions` | -| `RenderPath` | [`src/rendering/render_path.cpp`](../src/rendering/render_path.cpp) | turns the scene into pixels on whatever device is active; owns the two renderers and the environment cubemap | -| `RHI::Device` | [`src/rendering/rhi.h`](../src/rendering/rhi.h) | the portable gpu interface every backend implements | - -two things carry most of the weight here. `Scene` is plain data with no knowledge -of the backend or the ui, and everything that touches the gpu goes through one -narrow interface (the rhi). the `Application` stays thin: it hands the document to -`Scene`, the pixels to `RenderPath`, and the controls to `UILayer`. - -## the rhi: one interface, two backends - -donut runs on both opengl and vulkan (through moltenvk on macos) from one codebase. -all rendering is written once against an abstract render hardware interface in the -`Donut::RHI` namespace, and each api supplies an implementation. - -```mermaid -flowchart LR - Renderers["SceneRenderer<br/>BlackHoleRenderer<br/>written once"] --> RHI["RHI::Device / CommandList<br/>Buffer · Texture · Pipeline · RenderTarget"] - RHI --> GL["platform/opengl/<br/>OpenGLDevice"] - RHI --> VK["platform/vulkan/<br/>VulkanDevice"] - GL --> GLAPI[("OpenGL")] - VK --> VKAPI[("Vulkan / MoltenVK")] -``` - -the interface (in [`rhi.h`](../src/rendering/rhi.h)) is small and shaped for modern -gpus: - -- `Device` is the factory and frame driver: `create_buffer`, `create_texture`, - `create_cubemap_from_hdri`, `create_render_target`, `create_pipeline`; - `begin_frame` / `end_frame`; `set_vsync`, `resize`, `wait_idle`; the imgui hooks; - and the export helpers `run_offscreen`, `read_render_target`, - `read_render_target_float`. -- `CommandList` records work: `begin_render_pass` / `end_render_pass` (a `nullptr` - target means the swapchain), `bind_pipeline`, `set_viewport`, `bind_uniform`, - `bind_texture`, `bind_vertex_buffer`, `draw`. -- `Buffer`, `Texture`, `Pipeline` and `RenderTarget` are opaque gpu resources. -- `Format` is `{ None, Swapchain, RGBA8, RGBA16F, RGBA32F, D32 }`. `Swapchain` - means whatever the presented image is, resolved per backend; `RGBA32F` is what - makes raw floating-point export possible. - -the backend is chosen once at startup, before the window exists, since the two apis -want the window created differently: - -```mermaid -sequenceDiagram - participant A as Application ctor - participant S as SettingsManager - participant W as Window (GLFW) - participant D as RHI::Device - A->>S: read graphics.render_api - alt Vulkan - A->>A: vulkan_prepare_glfw (GLFW_NO_API) - end - A->>W: create window - A->>D: create_vulkan_device or create_opengl_device - A->>D: init(native window) -``` - -because the renderers only ever see the rhi, the same draw code gives -pixel-identical output on both backends. that parity is checked by rendering to an -off-screen target and comparing the read-back pixels. - -## a frame, end to end - -the main loop is `Application::run`: poll events, render, then let vsync pace the -frame or sleep to hit the target fps. each frame is assembled in -`Application::render_frame`: - -```mermaid -sequenceDiagram - participant Dev as Device - participant UI as UILayer - participant In as Input - participant RP as RenderPath - Dev->>Dev: begin_frame(clear) → CommandList - Dev->>Dev: imgui_new_frame - Note over RP: update the active camera's projection - UI->>UI: draw(ctx) → returns active View - In->>In: update_input(view) — orbit / FPS camera - RP->>RP: sync_hdri — reload cubemap if changed - RP->>RP: render(cmd, scene, view, w, h, moving, time) - Dev->>Dev: end_frame — submit + present -``` - -the `View` the ui returns — `None`, `Scene` or `BlackHole` — decides which -viewport is live and therefore what `RenderPath` draws. the `moving` flag, true -while the user is dragging or flying the camera, triggers the progressive-resolution -path described below. - -## the two renderers - -`RenderPath` owns two independent renderers, both written purely against the rhi. - -`SceneRenderer` ([`scene_renderer.cpp`](../src/rendering/scene_renderer.cpp)) is the -world editor: a conventional rasteriser that draws the placed spheres, the ground -grid, the selection outline and gizmo, and a near-black black-hole marker at the -origin, sized to the horizon and ringed with an amber accretion-glow outline so it -reads against the dark background. this is what you manipulate in the scene view. - -`BlackHoleRenderer` ([`black_hole_renderer.cpp`](../src/rendering/black_hole_renderer.cpp)) -is the geodesic ray tracer. it runs the physics shader from [`physics.md`](physics.md) -as a full-screen fragment pass into an off-screen target, then presents that target -to the screen. this is the expensive work, and it runs only when the centre view is -simulation, and during export. - -`RenderPath::render` routes to the right one based on the `View`: - -```mermaid -flowchart TD - R{View?} - R -->|Scene| SP["Swapchain pass:<br/>SceneRenderer.render + ImGui"] - R -->|None| EP["Swapchain pass:<br/>empty viewport + ImGui"] - R -->|BlackHole| GP["Off-screen geodesic pass →<br/>blit to swapchain + ImGui"] -``` - -## scene and simulation: one world - -the editor and the simulation are the same world, not two separate scenes. the -constant `SCENE_UNITS_PER_RS = 3.0` connects them: three editor grid units equal -one schwarzschild radius. `SceneRenderer` draws the black-hole marker's horizon at -that radius, and `BlackHoleRenderer` takes every placed `SceneObject`, multiplies -its position and radius by $r_s/3$ (the code's `SagA_rs / 3`) to reach physical metres, and uploads -them into the shader's `Objects` uniform (up to 16 spheres). - -so a sphere placed in the scene view shows up in the same spot in the simulation -view, except now the curved rays bend around the hole and lens it, and it can appear -stretched, doubled, or smeared into an arc. the spheres are passive lit objects, -planets and the like; they don't exert their own gravity, only the black hole bends -light. - -## the rendering pipeline - -when the simulation view is active, `BlackHoleRenderer::render_geodesic` does three -things each frame: - -1. fills the uniforms (`fill_uniforms`): the camera basis and fov; the - black-hole/disk parameters (radii converted to metres, temperature, brightness, - turbulence); the integration budget (`quality_steps`, clamped 1000–15000); and - the scene objects. -2. picks the off-screen target by the `moving` flag (below) and runs the geodesic - fragment shader over a full-screen quad into it. -3. blits that target to the swapchain (`blit`) with a present pipeline, flipping - vertically where needed so opengl and vulkan agree on orientation, then draws the - imgui overlay on top. - -inside the shader, each pixel builds a ray from the camera basis, fov and aspect, -marches the geodesic (see [physics](physics.md#the-equations-of-motion)), and shades -from whatever it hit: the opaque disk's redshifted blackbody, the black shadow, a -lit object, or the background. the background is the hdri loaded as a cubemap -(`create_cubemap_from_hdri`); escaped rays sample it at a mip level chosen from how -fast neighbouring rays diverge (`ddx`/`ddy`), so the strongly lensed background -blurs rather than aliasing into a shimmering fan. the colour channel is then -tone-mapped with an aces filmic curve. - -### progressive resolution and supersampling - -interactivity trades against quality through resolution and sample count, not by -touching the physics: - -| state | off-screen target | samples per pixel | -| --- | --- | --- | -| camera moving | `GEO_LO` = 480 × 270 | 1 | -| camera settled | `GEO_HI` = 960 × 540 | 4× rotated-grid supersampling | - -the integration budget is the same in both states, because the disk needs a high -step count to resolve at steep poses and lowering it during motion makes it -flicker. when the camera stops, the renderer switches to the larger target and the -fragment shader averages four sub-pixel samples in a rotated-grid ("4-rook") pattern -before tone-mapping, which cleans up the near-horizontal lensed edges and the thin -photon ring. - -## workspaces: tabs, docking and panels - -the interface is a docking shell with three workspaces along the top — -**simulation**, **scene editor** and **export** — each a tab with its own -independent layout, the way blender's workspace tabs work. a main menu bar sits -above the tab strip; below it is a full-viewport dock space with a pass-through -centre, so the live 3d render shows through the middle while the workspace's tool -panels attach to the edges — drag, tab, hide, or restore them like a normal -desktop app. rearranging one workspace never touches the others, and all three -layouts, the open workspace and each one's panel set persist in `imgui.ini` -between runs. - -each workspace fixes what the centre viewport shows — the `View` that -`UILayer::draw` returns to the `RenderPath` — and has its own set of panels. the -sphere panels are available in the black-hole workspaces too, since the lensed -render draws the spheres, though the gizmo itself only works in the scene editor: - -| workspace | view | panels (open by default in bold) | -| --- | --- | --- | -| simulation | `BlackHole` — the lensed black hole | **black hole**, **diagnostics**, settings, outliner, properties | -| scene editor | `Scene` — the world editor | **outliner**, **properties**, **diagnostics**, settings | -| export | `BlackHole` | **export**, **black hole**, **diagnostics**, settings, outliner, properties | - -the panels themselves: - -| panel | for | -| --- | --- | -| outliner | the sphere list: add, delete, select | -| properties | the selected sphere's transform and colour, plus the viewport gizmo | -| black hole | camera mode/fov, the accretion disk, and integration quality | -| settings | display & renderer: vsync, frame cap, resolution, fullscreen, ui scale, api, overlay, hdri | -| export | choose which observable channels, at what resolution and format, then render to disk | -| diagnostics | device, backend, fps and a frame-time graph | - -the **workspace** menu (or cmd/ctrl + 1/2/3) switches tabs, the **window** menu -toggles the open workspace's panels, and **layout → reset** rebuilds the open -workspace's default arrangement, or all three. - -underneath, each workspace is its own imgui dock space with a fixed id. only the -open one is submitted for real each frame; the other two get a keep-alive ping, -which freezes their trees in place instead of tearing them down — nothing is laid -out or undocked while a workspace is off screen. an imgui window can only live in -one dock tree, so every panel is instanced once per workspace under a stable -`###workspace.panel` id (the same code draws all of them), which is also what lets -imgui save and restore each tree on its own. a workspace with no saved tree is -seeded on the first frame with the `DockBuilder` api. imgui doesn't remember which -workspace was open or which panels were showing, so a small `[Donut][Workspaces]` -section in `imgui.ini` does. only panels are docked windows — the centre stays a -pass-through hole onto the full-frame 3d render, so `Application::update_input` -drives the camera whenever the cursor is over that centre (imgui reports it -doesn't want the mouse) and yields to the panels otherwise. - -## the export pipeline - -export writes out the physical quantities donut computes, not just a screenshot. it -is driven by `RenderPath::export_frame` -([`render_path.cpp`](../src/rendering/render_path.cpp)) and the rhi's off-screen -helpers. - -```mermaid -flowchart LR - Cfg["ExportConfig<br/>channels · resolution · format"] --> Loop - subgraph Loop["for each enabled channel"] - direction TB - RT["create_render_target<br/>RGBA8 (PNG) or RGBA32F (raw)"] --> OS["run_offscreen:<br/>render_export(channel, raw)"] - OS --> RB["read_render_target(_float)"] - RB --> WR["write PNG / PFM / CSV"] - end - WR --> Files["exports/donut_[channel]_[timestamp].[ext]"] -``` - -a few points worth knowing: - -- the export always renders the settled view (`moving = false`) from the simulation - camera, at the requested resolution, whatever the live window is doing. -- any combination of colour, redshift $g$, emission temperature and impact parameter - (the observables from [physics](physics.md#observable-channels)) can be exported in - one pass. -- png requests use an `RGBA8` target and the standard shader pipeline - (`m_geo_pipeline`), giving a viewable tone-mapped or false-coloured image. pfm and - csv requests use an `RGBA32F` target and an hdr pipeline variant - (`m_geo_pipeline_hdr`), so the file holds the actual floating-point values: $g$ as - a ratio, temperature in kelvin, impact parameter in $r_s$, colour as linear hdr - radiance. for the disk-only channels, the alpha channel carries a validity mask (1 - where a ray hit the disk, 0 elsewhere). -- formats are png via `stb_image_write`, pfm (portable float map — raw rgb float, - the usual choice for hdr data) via a small writer, and csv for the scalar channels, - one grid value per cell. -- `run_offscreen` records a transient command buffer, submits it and waits, with no - swapchain and no frame pacing. the target is then read back on the cpu and written - to a timestamped file under `exports/`. - -## the build system - -donut uses premake5 to generate the build: gnu makefiles on macos -(`premake5 gmake`) and a visual studio 2022 solution on windows -(`premake5 vs2022`). both backends compile into one binary; the choice between -them is made at runtime from the saved settings, so there is no separate "opengl -build" and "vulkan build". the one exception is windows without the vulkan sdk: -premake then leaves the vulkan backend out (`DONUT_NO_VULKAN`) and the binary is -opengl only. on windows `vulkan-1.dll` is delay-loaded, so a vulkan build still -starts on a machine with no vulkan driver, and any vulkan failure at startup falls -back to opengl. step-by-step instructions for both platforms are in the -[readme](../README.md#building). - -```bash -premake5 gmake && make config=debug donut # macos -premake5 vs2022 # windows, then build donut.sln -``` - -the shaders are authored once in slang (`assets/shaders/*.slang`) and compiled by -`tools/compile-shaders.sh` into `assets/shaders/generated/`: glsl 4.10 for opengl -and spir-v for vulkan. the generated files are committed, so windows builds use -them as-is; on macos the script runs as a prebuild step and keeps them current. - -`premake5 clean` is wired up as a custom action that removes the generated build -output (`bin/`, `bin-int/`, the makefiles) along with the transient runtime files -(`logs/`, `config/`, `imgui.ini`), for a genuine from-scratch reset. - -vendored third-party code lives under `ext/`. the portable renderers and the rhi -are under `src/rendering/`, with the two backends under `src/platform/opengl/` and -`src/platform/vulkan/`. - ---- - -see [`physics.md`](physics.md) for the maths behind the image, and the top-level -[`README.md`](../README.md) for a project overview. |
