diff options
Diffstat (limited to 'docs/architecture.md')
| -rw-r--r-- | docs/architecture.md | 232 |
1 files changed, 116 insertions, 116 deletions
diff --git a/docs/architecture.md b/docs/architecture.md index c244586..0868775 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,27 +1,27 @@ -# The Architecture of Donut +# 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 +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). +for the physics behind the image itself, see [`physics.md`](physics.md). -## Contents +## 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](#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 +## overview -Donut is split into a few pieces with distinct jobs, so the physics, the platform, +donut is split into a few pieces with distinct jobs, so the physics, the platform, and the interface can change independently. ```mermaid @@ -44,24 +44,24 @@ flowchart TD BHR -.reads.-> Scene ``` -| Component | File | Responsibility | +| 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, dockable panels, default layouts; 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 | +| `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, dockable panels, default layouts; 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 +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 +## 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. +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 @@ -72,23 +72,23 @@ flowchart LR VK --> VKAPI[("Vulkan / MoltenVK")] ``` -The interface (in [`rhi.h`](../src/rendering/rhi.h)) is small and shaped for modern -GPUs: +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; + `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. +- `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 +the backend is chosen once at startup, before the window exists, since the two apis want the window created differently: ```mermaid @@ -106,14 +106,14 @@ sequenceDiagram 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 +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 +## 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 +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 @@ -132,26 +132,26 @@ sequenceDiagram 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 +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 +## the two renderers -`RenderPath` owns two independent renderers, both written purely against the RHI. +`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. +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) +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. +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`: @@ -163,104 +163,104 @@ flowchart TD R -->|BlackHole| GP["Off-screen geodesic pass →<br/>blit to swapchain + ImGui"] ``` -## Scene and Simulation: one world +## scene and simulation: one world -The editor and the simulation are the same world, not two separate scenes. The +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 +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 +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, +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 +## the rendering pipeline -When the Simulation view is active, `BlackHoleRenderer::render_geodesic` does three +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 +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 +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. +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, +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 +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. +blurs rather than aliasing into a shimmering fan. the colour channel is then +tone-mapped with an aces filmic curve. -### Progressive resolution and supersampling +### progressive resolution and supersampling -Interactivity trades against quality through resolution and sample count, not by +interactivity trades against quality through resolution and sample count, not by touching the physics: -| State | Off-screen target | Samples per pixel | +| state | off-screen target | samples per pixel | | --- | --- | --- | -| Camera moving | `GEO_LO` = 480 × 270 | 1 | -| Camera settled | `GEO_HI` = 960 × 540 | 4× rotated-grid supersampling | +| 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 +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 +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. -## The workspace: docking and panels +## the workspace: docking and panels -The interface is a docking shell. A main menu bar sits above a full-viewport dock -space with a pass-through centre, so the live 3D render shows through the middle +the interface is a docking shell. a main menu bar sits above a full-viewport dock +space with a pass-through centre, so the live 3d render shows through the middle while dockable tool panels attach to the edges — drag, tab, hide, or restore them -like a normal desktop app. The layout persists in `imgui.ini` between runs. +like a normal desktop app. the layout persists in `imgui.ini` between runs. -The panels are separate windows, each toggled from the **Window** menu: +the panels are separate windows, each toggled from the **window** menu: -| Panel | For | +| 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 | -| Stats | Device, backend, FPS and frame time | +| 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 | +| stats | device, backend, fps and frame time | -Two menus drive the rest. **View** picks what the centre viewport shows — `Scene` +two menus drive the rest. **view** picks what the centre viewport shows — `Scene` (the world editor) or `Simulation` (the lensed black hole) — which is the `View` -`UILayer::draw` returns to the `RenderPath`. **Layout** applies one of the default -arrangements, each built programmatically with ImGui's `DockBuilder` API and paired +`UILayer::draw` returns to the `RenderPath`. **layout** applies one of the default +arrangements, each built programmatically with imgui's `DockBuilder` api and paired with a sensible view and panel set: -| Layout | View | Panels shown | +| layout | view | panels shown | | --- | --- | --- | -| Simulation (default) | `Simulation` | Black Hole, Stats | -| Scene editing | `Scene` | Outliner, Properties, Stats | -| Export | `Simulation` | Export, Black Hole, Stats | +| simulation (default) | `Simulation` | black hole, stats | +| scene editing | `Scene` | outliner, properties, stats | +| export | `Simulation` | export, black hole, stats | -On first launch (no saved `imgui.ini`) the Simulation layout is built by -`DockBuilder`; after that the user's arrangement is restored, and *Layout → Reset* -rebuilds the current preset. 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 +on first launch (no saved `imgui.ini`) the simulation layout is built by +`DockBuilder`; after that the user's arrangement is restored, and *layout → reset* +rebuilds the current preset. 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 +## the export pipeline -Export writes out the physical quantities Donut computes, not just a screenshot. It +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 +([`render_path.cpp`](../src/rendering/render_path.cpp)) and the rhi's off-screen helpers. ```mermaid @@ -275,48 +275,48 @@ flowchart LR WR --> Files["exports/donut_[channel]_[timestamp].[ext]"] ``` -A few points worth knowing: +a few points worth knowing: -- The export always renders the settled view (`moving = false`) from the simulation +- 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 +- 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 +- 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 + 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, +- 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 + 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 +## the build system -Donut uses premake5 to generate GNU Makefiles. Both backends compile into one +donut uses premake5 to generate gnu makefiles. 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". +there is no separate "opengl build" and "vulkan build". -Generate and build (arm64 macOS): +generate and build (arm64 macos): ```bash premake5 gmake && make config=debug ``` `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 +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 +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 +see [`physics.md`](physics.md) for the maths behind the image, and the top-level [`README.md`](../README.md) for a project overview. |
