From 216972e73ad44c61ada048301f83e88dac533cbb Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Fri, 1 May 2026 23:35:37 +0200 Subject: [PATCH] new UI system --- claude-rewrite-plan.md | 435 +++++++ examples/HelloAnimation/README.md | 32 - examples/HelloAnimation/main.cpp | 44 - examples/HelloAnimation/project.json | 15 - examples/HelloDrawing/README.md | 28 - examples/HelloDrawing/main.cpp | 23 - examples/HelloDrawing/project.json | 16 - examples/HelloGrid/README.md | 19 - examples/HelloGrid/main.cpp | 59 - examples/HelloGrid/project.json | 15 - examples/HelloInput/README.md | 44 - examples/HelloInput/main.cpp | 32 - examples/HelloInput/project.json | 15 - examples/HelloRotation/main.cpp | 36 - examples/HelloRotation/project.json | 16 - examples/HelloText/README.md | 32 - examples/HelloText/main.cpp | 33 - examples/HelloText/project.json | 17 - examples/HelloUI/README.md | 32 - examples/HelloUI/main.cpp | 40 - examples/HelloUI/project.json | 16 - examples/README.md | 50 - examples/VulkanAnimation/closesthit.glsl | 12 - examples/VulkanAnimation/main.cpp | 175 --- examples/VulkanAnimation/miss.glsl | 9 - examples/VulkanAnimation/project.json | 32 - examples/VulkanAnimation/raygen.glsl | 47 - examples/VulkanTriangle/main.cpp | 18 +- examples/VulkanTriangle/project.cpp | 1 - .../inter.ttf => VulkanUI/Inter.ttf} | Bin examples/VulkanUI/main.cpp | 186 ++- examples/VulkanUI/project.cpp | 27 + examples/VulkanUI/project.json | 23 - examples/VulkanUI/raygen.glsl | 48 - implementations/Crafter.Graphics-Device.cpp | 83 +- implementations/Crafter.Graphics-Font.cpp | 20 +- implementations/Crafter.Graphics-Mesh.cpp | 7 +- .../Crafter.Graphics-MouseElement.cpp | 44 - .../Crafter.Graphics-RenderingElement3D.cpp | 7 +- .../Crafter.Graphics-Rendertarget.cpp | 141 --- implementations/Crafter.Graphics-Shm.cpp | 76 -- implementations/Crafter.Graphics-UIAtlas.cpp | 130 ++ .../Crafter.Graphics-UIRenderer.cpp | 354 ++++++ implementations/Crafter.Graphics-UIScene.cpp | 166 +++ implementations/Crafter.Graphics-Window.cpp | 311 ++--- ...Crafter.Graphics-DescriptorHeapVulkan.cppm | 68 +- interfaces/Crafter.Graphics-Device.cppm | 6 +- interfaces/Crafter.Graphics-Font.cppm | 35 +- interfaces/Crafter.Graphics-GridElement.cppm | 64 - interfaces/Crafter.Graphics-ImageVulkan.cppm | 10 +- interfaces/Crafter.Graphics-Mesh.cppm | 4 - .../Crafter.Graphics-PipelineRTVulkan.cppm | 7 +- ...ment.cppm => Crafter.Graphics-RTPass.cppm} | 42 +- .../Crafter.Graphics-RenderPass.cppm | 26 +- .../Crafter.Graphics-RenderingElement2D.cppm | 449 ------- ...after.Graphics-RenderingElement2DBase.cppm | 136 --- ...ter.Graphics-RenderingElement2DVulkan.cppm | 477 -------- .../Crafter.Graphics-RenderingElement3D.cppm | 6 +- interfaces/Crafter.Graphics-Rendertarget.cppm | 326 ----- .../Crafter.Graphics-SamplerVulkan.cppm | 4 - ...ter.Graphics-ShaderBindingTableVulkan.cppm | 7 +- interfaces/Crafter.Graphics-ShaderVulkan.cppm | 7 +- interfaces/Crafter.Graphics-Transform2D.cppm | 77 -- interfaces/Crafter.Graphics-Types.cppm | 4 - interfaces/Crafter.Graphics-UI.cppm | 30 + interfaces/Crafter.Graphics-UIAtlas.cppm | 100 ++ interfaces/Crafter.Graphics-UIDrawList.cppm | 167 +++ interfaces/Crafter.Graphics-UIHit.cppm | 50 + interfaces/Crafter.Graphics-UILayout.cppm | 73 ++ interfaces/Crafter.Graphics-UILength.cppm | 98 ++ interfaces/Crafter.Graphics-UIRenderer.cppm | 110 ++ interfaces/Crafter.Graphics-UIScene.cppm | 111 ++ interfaces/Crafter.Graphics-UITheme.cppm | 80 ++ interfaces/Crafter.Graphics-UIWidget.cppm | 189 +++ interfaces/Crafter.Graphics-UIWidgets.cppm | 989 ++++++++++++++++ interfaces/Crafter.Graphics-VulkanBuffer.cppm | 6 +- .../Crafter.Graphics-VulkanTransition.cppm | 7 +- interfaces/Crafter.Graphics-Window.cppm | 32 +- interfaces/Crafter.Graphics.cppm | 14 +- lib/stb_image_write.h | 1048 +++++++++++++++++ project.cpp | 61 +- shaders/ui.comp.glsl | 192 +++ 82 files changed, 4836 insertions(+), 3242 deletions(-) create mode 100644 claude-rewrite-plan.md delete mode 100644 examples/HelloAnimation/README.md delete mode 100644 examples/HelloAnimation/main.cpp delete mode 100644 examples/HelloAnimation/project.json delete mode 100644 examples/HelloDrawing/README.md delete mode 100644 examples/HelloDrawing/main.cpp delete mode 100644 examples/HelloDrawing/project.json delete mode 100644 examples/HelloGrid/README.md delete mode 100644 examples/HelloGrid/main.cpp delete mode 100644 examples/HelloGrid/project.json delete mode 100644 examples/HelloInput/README.md delete mode 100644 examples/HelloInput/main.cpp delete mode 100644 examples/HelloInput/project.json delete mode 100644 examples/HelloRotation/main.cpp delete mode 100644 examples/HelloRotation/project.json delete mode 100644 examples/HelloText/README.md delete mode 100644 examples/HelloText/main.cpp delete mode 100644 examples/HelloText/project.json delete mode 100644 examples/HelloUI/README.md delete mode 100644 examples/HelloUI/main.cpp delete mode 100644 examples/HelloUI/project.json delete mode 100644 examples/README.md delete mode 100644 examples/VulkanAnimation/closesthit.glsl delete mode 100644 examples/VulkanAnimation/main.cpp delete mode 100644 examples/VulkanAnimation/miss.glsl delete mode 100644 examples/VulkanAnimation/project.json delete mode 100644 examples/VulkanAnimation/raygen.glsl rename examples/{HelloText/inter.ttf => VulkanUI/Inter.ttf} (100%) create mode 100644 examples/VulkanUI/project.cpp delete mode 100644 examples/VulkanUI/project.json delete mode 100644 examples/VulkanUI/raygen.glsl delete mode 100644 implementations/Crafter.Graphics-MouseElement.cpp delete mode 100644 implementations/Crafter.Graphics-Rendertarget.cpp delete mode 100644 implementations/Crafter.Graphics-Shm.cpp create mode 100644 implementations/Crafter.Graphics-UIAtlas.cpp create mode 100644 implementations/Crafter.Graphics-UIRenderer.cpp create mode 100644 implementations/Crafter.Graphics-UIScene.cpp delete mode 100644 interfaces/Crafter.Graphics-GridElement.cppm rename interfaces/{Crafter.Graphics-MouseElement.cppm => Crafter.Graphics-RTPass.cppm} (50%) rename implementations/Crafter.Graphics-Transform2D.cpp => interfaces/Crafter.Graphics-RenderPass.cppm (54%) delete mode 100644 interfaces/Crafter.Graphics-RenderingElement2D.cppm delete mode 100644 interfaces/Crafter.Graphics-RenderingElement2DBase.cppm delete mode 100644 interfaces/Crafter.Graphics-RenderingElement2DVulkan.cppm delete mode 100644 interfaces/Crafter.Graphics-Rendertarget.cppm delete mode 100644 interfaces/Crafter.Graphics-Transform2D.cppm create mode 100644 interfaces/Crafter.Graphics-UI.cppm create mode 100644 interfaces/Crafter.Graphics-UIAtlas.cppm create mode 100644 interfaces/Crafter.Graphics-UIDrawList.cppm create mode 100644 interfaces/Crafter.Graphics-UIHit.cppm create mode 100644 interfaces/Crafter.Graphics-UILayout.cppm create mode 100644 interfaces/Crafter.Graphics-UILength.cppm create mode 100644 interfaces/Crafter.Graphics-UIRenderer.cppm create mode 100644 interfaces/Crafter.Graphics-UIScene.cppm create mode 100644 interfaces/Crafter.Graphics-UITheme.cppm create mode 100644 interfaces/Crafter.Graphics-UIWidget.cppm create mode 100644 interfaces/Crafter.Graphics-UIWidgets.cppm create mode 100644 lib/stb_image_write.h create mode 100644 shaders/ui.comp.glsl diff --git a/claude-rewrite-plan.md b/claude-rewrite-plan.md new file mode 100644 index 0000000..2aa2199 --- /dev/null +++ b/claude-rewrite-plan.md @@ -0,0 +1,435 @@ +# UI System for Crafter.Graphics2 + +## Context + +The old UI system (still visible in [/home/jorijn/repos/Crafter/Crafter.Graphics/](../../repos/Crafter/Crafter.Graphics/)) was stripped out of Crafter.Graphics2 because it was painful to use. Real-world usages — [/home/jorijn/repos/3DForts/implementations/Forts3D-MainMenu.cpp](../../repos/3DForts/implementations/Forts3D-MainMenu.cpp) (250-line constructor) and [/home/jorijn/repos/3DForts/implementations/Forts3D-OptionsMenu.cpp](../../repos/3DForts/implementations/Forts3D-OptionsMenu.cpp) (510 lines, manual descriptor surgery on every page change) — show the symptoms: 7-param `Anchor2D` repeated everywhere, manual `parent.children.push_back`, manual per-frame `UpdatePosition`, 8-`EventListener`-fields-per-widget callback boilerplate, no widget abstraction at all (users compose `RenderingElement2D` + `MouseElement` + `EventListener` by hand), no batching, no glyph atlas, character-by-character glyph render every frame. + +The new system must be: +- User-friendly and future-proof (declarative composition, automatic layout, no manual lifetime juggling). +- At least as performant as the old one (single batched draw, glyph atlas, dirty-tracked layout). +- Cleanly integrated into the existing 3D ray-traced pipeline. **Any change that touches the 3D path needs explicit OK from the user.** +- CPU-runnable as a nice-to-have — but explicitly *not* via a custom CPU rasterizer if it can't beat llvmpipe. + +## Design summary + +**Three-layer architecture:** + +1. **Widget layer (user-facing):** declarative builder API. Widgets are value types with chained `.method()` configuration; composite containers (`VStack`, `HStack`, `Stack`, `Overlay`, `Grid`, `TabView`, `ScrollView`) take children as `&&` parameter packs and own them inside a `UIScene` arena. The only handle user code keeps is `WidgetRef` (a stable typed reference into the scene). +2. **Layout / event layer:** two-pass measure/arrange (WPF/Avalonia/Flutter convention). Hit-testing is automatic from the laid-out tree; events route via capture → tunnel → bubble. Focus is tree-cycled with Tab. +3. **Rendering layer:** widgets emit `UIItem` records into a single per-frame SSBO; one compute-shader dispatch composites everything onto the swapchain image. SDF glyph atlas means one texture serves all sizes/scales. + +**Key decisions (locked):** + +- **Compute shader, not graphics pipeline.** The swapchain has only `VK_IMAGE_USAGE_STORAGE_BIT` ([Crafter.Graphics-Window.cpp:958](../../repos/Crafter/Crafter.Graphics2/implementations/Crafter.Graphics-Window.cpp#L958)). A compute pass writes the same way the existing RT pipeline does, in the same `VK_IMAGE_LAYOUT_GENERAL`. No swapchain change, no renderpass machinery, no extra barriers. +- **SDF glyph atlas, not per-size raster.** One R8 atlas (1024² growable to 4096²) built lazily from `stb_truetype`. Survives DPI / fractional-scale changes without re-bake. Aliases cleanly at any scale via shader smoothstep. +- **No tile-binning in V1.** A naive front-to-back per-pixel item scan over a ≤500-item draw list comfortably hits sub-millisecond on integrated GPUs. The data layout supports adding tile-binning later without changing the public API. +- **No custom CPU rasterizer.** Same Vulkan code path runs on llvmpipe via the loader. Documented as the CPU fallback. +- **One descriptor heap, bound once per frame, never re-bound.** Per the [`VK_EXT_descriptor_heap` proposal](https://docs.vulkan.org/features/latest/features/proposals/VK_EXT_descriptor_heap.html), heap rebinds are expensive and applications should "stick to the same heap throughout the lifetime of the application". UIScene allocates its slots from the same `Window::descriptorHeap` the user already creates for 3D — never its own. Detail in §"Descriptor management". +- **Window owns a list of `RenderPass*`, not a single pipeline pointer.** Replaces the current `Window::pipeline` / `Window::descriptorHeap` direct-bind model. Future-proofs for post-processing, debug overlays, RTT, multi-pass effects. Detail in §"Window integration". +- **Layout is dirty-tracked.** `Observable::set()` marks owning widgets dirty; layout re-walks only the dirty subtree. +- **Text supports per-glyph styling** via `TextRun`. Each glyph is already a separate draw-list item, so per-run styling adds zero shader complexity — just per-item properties. + +## Window integration — `RenderPass` refactor + +The current `Window::Render` ([Crafter.Graphics-Window.cpp:730-844](../../repos/Crafter/Crafter.Graphics2/implementations/Crafter.Graphics-Window.cpp#L730-L844)) hardcodes a single ray-tracing pipeline: it binds `Window::pipeline` ([Crafter.Graphics-Window.cppm:193](../../repos/Crafter/Crafter.Graphics2/interfaces/Crafter.Graphics-Window.cppm#L193)) and calls `vkCmdTraceRaysKHR` directly. That assumption breaks the moment we want UI on top, post-processing, debug overlays, or any multi-pass effect. + +**Refactor**: replace the single-pipeline pointer with a vector of render passes. + +```cpp +// new module: Crafter.Graphics-RenderPass.cppm +struct RenderPass { + virtual void Record(VkCommandBuffer cmd, std::uint32_t frameIdx) = 0; + virtual ~RenderPass() = default; +}; + +// in Window (replaces `pipeline` and `descriptorHeap` pointer fields) +std::vector passes; +DescriptorHeapVulkan* descriptorHeap; // still here, but now SHARED across all passes +std::optional> clearColor; // optional initial clear; default nullopt +``` + +`Window::Render` becomes: + +1. `vkAcquireNextImageKHR`. +2. Begin command buffer. +3. Barrier `UNDEFINED → GENERAL` (existing, unchanged). +4. Bind shared descriptor heap (resource + sampler) **once**. +5. If `clearColor.has_value()`: `vkCmdClearColorImage`. +6. For each `pass : passes`: `pass->Record(cmd, currentBuffer);` — Window inserts a storage→storage memory barrier between consecutive passes that both write to the swapchain image. (V1: insert always; cheap enough. V2: pass-declared write/read sets.) +7. Barrier `GENERAL → PRESENT_SRC_KHR` (existing, unchanged). +8. End / submit / present (existing, unchanged). + +**3D usage** (replaces the inline `vkCmdTraceRaysKHR` in `Window::Render`): + +```cpp +// new helper struct shipped with the library: Crafter.Graphics-RTPass.cppm +struct RTPass : RenderPass { + PipelineRTVulkan* pipeline; + void Record(VkCommandBuffer cmd, std::uint32_t frame) override { + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR, pipeline->pipeline); + Device::vkCmdTraceRaysKHR(cmd, &pipeline->raygenRegion, &pipeline->missRegion, + &pipeline->hitRegion, &pipeline->callableRegion, + width, height, 1); + } +}; + +// user code (replaces existing pattern in VulkanTriangle/main.cpp:195-196) +RTPass rt{&pipeline}; +window.passes.push_back(&rt); +``` + +**UI usage**: `UIScene` *is* a `RenderPass` (publicly inherits or composes). Its constructor `push_back`s itself onto `window.passes`. The compute dispatch happens inside `UIScene::Record`. + +**UI-only scenes** (main menu with no 3D): the user simply doesn't add an `RTPass`. UI is the only pass. Optionally set `window.clearColor = {0,0,0,1}` for a known background. No stock pipeline, no `nullptr` checks needed. + +This **is** an API break: `VulkanTriangle/main.cpp` line 195-196 changes from `window.pipeline = &pipeline; window.descriptorHeap = &descriptorHeap;` to `window.descriptorHeap = &descriptorHeap; RTPass rt{&pipeline}; window.passes.push_back(&rt);`. The user OK'd this in feedback ("I don't mind breaking the existing API as long as it's discussed and done for a good reason"). The reason: future-proof multi-pass support, decouples Window from RT specifically. + +## Public API — concrete shape + +### Static main menu (replaces ~250 lines of [Forts3D-MainMenu.cpp:24-273](../../repos/3DForts/implementations/Forts3D-MainMenu.cpp#L24-L273)) + +```cpp +import Crafter.Graphics; +using namespace Crafter::UI; + +UIScene scene(window); + +scene.Root( + Stack{}.background(Image{assetFolder/"Background.tex"}).children( + Image{logoTex}.anchor(Anchor::TopCenter).size(Length::Pct(80), Length::Auto()), + + VStack{}.anchor(Anchor::BottomLeft).padding(8, 16).spacing(6).children( + Button{"Sandbox"}.style(themes.menu).onClick([&]{ SwitchScene(Active::Game); }), + Button{"Options"}.style(themes.menu).onClick([&]{ SwitchScene(Active::Options); }), + Button{"Exit"} .style(themes.exit).onClick([&]{ std::_Exit(0); }) + ), + + Text{std::format("V1.0.0-{}", BUILD_TARGET)} + .anchor(Anchor::BottomRight).padding(8).size(15).color(textColor) + ) +); +``` + +### Dynamic options screen with tabs and bound input fields + +```cpp +struct OptionsScene { + UIScene scene; + Options temp = options; + + OptionsScene(Window& w) : scene(w) { + scene.Root(VStack{}.children( + HStack{}.height(Length::Pct(7.5)).background(panelBg).padding(8).children( + Button{"Exit"}.onClick([&]{ SwitchScene(Active::Main); }), + Spacer{}, Text{"OPTIONS"}.size(30), Spacer{}, + Button{"Save"}.onClick([&]{ options = temp; SwitchScene(Active::Main); }) + ), + TabView{} + .tab("Graphics", VStack{}.spacing(4).children( + OptionRow{"Resolution"}.right(InputField{}.bind(temp.resX, temp.resY)), + OptionRow{"Max lights"}.right(InputField{}.bind(temp.maxLights)) + )) + .tab("Input", BuildInputPage()) + .tab("Audio", BuildAudioPage()) + )); + } +}; +``` + +`InputField{}.bind(member)` does both display (via `std::format`) and parse (via `std::from_chars`); no manual `TryParse`. `TabView` swaps content automatically on tab click — no manual erase/push_back/`UpdateElements`/`CreateBuffer`/`ReorderBuffer` (compare to [Forts3D-OptionsMenu.cpp:307-509](../../repos/3DForts/implementations/Forts3D-OptionsMenu.cpp#L307-L509)). + +### Frame-updated HUD overlay + +```cpp +Observable fps; +Observable health{100}; + +scene.Root(Overlay{}.children( + Text{}.bindFmt("{:.1f} fps", fps).anchor(Anchor::TopRight).padding(8), + ProgressBar{}.bindValue(health, 0, 100).anchor(Anchor::BottomCenter).size(Length::Px(300), Length::Px(20)) +)); + +window.onUpdate += [&](FrameTime t){ fps = 1.0 / t.delta.count(); health = currentHealth; }; +``` + +`Observable` mutation marks only its widget dirty; layout re-walks only that subtree; only the affected items in the SSBO are rewritten. + +### Per-glyph text styling + +Most call sites want a single style — that's the simple shorthand: + +```cpp +Text{"Welcome"}.size(24).color(textColor); +``` + +For mixed styling — common in HUDs ("Damage: 250" with the number colored), tooltips, dialog text, or future code-editor-like surfaces — pass an explicit list of `TextRun`: + +```cpp +Text{}.runs( + TextRun{"Health: "}.color(white), + TextRun{std::format("{}", hp)}.color(hp < 25 ? red : green).bold(), + TextRun{" / "}.color(grey), + TextRun{"100"}.color(white) +).size(20); // base size; runs can override +``` + +Each run can independently set `color`, `size`, `weight`, `italic`, `underline`, `strikethrough`, and (V2) `font`. Internally each glyph already produces its own `UIItem`, so per-run styling is just per-item properties — zero shader complexity. Layout splits the runs at line-wrap points naturally. + +Bound text uses the same machinery: + +```cpp +Text{}.bind(observableRuns); // Observable> +Text{}.bindFmt("{:.1f} fps", fpsObservable); // single-style shorthand for the common case +``` + +### User-defined composites + +```cpp +struct OptionRow { + std::string label; + Widget rightChild; + OptionRow(std::string l) : label(std::move(l)) {} + OptionRow& right(Widget w) && { rightChild = std::move(w); return *this; } + operator Widget() && { + return HStack{}.padding(4, 8).children( + Text{label}.size(18).expand(), std::move(rightChild) + ); + } +}; +``` + +No inheritance, no event-listener fields, no `parent.children.push_back`. A composite is just a function returning a `Widget`. + +## Layout system + +- **Two-pass measure/arrange.** `Measure(availableSize) → desiredSize`; `Arrange(finalRect)`. Cached per-node in `LayoutResult { Rect rectPx; Rect clipPx; }`. +- **Units:** `Length::Px(float)` (logical px), `Length::Pct(float)` (% of parent on same axis), `Length::Auto()` (use measured desired), `Length::Frac(float)` (weighted fill — Flutter `Expanded`). +- **DPI / fractional-scale:** `UIScene` reads `Window::scale` ([Crafter.Graphics-Window.cppm:136](../../repos/Crafter/Crafter.Graphics2/interfaces/Crafter.Graphics-Window.cppm#L136)) once per layout pass. `Px` values multiply by scale exactly once at logical→device conversion. `Pct` is scale-invariant by construction. +- **Containers V1:** `Stack`, `HStack`, `VStack`, `Overlay`, `Grid`. No CSS-style auto-everything. + +## Event / interaction model + +`UIScene` subscribes to existing `Window` events ([Crafter.Graphics-Window.cppm:75-95](../../repos/Crafter/Crafter.Graphics2/interfaces/Crafter.Graphics-Window.cppm#L75-L95)) — no new Window input API needed. + +- **Hit-testing:** top-down walk, gather containers whose `clipPx` contains cursor, pick topmost interactive leaf. +- **Routing:** capture (drag/modal) → tunnel (`onPreviewKeyDown`) → bubble (`onKeyDown`). Handler returns `Handled` to stop. +- **Focus:** `UIScene::focused` is a `WidgetRef<>`. Click grabs focus. Tab/Shift-Tab cycles focusables in tree order. `onTextInput` / `onKeyDown` start bubbling from `focused`. +- **Drag/scroll:** `Capture()` / `Release()` on the event arg. `ScrollView` captures wheel and drag-with-button-down. + +## Rendering pipeline + +### Per-frame draw list + +Single mapped SSBO `VulkanBuffer itemBuf[Window::numFrames]` — matches the existing 3-frame ring pattern ([Crafter.Graphics-Window.cppm:180](../../repos/Crafter/Crafter.Graphics2/interfaces/Crafter.Graphics-Window.cppm#L180)). + +```glsl +struct UIItem { + uint type; // 0=rect, 1=roundRect, 2=glyph, 3=image, 4=line, 5=clipPush, 6=clipPop + uint flags; + vec2 posPx; // top-left, device px + vec2 sizePx; + vec4 color; // primary + vec4 colorB; // gradient stop / shadow + vec4 uvRect; // glyph atlas uv or image source rect + uint imageIdx; // bindless image slot (0 = none / use atlas) + uint cornerRadiusPx; + vec2 reserved; +}; // 96 bytes +``` + +Tree walk emits items front-to-back. Clip stack is encoded inline as `clipPush`/`clipPop` items (Skia/Slate trick — keeps shader simple). + +### Compute shader (skeleton) + +`shaders/ui.comp.glsl` → `ui.spv`, loaded via existing [Crafter.Graphics-ShaderVulkan.cppm:39](../../repos/Crafter/Crafter.Graphics2/interfaces/Crafter.Graphics-ShaderVulkan.cppm#L39). + +```glsl +layout(local_size_x = 16, local_size_y = 16) in; +layout(binding = 0, rgba8) uniform image2D outImage; // swapchain +layout(binding = 1) buffer ItemBuffer { UIItem items[]; }; +layout(binding = 2) uniform sampler2D fontAtlas; // SDF +layout(binding = 3) uniform sampler2D bindlessImages[]; + +void main() { + ivec2 p = ivec2(gl_GlobalInvocationID.xy); + vec4 dst = imageLoad(outImage, p); // composite OVER existing 3D content + ClipStack stack; + for (uint i = 0; i < pc.itemCount; ++i) { + UIItem it = items[i]; + if (it.type == TYPE_CLIP_PUSH) { stack.push(it); continue; } + if (it.type == TYPE_CLIP_POP) { stack.pop(); continue; } + if (!stack.contains(p)) continue; + vec4 src = ShadeItem(it, vec2(p)); + dst = vec4(src.rgb + dst.rgb*(1-src.a), src.a + dst.a*(1-src.a)); + } + imageStore(outImage, p, dst); +} +``` + +Dispatch: `vkCmdDispatch(cmd, ceil(width/16), ceil(height/16), 1)` once per frame. Tile-binning V2 replaces the inner loop with a per-tile range without API change. + +### Glyph atlas + +- One `ImageVulkan` 1024² (growable). Created once at `UIScene` init. +- Shelf-allocator inserts glyphs lazily; `Font` already exposes `stbtt_fontinfo` ([Crafter.Graphics-Font.cppm:35](../../repos/Crafter/Crafter.Graphics2/interfaces/Crafter.Graphics-Font.cppm#L35)). +- SDF rasterized via `stbtt_GetGlyphSDF`. One atlas serves all sizes via shader-side smoothstep on screen-space derivative. +- Updates batched: layout collects missing glyphs per frame, single `ImageVulkan::Update` ([Crafter.Graphics-ImageVulkan.cppm:97](../../repos/Crafter/Crafter.Graphics2/interfaces/Crafter.Graphics-ImageVulkan.cppm#L97)) before dispatch. + +### Descriptor management — single shared heap + +**Hard rule**: the descriptor heap is bound exactly once per frame by `Window::Render`, and never re-bound. All passes (RT, UI, future post-processing) read from the same heap; they index into different *slot ranges* via push constants or specialization constants. + +This requires extending `DescriptorHeapVulkan` ([Crafter.Graphics-DescriptorHeapVulkan.cppm](../../repos/Crafter/Crafter.Graphics2/interfaces/Crafter.Graphics-DescriptorHeapVulkan.cppm)) with a slot-allocator API on top of its existing fixed-size pre-allocation: + +```cpp +struct DescriptorHeapVulkan { + // existing: resourceHeap[Window::numFrames], samplerHeap[Window::numFrames], etc. + + // new — bump allocators with optional free-lists in V2 + struct ImageSlotRange { std::uint16_t firstElement; std::uint16_t count; }; + struct BufferSlotRange { std::uint16_t firstElement; std::uint16_t count; }; + struct SamplerSlotRange{ std::uint16_t firstElement; std::uint16_t count; }; + + ImageSlotRange AllocateImageSlots(std::uint16_t count); + BufferSlotRange AllocateBufferSlots(std::uint16_t count); + SamplerSlotRange AllocateSamplerSlots(std::uint16_t count); + + // existing helpers GetBufferOffset / GetBufferOffsetElement still useful + // for translating a slot-range to a host-write address. +}; +``` + +**3D usage (migration)**: today, [VulkanTriangle/main.cpp:162-187](../../repos/Crafter/Crafter.Graphics2/examples/VulkanTriangle/main.cpp#L162-L187) hand-computes addresses with `descriptorHeap.bufferStartElement` and `descriptorHeap.bufferStartOffset`. Migrated: + +```cpp +auto rtImageSlots = descriptorHeap.AllocateImageSlots(3); // 3 swapchain views +auto rtTlasSlots = descriptorHeap.AllocateBufferSlots(3); // 3 TLAS addrs +// vkWriteResourceDescriptorsEXT writes into those specific offsets +// raygen.glsl reads them via specialization constants for the slot indices +``` + +**UI usage**: `UIScene::Initialize(window)` calls: + +```cpp +auto atlasSlot = window.descriptorHeap->AllocateImageSlots(1); +auto bindlessSlots = window.descriptorHeap->AllocateImageSlots(64); // Image{} widgets +auto itemBufSlots = window.descriptorHeap->AllocateBufferSlots(Window::numFrames); +auto samplerSlots = window.descriptorHeap->AllocateSamplerSlots(2); // linear + nearest +``` + +UI's compute shader uses these slot indices passed in via push constants. **No second heap, no rebind**. + +**Sizing the heap**: the user must size `DescriptorHeapVulkan::Initialize(images, buffers, samplers)` to fit *all* subsystems combined. The library can ship a `Window::EnsureDescriptorHeap(images, buffers, samplers)` helper that lazily creates a default-sized heap (e.g. 128 images, 32 buffers, 16 samplers) if the user hasn't explicitly created one — UI calls this first thing. + +**For UI-only scenes** (no 3D): user can skip creating a heap entirely; `UIScene` calls `EnsureDescriptorHeap` and gets a sensible default. + +### Dispatch site (inside `UIScene::Record(cmd, frame)`) + +1. `vkCmdBindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, uiPipeline)`. +2. (No heap binding — already bound by `Window::Render`.) +3. Push constants: item count, surface size, scale, slot-range starts (atlas, bindless base, item buf, samplers). +4. `vkCmdDispatch(cmd, ceil(width/16), ceil(height/16), 1)`. + +The `Window`-inserted storage→storage memory barrier between passes ensures the previous pass's writes are visible. + +## New files (matching existing convention) + +Add to [interfaces/](../../repos/Crafter/Crafter.Graphics2/interfaces/): + +| File | Role | +|---|---| +| `Crafter.Graphics-RenderPass.cppm` | `RenderPass` base. Used by both UI and 3D. | +| `Crafter.Graphics-RTPass.cppm` | `RTPass` helper that records a `vkCmdTraceRaysKHR` call from a `PipelineRTVulkan*`. | +| `Crafter.Graphics-UI.cppm` | `:UI` partition; re-exports the sub-partitions. | +| `Crafter.Graphics-UI-Length.cppm` | `Length`, `Anchor`, `Edges`, `Color`. | +| `Crafter.Graphics-UI-Widget.cppm` | `Widget` (handle), `WidgetRef`, `Observable`. | +| `Crafter.Graphics-UI-Widgets.cppm` | Stock widgets (see scope below) including `Text` + `TextRun`. | +| `Crafter.Graphics-UI-Layout.cppm` | Measure/arrange engine. | +| `Crafter.Graphics-UI-Hit.cppm` | Hit-testing + capture/tunnel/bubble router. | +| `Crafter.Graphics-UI-Theme.cppm` | `Theme` struct + `themes::default_dark`. | +| `Crafter.Graphics-UI-Atlas.cppm` | SDF glyph atlas atop `Font`. | +| `Crafter.Graphics-UI-DrawList.cppm` | `UIItem` + tree→buffer emitter. | +| `Crafter.Graphics-UI-Renderer.cppm` | Compute pipeline, per-frame item buffers, dispatch (implements `RenderPass`). | +| `Crafter.Graphics-UI-Scene.cppm` | `UIScene` — the only thing user code constructs; owns its `RenderPass` instance. | + +Add to [implementations/](../../repos/Crafter/Crafter.Graphics2/implementations/): +`Crafter.Graphics-UI-Layout.cpp`, `Crafter.Graphics-UI-Hit.cpp`, `Crafter.Graphics-UI-Atlas.cpp`, `Crafter.Graphics-UI-Renderer.cpp`, `Crafter.Graphics-UI-Scene.cpp`. (`RenderPass` and `RTPass` headers are header-only.) + +Add `shaders/ui.comp.glsl` — compiled to `ui.spv`, loaded via existing [Crafter.Graphics-ShaderVulkan.cppm:39](../../repos/Crafter/Crafter.Graphics2/interfaces/Crafter.Graphics-ShaderVulkan.cppm#L39) `ifstream` pattern. + +### Updates to existing files + +- [project.cpp](../../repos/Crafter/Crafter.Graphics2/project.cpp): bump `ifaces` array from 17 → 30 (RenderPass + RTPass + 11 UI), `impls` from 5 → 10. +- [interfaces/Crafter.Graphics.cppm](../../repos/Crafter/Crafter.Graphics2/interfaces/Crafter.Graphics.cppm): add `export import :RenderPass;`, `export import :RTPass;`, `export import :UI;`. +- [interfaces/Crafter.Graphics-Window.cppm](../../repos/Crafter/Crafter.Graphics2/interfaces/Crafter.Graphics-Window.cppm): replace `PipelineRTVulkan* pipeline;` field (line 193) with `std::vector passes;` and `std::optional> clearColor;`. Keep `DescriptorHeapVulkan* descriptorHeap;` (now shared across all passes). Add forward decl of `RenderPass`. +- [implementations/Crafter.Graphics-Window.cpp](../../repos/Crafter/Crafter.Graphics2/implementations/Crafter.Graphics-Window.cpp) `Window::Render` lines 730-844: replace the inline RT-bind-and-trace block (780-804) with: bind heap once (782-802 stays, lifted out of the per-pass code path), optional `vkCmdClearColorImage` if `clearColor`, then `for (auto* p : passes) { p->Record(cmd, currentBuffer); insertStorageBarrier(); }`. +- [interfaces/Crafter.Graphics-DescriptorHeapVulkan.cppm](../../repos/Crafter/Crafter.Graphics2/interfaces/Crafter.Graphics-DescriptorHeapVulkan.cppm): add slot-allocator API (`AllocateImageSlots`, `AllocateBufferSlots`, `AllocateSamplerSlots`) plus internal bump-allocator state. Existing static helpers stay. +- [examples/VulkanTriangle/main.cpp](../../repos/Crafter/Crafter.Graphics2/examples/VulkanTriangle/main.cpp): migrate to slot-allocator + `RTPass` + `window.passes.push_back`. Lines 162-196 are the affected region; replacement is shorter. + +## Migration story (for projects like 3DForts and `VulkanTriangle`) + +| Old | New | +|---|---| +| Compose `RenderingElement2D` + `MouseElement` + `EventListener` per widget | Stock widgets from `:UI-Widgets`. | +| 7-param `Anchor2D` everywhere | Builder methods (`anchor`, `padding`, `size`); often unneeded inside layout containers. | +| Manual `parent.children.push_back` | `.children(...)` | +| Per-frame `UpdateElements` / `CreateBuffer` / `ReorderBuffer` / `UpdatePosition` | Implicit; `UIScene` does it. | +| Manual `DescriptorHeapVulkan::Initialize` + `WriteDescriptors` per scene with hand-computed offsets | One shared `DescriptorHeapVulkan` for the application; subsystems call `AllocateImageSlots`/`AllocateBufferSlots`/`AllocateSamplerSlots`. | +| Custom `RaygenMenu.spv` shader per UI scene | Drop; UI uses one library compute shader. | +| `window.pipeline = &p; window.descriptorHeap = &h;` | `window.descriptorHeap = &h; window.passes.push_back(&pass);` for each pass (RT, UI, etc.). UI-only scenes don't push an RT pass — they just rely on `window.clearColor` for background. | + +The `VulkanTriangle` example migration is the smallest concrete example: lines 162-196 of [main.cpp](../../repos/Crafter/Crafter.Graphics2/examples/VulkanTriangle/main.cpp#L162-L196) shrink because slot offsets come from the allocator instead of being hand-computed; `window.pipeline = ...` becomes `RTPass rt{&pipeline}; window.passes.push_back(&rt);`. + +Migration is mostly *deletion*. A typical 350-line constructor becomes ~30 lines of declarative tree. + +## Verification plan + +1. **Build:** `cd Crafter.Graphics2 && crafter-build --vulkan` produces the static lib including all new modules. +2. **GPU smoke:** `examples/UI/main.cpp` (a new example) — a window showing nested stacks, a button that toggles its label, a focused text input, and a progress bar bound to an `Observable` driven from `onUpdate`. All on real GPU. +3. **CPU smoke:** same example with `VK_ICD_FILENAMES=…/lvp_icd.x86_64.json` to force llvmpipe. Confirm visual parity, framerate ≥30 fps for a static menu. +4. **Performance regression check:** port `Forts3D-MainMenu.cpp` and `Forts3D-OptionsMenu.cpp` to the new API; A/B compare frame time on the same hardware. Target: equal or better than old. +5. **3D pipeline non-regression:** after Phase 1, `examples/VulkanTriangle/main.cpp` (now using `RTPass` + `window.passes`) renders identically to its pre-refactor behavior — same triangle, same colors, same framerate. This is the gate before any UI code is written. +6. **Mixed scene:** a third example combines `VulkanTriangle`'s ray-traced triangle with an HUD overlay (two-pass: `RTPass` + `UIScene`); confirms the `RenderPass`/inter-pass-barrier integration works end-to-end and UI composites correctly over RT output. + +## Implementation order + +Roughly 18-20 working days (~4 weeks) for one engineer. **Phase 1 is the foundational refactor that touches the 3D path; it must land cleanly before any UI code is written, with `VulkanTriangle` working at every step.** + +**Phase 1 — refactor** (must keep `VulkanTriangle` running after each step): +1. Add `RenderPass` base + `RTPass` helper. ½ day. +2. Extend `DescriptorHeapVulkan` with slot-allocator API (bump allocators). ½ day. +3. Refactor `Window::Render`: replace single-pipeline bind/trace with `passes` loop, lift heap-bind out, add optional `clearColor` clear, insert inter-pass barriers. 1 day. +4. Migrate `VulkanTriangle/main.cpp` to the new API. ½ day. **Gate to Phase 2 — must run identically to today.** + +**Phase 2 — UI core**: +5. `UI-Length`, `UI-Widget` (values + handle + observable). 1 day. +6. `UI-Layout` measure/arrange engine. 1.5 days. +7. Stock widgets in priority order: stacks → Text (with `TextRun`) → Button → Image → InputField → ScrollView → TabView → ProgressBar. 3.5 days. +8. `UI-Theme`. ½ day. +9. `UI-Hit` (hit-test + capture/tunnel/bubble router). 1 day. + +**Phase 3 — UI rendering**: +10. `UI-Atlas` SDF on top of `Font`. 1.5 days. +11. `UI-DrawList`. ½ day. +12. `shaders/ui.comp.glsl`. 1 day, iterating with renderer. +13. `UI-Renderer` (`RenderPass`-implementing compute dispatcher). 2 days. +14. `UI-Scene` — wires everything to `Window` and registers itself in `window.passes`. ½ day. + +**Phase 4 — validation & migration**: +15. `examples/UI/main.cpp` showing static menu, dynamic input, HUD overlay over RT. 1 day. +16. Migrate `Forts3D-MainMenu` + `Forts3D-OptionsMenu` as the integration test. 1 day each. + +## Decisions locked from user feedback + +- **Window integration:** `Window::passes` refactor (not the `onAfterTrace` event) — chosen for future-proofing despite breaking `VulkanTriangle`. Migration is part of Phase 1. +- **Descriptor heap:** single shared heap, slot-allocator API on `DescriptorHeapVulkan`. **Never re-bound mid-frame.** This was the hard dealbreaker; the original "UI owns its own heap" plan is dropped. +- **Widget set V1:** `Stack`, `HStack`, `VStack`, `Overlay`, `Grid`, `Text` (with `TextRun`), `Button`, `Image`, `InputField` (string / integral / float / bound enum), `ScrollView`, `TabView`, `ProgressBar`, `Spacer`. Slider/Checkbox/Dropdown deferred to V2 unless requested. +- **Theming:** flat `Theme` struct with named slots, per-instance override via `.style(...)`. No cascading. +- **Animation:** the existing `Animation` is not woven into the UI API; users may drive `Observable` from any source (including `Animation` if they want). No `Animated` adapter, no `.fadeIn(2s)` shortcuts in V1. +- **Text V1:** single-font, LTR only, soft-wrap on space, kerning from stb. **Per-glyph styling is in V1** via `TextRun` (cheap to add, addresses the "text was a mess" feedback). ICU-grade BiDi/complex-shaping is V2+. +- **SPIR-V delivery:** `ifstream` the `.spv` (matches existing `VulkanShader` pattern). +- **Multi-window:** one `UIScene` per `Window`. Users wanting cross-window mirroring do it manually. +- **RTT / world-space UI:** V2. +- **Hot-reload:** V2 (nice to have, not blocking). + +- **Heap auto-creation:** `UIScene` calls `Window::EnsureDescriptorHeap(128 images, 32 buffers, 16 samplers)` if the user hasn't already attached one. Users wanting tighter control can pre-create their own heap before constructing `UIScene`. diff --git a/examples/HelloAnimation/README.md b/examples/HelloAnimation/README.md deleted file mode 100644 index 6068a7e..0000000 --- a/examples/HelloAnimation/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# HelloWindow Example - -## Description - -This example demonstrates how to draw pixels to a window. - -## Expected Result - -A window with a green and blue colored square, when clicking on the square it logs the coordinates relative to the square. - -## Highlighted Code Snippet - -```cpp -UiElement& element = window.elements.emplace_back( - 0.5, - 0.5, - 2, - 1, - 0.5f, - 0.5f, - 0.5, - 0.5, - 0, - false -); -``` - -## How to Run - -```bash -crafter-build build executable -r -``` \ No newline at end of file diff --git a/examples/HelloAnimation/main.cpp b/examples/HelloAnimation/main.cpp deleted file mode 100644 index db17b1c..0000000 --- a/examples/HelloAnimation/main.cpp +++ /dev/null @@ -1,44 +0,0 @@ -import Crafter.Event; -import Crafter.Graphics; -import std; -using namespace Crafter; - -int main() { - WindowWayland window(1280, 720, "Hello Input!"); - - RenderingElementScaling element( - OpaqueType::FullyOpaque, //opaque, wether the element is opague or semi-transparant - 2, //bufferWidth: the width of this elements pixel buffer - 1, //bufferHeight: the height of this elements pixel buffer - FractionalToMapped(0.5), //anchorX: relative position where this elements x anchor (top-left) is placed to its parent x anchor - FractionalToMapped(0.5), //anchorY: relative position where this elements y anchor (top-left) is placed to its parent y anchor - FractionalToMapped(0.5), //relativeSizeX: the relative x size this element should be scaled to compared to its parent - FractionalToMapped(0.5), //relativeSizeY: the relative y size this element should be scaled to compared to its parent - FractionalToMapped(0.5), //anchorOffsetX: the amount this element's anchor should be offset from the top left corner (0.5 to in the middle) - FractionalToMapped(0.5), //anchorOffsetY: the amount this element's anchor should be offset from the top left corner (0.5 to place it in the middle) - 0 //z: this elements Z position - ); - - window.elements.push_back(&element); - element.buffer = {{255, 0, 0 ,255}, {0, 255, 0 ,255}}; - element.UpdatePosition(window); - - Animation> anim({ - {std::chrono::seconds(5), FractionalToMapped(-0.5), FractionalToMapped(1.5)}, - }); - - anim.Start(std::chrono::high_resolution_clock::now()); - - EventListener updateListener(&window.onUpdate, [&](FrameTime time){ - std::tuple value = anim.Play(time.now); - element.anchorX = std::get<0>(value); - element.UpdatePosition(window); - if(anim.currentFrame == anim.keyframes.size()) { - anim.Start(time.now); - } - window.LogTiming(); - }); - - window.StartUpdate(); - window.StartSync(); -} diff --git a/examples/HelloAnimation/project.json b/examples/HelloAnimation/project.json deleted file mode 100644 index 3762cb1..0000000 --- a/examples/HelloAnimation/project.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "crafter-graphics", - "configurations": [ - { - "name": "executable", - "implementations": ["main"], - "dependencies": [ - { - "path":"../../project.json", - "configuration":"lib-wayland-timing" - } - ] - } - ] -} diff --git a/examples/HelloDrawing/README.md b/examples/HelloDrawing/README.md deleted file mode 100644 index 3cc3977..0000000 --- a/examples/HelloDrawing/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# HelloWindow Example - -## Description - -This example demonstrates how to draw pixels to a window. - -## Expected Result - -A window with a red colored square. - -## Highlighted Code Snippet - -```cpp -for(std::uint_fast32_t x = 0; x < 1280; x++) { - for(std::uint_fast32_t y = 0; y < 720; y++) { - window.framebuffer[x*720+y].r = 255; - window.framebuffer[x*720+y].g = 0; - window.framebuffer[x*720+y].b = 0; - window.framebuffer[x*720+y].a = 255; - } -} -``` - -## How to Run - -```bash -crafter-build build executable -r -``` diff --git a/examples/HelloDrawing/main.cpp b/examples/HelloDrawing/main.cpp deleted file mode 100644 index 61a96e6..0000000 --- a/examples/HelloDrawing/main.cpp +++ /dev/null @@ -1,23 +0,0 @@ -import Crafter.Graphics; -import std; -using namespace Crafter; - -constexpr std::uint32_t width = 1280; -constexpr std::uint32_t height = 720; - -int main() { - Device::Initialize(); - Window window(width, height, "Hello Drawing!"); - - for(std::uint32_t x = 0; x < width; x++) { - for(std::uint32_t y = 0; y < height; y++) { - window.renderer.buffer[x*height+y].r = 255; - window.renderer.buffer[x*height+y].g = 0; - window.renderer.buffer[x*height+y].b = 0; - window.renderer.buffer[x*height+y].a = 255; - } - } - - window.Render(); - window.StartSync(); -} diff --git a/examples/HelloDrawing/project.json b/examples/HelloDrawing/project.json deleted file mode 100644 index 976af27..0000000 --- a/examples/HelloDrawing/project.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "crafter-graphics", - "configurations": [ - { - "name": "executable", - "implementations": ["main"], - "dependencies": [ - { - "path":"../../project.json", - "configuration":"lib-wayland-debug" - } - ], - "debug": true - } - ] -} diff --git a/examples/HelloGrid/README.md b/examples/HelloGrid/README.md deleted file mode 100644 index 2b0f612..0000000 --- a/examples/HelloGrid/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# HelloGrid Example - -This example demonstrates the usage of the GridElement class which arranges its children in configurable grid patterns. - -## Features Shown - -- Creating a GridElement with specified columns and rows -- Setting spacing between grid cells -- Adding multiple child elements to the grid -- Automatic positioning of children in a grid layout -- Different colored elements to visualize the grid structure - -## How It Works - -The GridElement automatically distributes its children across a grid defined by: -- Number of columns and rows -- Horizontal and vertical spacing between elements - -Each child element is positioned in row-major order, filling the grid from left to right and top to bottom. \ No newline at end of file diff --git a/examples/HelloGrid/main.cpp b/examples/HelloGrid/main.cpp deleted file mode 100644 index 9f1f6eb..0000000 --- a/examples/HelloGrid/main.cpp +++ /dev/null @@ -1,59 +0,0 @@ -import Crafter.Event; -import Crafter.Graphics; -import std; -using namespace Crafter; - -int main() { - WindowWayland window(1280, 720, "Hello Grid!"); - - // Create a GridElement with 3 columns and 2 rows - GridElement grid( - 3, // columns - 2, // rows - FractionalToMapped(0.1), // spacingX - FractionalToMapped(0.1), // spacingY - FractionalToMapped(0), // anchorX - FractionalToMapped(0), // anchorY - FractionalToMapped(1), // relativeWidth - FractionalToMapped(1), // relativeHeight - FractionalToMapped(0), // anchorOffsetX - FractionalToMapped(0), // anchorOffsetY - 0 // z - ); - - for (int i = 0; i < 6; i++) { - RenderingElementScaling* rendering = new RenderingElementScaling( - OpaqueType::FullyOpaque, - 1, - 1, - FractionalToMapped(0), // anchorX - FractionalToMapped(0), // anchorY - FractionalToMapped(1.0), // relativeSizeX (will be overridden by grid) - FractionalToMapped(1.0), // relativeSizeY (will be overridden by grid) - FractionalToMapped(0.0), // anchorOffsetX - FractionalToMapped(0.0), // anchorOffsetY - 0 // z - ); - - // // Set different colors for each element - switch (i % 6) { - case 0: rendering->buffer = {{255, 0, 0, 255}}; break; // Red - case 1: rendering->buffer = {{0, 255, 0, 255}}; break; // Green - case 2: rendering->buffer = {{0, 0, 255, 255}}; break; // Blue - case 3: rendering->buffer = {{255, 255, 0, 255}}; break; // Yellow - case 4: rendering->buffer = {{255, 0, 255, 255}}; break; // Magenta - case 5: rendering->buffer = {{0, 255, 255, 255}}; break; // Cyan - } - - grid.children.push_back(rendering); - } - - // Add the grid to the window - window.elements.push_back(&grid); - - // Update positions to arrange children in grid - grid.UpdatePosition(window); - - window.Render(); - window.StartSync(); -} \ No newline at end of file diff --git a/examples/HelloGrid/project.json b/examples/HelloGrid/project.json deleted file mode 100644 index 70e3d7a..0000000 --- a/examples/HelloGrid/project.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "crafter-graphics", - "configurations": [ - { - "name": "executable", - "implementations": ["main"], - "dependencies": [ - { - "path":"../../project.json", - "configuration":"lib-wayland" - } - ] - } - ] -} diff --git a/examples/HelloInput/README.md b/examples/HelloInput/README.md deleted file mode 100644 index 10594c3..0000000 --- a/examples/HelloInput/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# HelloInput Example - -## Description - -This example demonstrates how to handle basic input events by: - -- Create a window using `WindowWaylandWayland` -- Register event listeners using `EventListener` for: - - Mouse click events - - Specific key events - - General keypress events -- Print formatted feedback to the console when events are triggered - -## Expected Result - -When you interact with the window, you might see console output like: - -Clicked on X:450 Y:320! -Pressed specifically the a key! -Pressed the b key! - -Make sure that the window has focus. - -## Highlighted Code Snippet - -```cpp -EventListener clickListener(&window.onMouseLeftClick, [&window](MousePoint point){ - std::cout << std::format("Clicked on X:{} Y:{}!", MappedToPixel(point.x, window.width), MappedToPixel(point.y, window.height)); -}); - -EventListener keyAListener(&window.onKeyDown['a'], [](){ - std::cout << std::format("Pressed specifically the a key!"); -}); - -EventListener anyKeyListener(&window.onAnyKeyDown, [](char key){ - std::cout << std::format("Pressed the {} key!", key); -}); -``` - -## How to Run - -```bash -crafter-build build executable -r -``` \ No newline at end of file diff --git a/examples/HelloInput/main.cpp b/examples/HelloInput/main.cpp deleted file mode 100644 index 770e51b..0000000 --- a/examples/HelloInput/main.cpp +++ /dev/null @@ -1,32 +0,0 @@ -import Crafter.Graphics; -import Crafter.Event; -import std; -using namespace Crafter; - -int main() { - WindowWayland window(1280, 720, "Hello Input!"); - - // Listen for left mouse click events on the window - // The callback receives the MousePoint struct containing the click coordinates in float pixels from the top left corner - EventListener clickListener(&window.onMouseLeftClick, [&window](MousePoint point){ - // Print the coordinates where the user clicked, we recieve the point in mapped space so we must convert it to pixels first - std::cout << std::format("Clicked on X:{} Y:{}!", MappedToPixelBoundless(point.x, window.width), MappedToPixelBoundless(point.y, window.height)) << std::endl; - }); - - // Listen specifically for the 'a' key being pressed down - // The callback takes no parameters since the key is fixed - EventListener keyAListener(&window.onKeyDown['a'], [](){ - // Print confirmation of 'a' key press - std::cout << std::format("Pressed specifically the a key!") << std::endl; - }); - - // Listen for any key press on the window - // The callback receives the character of the key pressed - EventListener anyKeyListener(&window.onAnyKeyDown, [](char key){ - // Print which key was pressed - std::cout << std::format("Pressed the {} key!", key) << std::endl; - }); - - //Start the window event loop, unless the window is started events will not trigger. - window.StartSync(); -} diff --git a/examples/HelloInput/project.json b/examples/HelloInput/project.json deleted file mode 100644 index 70e3d7a..0000000 --- a/examples/HelloInput/project.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "crafter-graphics", - "configurations": [ - { - "name": "executable", - "implementations": ["main"], - "dependencies": [ - { - "path":"../../project.json", - "configuration":"lib-wayland" - } - ] - } - ] -} diff --git a/examples/HelloRotation/main.cpp b/examples/HelloRotation/main.cpp deleted file mode 100644 index 94e7c59..0000000 --- a/examples/HelloRotation/main.cpp +++ /dev/null @@ -1,36 +0,0 @@ -import Crafter.Event; -import Crafter.Graphics; -import std; -using namespace Crafter; - -int main() { - WindowWayland window(200, 200, "Hello Input!"); - - RenderingElementScalingRotating2D element( - OpaqueType::SemiOpaque, //opaque, wether the element is opague or semi-transparant - 1, //bufferWidth: the width of this elements pixel buffer - 1, //bufferHeight: the height of this elements pixel buffer - FractionalToMappedBoundlessU(0.125), - FractionalToMapped(0.5), //anchorX: relative position where this elements x anchor (top-left) is placed to its parent x anchor - FractionalToMapped(0.5), //anchorY: relative position where this elements y anchor (top-left) is placed to its parent y anchor - FractionalToMapped(0.5), //relativeSizeX: the relative x size this element should be scaled to compared to its parent - FractionalToMapped(0.5), //relativeSizeY: the relative y size this element should be scaled to compared to its parent - FractionalToMapped(0.5), //anchorOffsetX: the amount this element's anchor should be offset from the top left corner (0.5 to in the middle) - FractionalToMapped(0.5), //anchorOffsetY: the amount this element's anchor should be offset from the top left corner (0.5 to place it in the middle) - 0, //z: this elements Z position - ); - - window.elements.push_back(&element); - element.buffer = {{255, 0, 0 ,255}}; - element.UpdatePosition(window); - - EventListener updateListener(&window.onUpdate, [&](FrameTime time){ - element.rotation += 50000000000000000; - std::cout << element.rotation << std::endl; - element.UpdatePosition(window); - window.LogTiming(); - }); - - window.StartUpdate(); - window.StartSync(); -} diff --git a/examples/HelloRotation/project.json b/examples/HelloRotation/project.json deleted file mode 100644 index 755d2b9..0000000 --- a/examples/HelloRotation/project.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "crafter-graphics", - "configurations": [ - { - "name": "executable", - "implementations": ["main"], - "dependencies": [ - { - "path":"../../project.json", - "configuration":"lib-wayland-timing" - } - ], - "debug": true - } - ] -} diff --git a/examples/HelloText/README.md b/examples/HelloText/README.md deleted file mode 100644 index 6068a7e..0000000 --- a/examples/HelloText/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# HelloWindow Example - -## Description - -This example demonstrates how to draw pixels to a window. - -## Expected Result - -A window with a green and blue colored square, when clicking on the square it logs the coordinates relative to the square. - -## Highlighted Code Snippet - -```cpp -UiElement& element = window.elements.emplace_back( - 0.5, - 0.5, - 2, - 1, - 0.5f, - 0.5f, - 0.5, - 0.5, - 0, - false -); -``` - -## How to Run - -```bash -crafter-build build executable -r -``` \ No newline at end of file diff --git a/examples/HelloText/main.cpp b/examples/HelloText/main.cpp deleted file mode 100644 index fbccd78..0000000 --- a/examples/HelloText/main.cpp +++ /dev/null @@ -1,33 +0,0 @@ -import Crafter.Event; -import Crafter.Graphics; -import std; -using namespace Crafter; - -int main() { - WindowWayland window(1280, 720, "Hello Input!"); - - RenderingElement element( - { - FractionalToMapped(0), //anchorX: relative position where this elements x anchor (top-left) is placed to its parent x anchor - FractionalToMapped(0.5), //anchorY: relative position where this elements y anchor (top-left) is placed to its parent y anchor - FractionalToMapped(0.1), //relativeSizeX: the relative x size this element should be scaled to compared to its parent - FractionalToMapped(1), //relativeSizeY: the relative y size this element should be scaled to compared to its parent - FractionalToMapped(0), //anchorOffsetX: the amount this element's anchor should be offset from the top left corner (0.5 to in the middle) - FractionalToMapped(0), //anchorOffsetY: the amount this element's anchor should be offset from the top left corner (0.5 to place it in the middle) - 0 //z: this elements Z position - }, - OpaqueType::FullyOpaque - ); - - Font font("inter.ttf"); - std::string text = "testtttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt"; - element.UpdatePosition(window); - std::vector lines = element.ResizeText(window, text, 200, font, TextOverflowMode::Clip, TextScaleMode::Element); // anchor.width automatically scales with our text - element.RenderText(window, lines, 200, {0,0,0,255}, font); - - window.elements.push_back(&element); - - window.Render(); - window.StartSync(); -} - diff --git a/examples/HelloText/project.json b/examples/HelloText/project.json deleted file mode 100644 index 8542e7e..0000000 --- a/examples/HelloText/project.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "crafter-graphics", - "configurations": [ - { - "name": "executable", - "implementations": ["main"], - "dependencies": [ - { - "path":"../../project.json", - "configuration":"lib-wayland-debug" - } - ], - "debug": true, - "additional_files": ["inter.ttf"] - } - ] -} diff --git a/examples/HelloUI/README.md b/examples/HelloUI/README.md deleted file mode 100644 index 6068a7e..0000000 --- a/examples/HelloUI/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# HelloWindow Example - -## Description - -This example demonstrates how to draw pixels to a window. - -## Expected Result - -A window with a green and blue colored square, when clicking on the square it logs the coordinates relative to the square. - -## Highlighted Code Snippet - -```cpp -UiElement& element = window.elements.emplace_back( - 0.5, - 0.5, - 2, - 1, - 0.5f, - 0.5f, - 0.5, - 0.5, - 0, - false -); -``` - -## How to Run - -```bash -crafter-build build executable -r -``` \ No newline at end of file diff --git a/examples/HelloUI/main.cpp b/examples/HelloUI/main.cpp deleted file mode 100644 index 3d19610..0000000 --- a/examples/HelloUI/main.cpp +++ /dev/null @@ -1,40 +0,0 @@ -import Crafter.Event; -import Crafter.Graphics; -import std; -using namespace Crafter; - -int main() { - Device::Initialize(); - Window window(1280, 720, "Hello Drawing!"); - - RenderingElement2D element( - { - 0.5, //anchorX: relative position where this elements x anchor (top-left) is placed to its parent x anchor - 0.5, //anchorY: relative position where this elements y anchor (top-left) is placed to its parent y anchor - 0.5, //relativeSizeX: the relative x size this element should be scaled to compared to its parent - 0.5, //relativeSizeY: the relative y size this element should be scaled to compared to its parent - 0.5, //anchorOffsetX: the amount this element's anchor should be offset from the top left corner (0.5 to in the middle) - 0.5, //anchorOffsetY: the amount this element's anchor should be offset from the top left corner (0.5 to place it in the middle) - 0 //z: this elements Z position - }, - OpaqueType::FullyOpaque, - 2, - 1 - ); - - MouseElement mouse(window); - element.children.push_back(&mouse); - window.renderer.elements.push_back(&element); - - element.scalingBuffer = {{255, 0, 0 ,255}, {0, 255, 0 ,255}}; - element.UpdatePosition(window.renderer); - - EventListener clickListener(&mouse.onMouseLeftClick, [&window]() { - std::println("Clicked on X:{} Y:{}!", - window.currentMousePos.x, window.currentMousePos.y - ); - }); - - window.Render(); - window.StartSync(); -} diff --git a/examples/HelloUI/project.json b/examples/HelloUI/project.json deleted file mode 100644 index 976af27..0000000 --- a/examples/HelloUI/project.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "crafter-graphics", - "configurations": [ - { - "name": "executable", - "implementations": ["main"], - "dependencies": [ - { - "path":"../../project.json", - "configuration":"lib-wayland-debug" - } - ], - "debug": true - } - ] -} diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 8940b37..0000000 --- a/examples/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Crafter.Graphics Examples -Welcome to the **Crafter.Graphics** examples folder! -Here you'll find a variety of demos to help you learn and experiment with the features of the `Crafter.Graphics` library. - -## Getting Started -To run any example, navigate into its folder and use the following command: -```bash -crafter-build build executable -r -``` - -## Hello Series -This series explains the absolute basics. - -1. **HelloWindow** - Basic window creation using Crafter.Graphics. - -2. **HelloInput** - Handling keyboard and mouse input events. - -3. **HelloDrawing** - Introduction to drawing on a window. - -4. **HelloUI** - Creating and rendering user interface components. - -## Vulkan Series -This series explains the vulkan integration. - -1. **VulkanWindow** - HelloWindow vulkan edition. - -2. **VulkanTraingle** - Introduction to drawing on a window. - -3. **VulkanCube** - Creating a custom shader. - -4. **VulkanShader** - Creating a custom shader. - -## Notes -- Each example is self-contained and meant to be run individually. -- Make sure your environment is correctly set up with all dependencies required by `Crafter.Graphics`. -- A comaptible WSL envoirement can be set up by running these commands: -```cmd -wsl --update -wsl --install archlinux --name crafter --no-launch -wsl -d crafter pacman -Syu vulkan-devel vulkan-swrast clang git base-devel libpqxx onetbb boost lld glslang --noconfirm -wsl --terminate crafter -``` diff --git a/examples/VulkanAnimation/closesthit.glsl b/examples/VulkanAnimation/closesthit.glsl deleted file mode 100644 index 9a511b6..0000000 --- a/examples/VulkanAnimation/closesthit.glsl +++ /dev/null @@ -1,12 +0,0 @@ -#version 460 -#extension GL_EXT_ray_tracing : enable -#extension GL_EXT_nonuniform_qualifier : enable - -layout(location = 0) rayPayloadInEXT vec3 hitValue; -hitAttributeEXT vec2 attribs; - -void main() -{ - const vec3 barycentricCoords = vec3(1.0f - attribs.x - attribs.y, attribs.x, attribs.y); - hitValue = barycentricCoords; -} \ No newline at end of file diff --git a/examples/VulkanAnimation/main.cpp b/examples/VulkanAnimation/main.cpp deleted file mode 100644 index 8bd1b9d..0000000 --- a/examples/VulkanAnimation/main.cpp +++ /dev/null @@ -1,175 +0,0 @@ -#include "vulkan/vulkan.h" - -import Crafter.Graphics; -using namespace Crafter; -import std; -import Crafter.Event; -import Crafter.Math; - -typedef VulkanShaderConst<"raygen.spv", "main", VK_SHADER_STAGE_RAYGEN_BIT_KHR> Raygenspv; -typedef VulkanShaderConst<"closesthit.spv", "main", VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR> Closesthitspv; -typedef VulkanShaderConst<"miss.spv", "main", VK_SHADER_STAGE_MISS_BIT_KHR> Misspv; -typedef std::tuple AllShaders; -typedef std::tuple< - ShaderGroup<0, VK_SHADER_UNUSED_KHR, VK_SHADER_UNUSED_KHR, VK_SHADER_UNUSED_KHR>, - ShaderGroup<1, VK_SHADER_UNUSED_KHR, VK_SHADER_UNUSED_KHR, VK_SHADER_UNUSED_KHR>, - ShaderGroup -> ShaderGroups; -typedef PipelineRTVulkanConst Pipeline; -typedef DescriptorSetLayoutVulkanConst<1, {{ - { - .binding = 0, - .descriptorType = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR, - .descriptorCount = 1, - .stageFlags = VK_SHADER_STAGE_RAYGEN_BIT_KHR, - }, -}}> descriptorSetLayoutTlas; -typedef DescriptorSetLayoutVulkanConst<1, {{ - { - .binding = 0, - .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, - .descriptorCount = 1, - .stageFlags = VK_SHADER_STAGE_RAYGEN_BIT_KHR, - } -}}> descriptorSetLayoutImage; - -int main() { - Device::CreateDevice(); - WindowVulkan window(1280, 720, "HelloVulkan"); - VkCommandBuffer cmd = window.StartInit(); - - Raygenspv::CreateShader(); - Closesthitspv::CreateShader(); - Misspv::CreateShader(); - - ShaderBindingTableVulkanConst::Init(); - - descriptorSetLayoutTlas::Init(); - descriptorSetLayoutImage::Init(); - std::array layouts {{descriptorSetLayoutTlas::layout, descriptorSetLayoutImage::layout, descriptorSetLayoutImage::layout, descriptorSetLayoutImage::layout}}; - - DescriptorPool pool; - pool.sets.resize(4); - pool.BuildPool(DescriptorPool::GetPoolSizes(), layouts); - - Pipeline::Init(cmd, layouts); - window.SetPipelineRT(); - - Mesh triangleMesh; - std::array, 3> verts {{{-150, -150, 100}, {0, 150, 100}, {150, -150, 100}}}; - std::array index {{2,1,0}}; - triangleMesh.Build(verts, index, cmd); - - RenderingElement3D renderer = { - .instance = { - .instanceCustomIndex = 0, - .mask = 0xFF, - .instanceShaderBindingTableRecordOffset = 0, - .flags = VK_GEOMETRY_INSTANCE_FORCE_OPAQUE_BIT_KHR, - .accelerationStructureReference = triangleMesh.blasAddr - } - }; - - RenderingElement3D::elements.emplace_back(&renderer); - MatrixRowMajor transform = MatrixRowMajor::Identity(); - std::memcpy(renderer.instance.transform.matrix, transform.m, sizeof(transform.m)); - - RenderingElement3D::tlases.resize(Window::numFrames); - RenderingElement3D::BuildTLAS(cmd, 0); - RenderingElement3D::BuildTLAS(cmd, 1); - RenderingElement3D::BuildTLAS(cmd, 2); - - VkDescriptorImageInfo imageInfo[Window::numFrames] = { - { - .imageView = window.imageViews[0], - .imageLayout = VK_IMAGE_LAYOUT_GENERAL - }, - { - .imageView = window.imageViews[1], - .imageLayout = VK_IMAGE_LAYOUT_GENERAL - }, - { - .imageView = window.imageViews[2], - .imageLayout = VK_IMAGE_LAYOUT_GENERAL - }, - }; - - VkWriteDescriptorSetAccelerationStructureKHR writeDescriptorSetAccelerationStructure { - .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET_ACCELERATION_STRUCTURE_KHR, - .accelerationStructureCount = 1, - .pAccelerationStructures = &RenderingElement3D::tlases[0].accelerationStructure - }; - - VkWriteDescriptorSet write[4]; - - write[0] = { - .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, - .pNext = &writeDescriptorSetAccelerationStructure, - .dstSet = pool.sets[0], - .dstBinding = 0, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR, - }; - - for(std::uint32_t i = 0; i < Window::numFrames; i++) { - write[i+1] = { - .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, - .dstSet = pool.sets[i+1], - .dstBinding = 0, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, - .pImageInfo = &imageInfo[i] - }; - } - vkUpdateDescriptorSets(Device::device, 4, write, 0, nullptr); - window.descriptorsRt = {pool.sets[0], pool.sets[1]}; - - /* - FinishInit executes all commands recorded to StartInit. - This must be called before the the event loops starts if you called StartInit before. - */ - window.FinishInit(); - - Animation> anim({ - {std::chrono::seconds(3), -600, 600}, - }); - - anim.Start(std::chrono::high_resolution_clock::now()); - - EventListener updateListener(&window.onRender, [&](){ - float value = std::get<0>(anim.Play(window.currentFrameTime.now)); - - if(anim.currentFrame == anim.keyframes.size()) { - anim.Start(window.currentFrameTime.now); - } - - MatrixRowMajor transform = MatrixRowMajor::Translation(value, 0, 0); - std::memcpy(renderer.instance.transform.matrix, transform.m, sizeof(transform.m)); - RenderingElement3D::BuildTLAS(window.drawCmdBuffers[window.currentBuffer], window.currentBuffer); - - VkWriteDescriptorSetAccelerationStructureKHR writeDescriptorSetAccelerationStructure { - .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET_ACCELERATION_STRUCTURE_KHR, - .accelerationStructureCount = 1, - .pAccelerationStructures = &RenderingElement3D::tlases[window.currentBuffer].accelerationStructure - }; - - VkWriteDescriptorSet write = { - .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, - .pNext = &writeDescriptorSetAccelerationStructure, - .dstSet = pool.sets[0], - .dstBinding = 0, - .dstArrayElement = 0, - .descriptorCount = 1, - .descriptorType = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR, - }; - vkUpdateDescriptorSets(Device::device, 1, &write, 0, nullptr); - - window.descriptorsRt[1] = pool.sets[window.currentBuffer+1]; - }); - - window.Render(); - window.StartUpdate(); - window.StartSync(); -} diff --git a/examples/VulkanAnimation/miss.glsl b/examples/VulkanAnimation/miss.glsl deleted file mode 100644 index 1478ecf..0000000 --- a/examples/VulkanAnimation/miss.glsl +++ /dev/null @@ -1,9 +0,0 @@ -#version 460 -#extension GL_EXT_ray_tracing : enable - -layout(location = 0) rayPayloadInEXT vec3 hitValue; - -void main() -{ - hitValue = vec3(1, 1, 1); -} \ No newline at end of file diff --git a/examples/VulkanAnimation/project.json b/examples/VulkanAnimation/project.json deleted file mode 100644 index b30faa8..0000000 --- a/examples/VulkanAnimation/project.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "crafter-graphics", - "configurations": [ - { - "name": "executable", - "implementations": ["main"], - "dependencies": [ - { - "path":"../../project.json", - "configuration":"lib-vulkan-debug" - } - ], - "shaders": [ - { - "path":"raygen.glsl", - "type": 6, - "entrypoint":"main" - }, - { - "path":"closesthit.glsl", - "type": 9, - "entrypoint":"main" - }, - { - "path":"miss.glsl", - "type": 10, - "entrypoint":"main" - } - ] - } - ] -} diff --git a/examples/VulkanAnimation/raygen.glsl b/examples/VulkanAnimation/raygen.glsl deleted file mode 100644 index 1cbdb98..0000000 --- a/examples/VulkanAnimation/raygen.glsl +++ /dev/null @@ -1,47 +0,0 @@ -#version 460 -#extension GL_EXT_ray_tracing : enable -#extension GL_EXT_shader_image_load_formatted : enable - -layout(binding = 0, set = 0) uniform accelerationStructureEXT topLevelAS; -layout(binding = 0, set = 1, rgba8) uniform writeonly image2D image; - -layout(location = 0) rayPayloadEXT vec3 hitValue; - -void main() -{ - // Pixel coordinates - uvec2 pixel = gl_LaunchIDEXT.xy; - uvec2 resolution = gl_LaunchSizeEXT.xy; - - // Normalized coordinates in range [-1, 1] - vec2 uv = (vec2(pixel) + 0.5) / vec2(resolution); - vec2 ndc = uv * 2.0 - 1.0; - - // Camera parameters - vec3 origin = vec3(0.0, 0.0, -300.0); - - float aspect = float(resolution.x) / float(resolution.y); - float fov = radians(60.0); - float tanHalfFov = tan(fov * 0.5); - - // Simple pinhole camera facing +Z - vec3 direction = normalize(vec3( - ndc.x * aspect * tanHalfFov, - -ndc.y * tanHalfFov, - 1.0 - )); - - traceRayEXT( - topLevelAS, - gl_RayFlagsNoneEXT, - 0xff, - 0, 0, 0, - origin, - 0.001, - direction, - 10000.0, - 0 - ); - - imageStore(image, ivec2(pixel), vec4(hitValue, 1.0)); -} diff --git a/examples/VulkanTriangle/main.cpp b/examples/VulkanTriangle/main.cpp index 7687e93..1c5184c 100644 --- a/examples/VulkanTriangle/main.cpp +++ b/examples/VulkanTriangle/main.cpp @@ -96,6 +96,9 @@ int main() { window.FinishInit(); + auto imgSlots = descriptorHeap.AllocateImageSlots(1); + auto bufSlots = descriptorHeap.AllocateBufferSlots(1); + VkDeviceAddressRangeKHR tlasRange0 = { .address = RenderingElement3D::tlases[0].address, }; @@ -161,27 +164,27 @@ int main() { VkHostAddressRangeEXT destinations[6] = { { - .address = descriptorHeap.resourceHeap[0].value + descriptorHeap.bufferStartOffset, + .address = descriptorHeap.resourceHeap[0].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement), .size = Device::descriptorHeapProperties.bufferDescriptorSize }, { - .address = descriptorHeap.resourceHeap[0].value, + .address = descriptorHeap.resourceHeap[0].value + descriptorHeap.ImageByteOffset(imgSlots.firstElement), .size = Device::descriptorHeapProperties.imageDescriptorSize }, { - .address = descriptorHeap.resourceHeap[1].value + descriptorHeap.bufferStartOffset, + .address = descriptorHeap.resourceHeap[1].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement), .size = Device::descriptorHeapProperties.bufferDescriptorSize }, { - .address = descriptorHeap.resourceHeap[1].value, + .address = descriptorHeap.resourceHeap[1].value + descriptorHeap.ImageByteOffset(imgSlots.firstElement), .size = Device::descriptorHeapProperties.imageDescriptorSize }, { - .address = descriptorHeap.resourceHeap[2].value + descriptorHeap.bufferStartOffset, + .address = descriptorHeap.resourceHeap[2].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement), .size = Device::descriptorHeapProperties.bufferDescriptorSize }, { - .address = descriptorHeap.resourceHeap[2].value, + .address = descriptorHeap.resourceHeap[2].value + descriptorHeap.ImageByteOffset(imgSlots.firstElement), .size = Device::descriptorHeapProperties.imageDescriptorSize }, }; @@ -192,8 +195,9 @@ int main() { descriptorHeap.resourceHeap[1].FlushDevice(); descriptorHeap.resourceHeap[2].FlushDevice(); - window.pipeline = &pipeline; window.descriptorHeap = &descriptorHeap; + RTPass rtPass(&pipeline); + window.passes.push_back(&rtPass); window.Render(); window.StartSync(); diff --git a/examples/VulkanTriangle/project.cpp b/examples/VulkanTriangle/project.cpp index 826719b..fca4a1e 100644 --- a/examples/VulkanTriangle/project.cpp +++ b/examples/VulkanTriangle/project.cpp @@ -5,7 +5,6 @@ using namespace Crafter; extern "C" Configuration CrafterBuildProject(std::span args) { std::vector graphicsArgs(args.begin(), args.end()); - graphicsArgs.push_back("--vulkan"); Configuration* graphics = LocalProject({ .projectFile = "../../project.cpp", .args = graphicsArgs, diff --git a/examples/HelloText/inter.ttf b/examples/VulkanUI/Inter.ttf similarity index 100% rename from examples/HelloText/inter.ttf rename to examples/VulkanUI/Inter.ttf diff --git a/examples/VulkanUI/main.cpp b/examples/VulkanUI/main.cpp index 325c38a..6371643 100644 --- a/examples/VulkanUI/main.cpp +++ b/examples/VulkanUI/main.cpp @@ -1,133 +1,85 @@ #include "vulkan/vulkan.h" -#include import Crafter.Graphics; -using namespace Crafter; -import std; import Crafter.Event; import Crafter.Math; +import std; +using namespace Crafter; int main() { Device::Initialize(); - Window window(1280, 720, "HelloVulkan"); - VkCommandBuffer cmd = window.StartInit(); - DescriptorHeapVulkan descriptorHeap; - descriptorHeap.Initialize(1,2,0); - - VkSpecializationMapEntry entry = { - .constantID = 0, - .offset = 0, - .size = sizeof(uint16_t) - }; - - VkSpecializationInfo specilizationInfo = { - .mapEntryCount = 1, - .pMapEntries = &entry, - .dataSize = sizeof(uint16_t), - .pData = &descriptorHeap.bufferStartElement - }; - - std::array shaders{{ - {"raygen.spv", "main", VK_SHADER_STAGE_RAYGEN_BIT_KHR, &specilizationInfo} - }}; - - ShaderBindingTableVulkan shaderTable; - shaderTable.Init(shaders); - - std::array raygenGroups {{ - { - .sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR, - .type = VK_RAY_TRACING_SHADER_GROUP_TYPE_GENERAL_KHR, - .generalShader = 0, - .closestHitShader = VK_SHADER_UNUSED_KHR, - .anyHitShader = VK_SHADER_UNUSED_KHR, - .intersectionShader = VK_SHADER_UNUSED_KHR, - }, - }}; - std::array missGroups; - std::array hitGroups; - - - PipelineRTVulkan pipeline; - pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, shaderTable); - + Window window(1280, 720, "VulkanUI"); + window.StartInit(); window.FinishInit(); - RenderingElement2DVulkan element( - { - 0.5, //anchorX: relative position where this elements x anchor (top-left) is placed to its parent x anchor - 0.5, //anchorY: relative position where this elements y anchor (top-left) is placed to its parent y anchor - 0.5, //relativeSizeX: the relative x size this element should be scaled to compared to its parent - 0.5, //relativeSizeY: the relative y size this element should be scaled to compared to its parent - 0.5, //anchorOffsetX: the amount this element's anchor should be offset from the top left corner (0.5 to in the middle) - 0.5, //anchorOffsetY: the amount this element's anchor should be offset from the top left corner (0.5 to place it in the middle) - 0 //z: this elements Z position - }, - 2, - 1 + Font font("Inter.ttf"); + + UI::Theme theme = UI::themes::default_dark(); + theme.defaultFont = &font; + + // ───────────────────────────────────────────────────────────────────── + // Wire the scene: it auto-creates a descriptor heap, plugs into the + // window's pass list, hooks mouse + update events, and drives a + // compute-shader UI pass per frame. + // ───────────────────────────────────────────────────────────────────── + UI::UIScene scene; + scene.Initialize(window, "ui.comp.spv"); + scene.background(UI::Color{0.06f, 0.07f, 0.10f, 1.0f}); + + scene.Root( + UI::VStack{} + .padding(20) + .spacing(12) + .children( + UI::Text{"Crafter UI — V1"}.font(font).size(28), + + UI::Text{}.font(font).size(14).runs( + UI::TextRun{"Click "}, + UI::TextRun{"Quit"}.color(UI::Color{1.0f, 0.55f, 0.55f}).bold(), + UI::TextRun{" to close the window. Tabs switch on click. "}, + UI::TextRun{"Have fun!"}.color(UI::Color{0.55f, 0.85f, 1.0f}) + ), + + UI::HStack{} + .width(UI::Length::Frac(1)) + .spacing(8) + .children( + UI::Button{"Play"} .font(font).style(theme.primary) .onClick([]{ std::println(std::cerr, "[click] Play"); }), + UI::Button{"Options"}.font(font).style(theme.secondary).onClick([]{ std::println(std::cerr, "[click] Options"); }), + UI::Spacer{}, + UI::Button{"Quit"} .font(font).style(theme.danger) .onClick([]{ std::println(std::cerr, "[click] Quit"); std::_Exit(0); }) + ), + + UI::ProgressBar{} + .value(0.42f) + .size(UI::Length::Frac(1), UI::Length::Px(20)) + .foreground(theme.primary.background), + + UI::TabView{} + .font(font) + .width(UI::Length::Frac(1)) + .height(UI::Length::Px(220)) + .tab("Graphics", UI::VStack{}.padding(8).spacing(8).children( + UI::Text{"Resolution"}.font(font).size(14), + UI::InputField{"1920x1080"}.font(font).style(theme.input), + UI::Text{"Max lights"}.font(font).size(14), + UI::InputField{"32"}.font(font).style(theme.input) + )) + .tab("Input", UI::VStack{}.padding(8).spacing(8).children( + UI::Text{"Mouse sensitivity"}.font(font).size(14), + UI::InputField{"1.0"}.font(font).style(theme.input) + )) + .tab("Audio", UI::VStack{}.padding(8).spacing(8).children( + UI::Text{"Master volume"}.font(font).size(14), + UI::InputField{"80"}.font(font).style(theme.input) + )) + ) ); - for(std::uint8_t i = 0; i < Window::numFrames; i++) { - reinterpret_cast, true>*>(element.buffers[i])->value[0] = {1, 0, 0, 1}; - reinterpret_cast, true>*>(element.buffers[i])->value[1] = {0, 1, 0, 1}; - reinterpret_cast, true>*>(element.buffers[i])->FlushDevice(); - } - RendertargetVulkan rendertarget(1280, 720, {&element}); - - VkImageDescriptorInfoEXT imageInfo0 = { - .sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT, - .pView = &window.imageViews[0], - .layout = VK_IMAGE_LAYOUT_GENERAL - }; - - VkImageDescriptorInfoEXT imageInfo1 = { - .sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT, - .pView = &window.imageViews[1], - .layout = VK_IMAGE_LAYOUT_GENERAL - }; - - VkImageDescriptorInfoEXT imageInfo2 = { - .sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT, - .pView = &window.imageViews[2], - .layout = VK_IMAGE_LAYOUT_GENERAL - }; - - std::array infos; - infos[0] = { - .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, - .type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, - .data = { .pImage = &imageInfo0 } - }; - infos[1] = { - .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, - .type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, - .data = { .pImage = &imageInfo1 } - }; - infos[2] = { - .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, - .type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, - .data = { .pImage = &imageInfo2 } - }; - - std::array ranges; - ranges[0] = { - .address = descriptorHeap.resourceHeap[0].value, - .size = Device::descriptorHeapProperties.imageDescriptorSize - }; - ranges[1] = { - .address = descriptorHeap.resourceHeap[1].value, - .size = Device::descriptorHeapProperties.imageDescriptorSize - }, - ranges[2] = { - .address = descriptorHeap.resourceHeap[2].value, - .size = Device::descriptorHeapProperties.imageDescriptorSize - }, - - rendertarget.WriteDescriptors(infos, ranges, 3, descriptorHeap.bufferStartOffset, descriptorHeap); - - window.pipeline = &pipeline; - window.descriptorHeap = &descriptorHeap; window.Render(); + window.SaveFrame("frame.png"); + + window.StartUpdate(); // continuous rendering — UIScene re-emits per frame window.StartSync(); } diff --git a/examples/VulkanUI/project.cpp b/examples/VulkanUI/project.cpp new file mode 100644 index 0000000..24d3c69 --- /dev/null +++ b/examples/VulkanUI/project.cpp @@ -0,0 +1,27 @@ +import std; +import Crafter.Build; +namespace fs = std::filesystem; +using namespace Crafter; + +extern "C" Configuration CrafterBuildProject(std::span args) { + std::vector graphicsArgs(args.begin(), args.end()); + Configuration* graphics = LocalProject({ + .projectFile = "../../project.cpp", + .args = graphicsArgs, + }); + + Configuration cfg; + cfg.path = "./"; + cfg.name = "VulkanUI"; + cfg.outputName = "VulkanUI"; + ApplyStandardArgs(cfg, args); + cfg.dependencies = { graphics }; + + std::array ifaces = {}; + std::array impls = { "main" }; + cfg.GetInterfacesAndImplementations(ifaces, impls); + + cfg.files.push_back("Inter.ttf"); + + return cfg; +} diff --git a/examples/VulkanUI/project.json b/examples/VulkanUI/project.json deleted file mode 100644 index 31cecf9..0000000 --- a/examples/VulkanUI/project.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "crafter-graphics", - "configurations": [ - { - "name": "executable", - "implementations": ["main"], - "dependencies": [ - { - "path":"../../project.json", - "configuration":"lib-wayland-vulkan-debug" - } - ], - "debug": true, - "shaders": [ - { - "path":"raygen.glsl", - "type": 6, - "entrypoint":"main" - } - ] - } - ] -} diff --git a/examples/VulkanUI/raygen.glsl b/examples/VulkanUI/raygen.glsl deleted file mode 100644 index 849b983..0000000 --- a/examples/VulkanUI/raygen.glsl +++ /dev/null @@ -1,48 +0,0 @@ -#version 460 -#extension GL_EXT_ray_tracing : enable -#extension GL_EXT_shader_image_load_formatted : enable -#extension GL_EXT_shader_explicit_arithmetic_types_int16 : enable -#extension GL_EXT_descriptor_heap : enable -#extension GL_EXT_nonuniform_qualifier : enable -#extension GL_EXT_shader_explicit_arithmetic_types_float16 : enable - - -struct UIScaledData{ - int16_t x; - int16_t y; - int16_t sizeX; - int16_t sizeY; - uint16_t bufferX; - uint16_t bufferY; -}; - -layout(std430, descriptor_heap) buffer UIScaledDataBuffer { - uint16_t count; - uint16_t pad[5]; - UIScaledData data[]; -} UITransformBuffer[]; - -layout(std430, descriptor_heap) buffer UIPixelBufferr { - f16vec4 pixels[]; -} UIPixelBuffer[]; - -layout(constant_id = 0) const uint16_t bufferStart = 0us; -layout(descriptor_heap) uniform writeonly image2D image[]; - -void main() -{ - uvec2 pixel = gl_LaunchIDEXT.xy; - uvec2 resolution = gl_LaunchSizeEXT.xy; - - vec4 hitValue = vec4(0); - - for (uint16_t i = 1us; i < UITransformBuffer[bufferStart].count+1; i++) { - if(pixel.x > UITransformBuffer[bufferStart].data[i].x && pixel.x < UITransformBuffer[bufferStart].data[i].x + UITransformBuffer[bufferStart].data[i].sizeX && pixel.y > UITransformBuffer[bufferStart].data[i].y && pixel.y < UITransformBuffer[bufferStart].data[i].y + UITransformBuffer[bufferStart].data[i].sizeY) { - int16_t srcX = int16_t(float(pixel.x - UITransformBuffer[bufferStart].data[i].x) * float(UITransformBuffer[bufferStart].data[i].bufferX) / float(UITransformBuffer[bufferStart].data[i].sizeX)); - int16_t srcY = int16_t(float(pixel.y - UITransformBuffer[bufferStart].data[i].y) * float(UITransformBuffer[bufferStart].data[i].bufferY) / float(UITransformBuffer[bufferStart].data[i].sizeY)); - hitValue = vec4(UIPixelBuffer[bufferStart + 1].pixels[srcY * UITransformBuffer[bufferStart].data[i].bufferX + srcX]); - } - } - - imageStore(image[0], ivec2(pixel), hitValue); -} diff --git a/implementations/Crafter.Graphics-Device.cpp b/implementations/Crafter.Graphics-Device.cpp index d65d583..55c8758 100644 --- a/implementations/Crafter.Graphics-Device.cpp +++ b/implementations/Crafter.Graphics-Device.cpp @@ -20,11 +20,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA module; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN #include "vulkan/vulkan.h" #include "vulkan/vk_enum_string_helper.h" #define GET_EXTENSION_FUNCTION(_id) ((PFN_##_id)(vkGetInstanceProcAddr(instance, #_id))) -#endif #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #include @@ -45,12 +43,10 @@ module; module Crafter.Graphics:Device_impl; import :Device; import :Window; -import :MouseElement; import :Types; import std; using namespace Crafter; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN const char* const instanceExtensionNames[] = { "VK_EXT_debug_utils", "VK_KHR_surface", @@ -175,7 +171,6 @@ VkBool32 onError(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMe return 0; } -#endif #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND @@ -364,50 +359,19 @@ void Device::pointer_handle_button(void* data, wl_pointer* pointer, std::uint32_ if(state == WL_POINTER_BUTTON_STATE_PRESSED) { Device::focusedWindow->mouseLeftHeld = true; Device::focusedWindow->onMouseLeftClick.Invoke(); - for(MouseElement* element : Device::focusedWindow->mouseElements) { - if(element) { - if(Device::focusedWindow->currentMousePos.x >= element->scaled.position.x && Device::focusedWindow->currentMousePos.x <= element->scaled.position.x+element->scaled.size.x && Device::focusedWindow->currentMousePos.y > element->scaled.position.y && Device::focusedWindow->currentMousePos.y < element->scaled.position.y+element->scaled.size.y) { - element->onMouseLeftClick.Invoke(); - } - } - } } else { Device::focusedWindow->mouseLeftHeld = false; Device::focusedWindow->onMouseLeftRelease.Invoke(); - for(MouseElement* element : Device::focusedWindow->mouseElements) { - if(element) { - if(Device::focusedWindow->currentMousePos.x >= element->scaled.position.x && Device::focusedWindow->currentMousePos.x <= element->scaled.position.x+element->scaled.size.x && Device::focusedWindow->currentMousePos.y > element->scaled.position.y && Device::focusedWindow->currentMousePos.y < element->scaled.position.y+element->scaled.size.y) { - element->onMouseLeftRelease.Invoke(); - } - } - } } } else if(button == BTN_RIGHT){ if(state == WL_POINTER_BUTTON_STATE_PRESSED) { Device::focusedWindow->mouseRightHeld = true; Device::focusedWindow->onMouseRightClick.Invoke(); - for(MouseElement* element : Device::focusedWindow->mouseElements) { - if(element) { - if(Device::focusedWindow->currentMousePos.x >= element->scaled.position.x && Device::focusedWindow->currentMousePos.x <= element->scaled.position.x+element->scaled.size.x && Device::focusedWindow->currentMousePos.y > element->scaled.position.y && Device::focusedWindow->currentMousePos.y < element->scaled.position.y+element->scaled.size.y) { - element->onMouseRightClick.Invoke(); - } - } - } } else { Device::focusedWindow->mouseRightHeld = false; - Device::focusedWindow->onMouseRightRelease.Invoke(); - for(MouseElement* element : Device::focusedWindow->mouseElements) { - if(element) { - if(Device::focusedWindow->currentMousePos.x >= element->scaled.position.x && Device::focusedWindow->currentMousePos.x <= element->scaled.position.x+element->scaled.size.x && Device::focusedWindow->currentMousePos.y > element->scaled.position.y && Device::focusedWindow->currentMousePos.y < element->scaled.position.y+element->scaled.size.y) { - element->onMouseRightRelease.Invoke(); - } - } - } + Device::focusedWindow->onMouseRightRelease.Invoke(); } } - Device::focusedWindow->mouseElements.erase(std::remove(Device::focusedWindow->mouseElements.begin(), Device::focusedWindow->mouseElements.end(), static_cast(nullptr)), Device::focusedWindow->mouseElements.end()); - Device::focusedWindow->mouseElements.insert(Device::focusedWindow->mouseElements.end(), Device::focusedWindow->pendingMouseElements.begin(), Device::focusedWindow->pendingMouseElements.end()); - Device::focusedWindow->pendingMouseElements.clear(); } void Device::PointerListenerHandleMotion(void* data, wl_pointer* wl_pointer, std::uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) { @@ -416,21 +380,6 @@ void Device::PointerListenerHandleMotion(void* data, wl_pointer* wl_pointer, std Device::focusedWindow->currentMousePos = pos * Device::focusedWindow->scale; //Device::focusedWindow->mouseDelta = {Device::focusedWindow->currentMousePos.x-Device::focusedWindow->lastMousePos.x, Device::focusedWindow->currentMousePos.y-Device::focusedWindow->lastMousePos.y}; Device::focusedWindow->onMouseMove.Invoke(); - for(MouseElement* element : Device::focusedWindow->mouseElements) { - if(element) { - if(Device::focusedWindow->currentMousePos.x >= element->scaled.position.x && Device::focusedWindow->currentMousePos.x <= element->scaled.position.x+element->scaled.size.x && Device::focusedWindow->currentMousePos.y > element->scaled.position.y && Device::focusedWindow->currentMousePos.y < element->scaled.position.y+element->scaled.size.y) { - element->onMouseMove.Invoke(); - if(!element->mouseHover) { - element->mouseHover = true; - element->onMouseEnter.Invoke(); - } - } else if(element->mouseHover) { - element->mouseHover = false; - element->onMouseLeave.Invoke(); - } - } - } - Device::focusedWindow->mouseElements.erase(std::remove(Device::focusedWindow->mouseElements.begin(), Device::focusedWindow->mouseElements.end(), static_cast(nullptr)), Device::focusedWindow->mouseElements.end()); } void Device::PointerListenerHandleEnter(void* data, wl_pointer* wl_pointer, std::uint32_t serial, wl_surface* surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { @@ -559,7 +508,6 @@ void Device::Initialize() { } #endif - #ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; app.pApplicationName = ""; app.pEngineName = "Crafter.Graphics"; @@ -727,8 +675,15 @@ void Device::Initialize() { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, .pNext = &bit16, .shaderFloat16 = VK_TRUE, + // Bindless / runtime descriptor array indexing — needed for the + // descriptor_heap shader path. + .shaderUniformBufferArrayNonUniformIndexing = VK_TRUE, + .shaderSampledImageArrayNonUniformIndexing = VK_TRUE, + .shaderStorageBufferArrayNonUniformIndexing = VK_TRUE, + .shaderStorageImageArrayNonUniformIndexing = VK_TRUE, .runtimeDescriptorArray = VK_TRUE, - .bufferDeviceAddress = VK_TRUE + .scalarBlockLayout = VK_TRUE, + .bufferDeviceAddress = VK_TRUE }; VkPhysicalDeviceRayTracingPipelineFeaturesKHR physicalDeviceRayTracingPipelineFeatures{ @@ -747,8 +702,17 @@ void Device::Initialize() { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = &deviceAccelerationStructureFeature, .features = { - .samplerAnisotropy = VK_TRUE, - .shaderInt16 = VK_TRUE + // Order matches VkPhysicalDeviceFeatures declaration so the + // designated-initializer-order warning stays quiet. + .samplerAnisotropy = VK_TRUE, + .shaderStorageImageReadWithoutFormat = VK_TRUE, + .shaderStorageImageWriteWithoutFormat = VK_TRUE, + // Bindless dynamic indexing — required to index `images[]`, + // `textures[]`, `samplers[]`, `itemHeap[]` with a runtime value. + .shaderSampledImageArrayDynamicIndexing = VK_TRUE, + .shaderStorageBufferArrayDynamicIndexing = VK_TRUE, + .shaderStorageImageArrayDynamicIndexing = VK_TRUE, + .shaderInt16 = VK_TRUE } }; @@ -806,12 +770,12 @@ void Device::Initialize() { vkCmdBindResourceHeapEXT = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkCmdBindResourceHeapEXT")); vkCmdBindSamplerHeapEXT = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkCmdBindSamplerHeapEXT")); vkWriteResourceDescriptorsEXT = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkWriteResourceDescriptorsEXT")); + vkWriteSamplerDescriptorsEXT = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkWriteSamplerDescriptorsEXT")); + vkCmdPushDataEXT = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkCmdPushDataEXT")); vkGetPhysicalDeviceDescriptorSizeEXT = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceDescriptorSizeEXT")); vkGetDeviceFaultInfoEXT = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkGetDeviceFaultInfoEXT")); - #endif } -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN std::uint32_t Device::GetMemoryType(uint32_t typeBits, VkMemoryPropertyFlags properties) { for (uint32_t i = 0; i < memoryProperties.memoryTypeCount; i++) { @@ -826,5 +790,4 @@ std::uint32_t Device::GetMemoryType(uint32_t typeBits, VkMemoryPropertyFlags pro } throw std::runtime_error("Could not find a matching memory type"); -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/implementations/Crafter.Graphics-Font.cpp b/implementations/Crafter.Graphics-Font.cpp index 7d7c3ca..18bbf89 100644 --- a/implementations/Crafter.Graphics-Font.cpp +++ b/implementations/Crafter.Graphics-Font.cpp @@ -61,10 +61,26 @@ Font::Font(const std::filesystem::path& fontFilePath) { std::uint32_t Font::GetLineWidth(const std::string_view text, float size) { float scale = stbtt_ScaleForPixelHeight(&font, size); std::uint32_t lineWidth = 0; - for (const char c : text) { + std::size_t i = 0; + while (i < text.size()) { + std::uint32_t cp = DecodeUtf8(text, i); + if (cp == 0) break; int advance, lsb; - stbtt_GetCodepointHMetrics(&font, c, &advance, &lsb); + stbtt_GetCodepointHMetrics(&font, static_cast(cp), &advance, &lsb); lineWidth += (int)(advance * scale); } return lineWidth; +} + +float Font::LineHeight(float size) { + float scale = stbtt_ScaleForPixelHeight(&font, size); + return (ascent - descent + lineGap) * scale; +} + +float Font::AscentPx(float size) { + return ascent * stbtt_ScaleForPixelHeight(&font, size); +} + +float Font::ScaleForSize(float size) { + return stbtt_ScaleForPixelHeight(&font, size); } \ No newline at end of file diff --git a/implementations/Crafter.Graphics-Mesh.cpp b/implementations/Crafter.Graphics-Mesh.cpp index 5db44be..0b83c12 100644 --- a/implementations/Crafter.Graphics-Mesh.cpp +++ b/implementations/Crafter.Graphics-Mesh.cpp @@ -18,9 +18,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN #include "vulkan/vulkan.h" -#endif module Crafter.Graphics:Mesh_impl; import Crafter.Math; import :Mesh; @@ -30,8 +28,6 @@ import std; using namespace Crafter; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN - void Mesh::Build(std::span> verticies, std::span indicies, VkCommandBuffer cmd) { vertexBuffer.Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_2_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, verticies.size()); indexBuffer.Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_2_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, indicies.size()); @@ -129,5 +125,4 @@ void Mesh::Build(std::span> verticies, std::span -#endif module Crafter.Graphics:RenderingElement3D_impl; import :RenderingElement3D; import std; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN using namespace Crafter; @@ -132,6 +129,4 @@ void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index) { .accelerationStructure = tlases[index].accelerationStructure }; tlases[index].address = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo); -} - -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/implementations/Crafter.Graphics-Rendertarget.cpp b/implementations/Crafter.Graphics-Rendertarget.cpp deleted file mode 100644 index 52de2ee..0000000 --- a/implementations/Crafter.Graphics-Rendertarget.cpp +++ /dev/null @@ -1,141 +0,0 @@ -/* -Crafter®.Graphics -Copyright (C) 2026 Catcrafts® -catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License version 3.0 as published by the Free Software Foundation; - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -module; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN -#include -#endif -module Crafter.Graphics:Rendertarget_impl; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN -import :Rendertarget; -import :Window; -import :DescriptorHeapVulkan; -import :RenderingElement2DVulkan; -import std; -using namespace Crafter; - - -RendertargetVulkan::RendertargetVulkan(std::uint16_t sizeX, std::uint16_t sizeY) : RendertargetBase(sizeX, sizeY) { - -} - -void RendertargetVulkan::UpdateElements() { - elements.clear(); - std::sort(transform.children.begin(), transform.children.end(), [](Transform2D* a, Transform2D* b){ return a->anchor.z < b->anchor.z; }); - for(Transform2D* child : transform.children) { - SetOrderResursive(child); - } -} - -void RendertargetVulkan::CreateBuffer(std::uint8_t frame) { - transformBuffer[frame].Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, elements.size()+1); - RenderingElement2DVulkanTransformInfo* val = reinterpret_cast(reinterpret_cast(transformBuffer[frame].value) + sizeof(RenderingElement2DVulkanTransformInfo)); - std::uint16_t* sizePtr = reinterpret_cast(transformBuffer[frame].value); - *sizePtr = static_cast(elements.size()); - for(std::uint16_t i = 0; i < elements.size(); i++) { - val[i].bufferX = elements[i]->bufferX; - val[i].bufferY = elements[i]->bufferY; - } - transformBuffer[frame].FlushDevice(); -} - -void RendertargetVulkan::ReorderBuffer(std::uint8_t frame) { - RenderingElement2DVulkanTransformInfo* val = reinterpret_cast(reinterpret_cast(transformBuffer[frame].value) + sizeof(RenderingElement2DVulkanTransformInfo)); - for(std::uint16_t i = 0; i < elements.size(); i++) { - val[i].scaled = elements[i]->scaled; - val[i].bufferX = elements[i]->bufferX; - val[i].bufferY = elements[i]->bufferY; - } - transformBuffer[frame].FlushDevice(); -} - -void RendertargetVulkan::WriteDescriptors(std::span infos, std::span ranges, std::uint16_t start, std::uint32_t bufferOffset, DescriptorHeapVulkan& descriptorHeap) { - VkDeviceAddressRangeKHR transformRanges[Window::numFrames] = { - { - .address = transformBuffer[0].address, - .size = transformBuffer[0].size - }, - { - .address = transformBuffer[1].address, - .size = transformBuffer[1].size - }, - { - .address = transformBuffer[2].address, - .size = transformBuffer[2].size - } - }; - - for(std::uint8_t i = 0; i < Window::numFrames; i++) { - ranges[start + i] = { - .address = descriptorHeap.resourceHeap[i].value + bufferOffset, - .size = Device::descriptorHeapProperties.bufferDescriptorSize - }; - infos[start + i] = { - .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, - .type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, - .data = { .pAddressRange = &transformRanges[i]} - }; - } - - start += 3; - bufferOffset += Device::descriptorHeapProperties.bufferDescriptorSize; - - std::vector bufferRanges(elements.size() * Window::numFrames); - - std::uint16_t rangeOffset = 0; - - for(std::uint8_t i2 = 0; i2 < Window::numFrames; i2++) { - for(std::uint16_t i = 0; i < elements.size(); i++) { - ranges[start + i] = { - .address = descriptorHeap.resourceHeap[i2].value + bufferOffset + Device::descriptorHeapProperties.bufferDescriptorSize * i, - .size = Device::descriptorHeapProperties.bufferDescriptorSize - }; - infos[start + i] = { - .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, - .type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, - .data = { .pAddressRange = &bufferRanges[i]} - }; - bufferRanges[rangeOffset + i] = { - .address = elements[i]->buffers[i2]->address, - .size = elements[i]->buffers[i2]->size - }; - } - start += elements.size(); - rangeOffset += elements.size(); - } - - Device::vkWriteResourceDescriptorsEXT(Device::device, start, infos.data(), ranges.data()); - - for(std::uint8_t i = 0; i < Window::numFrames; i++) { - descriptorHeap.resourceHeap[i].FlushDevice(); - } -} - -void RendertargetVulkan::SetOrderResursive(Transform2D* elementTransform) { - RenderingElement2DVulkanBase* renderer = dynamic_cast(elementTransform); - if(renderer) { - renderer->index = elements.size(); - elements.push_back(renderer); - } - std::sort(elementTransform->children.begin(), elementTransform->children.end(), [](Transform2D* a, Transform2D* b){ return a->anchor.z < b->anchor.z; }); - for(Transform2D* childTransform : elementTransform->children) { - SetOrderResursive(childTransform); - } -} -#endif \ No newline at end of file diff --git a/implementations/Crafter.Graphics-Shm.cpp b/implementations/Crafter.Graphics-Shm.cpp deleted file mode 100644 index b2dcc6f..0000000 --- a/implementations/Crafter.Graphics-Shm.cpp +++ /dev/null @@ -1,76 +0,0 @@ -/* -Crafter®.Graphics -Copyright (C) 2026 Catcrafts® -catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License version 3.0 as published by the Free Software Foundation; - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -module; -#include -#include -#include -#include -#include -#include -#include -#include -module Crafter.Graphics:Shm_impl; -import :Shm; -import std; - -using namespace Crafter; - -void Crafter::randname(char *buf) { - struct timespec ts; - clock_gettime(CLOCK_REALTIME, &ts); - long r = ts.tv_nsec; - for (int i = 0; i < 6; ++i) { - buf[i] = 'A'+(r&15)+(r&16)*2; - r >>= 5; - } -} - -int Crafter::anonymous_shm_open(void) { - char name[] = "/hello-wayland-XXXXXX"; - int retries = 100; - - do { - randname(name + strlen(name) - 6); - - --retries; - // shm_open guarantees that O_CLOEXEC is set - int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); - if (fd >= 0) { - shm_unlink(name); - return fd; - } - } while (retries > 0 && errno == EEXIST); - - return -1; -} - -int Crafter::create_shm_file(off_t size) { - int fd = anonymous_shm_open(); - if (fd < 0) { - return fd; - } - - if (ftruncate(fd, size) < 0) { - close(fd); - return -1; - } - - return fd; -} diff --git a/implementations/Crafter.Graphics-UIAtlas.cpp b/implementations/Crafter.Graphics-UIAtlas.cpp new file mode 100644 index 0000000..1f9c40f --- /dev/null +++ b/implementations/Crafter.Graphics-UIAtlas.cpp @@ -0,0 +1,130 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +module; +#include "vulkan/vulkan.h" +#include "../lib/stb_truetype.h" +module Crafter.Graphics:UIAtlas_impl; +import :UIAtlas; +import :Font; +import :ImageVulkan; +import :Device; +import std; + +using namespace Crafter; +using namespace Crafter::UI; + +void FontAtlas::Initialize(VkCommandBuffer cmd) { + image.Create( + kAtlasSize, kAtlasSize, /*mipLevels*/ 1, cmd, + VK_FORMAT_R8_UNORM, + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + ); + // Staging buffer is mapped; clear it so empty atlas regions sample as + // distance < onedge (i.e. fully outside any glyph). + std::memset(image.buffer.value, 0, kAtlasSize * kAtlasSize); + dirty = true; +} + +bool FontAtlas::ShelfPlace(int w, int h, int& outX, int& outY) { + // Try existing shelves first — same height heuristic keeps fragmentation low. + for (Shelf& s : shelves_) { + if (h <= s.height && s.cursorX + w <= kAtlasSize) { + outX = s.cursorX; + outY = s.y; + s.cursorX += w; + return true; + } + } + // New shelf below current ones. + if (nextShelfY_ + h > kAtlasSize) return false; + Shelf s{}; + s.y = nextShelfY_; + s.height = h; + s.cursorX = w; + outX = 0; + outY = s.y; + shelves_.push_back(s); + nextShelfY_ += h; + return true; +} + +bool FontAtlas::Ensure(Font& font, std::uint32_t codepoint) { + Key key{&font, codepoint}; + if (cache_.contains(key)) return true; + + float fontScale = stbtt_ScaleForPixelHeight(&font.font, kBaseSize); + + // Advance is always present, even for empty glyphs (e.g. space). + int advanceUnits = 0, lsb = 0; + stbtt_GetCodepointHMetrics(&font.font, static_cast(codepoint), &advanceUnits, &lsb); + + int sw = 0, sh = 0, xoff = 0, yoff = 0; + unsigned char* sdf = stbtt_GetCodepointSDF( + &font.font, fontScale, static_cast(codepoint), + kPadding, static_cast(kOnEdgeValue), kPixelDistScale, + &sw, &sh, &xoff, &yoff + ); + + Glyph g{}; + g.advance = advanceUnits * fontScale; + g.xoff = static_cast(xoff); + g.yoff = static_cast(yoff); + + if (sdf && sw > 0 && sh > 0) { + int px = 0, py = 0; + if (!ShelfPlace(sw, sh, px, py)) { + stbtt_FreeSDF(sdf, nullptr); + return false; // V1: silently drop overflow; V2: grow atlas + } + // Blit row-by-row into the mapped staging buffer. + for (int row = 0; row < sh; ++row) { + std::memcpy( + image.buffer.value + (py + row) * kAtlasSize + px, + sdf + row * sw, + static_cast(sw) + ); + } + stbtt_FreeSDF(sdf, nullptr); + + g.w = static_cast(sw); + g.h = static_cast(sh); + g.u0 = static_cast(px) / kAtlasSize; + g.v0 = static_cast(py) / kAtlasSize; + g.u1 = static_cast(px + sw) / kAtlasSize; + g.v1 = static_cast(py + sh) / kAtlasSize; + dirty = true; + } + // For empty glyphs (whitespace) we still cache the entry — the size-0 + // fields tell the emitter to skip the quad but advance the cursor. + + cache_.emplace(key, g); + return true; +} + +const Glyph* FontAtlas::Lookup(Font& font, std::uint32_t codepoint) const { + auto it = cache_.find(Key{&font, codepoint}); + return it == cache_.end() ? nullptr : &it->second; +} + +void FontAtlas::Update(VkCommandBuffer cmd) { + if (!dirty) return; + image.Update(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + dirty = false; +} diff --git a/implementations/Crafter.Graphics-UIRenderer.cpp b/implementations/Crafter.Graphics-UIRenderer.cpp new file mode 100644 index 0000000..0b4dfab --- /dev/null +++ b/implementations/Crafter.Graphics-UIRenderer.cpp @@ -0,0 +1,354 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +module; +#include "vulkan/vulkan.h" +module Crafter.Graphics:UIRenderer_impl; +import :UIRenderer; +import :Device; +import :Window; +import :DescriptorHeapVulkan; +import :VulkanBuffer; +import :ShaderVulkan; +import :ImageVulkan; +import :UIDrawList; +import :UIAtlas; +import std; + +using namespace Crafter; +using namespace Crafter::UI; + +namespace { + // Push-constant block — must match shaders/ui.comp.glsl. The shader's + // `vec2 surfaceSize` field has 8-byte alignment under std430, so we + // insert explicit padding after `itemCount` to keep the C++ and GLSL + // layouts byte-identical (40 bytes total). + struct PC { + std::uint32_t itemCount; // 0 + std::uint32_t _pad0; // 4 + float surfaceSize[2]; // 8 + float scale; // 16 + std::uint32_t outImageHeapIdx; // 20 + std::uint32_t itemBufHeapIdx; // 24 + std::uint32_t atlasTextureHeapIdx; // 28 + std::uint32_t bindlessBaseHeapIdx; // 32 + std::uint32_t linearSamplerHeapIdx; // 36 + }; + static_assert(sizeof(PC) == 40, "PC layout must match shader push-constant block"); + static_assert(sizeof(PC) <= 128, "Push-constant block exceeds the spec-mandated minimum (128 bytes)"); +} + +void UIRenderer::Initialize(Window& window, + VkCommandBuffer initCmd, + const std::filesystem::path& spvPath, + std::uint16_t bindlessImageCount) { + if (!window.descriptorHeap) { + throw std::runtime_error("UIRenderer::Initialize: window.descriptorHeap must be set first"); + } + window_ = &window; + bindlessCount_ = bindlessImageCount; + auto& heap = *window.descriptorHeap; + + // Slot allocation. Layout in the resource heap (image-typed indexing): + // [outImageBase_ ..] : Window::numFrames swapchain views (storage) + // [atlasImageSlot_] : 1 sampled SDF atlas + // [bindlessBase_ ..] : bindlessImageCount user image slots + auto imgSlots = heap.AllocateImageSlots( + Window::numFrames + 1 + bindlessImageCount + ); + outImageBase_ = imgSlots.firstElement; + atlasImageSlot_ = imgSlots.firstElement + Window::numFrames; + bindlessBase_ = imgSlots.firstElement + Window::numFrames + 1; + + // One SSBO per swapchain frame. + auto bufSlots = heap.AllocateBufferSlots(Window::numFrames); + itemBufBase_ = bufSlots.firstElement; + + // One linear sampler. + auto sampSlots = heap.AllocateSamplerSlots(1); + linearSamplerSlot_ = sampSlots.firstElement; + + // Initial item-buffer capacity (grows on demand). + GrowItemBuffersIfNeeded(256); + + // Atlas image — Initialize records a layout transition into initCmd. + atlas.Initialize(initCmd); + + CreatePipeline(spvPath); + WriteSwapchainDescriptors(); + WriteAtlasDescriptor(); + WriteSamplerDescriptors(); + WriteItemBufferDescriptors(); + + for (auto& h : heap.resourceHeap) h.FlushDevice(); + for (auto& h : heap.samplerHeap) h.FlushDevice(); +} + +void UIRenderer::GrowItemBuffersIfNeeded(std::uint32_t needed) { + if (needed <= itemCapacity_) return; + std::uint32_t newCap = itemCapacity_ ? itemCapacity_ * 2 : 256; + while (newCap < needed) newCap *= 2; + itemCapacity_ = static_cast(std::min(newCap, 65535)); + + for (auto& b : itemBufs_) { + b.Resize( + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + itemCapacity_ + ); + } + // Item buffer descriptors point at the buffers' device addresses, so + // they must be re-written after Resize. + if (window_) WriteItemBufferDescriptors(); +} + +void UIRenderer::SetItems(std::span items) { + if (items.size() > itemCapacity_) { + GrowItemBuffersIfNeeded(static_cast(items.size())); + } + pendingItemCount = static_cast(items.size()); + auto& buf = itemBufs_[window_->currentBuffer]; + if (!items.empty()) { + std::memcpy(buf.value, items.data(), items.size() * sizeof(UIItem)); + } + buf.FlushDevice(); +} + +void UIRenderer::Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) { + // Make sure any glyph rasterisation done during Emit lands on the GPU + // before we sample the atlas. + atlas.Update(cmd); + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_); + + PC pc{}; + pc.itemCount = pendingItemCount; + pc.surfaceSize[0] = static_cast(window.width); + pc.surfaceSize[1] = static_cast(window.height); + #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND + pc.scale = window.scale; + #else + pc.scale = 1.0f; + #endif + pc.outImageHeapIdx = outImageBase_ + frameIdx; + // Buffer-typed shader views index the *whole* heap in buffer-descriptor + // units, so we offset past the image region: bufferStartElement is the + // first element index where buffer descriptors actually live. + pc.itemBufHeapIdx = window.descriptorHeap->bufferStartElement + + itemBufBase_ + frameIdx; + pc.atlasTextureHeapIdx = atlasImageSlot_; + pc.bindlessBaseHeapIdx = bindlessBase_; + pc.linearSamplerHeapIdx = linearSamplerSlot_; + + // Pipelines created with VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT + // use vkCmdPushDataEXT for push constants (the spec requires layout to + // be VK_NULL_HANDLE in that mode, which means vkCmdPushConstants has + // nowhere to attach to). + VkPushDataInfoEXT pushInfo{ + .sType = VK_STRUCTURE_TYPE_PUSH_DATA_INFO_EXT, + .offset = 0, + .data = { .address = &pc, .size = sizeof(PC) }, + }; + Device::vkCmdPushDataEXT(cmd, &pushInfo); + + std::uint32_t gx = (window.width + 15) / 16; + std::uint32_t gy = (window.height + 15) / 16; + vkCmdDispatch(cmd, gx, gy, 1); +} + +void UIRenderer::CreatePipeline(const std::filesystem::path& spvPath) { + VulkanShader shader(spvPath, "main", VK_SHADER_STAGE_COMPUTE_BIT, nullptr); + + // Spec: "If VkPipelineCreateFlags2CreateInfoKHR::flags includes + // VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT, layout must be + // VK_NULL_HANDLE." Push constants are then attached via + // vkCmdPushDataEXT at draw time, not via the layout. + VkPipelineCreateFlags2CreateInfo flags2{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_CREATE_FLAGS_2_CREATE_INFO, + .flags = VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT, + }; + VkComputePipelineCreateInfo info{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .pNext = &flags2, + .stage = { + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .stage = VK_SHADER_STAGE_COMPUTE_BIT, + .module = shader.shader, + .pName = "main", + }, + .layout = VK_NULL_HANDLE, + }; + Device::CheckVkResult(vkCreateComputePipelines( + Device::device, VK_NULL_HANDLE, 1, &info, nullptr, &pipeline_)); +} + +// ─── descriptor writes ─────────────────────────────────────────────────── + +void UIRenderer::WriteSwapchainDescriptors() { + auto& heap = *window_->descriptorHeap; + + // One write per (frame, frame index) pairing — same swapchain view per + // frame index for each per-frame heap copy. + std::array infos{}; + std::array resources{}; + std::array destinations{}; + + std::size_t k = 0; + for (std::uint32_t heapFrame = 0; heapFrame < Window::numFrames; ++heapFrame) { + for (std::uint32_t imgFrame = 0; imgFrame < Window::numFrames; ++imgFrame) { + infos[k] = { + .sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT, + .pView = &window_->imageViews[imgFrame], + .layout = VK_IMAGE_LAYOUT_GENERAL, + }; + resources[k] = { + .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, + .type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .data = { .pImage = &infos[k] }, + }; + destinations[k] = { + .address = heap.resourceHeap[heapFrame].value + + heap.ImageByteOffset(static_cast(outImageBase_ + imgFrame)), + .size = Device::descriptorHeapProperties.imageDescriptorSize, + }; + ++k; + } + } + Device::vkWriteResourceDescriptorsEXT( + Device::device, static_cast(k), + resources.data(), destinations.data() + ); +} + +void UIRenderer::WriteAtlasDescriptor() { + auto& heap = *window_->descriptorHeap; + + // Build a stable VkImageViewCreateInfo for the atlas. ImageVulkan + // pre-creates a VkImageView, but the descriptor-heap path needs a + // pointer to a create-info — keep one on the renderer so the + // pointers we hand to vkWriteResourceDescriptorsEXT stay valid. + atlasViewCreateInfo_ = { + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .image = atlas.image.image, + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = VK_FORMAT_R8_UNORM, + .components = { + VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, + VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, + }, + .subresourceRange = { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + + std::array infos{}; + std::array resources{}; + std::array destinations{}; + + for (std::uint32_t f = 0; f < Window::numFrames; ++f) { + infos[f] = { + .sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT, + .pView = &atlasViewCreateInfo_, + .layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + }; + resources[f] = { + .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, + .type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, + .data = { .pImage = &infos[f] }, + }; + destinations[f] = { + .address = heap.resourceHeap[f].value + + heap.ImageByteOffset(atlasImageSlot_), + .size = Device::descriptorHeapProperties.imageDescriptorSize, + }; + } + Device::vkWriteResourceDescriptorsEXT( + Device::device, Window::numFrames, resources.data(), destinations.data() + ); +} + +void UIRenderer::WriteSamplerDescriptors() { + auto& heap = *window_->descriptorHeap; + + VkSamplerCreateInfo info{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .magFilter = VK_FILTER_LINEAR, + .minFilter = VK_FILTER_LINEAR, + .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR, + .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .maxAnisotropy = 1.0f, + .minLod = 0.0f, + .maxLod = VK_LOD_CLAMP_NONE, + }; + std::array infos{}; + std::array destinations{}; + for (std::uint32_t f = 0; f < Window::numFrames; ++f) { + infos[f] = info; + destinations[f] = { + .address = heap.samplerHeap[f].value + + heap.SamplerByteOffset(linearSamplerSlot_), + .size = Device::descriptorHeapProperties.samplerDescriptorSize, + }; + } + Device::vkWriteSamplerDescriptorsEXT( + Device::device, Window::numFrames, infos.data(), destinations.data() + ); +} + +void UIRenderer::WriteItemBufferDescriptors() { + auto& heap = *window_->descriptorHeap; + + std::array ranges{}; + std::array resources{}; + std::array destinations{}; + + std::size_t k = 0; + for (std::uint32_t heapFrame = 0; heapFrame < Window::numFrames; ++heapFrame) { + for (std::uint32_t bufFrame = 0; bufFrame < Window::numFrames; ++bufFrame) { + ranges[k] = { + .address = itemBufs_[bufFrame].address, + .size = itemBufs_[bufFrame].size, + }; + resources[k] = { + .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, + .type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, + .data = { .pAddressRange = &ranges[k] }, + }; + destinations[k] = { + .address = heap.resourceHeap[heapFrame].value + heap.BufferByteOffset(static_cast(itemBufBase_ + bufFrame)), + .size = Device::descriptorHeapProperties.bufferDescriptorSize, + }; + ++k; + } + } + Device::vkWriteResourceDescriptorsEXT( + Device::device, static_cast(k), + resources.data(), destinations.data() + ); +} + +void UIRenderer::CreateLinearSampler() { + // Not used — VK_EXT_descriptor_heap writes the sampler create-info + // directly into the heap (see WriteSamplerDescriptors). +} diff --git a/implementations/Crafter.Graphics-UIScene.cpp b/implementations/Crafter.Graphics-UIScene.cpp new file mode 100644 index 0000000..575eed3 --- /dev/null +++ b/implementations/Crafter.Graphics-UIScene.cpp @@ -0,0 +1,166 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +module; +#include "vulkan/vulkan.h" +module Crafter.Graphics:UIScene_impl; +import :UIScene; +import :Window; +import :Types; +import :DescriptorHeapVulkan; +import :UIRenderer; +import :UIHit; +import :UILayout; +import :UIDrawList; +import :UIWidget; +import Crafter.Event; +import std; + +using namespace Crafter; +using namespace Crafter::UI; + +UIScene::~UIScene() { + // Release listeners before the rest of the scene tears down. + mouseListener_.reset(); + updateListener_.reset(); + textListener_.reset(); + keyListener_.reset(); + focused_ = nullptr; + + if (window_) { + // De-register the renderer pass. + auto& v = window_->passes; + v.erase(std::remove(v.begin(), v.end(), static_cast(&renderer)), v.end()); + + // Clear the descriptor-heap pointer if we owned it; the heap's + // destructor releases its Vulkan buffers on its own. + if (ownsHeap_ && window_->descriptorHeap == &ownedHeap_) { + window_->descriptorHeap = nullptr; + } + } +} + +float UIScene::WindowScale() const { + if (!window_) return 1.0f; + #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND + return window_->scale; + #else + return 1.0f; + #endif +} + +void UIScene::Initialize(Window& window, const std::filesystem::path& spvPath) { + window_ = &window; + + // Auto-create a heap for UI-only apps. Generous defaults so most + // user-augmented heaps will fit too — if the user wants to share with + // 3D content, they should pre-create their own heap and attach it + // before calling Initialize. + if (!window.descriptorHeap) { + ownedHeap_.Initialize(/*images*/ 388, /*buffers*/ 35, /*samplers*/ 17); + window.descriptorHeap = &ownedHeap_; + ownsHeap_ = true; + } + + // One-shot init — needed by the atlas image transition. Each + // StartInit/FinishInit pair reuses the per-frame command buffer. + VkCommandBuffer cmd = window.StartInit(); + renderer.Initialize(window, cmd, spvPath); + window.FinishInit(); + + // Register as a RenderPass (after any other pass already in + // window.passes — typically RTPass for mixed scenes). + window.passes.push_back(&renderer); + + // Mouse: update focus to the topmost focusable under the cursor (or + // null if none), then dispatch the click via the bubble chain. + mouseListener_ = std::make_unique>( + &window.onMouseLeftClick, + [this]() { + if (!root_) return; + float x = window_->currentMousePos.x; + float y = window_->currentMousePos.y; + + Widget* hit = UI::HitTest(*root_, x, y); + Widget* focusTarget = nullptr; + for (Widget* w = hit; w != nullptr; w = w->parent) { + if (w->IsFocusable()) { focusTarget = w; break; } + } + SetFocus(focusTarget); + + UI::DispatchClick(*root_, x, y); + } + ); + + // Text input: only the currently-focused widget receives it. + textListener_ = std::make_unique>( + &window.onTextInput, + [this](std::string_view t) { + if (focused_) focused_->OnTextInput(t); + } + ); + + // Non-character keys (Backspace, arrows, Enter, …). + keyListener_ = std::make_unique>( + &window.onAnyKeyDown, + [this](CrafterKeys key) { + if (focused_) focused_->OnKeyDown(key); + } + ); + + // Per-frame: re-layout, emit, push items. + updateListener_ = std::make_unique>( + &window.onUpdate, + [this](FrameTime) { RebuildFrame(); } + ); +} + +void UIScene::SetFocus(Widget* w) { + if (w == focused_) return; + if (focused_) focused_->OnBlur(); + focused_ = w; + if (focused_) focused_->OnFocus(); +} + +void UIScene::RebuildFrame() { + if (!root_ || !window_) return; + float sc = WindowScale(); + + // Layout the tree against the current surface size. + UI::RunLayout( + *root_, + { static_cast(window_->width), static_cast(window_->height) }, + sc + ); + + // Emit draw items. + drawList.Reset(); + drawList.atlas = &renderer.atlas; + drawList.bindlessBaseHeapIdx = renderer.BindlessBaseHeapIdx(); + drawList.scale = sc; + if (background_) { + drawList.AddRect( + { 0, 0, static_cast(window_->width), static_cast(window_->height) }, + *background_ + ); + } + UI::EmitTree(*root_, drawList); + + // Stage to GPU. + renderer.SetItems(drawList.items); +} diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index 57a63ee..2f554df 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -37,16 +37,13 @@ module; #include #include #include -#ifdef CRAFTER_GRAPHICS_RENDERER_SOFTWARE #include #include #endif -#endif #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32 #include #include #endif -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN #include "vulkan/vulkan.h" #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #include "vulkan/vulkan_wayland.h" @@ -54,17 +51,14 @@ module; #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32 #include "vulkan/vulkan_win32.h" #endif -#endif +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "../lib/stb_image_write.h" module Crafter.Graphics:Window_impl; import :Window; -import :Transform2D; -import :MouseElement; import :Device; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN import :VulkanTransition; import :DescriptorHeapVulkan; -import :PipelineRTVulkan; -#endif +import :RenderPass; import std; using namespace Crafter; @@ -336,52 +330,24 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { case WM_LBUTTONDOWN: { window->mouseLeftHeld = true; window->onMouseLeftClick.Invoke(); - for(MouseElement* element : window->mouseElements) { - if(element) { - if(window->currentMousePos.x >= element->scaled.position.x && window->currentMousePos.x <= element->scaled.position.x+element->scaled.size.x && window->currentMousePos.y > element->scaled.position.y && window->currentMousePos.y < element->scaled.position.y+element->scaled.size.y) { - element->onMouseLeftClick.Invoke(); - } - } - } break; } case WM_LBUTTONUP: { window->mouseLeftHeld = false; window->onMouseLeftRelease.Invoke(); - for(MouseElement* element : window->mouseElements) { - if(element) { - if(window->currentMousePos.x >= element->scaled.position.x && window->currentMousePos.x <= element->scaled.position.x+element->scaled.size.x && window->currentMousePos.y > element->scaled.position.y && window->currentMousePos.y < element->scaled.position.y+element->scaled.size.y) { - element->onMouseLeftRelease.Invoke(); - } - } - } break; } case WM_RBUTTONDOWN: { window->mouseRightHeld = true; window->onMouseRightClick.Invoke(); - for(MouseElement* element : window->mouseElements) { - if(element) { - if(window->currentMousePos.x >= element->scaled.position.x && window->currentMousePos.x <= element->scaled.position.x+element->scaled.size.x && window->currentMousePos.y > element->scaled.position.y && window->currentMousePos.y < element->scaled.position.y+element->scaled.size.y) { - element->onMouseRightClick.Invoke(); - } - } - } break; } case WM_RBUTTONUP: { window->mouseRightHeld = false; window->onMouseRightRelease.Invoke(); - for(MouseElement* element : window->mouseElements) { - if(element) { - if(window->currentMousePos.x >= element->scaled.position.x && window->currentMousePos.x <= element->scaled.position.x+element->scaled.size.x && window->currentMousePos.y > element->scaled.position.y && window->currentMousePos.y < element->scaled.position.y+element->scaled.size.y) { - element->onMouseRightRelease.Invoke(); - } - } - } break; } @@ -405,11 +371,7 @@ Window::Window(std::uint32_t width, std::uint32_t height, const std::string_view SetTitle(title); } -#ifdef CRAFTER_GRAPHICS_RENDERER_SOFTWARE -Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height(height), renderer(width, height) { -#else Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height(height) { -#endif #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND Device::windows.push_back(this); surface = wl_compositor_create_surface(Device::compositor); @@ -434,39 +396,7 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height wp_viewport_set_destination(wpViewport, std::ceil(width/scale), std::ceil(height/scale)); wl_surface_commit(surface); - #ifdef CRAFTER_GRAPHICS_RENDERER_SOFTWARE - // Create a wl_buffer, attach it to the surface and commit the surface - int stride = width * 4; - int size = stride * height; - - // Allocate a shared memory file with the right size - int fd = create_shm_file(size); - if (fd < 0) { - throw std::runtime_error(std::format("creating a buffer file for {}B failed", size)); - } - - // Map the shared memory file - renderer.buffer[0] = reinterpret_cast*>(mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)); - if (renderer.buffer[0] == MAP_FAILED) { - throw std::runtime_error("mmap failed"); - } - - wl_shm_pool *pool = wl_shm_create_pool(Device::shm, fd, size); - buffer = wl_shm_pool_create_buffer(pool, 0, width, height, stride, WL_SHM_FORMAT_ARGB8888); - wl_shm_pool_destroy(pool); - - close(fd); - - if (buffer == nullptr) { - throw std::runtime_error("wl_buffer creation failed"); - } - - wl_surface_attach(surface, buffer, 0, 0); - wl_surface_commit(surface); #endif - #endif - - #ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32 // Initialize the window class @@ -574,7 +504,6 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height submitInfo.signalSemaphoreCount = 1; submitInfo.pSignalSemaphores = &semaphores.renderComplete; submitInfo.pNext = VK_NULL_HANDLE; - #endif lastMousePos = {0,0}; mouseDelta = {0,0}; @@ -588,13 +517,11 @@ void Window::SetTitle(const std::string_view title) { } void Window::SetCusorImage(std::uint16_t sizeX, std::uint16_t sizeY) { - new (&cursorRenderer) Rendertarget(sizeX, sizeY); #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND if(cursorSurface == nullptr) { cursorSurface = wl_compositor_create_surface(Device::compositor); } else { wl_buffer_destroy(cursorWlBuffer); - munmap(cursorRenderer.buffer[0], cursorBufferOldSize); } int stride = sizeX * 4; @@ -607,11 +534,6 @@ void Window::SetCusorImage(std::uint16_t sizeX, std::uint16_t sizeY) { throw std::runtime_error(std::format("creating a buffer file for {}B failed", size)); } - cursorRenderer.buffer[0] = reinterpret_cast*>(mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)); - if (cursorRenderer.buffer[0] == MAP_FAILED) { - throw std::runtime_error("mmap failed"); - } - wl_shm_pool *pool = wl_shm_create_pool(Device::shm, fd, size); cursorWlBuffer = wl_shm_pool_create_buffer(pool, 0, sizeX, sizeY, stride, WL_SHM_FORMAT_ARGB8888); wl_shm_pool_destroy(pool); @@ -673,21 +595,11 @@ void Window::SetCusorImageDefault() { void Window::UpdateCursorImage() { #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND - cursorRenderer.Render(0); - for(std::uint32_t i = 0; i < cursorBufferOldSize / 4; i++) { - std::swap(cursorRenderer.buffer[0][i].b, cursorRenderer.buffer[0][i].r); - } wl_surface_attach(cursorSurface, cursorWlBuffer, 0, 0); wl_surface_damage(cursorSurface, 0, 0, 9999999, 99999999); wl_surface_commit(cursorSurface); #endif #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32 - cursorRenderer.Render(0); - - // Swap R and B channels (renderer is RGBA, GDI DIB is BGRA) - for (std::uint32_t i = 0; i < (std::uint32_t)(cursorSizeX * cursorSizeY); i++) { - std::swap(cursorRenderer.buffer[0][i].r, cursorRenderer.buffer[0][i].b); - } // Create a mask bitmap (all zeros = fully opaque, alpha comes from color bitmap) HBITMAP hMask = CreateBitmap(cursorSizeX, cursorSizeY, 1, 1, nullptr); @@ -781,15 +693,6 @@ void Window::Update() { } void Window::Render() { - #ifdef CRAFTER_GRAPHICS_RENDERER_SOFTWARE - renderer.Render(0); - #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND - wl_surface_attach(surface, buffer, 0, 0); - wl_surface_commit(surface); - wl_surface_damage(surface, 0, 0, 10000, 100000); - #endif - #endif - #ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN // Acquire the next image from the swap chain Device::CheckVkResult(vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX, semaphores.presentComplete, (VkFence)nullptr, ¤tBuffer)); submitInfo.commandBufferCount = 1; @@ -810,7 +713,7 @@ void Window::Render() { VkImageMemoryBarrier image_memory_barrier { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .srcAccessMask = 0, - .dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, .oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, .newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, @@ -819,7 +722,7 @@ void Window::Render() { .subresourceRange = range }; - vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, 0, 0, nullptr, 0, nullptr, 1, &image_memory_barrier); + vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &image_memory_barrier); onUpdate.Invoke({startTime, startTime-lastFrameBegin}); #ifdef CRAFTER_TIMING @@ -831,31 +734,48 @@ void Window::Render() { } #endif - vkCmdBindPipeline(drawCmdBuffers[currentBuffer], VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR, pipeline->pipeline); - - VkBindHeapInfoEXT resourceHeapInfo = { - .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, - .heapRange = { - .address = descriptorHeap->resourceHeap[currentBuffer].address, - .size = static_cast(descriptorHeap->resourceHeap[currentBuffer].size) - }, - .reservedRangeOffset = (descriptorHeap->resourceHeap[currentBuffer].size - Device::descriptorHeapProperties.minResourceHeapReservedRange) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1), - .reservedRangeSize = Device::descriptorHeapProperties.minResourceHeapReservedRange - }; - Device::vkCmdBindResourceHeapEXT(drawCmdBuffers[currentBuffer], &resourceHeapInfo); + if (descriptorHeap) { + VkBindHeapInfoEXT resourceHeapInfo = { + .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, + .heapRange = { + .address = descriptorHeap->resourceHeap[currentBuffer].address, + .size = static_cast(descriptorHeap->resourceHeap[currentBuffer].size) + }, + .reservedRangeOffset = (descriptorHeap->resourceHeap[currentBuffer].size - Device::descriptorHeapProperties.minResourceHeapReservedRange) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1), + .reservedRangeSize = Device::descriptorHeapProperties.minResourceHeapReservedRange + }; + Device::vkCmdBindResourceHeapEXT(drawCmdBuffers[currentBuffer], &resourceHeapInfo); - VkBindHeapInfoEXT samplerHeapInfo = { - .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, - .heapRange = { - .address = descriptorHeap->samplerHeap[currentBuffer].address, - .size = static_cast(descriptorHeap->samplerHeap[currentBuffer].size) - }, - .reservedRangeOffset = descriptorHeap->samplerHeap[currentBuffer].size - Device::descriptorHeapProperties.minSamplerHeapReservedRange, - .reservedRangeSize = Device::descriptorHeapProperties.minSamplerHeapReservedRange - }; - Device::vkCmdBindSamplerHeapEXT(drawCmdBuffers[currentBuffer], &samplerHeapInfo); + VkBindHeapInfoEXT samplerHeapInfo = { + .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, + .heapRange = { + .address = descriptorHeap->samplerHeap[currentBuffer].address, + .size = static_cast(descriptorHeap->samplerHeap[currentBuffer].size) + }, + .reservedRangeOffset = descriptorHeap->samplerHeap[currentBuffer].size - Device::descriptorHeapProperties.minSamplerHeapReservedRange, + .reservedRangeSize = Device::descriptorHeapProperties.minSamplerHeapReservedRange + }; + Device::vkCmdBindSamplerHeapEXT(drawCmdBuffers[currentBuffer], &samplerHeapInfo); + } - Device::vkCmdTraceRaysKHR(drawCmdBuffers[currentBuffer], &pipeline->raygenRegion, &pipeline->missRegion, &pipeline->hitRegion, &pipeline->callableRegion, width, height, 1); + // Note: vkCmdClearColorImage is unavailable here — the swapchain is + // created with VK_IMAGE_USAGE_STORAGE_BIT only (no TRANSFER_DST_BIT). + // Passes that need a background should write one explicitly (UIScene + // exposes a `background()` setter for this purpose). + (void)clearColor; + + for (std::size_t i = 0; i < passes.size(); ++i) { + passes[i]->Record(drawCmdBuffers[currentBuffer], currentBuffer, *this); + + if (i + 1 < passes.size()) { + VkMemoryBarrier mb { + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, + }; + vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &mb, 0, nullptr, 0, nullptr); + } + } VkImageMemoryBarrier image_memory_barrier2 { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, @@ -869,7 +789,7 @@ void Window::Render() { .subresourceRange = range }; - vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &image_memory_barrier2); + vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &image_memory_barrier2); Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer])); @@ -894,7 +814,6 @@ void Window::Render() { Device::CheckVkResult(result); } Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); - #endif } #ifdef CRAFTER_TIMING @@ -935,7 +854,6 @@ void Window::LogTiming() { } #endif -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN void Window::CreateSwapchain() { // Store the current swap chain handle so we can use it later on to ease up recreation @@ -1009,7 +927,7 @@ void Window::CreateSwapchain() swapchainCI.imageFormat = colorFormat; swapchainCI.imageColorSpace = colorSpace; swapchainCI.imageExtent = { swapchainExtent.width, swapchainExtent.height }; - swapchainCI.imageUsage = VK_IMAGE_USAGE_STORAGE_BIT; + swapchainCI.imageUsage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; swapchainCI.preTransform = (VkSurfaceTransformFlagBitsKHR)preTransform; swapchainCI.imageArrayLayers = 1; swapchainCI.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; @@ -1120,8 +1038,6 @@ void Window::EndCmd(VkCommandBuffer cmd) { Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); } -#endif - #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND void Window::wl_surface_frame_done(void* data, struct wl_callback *cb, uint32_t time) { wl_callback_destroy(cb); @@ -1167,4 +1083,135 @@ void Window::xdg_surface_handle_preferred_scale(void* data, wp_fractional_scale_ window->scale = scale / 120.0f; } -#endif \ No newline at end of file +#endif + +void Window::SaveFrame(const std::filesystem::path& path) { + // Staging buffer big enough for one RGBA frame. + VkDeviceSize bufSize = static_cast(width) * height * 4; + + VkBufferCreateInfo bci{ + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .size = bufSize, + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + }; + VkBuffer stagingBuf = VK_NULL_HANDLE; + Device::CheckVkResult(vkCreateBuffer(Device::device, &bci, nullptr, &stagingBuf)); + + VkMemoryRequirements memReqs; + vkGetBufferMemoryRequirements(Device::device, stagingBuf, &memReqs); + VkMemoryAllocateInfo mai{ + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = memReqs.size, + .memoryTypeIndex = Device::GetMemoryType(memReqs.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT), + }; + VkDeviceMemory stagingMem = VK_NULL_HANDLE; + Device::CheckVkResult(vkAllocateMemory(Device::device, &mai, nullptr, &stagingMem)); + Device::CheckVkResult(vkBindBufferMemory(Device::device, stagingBuf, stagingMem, 0)); + + // One-shot command buffer so we don't trash the per-frame ones. + VkCommandBufferAllocateInfo cba{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + .commandPool = Device::commandPool, + .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, + .commandBufferCount = 1, + }; + VkCommandBuffer cmd = VK_NULL_HANDLE; + Device::CheckVkResult(vkAllocateCommandBuffers(Device::device, &cba, &cmd)); + + VkCommandBufferBeginInfo cbi{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, + }; + Device::CheckVkResult(vkBeginCommandBuffer(cmd, &cbi)); + + VkImageSubresourceRange range{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }; + + // Render() leaves the image in PRESENT_SRC_KHR. + VkImageMemoryBarrier toSrc{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = 0, + .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT, + .oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + .newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = images[currentBuffer], + .subresourceRange = range, + }; + vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &toSrc); + + VkBufferImageCopy region{ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }, + .imageOffset = { 0, 0, 0 }, + .imageExtent = { width, height, 1 }, + }; + vkCmdCopyImageToBuffer(cmd, images[currentBuffer], + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, stagingBuf, 1, ®ion); + + VkImageMemoryBarrier back{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT, + .dstAccessMask = 0, + .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + .newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = images[currentBuffer], + .subresourceRange = range, + }; + vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + 0, 0, nullptr, 0, nullptr, 1, &back); + + Device::CheckVkResult(vkEndCommandBuffer(cmd)); + + VkSubmitInfo si{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, + .commandBufferCount = 1, + .pCommandBuffers = &cmd, + }; + Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &si, VK_NULL_HANDLE)); + Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); + + // Read back, swizzle BGRA → RGBA if needed, write PNG. + void* mapped = nullptr; + Device::CheckVkResult(vkMapMemory(Device::device, stagingMem, 0, VK_WHOLE_SIZE, 0, &mapped)); + const std::uint8_t* src = static_cast(mapped); + + std::vector rgba(static_cast(width) * height * 4); + bool bgr = (colorFormat == VK_FORMAT_B8G8R8A8_UNORM); + for (std::uint32_t i = 0; i < width * height; ++i) { + if (bgr) { + rgba[i*4+0] = src[i*4+2]; + rgba[i*4+1] = src[i*4+1]; + rgba[i*4+2] = src[i*4+0]; + rgba[i*4+3] = src[i*4+3]; + } else { + rgba[i*4+0] = src[i*4+0]; + rgba[i*4+1] = src[i*4+1]; + rgba[i*4+2] = src[i*4+2]; + rgba[i*4+3] = src[i*4+3]; + } + } + vkUnmapMemory(Device::device, stagingMem); + + stbi_write_png(path.string().c_str(), static_cast(width), static_cast(height), + 4, rgba.data(), static_cast(width) * 4); + + vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd); + vkDestroyBuffer(Device::device, stagingBuf, nullptr); + vkFreeMemory(Device::device, stagingMem, nullptr); +} \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-DescriptorHeapVulkan.cppm b/interfaces/Crafter.Graphics-DescriptorHeapVulkan.cppm index 614c19c..d1a4476 100644 --- a/interfaces/Crafter.Graphics-DescriptorHeapVulkan.cppm +++ b/interfaces/Crafter.Graphics-DescriptorHeapVulkan.cppm @@ -18,11 +18,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN #include "vulkan/vulkan.h" -#endif export module Crafter.Graphics:DescriptorHeapVulkan; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN import std; import :Device; import :Window; @@ -30,12 +27,23 @@ import :Types; import :VulkanBuffer; export namespace Crafter { + struct ImageSlotRange { std::uint16_t firstElement; std::uint16_t count; }; + struct BufferSlotRange { std::uint16_t firstElement; std::uint16_t count; }; + struct SamplerSlotRange { std::uint16_t firstElement; std::uint16_t count; }; + struct DescriptorHeapVulkan { VulkanBuffer resourceHeap[Window::numFrames]; VulkanBuffer samplerHeap[Window::numFrames]; std::uint32_t bufferStartOffset; std::uint16_t bufferStartElement; - + + std::uint16_t imageCapacity = 0; + std::uint16_t bufferCapacity = 0; + std::uint16_t samplerCapacity = 0; + std::uint16_t imageNext = 0; + std::uint16_t bufferNext = 0; + std::uint16_t samplerNext = 0; + void Initialize(std::uint16_t images, std::uint16_t buffers, std::uint16_t samplers) { std::uint32_t descriptorRegion = images * Device::descriptorHeapProperties.imageDescriptorSize + buffers * Device::descriptorHeapProperties.bufferDescriptorSize; std::uint32_t alignedDescriptorRegion = (descriptorRegion + Device::descriptorHeapProperties.imageDescriptorAlignment - 1) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1); @@ -47,11 +55,57 @@ export namespace Crafter { bufferStartElement = 1; } bufferStartOffset = bufferStartElement * Device::descriptorHeapProperties.bufferDescriptorSize; + + imageCapacity = images; + bufferCapacity = buffers; + samplerCapacity = samplers; + imageNext = 0; + bufferNext = 0; + samplerNext = 0; + for(std::uint8_t i = 0; i < Window::numFrames; i++) { resourceHeap[i].Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_DESCRIPTOR_HEAP_BIT_EXT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, resourceSize); samplerHeap[i].Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_DESCRIPTOR_HEAP_BIT_EXT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, samplerSize); } } + + ImageSlotRange AllocateImageSlots(std::uint16_t count) { + if (imageNext + count > imageCapacity) { + throw std::runtime_error(std::format("DescriptorHeapVulkan: out of image slots ({} requested, {} remaining of {})", count, imageCapacity - imageNext, imageCapacity)); + } + ImageSlotRange r{imageNext, count}; + imageNext += count; + return r; + } + + BufferSlotRange AllocateBufferSlots(std::uint16_t count) { + if (bufferNext + count > bufferCapacity) { + throw std::runtime_error(std::format("DescriptorHeapVulkan: out of buffer slots ({} requested, {} remaining of {})", count, bufferCapacity - bufferNext, bufferCapacity)); + } + BufferSlotRange r{bufferNext, count}; + bufferNext += count; + return r; + } + + SamplerSlotRange AllocateSamplerSlots(std::uint16_t count) { + if (samplerNext + count > samplerCapacity) { + throw std::runtime_error(std::format("DescriptorHeapVulkan: out of sampler slots ({} requested, {} remaining of {})", count, samplerCapacity - samplerNext, samplerCapacity)); + } + SamplerSlotRange r{samplerNext, count}; + samplerNext += count; + return r; + } + + std::uint32_t ImageByteOffset(std::uint16_t firstElement) const { + return firstElement * Device::descriptorHeapProperties.imageDescriptorSize; + } + std::uint32_t BufferByteOffset(std::uint16_t firstElement) const { + return bufferStartOffset + firstElement * Device::descriptorHeapProperties.bufferDescriptorSize; + } + std::uint32_t SamplerByteOffset(std::uint16_t firstElement) const { + return firstElement * Device::descriptorHeapProperties.samplerDescriptorSize; + } + inline static std::uint32_t GetBufferOffset(std::uint16_t images, std::uint16_t buffers) { std::uint32_t bufferStartElement = images * Device::descriptorHeapProperties.imageDescriptorSize / Device::descriptorHeapProperties.bufferDescriptorSize; @@ -66,10 +120,8 @@ export namespace Crafter { if(images > 0 && bufferStartElement == 0) { bufferStartElement = 1; } - + return bufferStartElement; } }; -} - -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-Device.cppm b/interfaces/Crafter.Graphics-Device.cppm index 214e970..7fbd501 100644 --- a/interfaces/Crafter.Graphics-Device.cppm +++ b/interfaces/Crafter.Graphics-Device.cppm @@ -18,9 +18,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN #include "vulkan/vulkan.h" -#endif #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #include #include @@ -98,7 +96,6 @@ export namespace Crafter { }; #endif - #ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN inline static VkInstance instance = VK_NULL_HANDLE; inline static VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE; inline static VkPhysicalDevice physDevice = VK_NULL_HANDLE; @@ -117,6 +114,8 @@ export namespace Crafter { inline static PFN_vkCmdBindResourceHeapEXT vkCmdBindResourceHeapEXT; inline static PFN_vkCmdBindSamplerHeapEXT vkCmdBindSamplerHeapEXT; inline static PFN_vkWriteResourceDescriptorsEXT vkWriteResourceDescriptorsEXT; + inline static PFN_vkWriteSamplerDescriptorsEXT vkWriteSamplerDescriptorsEXT; + inline static PFN_vkCmdPushDataEXT vkCmdPushDataEXT; inline static PFN_vkGetPhysicalDeviceDescriptorSizeEXT vkGetPhysicalDeviceDescriptorSizeEXT; inline static PFN_vkGetDeviceFaultInfoEXT vkGetDeviceFaultInfoEXT; @@ -132,6 +131,5 @@ export namespace Crafter { static void CheckVkResult(VkResult result); static std::uint32_t GetMemoryType(std::uint32_t typeBits, VkMemoryPropertyFlags properties); - #endif }; } \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-Font.cppm b/interfaces/Crafter.Graphics-Font.cppm index 3b98b35..be3b80b 100644 --- a/interfaces/Crafter.Graphics-Font.cppm +++ b/interfaces/Crafter.Graphics-Font.cppm @@ -26,14 +26,45 @@ export module Crafter.Graphics:Font; import std; namespace Crafter { + // Decode the UTF-8 codepoint at `text[i]` and advance `i` past it. + // Returns 0 once `i` reaches the end. Malformed sequences yield U+FFFD + // and the index is moved past one byte to keep iteration finite. + export inline std::uint32_t DecodeUtf8(std::string_view text, std::size_t& i) { + if (i >= text.size()) return 0; + std::uint8_t b0 = static_cast(text[i]); + + // Single-byte ASCII is the common path. + if (b0 < 0x80) { ++i; return b0; } + + int extra; + std::uint32_t cp; + if ((b0 & 0xE0) == 0xC0) { extra = 1; cp = b0 & 0x1F; } + else if ((b0 & 0xF0) == 0xE0) { extra = 2; cp = b0 & 0x0F; } + else if ((b0 & 0xF8) == 0xF0) { extra = 3; cp = b0 & 0x07; } + else { ++i; return 0xFFFD; } // continuation byte at start, or 5+-byte leader + + ++i; + for (int k = 0; k < extra; ++k) { + if (i >= text.size()) return 0xFFFD; + std::uint8_t b = static_cast(text[i]); + if ((b & 0xC0) != 0x80) return 0xFFFD; // missing continuation + cp = (cp << 6) | (b & 0x3Fu); + ++i; + } + return cp; + } + export class Font { public: std::vector fontBuffer; - std::int_fast32_t ascent; - std::int_fast32_t descent; + std::int_fast32_t ascent; + std::int_fast32_t descent; std::int_fast32_t lineGap; stbtt_fontinfo font; Font(const std::filesystem::path& font); std::uint32_t GetLineWidth(const std::string_view text, float size); + float LineHeight(float size); + float AscentPx(float size); // baseline offset from line-top + float ScaleForSize(float size); // stb's pixel-units-per-em factor }; } \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-GridElement.cppm b/interfaces/Crafter.Graphics-GridElement.cppm deleted file mode 100644 index 30565bf..0000000 --- a/interfaces/Crafter.Graphics-GridElement.cppm +++ /dev/null @@ -1,64 +0,0 @@ -/* -Crafter®.Graphics -Copyright (C) 2026 Catcrafts® -catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License version 3.0 as published by the Free Software Foundation; - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -export module Crafter.Graphics:GridElement; -import std; -import :Transform2D; -import :ForwardDeclarations; - -export namespace Crafter { - struct GridElement : Transform2D { - std::uint32_t columns; - std::uint32_t rows; - std::int32_t spacingX; - std::int32_t spacingY; - std::int32_t paddingX; - std::int32_t paddingY; - GridElement(std::uint32_t columns, std::uint32_t rows, std::int32_t spacingX, std::int32_t spacingY, std::int32_t paddingX, std::int32_t paddingY, Anchor2D anchor) : Transform2D(anchor), columns(columns), rows(rows), spacingX(spacingX), spacingY(spacingY), paddingX(paddingX), paddingY(paddingY) { - - } - void UpdatePosition(RendertargetBase& window, Transform2D& parent) override { - ScaleElement(parent); - std::int32_t cellWidth = (paddingX * 2) - (spacingX * (columns - 1)) / columns; - std::int32_t cellHeight = (paddingY * 2) - (spacingY * (rows - 1)) / rows; - - std::size_t childIndex = 0; - for (std::uint32_t row = 0; row < rows && childIndex < this->children.size(); ++row) { - for (std::uint32_t col = 0; col < columns && childIndex < this->children.size(); ++col) { - Transform2D* child = this->children[childIndex]; - - // Calculate position for this child - std::int32_t childX = (cellWidth * col) + (spacingX * col) + paddingX; - - std::int32_t childY = (cellHeight * row) + (spacingY * row) + paddingY; - - // Apply relative positioning - child->anchor.x = childX; - child->anchor.y = childY; - child->anchor.width = cellWidth; - child->anchor.height = cellHeight; - - // Update child position - child->UpdatePosition(window, *this); - childIndex++; - } - } - } - }; -} \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-ImageVulkan.cppm b/interfaces/Crafter.Graphics-ImageVulkan.cppm index 5ded767..cae15ba 100644 --- a/interfaces/Crafter.Graphics-ImageVulkan.cppm +++ b/interfaces/Crafter.Graphics-ImageVulkan.cppm @@ -19,16 +19,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 0215-1301 USA module; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN #include "vulkan/vulkan.h" -#endif export module Crafter.Graphics:ImageVulkan; import std; import :VulkanBuffer; export namespace Crafter { - #ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN template class ImageVulkan { public: @@ -45,7 +42,11 @@ export namespace Crafter { this->width = width; this->height = height; this->mipLevels = mipLevels; - buffer.Create(VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, width * height); + buffer.Create( + VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, + width * height + ); VkImageCreateInfo imageInfo = {}; imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; @@ -178,5 +179,4 @@ export namespace Crafter { vkCmdPipelineBarrier(cmd, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier); } }; - #endif } \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-Mesh.cppm b/interfaces/Crafter.Graphics-Mesh.cppm index 300d8a6..234c665 100644 --- a/interfaces/Crafter.Graphics-Mesh.cppm +++ b/interfaces/Crafter.Graphics-Mesh.cppm @@ -19,9 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA module; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN #include "vulkan/vulkan.h" -#endif export module Crafter.Graphics:Mesh; import std; @@ -29,7 +27,6 @@ import Crafter.Math; import :VulkanBuffer; export namespace Crafter { - #ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN class Mesh { public: VulkanBuffer scratchBuffer; @@ -43,5 +40,4 @@ export namespace Crafter { bool opaque; void Build(std::span> verticies, std::span indicies, VkCommandBuffer cmd); }; - #endif } \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-PipelineRTVulkan.cppm b/interfaces/Crafter.Graphics-PipelineRTVulkan.cppm index ac34b11..2cabb46 100644 --- a/interfaces/Crafter.Graphics-PipelineRTVulkan.cppm +++ b/interfaces/Crafter.Graphics-PipelineRTVulkan.cppm @@ -18,11 +18,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN #include "vulkan/vulkan.h" -#endif export module Crafter.Graphics:PipelineRTVulkan; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN import std; import :Device; import :VulkanBuffer; @@ -115,6 +112,4 @@ export namespace Crafter { vkDestroyPipeline(Device::device, pipeline, nullptr); } }; -} - -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-MouseElement.cppm b/interfaces/Crafter.Graphics-RTPass.cppm similarity index 50% rename from interfaces/Crafter.Graphics-MouseElement.cppm rename to interfaces/Crafter.Graphics-RTPass.cppm index 0297e86..2628be6 100644 --- a/interfaces/Crafter.Graphics-MouseElement.cppm +++ b/interfaces/Crafter.Graphics-RTPass.cppm @@ -16,29 +16,29 @@ You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ - -export module Crafter.Graphics:MouseElement; +module; +#include "vulkan/vulkan.h" +export module Crafter.Graphics:RTPass; import std; -import Crafter.Event; -import :Transform2D; -import :ForwardDeclarations; +import :RenderPass; +import :Window; +import :Device; +import :PipelineRTVulkan; export namespace Crafter { - struct MouseElement : Transform2D { - Event onMouseMove; - Event onMouseEnter; - Event onMouseLeave; - Event onMouseRightClick; - Event onMouseLeftClick; - Event onMouseRightHold; - Event onMouseLeftHold; - Event onMouseRightRelease; - Event onMouseLeftRelease; - bool mouseHover = false; + struct RTPass : RenderPass { + PipelineRTVulkan* pipeline; - MouseElement(); - MouseElement(Window& window); - MouseElement(Anchor2D anchor); - MouseElement(Anchor2D anchor, Window& window); + RTPass(PipelineRTVulkan* p) : pipeline(p) {} + + void Record(VkCommandBuffer cmd, std::uint32_t /*frameIdx*/, Window& window) override { + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR, pipeline->pipeline); + Device::vkCmdTraceRaysKHR(cmd, + &pipeline->raygenRegion, + &pipeline->missRegion, + &pipeline->hitRegion, + &pipeline->callableRegion, + window.width, window.height, 1); + } }; -} \ No newline at end of file +} diff --git a/implementations/Crafter.Graphics-Transform2D.cpp b/interfaces/Crafter.Graphics-RenderPass.cppm similarity index 54% rename from implementations/Crafter.Graphics-Transform2D.cpp rename to interfaces/Crafter.Graphics-RenderPass.cppm index ff7d4b0..fe0c121 100644 --- a/implementations/Crafter.Graphics-Transform2D.cpp +++ b/interfaces/Crafter.Graphics-RenderPass.cppm @@ -1,12 +1,11 @@ /* Crafter®.Graphics Copyright (C) 2026 Catcrafts® -Catcrafts.net +catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 3.0 of the License, or (at your option) any later version. +License version 3.0 as published by the Free Software Foundation; This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of @@ -17,17 +16,16 @@ You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ - -module Crafter.Graphics:Transform2D_impl; -import :Transform2D; -import :Rendertarget; -import :Types; -import :Font; +module; +#include "vulkan/vulkan.h" +export module Crafter.Graphics:RenderPass; import std; -using namespace Crafter; +export namespace Crafter { + struct Window; - -Anchor2D::Anchor2D(float x, float y, float width, float height, float offsetX, float offsetY, std::uint8_t z, bool maintainAspectRatio): x(x), y(y), width(width), height(height), offsetX(offsetX), offsetY(offsetY), z(z), maintainAspectRatio(maintainAspectRatio) { - -} \ No newline at end of file + struct RenderPass { + virtual void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) = 0; + virtual ~RenderPass() = default; + }; +} diff --git a/interfaces/Crafter.Graphics-RenderingElement2D.cppm b/interfaces/Crafter.Graphics-RenderingElement2D.cppm deleted file mode 100644 index dc2c184..0000000 --- a/interfaces/Crafter.Graphics-RenderingElement2D.cppm +++ /dev/null @@ -1,449 +0,0 @@ -/* -Crafter®.Graphics -Copyright (C) 2026 Catcrafts® -catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License version 3.0 as published by the Free Software Foundation; - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -module; -#include "../lib/stb_truetype.h" -export module Crafter.Graphics:RenderingElement2D; -import Crafter.Asset; -import std; -import :Transform2D; -import :RenderingElement2DBase; -import :Font; -import :Types; -import :Window; - -export namespace Crafter { - template requires ((!Rotating || Scaling) && (!Owning || Scaling)) - struct RenderingElement2D : RenderingElement2DBase, ScalingBase, RotatingBase { - RenderingElement2D() = default; - RenderingElement2D(Anchor2D anchor, OpaqueType opaque) : RenderingElement2DBase(anchor, opaque) { - - } - RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t rotation) requires(Rotating) : RenderingElement2DBase(anchor, opaque), RotatingBase(rotation) { - - } - RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, Vector* scalingBuffer) requires(Scaling && !Owning) : RenderingElement2DBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight, scalingBuffer) { - - } - RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, Vector* scalingBuffer, std::uint32_t rotation) requires(Scaling && !Owning && Rotating) : RenderingElement2DBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight, scalingBuffer), RotatingBase(rotation) { - - } - RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight) requires(Owning) : RenderingElement2DBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight) { - - } - RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, std::uint32_t rotation) requires(Owning && Rotating) : RenderingElement2DBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight) , RotatingBase(rotation) { - - } - RenderingElement2D(Anchor2D anchor, TextureAsset>& texture) requires(!Owning && Scaling) : RenderingElement2DBase(anchor, texture.opaque), ScalingBase(texture.pixels.data(), texture.sizeX, texture.sizeY) { - - } - RenderingElement2D(Anchor2D anchor, TextureAsset>& texture, std::uint32_t rotation) requires(!Owning && Scaling && Rotating) : RenderingElement2DBase(anchor, texture.opaque), ScalingBase(texture.pixels.data(), texture.sizeX, texture.sizeY), RotatingBase(rotation) { - - } - - RenderingElement2D(RenderingElement2D&) = delete; - RenderingElement2D& operator=(RenderingElement2D&) = delete; - - void ScaleNearestNeighbor() requires(Scaling) { - for (std::uint32_t y = 0; y < this->scaled.size.y; y++) { - std::uint32_t srcY = y * ScalingBase::bufferHeight / this->scaled.size.y; - for (std::uint32_t x = 0; x < this->scaled.size.x; x++) { - std::uint32_t srcX = x * ScalingBase::bufferWidth / this->scaled.size.x; - this->buffer[y * this->scaled.size.x + x] = ScalingBase::scalingBuffer[srcY * ScalingBase::bufferWidth + srcX]; - } - } - } - - void ScaleRotating() requires(Scaling) { - const float dstWidth = this->scaled.size.x; - const float dstHeight = this->scaled.size.y; - - const float c2 = std::abs(std::cos(RotatingBase::rotation)); - const float s2 = std::abs(std::sin(RotatingBase::rotation)); - - const float rotatedWidth = dstWidth * c2 + dstHeight * s2; - const float rotatedHeight = dstWidth * s2 + dstHeight * c2; - - const float diffX = std::ceil((rotatedWidth - dstWidth) * 0.5); - const float diffY = std::ceil((rotatedHeight - dstHeight) * 0.5); - - this->scaled.size.x += diffX + diffX; - this->scaled.size.y += diffY + diffY; - - this->scaled.position.x -= diffX; - this->scaled.position.y -= diffY; - - this->buffer.clear(); - this->buffer.resize(this->scaled.size.x * this->scaled.size.y); - - // Destination center - const float dstCx = (this->scaled.size.x - 1.0) * 0.5; - const float dstCy = (this->scaled.size.y - 1.0) * 0.5; - - // Source center - const float srcCx = (ScalingBase::bufferWidth - 1.0) * 0.5; - const float srcCy = (ScalingBase::bufferHeight - 1.0) * 0.5; - - const float c = std::cos(RotatingBase::rotation); - const float s = std::sin(RotatingBase::rotation); - - // Scale factors (destination → source) - const float scaleX = static_cast(ScalingBase::bufferWidth) / dstWidth; - const float scaleY = static_cast(ScalingBase::bufferHeight) / dstHeight; - - for (std::uint32_t yB = 0; yB < this->scaled.size.y; ++yB) { - for (std::uint32_t xB = 0; xB < this->scaled.size.x; ++xB) { - - // ---- Destination pixel relative to center ---- - const float dx = (static_cast(xB) - dstCx) * scaleX; - const float dy = (static_cast(yB) - dstCy) * scaleY; - - // ---- Inverse rotation ---- - const float sx = (c * dx - s * dy) + srcCx; - const float sy = (s * dx + c * dy) + srcCy; - - // ---- Nearest neighbour sampling ---- - const std::int32_t srcX = static_cast(std::round(sx)); - const std::int32_t srcY = static_cast(std::round(sy)); - - if (srcX >= 0 && srcX < ScalingBase::bufferWidth && srcY >= 0 && srcY < ScalingBase::bufferHeight) { - this->buffer[yB * this->scaled.size.x + xB] = ScalingBase::scalingBuffer[srcY * ScalingBase::bufferWidth + srcX]; - } - } - } - } - - - void UpdatePosition(RendertargetBase& window, Transform2D& parent) override { - ScaleData2D oldScale = this->scaled; - this->ScaleElement(parent); - if constexpr(Scaling && !Rotating) { - if(oldScale.size.x != this->scaled.size.x || oldScale.size.y != this->scaled.size.y) { - this->buffer.resize(this->scaled.size.x * this->scaled.size.y); - ScaleNearestNeighbor(); - } - } else if constexpr(Rotating) { - if(oldScale.size.x != this->scaled.size.x || oldScale.size.y != this->scaled.size.y) { - this->buffer.resize(this->scaled.size.x * this->scaled.size.y); - ScaleRotating(); - } - } else { - if(oldScale.size.x != this->scaled.size.x || oldScale.size.y != this->scaled.size.y) { - this->buffer.resize(this->scaled.size.x * this->scaled.size.y); - } - } - for(Transform2D* child : this->children) { - child->UpdatePosition(window, *this); - } - } - - std::vector ResizeText(RendertargetBase& window, Transform2D& parent, const std::string_view text, float& size, Font& font, TextOverflowMode overflowMode = TextOverflowMode::Clip, TextScaleMode scaleMode = TextScaleMode::None) { - float scale = stbtt_ScaleForPixelHeight(&font.font, size); - int baseline = (int)(font.ascent * scale); - - std::vector lines; - std::string_view remaining = text; - - std::uint32_t lineHeight = (font.ascent - font.descent) * scale; - - if(overflowMode == TextOverflowMode::Clip) { - while (!remaining.empty()) { - // Find next newline or end of string - auto newlinePos = remaining.find('\n'); - if (newlinePos != std::string_view::npos) { - lines.emplace_back(remaining.substr(0, newlinePos)); - remaining = remaining.substr(newlinePos + 1); - } else { - lines.emplace_back(remaining); - break; - } - } - std::uint32_t maxWidth = 0; - - for(const std::string_view line: lines) { - std::uint32_t lineWidth = 0; - for (const char c : line) { - int advance, lsb; - stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb); - lineWidth += (int)(advance * scale); - } - if(lineWidth > maxWidth) { - maxWidth = lineWidth; - } - } - - if(scaleMode == TextScaleMode::Element) { - std::int32_t logicalPerPixelY = this->anchor.height / this->scaled.size.y; - std::int32_t oldHeight = this->anchor.height; - std::int32_t logicalPerPixelX = this->anchor.width / this->scaled.size.x; - std::int32_t oldwidth = this->anchor.width; - this->anchor.height = lineHeight * logicalPerPixelY; - this->anchor.width = maxWidth * logicalPerPixelX; - if(oldHeight != this->anchor.height || oldwidth != this->anchor.width) { - UpdatePosition(window, parent); - } - } else if(scaleMode == TextScaleMode::Font) { - float lineHeightPerFont = lineHeight / size; - float lineWidthPerFont = maxWidth / size; - - float maxFontHeight = this->scaled.size.y / lineHeightPerFont; - float maxFontWidth = this->scaled.size.x / lineWidthPerFont; - - size = std::min(maxFontHeight, maxFontWidth); - } else { - if constexpr(Scaling) { - lines.resize(ScalingBase::bufferHeight / lines.size()); - } else { - lines.resize(this->scaled.size.y / lines.size()); - } - } - } else { - while (!remaining.empty()) { - std::string_view line; - auto newlinePos = remaining.find('\n'); - if (newlinePos != std::string_view::npos) { - line = remaining.substr(0, newlinePos); - remaining = remaining.substr(newlinePos + 1); - } else { - line = remaining; - remaining = ""; - } - - std::uint32_t lineWidth = 0; - std::size_t lastWrapPos = 0; // position of last space that can be used to wrap - std::size_t startPos = 0; - - for (std::size_t i = 0; i < line.size(); ++i) { - char c = line[i]; - - // get width of this character - int advance, lsb; - stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb); - lineWidth += (std::uint32_t)(advance * scale); - - // remember last space for wrapping - if (c == ' ') { - lastWrapPos = i; - } - - // if line exceeds width, wrap - if (lineWidth > this->scaled.size.x) { - std::size_t wrapPos; - if (lastWrapPos > startPos) { - wrapPos = lastWrapPos; // wrap at last space - } else { - wrapPos = i; // no space, hard wrap - } - - // push the line up to wrapPos - lines.push_back(line.substr(startPos, wrapPos - startPos)); - - // skip any spaces at the beginning of next line - startPos = wrapPos; - while (startPos < line.size() && line[startPos] == ' ') { - ++startPos; - } - - // reset width and i - lineWidth = 0; - i = startPos - 1; // -1 because loop will increment i - } - } - - // add the remaining part of the line - if (startPos < line.size()) { - lines.push_back(line.substr(startPos)); - } - } - - if(scaleMode == TextScaleMode::Element) { - float logicalPerPixelY = this->anchor.height / this->scaled.size.y; - float oldHeight = this->anchor.height; - this->anchor.height = lineHeight * logicalPerPixelY; - if(oldHeight != this->anchor.height) { - UpdatePosition(window, parent); - } - } else if(scaleMode == TextScaleMode::Font) { - float lineHeightPerFont = lineHeight / size; - size = this->scaled.size.y / lineHeightPerFont; - } else { - if constexpr(Scaling) { - lines.resize(ScalingBase::bufferHeight / lines.size()); - } else { - lines.resize(this->scaled.size.y / lines.size()); - } - } - } - - return lines; - } - - int utf8_decode(const char* s, int* bytes_consumed) { - unsigned char c = s[0]; - if (c < 0x80) { - *bytes_consumed = 1; - return c; - } else if ((c & 0xE0) == 0xC0) { - *bytes_consumed = 2; - return ((c & 0x1F) << 6) | (s[1] & 0x3F); - } else if ((c & 0xF0) == 0xE0) { - *bytes_consumed = 3; - return ((c & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F); - } else if ((c & 0xF8) == 0xF0) { - *bytes_consumed = 4; - return ((c & 0x07) << 18) | ((s[1] & 0x3F) << 12) | ((s[2] & 0x3F) << 6) | (s[3] & 0x3F); - } - *bytes_consumed = 1; - return 0xFFFD; // replacement char - } - - void RenderText(std::span lines, float size, Vector color, Font& font, TextAlignment alignment = TextAlignment::Left, std::uint32_t offsetX = 0, std::uint32_t offsetY = 0, OpaqueType opaque = OpaqueType::FullyOpaque) { - float scale = stbtt_ScaleForPixelHeight(&font.font, size); - int baseline = (int)(font.ascent * scale); - std::uint32_t lineHeight = (font.ascent - font.descent) * scale; - std::uint32_t currentY = baseline; - for(std::string_view line : lines) { - - std::uint32_t lineWidth = 0; - for (const char c : line) { - int advance, lsb; - stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb); - lineWidth += (int)(advance * scale); - } - - std::uint32_t x = 0; - switch (alignment) { - case TextAlignment::Left: - x = 0; - break; - case TextAlignment::Center: - x = (this->scaled.size.x - lineWidth) / 2; - break; - case TextAlignment::Right: - x = this->scaled.size.x - lineWidth; - break; - } - - const char* p = line.data(); - const char* end = p + line.size(); - - while (p < end) { - int bytes; - int codepoint = utf8_decode(p, &bytes); - p += bytes; - - int ax; - int lsb; - stbtt_GetCodepointHMetrics(&font.font, codepoint, &ax, &lsb); - - int c_x1, c_y1, c_x2, c_y2; - stbtt_GetCodepointBitmapBox(&font.font, codepoint, scale, scale, &c_x1, &c_y1, &c_x2, &c_y2); - - int w = c_x2 - c_x1; - int h = c_y2 - c_y1; - - std::vector bitmap(w * h); - stbtt_MakeCodepointBitmap(&font.font, bitmap.data(), w, h, w, scale, scale, codepoint); - - // Only render characters that fit within the scaled bounds - switch(opaque) { - case OpaqueType::FullyOpaque: { - for (int j = 0; j < h; j++) { - for (int i = 0; i < w; i++) { - int bufferX = x + i + c_x1 + offsetX; - int bufferY = currentY + j + c_y1 + offsetY; - - // Only draw pixels that are within our scaled buffer bounds - if constexpr(Scaling) { - if (bufferX >= 0 && bufferX < ScalingBase::bufferWidth && bufferY >= 0 && bufferY < ScalingBase::bufferHeight) { - ScalingBase::scalingBuffer[bufferY * ScalingBase::bufferWidth + bufferX] = {color.r, color.g, color.b, static_cast(bitmap[j * w + i])}; - } - } else { - if (bufferX >= 0 && bufferX < (int)this->scaled.size.x && bufferY >= 0 && bufferY < (int)this->scaled.size.y) { - this->buffer[bufferY * this->scaled.size.x + bufferX] = {color.r, color.g, color.b, static_cast(bitmap[j * w + i])}; - } - } - } - } - break; - } - case OpaqueType::SemiOpaque: - case OpaqueType::Transparent: { - for (int j = 0; j < h; j++) { - for (int i = 0; i < w; i++) { - int bufferX = x + i + c_x1 + offsetX; - int bufferY = currentY + j + c_y1 + offsetY; - - // Only draw pixels that are within our scaled buffer bounds - if constexpr(Scaling) { - if (bufferX >= 0 && bufferX < ScalingBase::bufferWidth && bufferY >= 0 && bufferY < ScalingBase::bufferHeight) { - ScalingBase::scalingBuffer[bufferY * ScalingBase::bufferWidth + bufferX] = {color.r, color.g, color.b, static_cast(bitmap[j * w + i])}; - } - } else { - if (bufferX >= 0 && bufferX < (int)this->scaled.size.x && bufferY >= 0 && bufferY < (int)this->scaled.size.y) { - if constexpr(std::same_as) { - std::uint8_t alpha = bitmap[j * w + i]; - - Vector dst = this->buffer[bufferY * this->scaled.size.x + bufferX]; - - float srcA = (alpha / 255.0f) * (color.a / 255.0f); - float dstA = dst.a / 255.0f; - - float oneMinusSrcA = 1.0f - color.a; - - float outA = srcA + dstA * (1.0f - srcA); - this->buffer[bufferY * this->scaled.size.x + bufferX] = Vector( - static_cast((color.r * srcA + dst.r * dstA * (1.0f - srcA)) / outA), - static_cast((color.g * srcA + dst.g * dstA * (1.0f - srcA)) / outA), - static_cast((color.b * srcA + dst.b * dstA * (1.0f - srcA)) / outA), - static_cast(outA * 255) - ); - } else if constexpr(std::same_as) { - std::uint8_t alpha = bitmap[j * w + i]; - _Float16 srcA = (_Float16(alpha)/_Float16(255.0f))*color.a; - Vector<_Float16, 4, Alignment> dst = this->buffer[bufferY * this->scaled.size.x + bufferX]; - - _Float16 outA = srcA + dst.a * (1.0f - srcA); - this->buffer[bufferY * this->scaled.size.x + bufferX] = Vector<_Float16, 4, Alignment>( - (color.r * srcA + dst.r * dst.a * (1.0f - srcA)), - (color.g * srcA + dst.g * dst.a * (1.0f - srcA)), - (color.b * srcA + dst.b * dst.a * (1.0f - srcA)), - outA - ); - } - } - } - } - } - break; - } - } - - x += (int)(ax * scale); - - if (p + 1 < end) { - int next; - x += (int)stbtt_GetGlyphKernAdvance(&font.font, codepoint, utf8_decode(p+1, &next)); - } - } - currentY += lineHeight; - } - } - }; -} \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-RenderingElement2DBase.cppm b/interfaces/Crafter.Graphics-RenderingElement2DBase.cppm deleted file mode 100644 index 37c80ec..0000000 --- a/interfaces/Crafter.Graphics-RenderingElement2DBase.cppm +++ /dev/null @@ -1,136 +0,0 @@ -/* -Crafter®.Graphics -Copyright (C) 2026 Catcrafts® -catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License version 3.0 as published by the Free Software Foundation; - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -export module Crafter.Graphics:RenderingElement2DBase; -import Crafter.Asset; -import Crafter.Math; -import std; -import :Transform2D; - -export namespace Crafter { - enum class TextAlignment { - Left, - Center, - Right - }; - - enum class TextVerticalAlignment { - Top, - Center, - Bottom - }; - - enum class TextOverflowMode { - Clip, - Wrap - }; - - enum class TextScaleMode { - None, - Font, - Element, - Buffer - }; - - template - struct RenderElement2DScalingOwning { - std::vector> scalingBuffer; - std::uint32_t bufferWidth; - std::uint32_t bufferHeight; - RenderElement2DScalingOwning() = default; - RenderElement2DScalingOwning(std::uint32_t bufferWidth, std::uint32_t bufferHeight) : scalingBuffer(bufferWidth*bufferHeight), bufferWidth(bufferWidth), bufferHeight(bufferHeight) { - - } - }; - - template - struct RenderElement2DScalingNonOwning { - Vector* scalingBuffer; - std::uint32_t bufferWidth; - std::uint32_t bufferHeight; - RenderElement2DScalingNonOwning() = default; - RenderElement2DScalingNonOwning(Vector* scalingBuffer, std::uint32_t bufferWidth, std::uint32_t bufferHeight) : scalingBuffer(scalingBuffer), bufferWidth(bufferWidth), bufferHeight(bufferHeight) { - - } - }; - - struct RenderElement2DRotating { - float rotation; - RenderElement2DRotating() = default; - RenderElement2DRotating(float rotation) : rotation(rotation) { - - } - }; - - - struct EmptyScalingBase {}; - struct EmptyRotatingBase {}; - - template - using ScalingBase = - std::conditional_t< - Scaling, - std::conditional_t, - RenderElement2DScalingNonOwning>, - EmptyScalingBase - >; - - template - using RotatingBase = - std::conditional_t< - Rotating, - RenderElement2DRotating, - EmptyRotatingBase - >; - - template - struct RenderingElement2DBase : Transform2D { - ScaleData2D oldScale[Frames]; - bool redraw[Frames]; - std::vector> buffer; - OpaqueType opaque; - RenderingElement2DBase(Anchor2D anchor) : Transform2D(anchor) { - for(std::uint8_t i = 0; i < Frames; i++) { - this->scaled.size.x = 0; - } - } - RenderingElement2DBase(Anchor2D anchor, OpaqueType opaque) : Transform2D(anchor), opaque(opaque) { - for(std::uint8_t i = 0; i < Frames; i++) { - this->scaled.size.x = 0; - } - } - void Redraw() { - for(std::uint8_t i = 0; i < Frames; i++) { - redraw[i] = true; - } - } - void CopyNearestNeighbor(Vector* dst, std::uint16_t dstSizeX, std::uint16_t dstScaledSizeX, std::uint16_t dstScaledSizeY, std::uint16_t offsetX, std::uint16_t offsetY) { - for (std::uint16_t y = 0; y < dstScaledSizeY; y++) { - std::uint16_t srcY = y * scaled.size.y / dstScaledSizeY; - std::uint16_t dstY = y + offsetY; - for (std::uint16_t x = 0; x < dstScaledSizeX; x++) { - std::uint16_t srcX = x * scaled.size.x / dstScaledSizeX; - std::uint16_t dstX = x + offsetX; - dst[dstY * dstSizeX + dstX] = buffer[srcY * this->scaled.size.x + srcX]; - } - } - } - }; -} \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-RenderingElement2DVulkan.cppm b/interfaces/Crafter.Graphics-RenderingElement2DVulkan.cppm deleted file mode 100644 index 81c5543..0000000 --- a/interfaces/Crafter.Graphics-RenderingElement2DVulkan.cppm +++ /dev/null @@ -1,477 +0,0 @@ -/* -Crafter®.Graphics -Copyright (C) 2026 Catcrafts® -catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License version 3.0 as published by the Free Software Foundation; - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -module; -#include "../lib/stb_truetype.h" -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN -#include -#endif -export module Crafter.Graphics:RenderingElement2DVulkan; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN -import Crafter.Asset; -import std; -import :Transform2D; -import :VulkanBuffer; -import :Types; -import :Window; -import :DescriptorHeapVulkan; -import :Font; - -export namespace Crafter { - struct RenderingElement2DVulkanBase : Transform2D { - std::uint16_t index; - std::uint16_t bufferX; - std::uint16_t bufferY; - std::array buffers; - RenderingElement2DVulkanBase(Anchor2D anchor) : Transform2D(anchor) { - - } - RenderingElement2DVulkanBase(Anchor2D anchor, std::uint16_t bufferX, std::uint16_t bufferY) : bufferX(bufferX), bufferY(bufferY), Transform2D(anchor) { - - } - RenderingElement2DVulkanBase(Anchor2D anchor, std::uint16_t bufferX, std::uint16_t bufferY, std::array&& buffers) : bufferX(bufferX), bufferY(bufferY), buffers(std::move(buffers)), Transform2D(anchor) { - - } - }; - - template - struct RenderingElement2DVulkan : RenderingElement2DVulkanBase { - RenderingElement2DVulkan(Anchor2D anchor) : RenderingElement2DVulkanBase(anchor) { - - } - RenderingElement2DVulkan(Anchor2D anchor, RendertargetBase& target, Transform2D& parent) requires(Owning) : RenderingElement2DVulkanBase(anchor) { - GetScale(target, parent); - this->bufferX = this->scaled.size.x; - this->bufferY = this->scaled.size.y; - if(Single) { - buffers[0] = new VulkanBuffer, Mapped>(); - static_cast, Mapped>*>(buffers[0])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY); - for(std::uint8_t i = 1; i < Window::numFrames; i++) { - buffers[i] = buffers[0]; - } - } else { - for(std::uint8_t i = 0; i < Window::numFrames; i++) { - buffers[i] = new VulkanBuffer, Mapped>(); - static_cast, Mapped>*>(buffers[i])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY); - } - } - } - - RenderingElement2DVulkan(Anchor2D anchor, std::uint16_t bufferX, std::uint16_t bufferY) requires(Owning) : RenderingElement2DVulkanBase(anchor, bufferX, bufferY) { - if constexpr(Single) { - buffers[0] = new VulkanBuffer, Mapped>(); - static_cast, Mapped>*>(buffers[0])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY); - for(std::uint8_t i = 1; i < Window::numFrames; i++) { - buffers[i] = buffers[0]; - } - } else { - for(std::uint8_t i = 0; i < Window::numFrames; i++) { - buffers[i] = new VulkanBuffer, Mapped>(); - static_cast, Mapped>*>(buffers[i])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY); - } - } - } - - RenderingElement2DVulkan(Anchor2D anchor, std::uint16_t bufferX, std::uint16_t bufferY, std::array&& buffers) requires(!Owning) : RenderingElement2DVulkanBase(anchor, bufferX, bufferY, std::move(buffers)) { - - } - - RenderingElement2DVulkan(Anchor2D anchor, const std::filesystem::path& assetPath) requires(Owning && Mapped) : RenderingElement2DVulkanBase(anchor) { - TextureAssetInfo info = TextureAsset<_Float16>::LoadInfo(assetPath); - this->bufferX = info.sizeX; - this->bufferY = info.sizeY; - if constexpr(Single) { - buffers[0] = new VulkanBuffer, Mapped>(); - static_cast, Mapped>*>(buffers[0])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY); - for(std::uint8_t i = 1; i < Window::numFrames; i++) { - buffers[i] = buffers[0]; - } - TextureAsset>::Load(assetPath, static_cast, Mapped>*>(buffers[0])->value, this->bufferX, this->bufferY); - } else { - for(std::uint8_t i = 0; i < Window::numFrames; i++) { - buffers[i] = new VulkanBuffer, Mapped>(); - static_cast, Mapped>*>(buffers[i])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY); - } - TextureAsset>::Load(assetPath, static_cast, Mapped>*>(buffers[0])->value, this->bufferX, this->bufferY); - for(std::uint8_t i = 1; i < Window::numFrames; i++) { - std::memcpy(static_cast, Mapped>*>(buffers[i])->value, static_cast, Mapped>*>(buffers[0])->value, this->bufferX * this->bufferY * sizeof(_Float16)); - } - } - } - - ~RenderingElement2DVulkan() { - if constexpr(Owning) { - if constexpr(Single) { - delete static_cast, Mapped>*>(buffers[0]); - } else { - for(VulkanBufferBase* buffer : buffers) { - delete static_cast, Mapped>*>(buffer); - } - } - } - } - - RenderingElement2DVulkan(RenderingElement2DVulkan&) = delete; - RenderingElement2DVulkan& operator=(RenderingElement2DVulkan&) = delete; - - void CreateBuffer(std::uint16_t bufferX, std::uint16_t bufferY) requires(Owning) { - this->bufferX = this->scaled.size.x; - this->bufferY = this->scaled.size.y; - if constexpr(Single) { - buffers[0] = new VulkanBuffer, Mapped>(); - static_cast, Mapped>*>(buffers[0])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY); - for(std::uint8_t i = 1; i < Window::numFrames; i++) { - buffers[i] = buffers[0]; - } - } else { - for(std::uint8_t i = 0; i < Window::numFrames; i++) { - buffers[i] = new VulkanBuffer, Mapped>(); - static_cast, Mapped>*>(buffers[i])->Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY); - } - } - } - - void ResizeBuffer(RendertargetVulkan& window, DescriptorHeapVulkan& descriptorHeap, std::uint16_t bufferOffset, std::uint16_t bufferX, std::uint16_t bufferY) requires(Owning) { - if constexpr(Single) { - static_cast, Mapped>*>(buffers[0])->Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY); - } else { - for(VulkanBufferBase* buffer : buffers) { - delete static_cast, Mapped>*>(buffer)->Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferX * bufferY); - } - } - this->bufferX = bufferX; - this->bufferY = bufferY; - for(std::uint8_t frame = 0; frame < Window::numFrames; frame++) { - RenderingElement2DVulkanTransformInfo* val = reinterpret_cast(reinterpret_cast(window.transformBuffer[frame].value) + sizeof(RenderingElement2DVulkanTransformInfo)); - val[index].bufferX = this->bufferX; - val[index].bufferY = this->bufferY; - window.transformBuffer[frame].FlushDevice(); - } - - VkHostAddressRangeEXT ranges[3] = { - { - .address = descriptorHeap.resourceHeap[0].value + bufferOffset + Device::descriptorHeapProperties.bufferDescriptorSize * index, - .size = Device::descriptorHeapProperties.bufferDescriptorSize - }, - { - .address = descriptorHeap.resourceHeap[1].value + bufferOffset + Device::descriptorHeapProperties.bufferDescriptorSize * index, - .size = Device::descriptorHeapProperties.bufferDescriptorSize - }, - { - .address = descriptorHeap.resourceHeap[2].value + bufferOffset + Device::descriptorHeapProperties.bufferDescriptorSize * index, - .size = Device::descriptorHeapProperties.bufferDescriptorSize - }, - }; - - VkDeviceAddressRangeKHR bufferRanges[3] { - { - .address = buffers[0]->address, - .size = buffers[0]->size - }, - { - .address = buffers[1]->address, - .size = buffers[1]->size - }, - { - .address = buffers[2]->address, - .size = buffers[2]->size - }, - }; - - VkResourceDescriptorInfoEXT infos[3] = { - { - .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, - .type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, - .data = { .pAddressRange = &bufferRanges[0]} - }, - { - .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, - .type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, - .data = { .pAddressRange = &bufferRanges[1]} - }, - { - .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT, - .type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, - .data = { .pAddressRange = &bufferRanges[2]} - }, - }; - - Device::vkWriteResourceDescriptorsEXT(Device::device, 3, infos, ranges); - for(std::uint8_t i = 0; i < Window::numFrames; i++) { - descriptorHeap.resourceHeap[i].FlushDevice(); - } - } - - void UpdatePosition(RendertargetBase& window2, Transform2D& parent) override { - RendertargetVulkan& window = static_cast(window2); - this->ScaleElement(parent); - RenderingElement2DVulkanTransformInfo* val = reinterpret_cast(reinterpret_cast(window.transformBuffer[window.frame].value) + sizeof(RenderingElement2DVulkanTransformInfo)); - val[index].scaled = this->scaled; - for(Transform2D* child : this->children) { - child->UpdatePosition(window, *this); - } - } - - void GetScale(RendertargetBase& window, Transform2D& parent) { - this->ScaleElement(parent); - for(Transform2D* child : this->children) { - child->UpdatePosition(window, *this); - } - } - - int utf8_decode(const char* s, int* bytes_consumed) { - unsigned char c = s[0]; - if (c < 0x80) { - *bytes_consumed = 1; - return c; - } else if ((c & 0xE0) == 0xC0) { - *bytes_consumed = 2; - return ((c & 0x1F) << 6) | (s[1] & 0x3F); - } else if ((c & 0xF0) == 0xE0) { - *bytes_consumed = 3; - return ((c & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F); - } else if ((c & 0xF8) == 0xF0) { - *bytes_consumed = 4; - return ((c & 0x07) << 18) | ((s[1] & 0x3F) << 12) | ((s[2] & 0x3F) << 6) | (s[3] & 0x3F); - } - *bytes_consumed = 1; - return 0xFFFD; // replacement char - } - - void RenderText(std::span lines, float size, Vector<_Float16, 4> color, Font& font, TextAlignment alignment = TextAlignment::Left, std::uint32_t offsetX = 0, std::uint32_t offsetY = 0, OpaqueType opaque = OpaqueType::FullyOpaque) requires(Mapped) { - float scale = stbtt_ScaleForPixelHeight(&font.font, size); - int baseline = (int)(font.ascent * scale); - std::uint32_t lineHeight = (font.ascent - font.descent) * scale; - std::uint32_t currentY = baseline; - for(std::string_view line : lines) { - - std::uint32_t lineWidth = 0; - for (const char c : line) { - int advance, lsb; - stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb); - lineWidth += (int)(advance * scale); - } - - std::uint32_t x = 0; - switch (alignment) { - case TextAlignment::Left: - x = 0; - break; - case TextAlignment::Center: - x = (this->scaled.size.x - lineWidth) / 2; - break; - case TextAlignment::Right: - x = this->scaled.size.x - lineWidth; - break; - } - - const char* p = line.data(); - const char* end = p + line.size(); - - while (p < end) { - int bytes; - int codepoint = utf8_decode(p, &bytes); - p += bytes; - - int ax; - int lsb; - stbtt_GetCodepointHMetrics(&font.font, codepoint, &ax, &lsb); - - int c_x1, c_y1, c_x2, c_y2; - stbtt_GetCodepointBitmapBox(&font.font, codepoint, scale, scale, &c_x1, &c_y1, &c_x2, &c_y2); - - int w = c_x2 - c_x1; - int h = c_y2 - c_y1; - - std::vector bitmap(w * h); - stbtt_MakeCodepointBitmap(&font.font, bitmap.data(), w, h, w, scale, scale, codepoint); - - // Only render characters that fit within the scaled bounds - switch(opaque) { - case OpaqueType::FullyOpaque: { - for (int j = 0; j < h; j++) { - for (int i = 0; i < w; i++) { - int bufferX = x + i + c_x1 + offsetX; - int bufferY = currentY + j + c_y1 + offsetY; - - if (bufferX >= 0 && bufferX < (int)this->bufferX && bufferY >= 0 && bufferY < (int)this->bufferY) { - for(std::uint8_t frame = 0; frame < Window::numFrames; frame++) { - static_cast, true>*>(buffers[frame])->value[bufferY * this->bufferX + bufferX] = {color.r, color.g, color.b, static_cast<_Float16>(bitmap[j * w + i])}; - } - } - } - } - break; - } - case OpaqueType::SemiOpaque: - case OpaqueType::Transparent: { - for (int j = 0; j < h; j++) { - for (int i = 0; i < w; i++) { - int bufferX = x + i + c_x1 + offsetX; - int bufferY = currentY + j + c_y1 + offsetY; - - if (bufferX >= 0 && bufferX < (int)this->bufferX && bufferY >= 0 && bufferY < (int)this->bufferY) { - std::uint8_t alpha = bitmap[j * w + i]; - _Float16 srcA = (_Float16(alpha)/_Float16(255.0f))*color.a; - for(std::uint8_t frame = 0; frame < Window::numFrames; frame++) { - Vector<_Float16, 4, 4> dst = static_cast, true>*>(buffers[frame])->value[bufferY * this->bufferX + bufferX]; - - _Float16 outA = srcA + dst.a * (1.0f - srcA); - static_cast, true>*>(buffers[frame])->value[bufferY * this->bufferX + bufferX] = Vector<_Float16, 4, 4>( - (color.r * srcA + dst.r * dst.a * (1.0f - srcA)), - (color.g * srcA + dst.g * dst.a * (1.0f - srcA)), - (color.b * srcA + dst.b * dst.a * (1.0f - srcA)), - outA - ); - } - } - } - } - break; - } - } - - x += (int)(ax * scale); - - if (p + 1 < end) { - int next; - x += (int)stbtt_GetGlyphKernAdvance(&font.font, codepoint, utf8_decode(p+1, &next)); - } - } - currentY += lineHeight; - } - } - - void RenderText(std::span lines, float size, Vector<_Float16, 4> color, Font& font, std::uint8_t frame, TextAlignment alignment = TextAlignment::Left, TextVerticalAlignment verticalAlignment = TextVerticalAlignment::Top, std::int32_t offsetX = 0, std::int32_t offsetY = 0, OpaqueType opaque = OpaqueType::FullyOpaque) requires(Mapped) { - float scale = stbtt_ScaleForPixelHeight(&font.font, size); - int baseline = (int)(font.ascent * scale); - std::uint32_t lineHeight = (font.ascent - font.descent) * scale; - std::uint32_t currentY = baseline; - - std::uint32_t ogOffsetX = offsetX; - std::uint32_t ogOffsetY = offsetY; - - for(std::string_view line : lines) { - offsetX = ogOffsetX; - offsetY = ogOffsetY; - - std::int32_t lineWidth = 0; - for (const char c : line) { - int advance, lsb; - stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb); - lineWidth += (int)(advance * scale); - } - - switch (alignment) { - case TextAlignment::Left: - break; - case TextAlignment::Center: - offsetX -= lineWidth / 2; - break; - case TextAlignment::Right: - offsetX -= lineWidth; - break; - } - - switch (verticalAlignment) { - case TextVerticalAlignment::Top: - break; - case TextVerticalAlignment::Center: - offsetY += (lineHeight / 2) - (size); - break; - case TextVerticalAlignment::Bottom: - offsetY += lineHeight; - break; - } - - const char* p = line.data(); - const char* end = p + line.size(); - - while (p < end) { - int bytes; - int codepoint = utf8_decode(p, &bytes); - p += bytes; - - int ax; - int lsb; - stbtt_GetCodepointHMetrics(&font.font, codepoint, &ax, &lsb); - - int c_x1, c_y1, c_x2, c_y2; - stbtt_GetCodepointBitmapBox(&font.font, codepoint, scale, scale, &c_x1, &c_y1, &c_x2, &c_y2); - - int w = c_x2 - c_x1; - int h = c_y2 - c_y1; - - std::vector bitmap(w * h); - stbtt_MakeCodepointBitmap(&font.font, bitmap.data(), w, h, w, scale, scale, codepoint); - - // Only render characters that fit within the scaled bounds - switch(opaque) { - case OpaqueType::FullyOpaque: { - for (int j = 0; j < h; j++) { - for (int i = 0; i < w; i++) { - int bufferX = offsetX + i + c_x1; - int bufferY = currentY + j + c_y1 + offsetY; - - if (bufferX >= 0 && bufferX < (int)this->bufferX && bufferY >= 0 && bufferY < (int)this->bufferY) { - static_cast, true>*>(buffers[frame])->value[bufferY * this->bufferX + bufferX] = {color.r, color.g, color.b, static_cast<_Float16>(bitmap[j * w + i])}; - } - } - } - break; - } - case OpaqueType::SemiOpaque: - case OpaqueType::Transparent: { - for (int j = 0; j < h; j++) { - for (int i = 0; i < w; i++) { - int bufferX = offsetX + i + c_x1; - int bufferY = currentY + j + c_y1 + offsetY; - - if (bufferX >= 0 && bufferX < (int)this->bufferX && bufferY >= 0 && bufferY < (int)this->bufferY) { - std::uint8_t alpha = bitmap[j * w + i]; - _Float16 srcA = (_Float16(alpha)/_Float16(255.0f))*color.a; - Vector<_Float16, 4, 4> dst = static_cast, true>*>(buffers[frame])->value[bufferY * this->bufferX + bufferX]; - - _Float16 outA = srcA + dst.a * (1.0f - srcA); - static_cast, true>*>(buffers[frame])->value[bufferY * this->bufferX + bufferX] = Vector<_Float16, 4, 4>( - (color.r * srcA + dst.r * dst.a * (1.0f - srcA)), - (color.g * srcA + dst.g * dst.a * (1.0f - srcA)), - (color.b * srcA + dst.b * dst.a * (1.0f - srcA)), - outA - ); - } - } - } - break; - } - } - - offsetX += (int)(ax * scale); - - if (p + 1 < end) { - int next; - offsetX += (int)stbtt_GetGlyphKernAdvance(&font.font, codepoint, utf8_decode(p+1, &next)); - } - } - currentY += lineHeight; - } - } - }; -} -#endif \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-RenderingElement3D.cppm b/interfaces/Crafter.Graphics-RenderingElement3D.cppm index 2b1814a..f56006c 100644 --- a/interfaces/Crafter.Graphics-RenderingElement3D.cppm +++ b/interfaces/Crafter.Graphics-RenderingElement3D.cppm @@ -18,11 +18,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN #include "vulkan/vulkan.h" -#endif export module Crafter.Graphics:RenderingElement3D; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN import std; import :Mesh; import :VulkanBuffer; @@ -45,5 +42,4 @@ export namespace Crafter { inline static TlasWithBuffer tlases[Window::numFrames]; static void BuildTLAS(VkCommandBuffer cmd, std::uint32_t index); }; -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-Rendertarget.cppm b/interfaces/Crafter.Graphics-Rendertarget.cppm deleted file mode 100644 index fb00861..0000000 --- a/interfaces/Crafter.Graphics-Rendertarget.cppm +++ /dev/null @@ -1,326 +0,0 @@ -/* -Crafter®.Graphics -Copyright (C) 2026 Catcrafts® -catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License version 3.0 as published by the Free Software Foundation; - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -module; -#include "../lib/stb_truetype.h" -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN -#include -#endif -export module Crafter.Graphics:Rendertarget; -import Crafter.Math; -import Crafter.Asset; -import std; -import :Types; -import :Transform2D; -import :RenderingElement2DBase; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN -import :Device; -import :VulkanBuffer; -#endif - -export namespace Crafter { - struct RendertargetBase { - #ifdef CRAFTER_TIMING - std::vector> renderTimings; - #endif - Transform2D transform; - std::uint16_t sizeX; - std::uint16_t sizeY; - RendertargetBase() = default; - RendertargetBase(std::uint16_t sizeX, std::uint16_t sizeY) : sizeX(sizeX), sizeY(sizeY), transform({0, 0, 1, 1, 0, 0, 0}){ - transform.scaled.size.x = sizeX; - transform.scaled.size.y = sizeY; - transform.scaled.position.x = 0; - transform.scaled.position.y = 0; - } - }; - - #ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN - struct RenderingElement2DVulkanBase; - - struct __attribute__((packed)) RenderingElement2DVulkanTransformInfo { - ScaleData2D scaled; // 0 - 8 bytes - std::uint16_t bufferX; // 8 - 2 bytes - std::uint16_t bufferY; // 10 - 2 bytes - //12 bytes total; - }; - - - struct DescriptorHeapVulkan; - struct RendertargetVulkan : RendertargetBase { - std::uint8_t frame; - std::vector elements; - VulkanBuffer transformBuffer[3]; - - RendertargetVulkan() = default; - RendertargetVulkan(std::uint16_t sizeX, std::uint16_t sizeY); - void UpdateElements(); - void CreateBuffer(std::uint8_t frame); - void ReorderBuffer(std::uint8_t frame); - void WriteDescriptors(std::span infos, std::span ranges, std::uint16_t start, std::uint32_t bufferOffset, DescriptorHeapVulkan& descriptorHeap); - void SetOrderResursive(Transform2D* elementTransform); - }; - #endif - - template - struct Rendertarget : RendertargetBase { - Vector* buffer[Frames]; - Rendertarget() = default; - Rendertarget(std::uint16_t sizeX, std::uint16_t sizeY) : RendertargetBase(sizeX, sizeY) { - - } - void RenderElement(Transform2D* elementTransform, std::uint8_t frame, std::vector&& dirtyRects) { - RenderingElement2DBase* element = dynamic_cast*>(elementTransform); - if(element) { - #ifdef CRAFTER_TIMING - auto start = std::chrono::high_resolution_clock::now(); - #endif - - if(element->scaled.size.x < 1 || element->scaled.size.y < 1) { - return; - } - - for(ClipRect dirty : dirtyRects) { - dirty.left = std::uint16_t(std::max(element->scaled.position.x, std::int16_t(dirty.left))); - dirty.top = std::uint16_t(std::max(element->scaled.position.y,std::int16_t(dirty.top))); - dirty.right = std::min(std::uint16_t(element->scaled.position.x+element->scaled.size.x), dirty.right); - dirty.bottom = std::min(std::uint16_t(element->scaled.position.y+element->scaled.size.y), dirty.bottom); - - if(dirty.right <= dirty.left || dirty.bottom <= dirty.top) { - continue; - } - - const Vector* src_buffer = element->buffer.data(); - std::uint16_t src_width = element->scaled.size.x; - std::uint16_t src_height = element->scaled.size.y; - - switch (element->opaque) { - case OpaqueType::FullyOpaque: { - for (std::uint16_t y = dirty.top; y < dirty.bottom; y++) { - std::uint16_t src_y = y - element->scaled.position.y; - std::uint16_t src_x = dirty.left - element->scaled.position.x; - std::memcpy(&this->buffer[frame][y * this->sizeX + dirty.left], &src_buffer[src_y * src_width + src_x], (dirty.right - dirty.left) * sizeof(Vector)); - } - break; - } - case OpaqueType::SemiOpaque: - case OpaqueType::Transparent: - if constexpr(std::same_as) { - for (std::uint16_t y = dirty.top; y < dirty.bottom; y++) { - std::uint16_t src_y = y - element->scaled.position.y; - std::uint16_t pixel_width = dirty.right - dirty.left; - - constexpr std::uint32_t simd_width = VectorF16<1, 1>::MaxElement / 4; - std::uint32_t rows = pixel_width / simd_width; - - for (std::uint32_t x = 0; x < rows; x++) { - std::uint16_t px = dirty.left + x * simd_width; - std::uint16_t src_x = px - element->scaled.position.x; - - VectorF16<4, simd_width> src(&src_buffer[src_y * src_width + src_x].v[0]); - VectorF16<4, simd_width> dst(&buffer[frame][y * this->sizeX + px].v[0]); - VectorF16<4, simd_width> oneMinusSrcA = VectorF16<4, simd_width>(1) - src.Shuffle<{{3, 3, 3, 3}}>(); - VectorF16<4, simd_width> result = VectorF16<4, simd_width>::MulitplyAdd(dst, oneMinusSrcA, src); - result.Store(&buffer[frame][y * this->sizeX + px].v[0]); - } - - std::uint32_t remainder = pixel_width - (rows * simd_width); - std::uint16_t remainder_start = dirty.left + rows * simd_width; - - for (std::uint8_t x = 0; x < remainder; x++) { - std::uint16_t px = remainder_start + x; - std::uint16_t src_x = px - element->scaled.position.x; - - Vector src = src_buffer[src_y * src_width + src_x]; - Vector dst = buffer[frame][y * this->sizeX + px]; - _Float16 oneMinusSrcA = (_Float16)1.0f - src.a; - - buffer[frame][y * this->sizeX + px] = Vector( - src.r + dst.r * oneMinusSrcA, - src.g + dst.g * oneMinusSrcA, - src.b + dst.b * oneMinusSrcA, - src.a + dst.a * oneMinusSrcA - ); - } - } - } else { - for (std::uint16_t y = dirty.top; y < dirty.bottom; y++) { - std::uint16_t src_y = y - element->scaled.position.y; - std::uint16_t src_x = dirty.left - element->scaled.position.x; - std::memcpy(&this->buffer[frame][y * this->sizeX + dirty.left], &src_buffer[src_y * src_width + src_x], (dirty.right - dirty.left) * sizeof(Vector)); - } - } - break; - } - } - #ifdef CRAFTER_TIMING - auto end = std::chrono::high_resolution_clock::now(); - renderTimings.push_back({element, element->scaled.size.x, element->scaled.size.y, end-start}); - #endif - } - std::sort(elementTransform->children.begin(), elementTransform->children.end(), [](Transform2D* a, Transform2D* b){ return a->anchor.z < b->anchor.z; }); - for(Transform2D* child : elementTransform->children) { - this->RenderElement(child, frame, std::move(dirtyRects)); - } - } - - void AddOldRects(Transform2D* elementTransform, std::uint8_t frame, std::vector& clipRects) { - RenderingElement2DBase* element = dynamic_cast*>(elementTransform); - if(element) { - if(element->scaled.position.x != element->oldScale[frame].position.x || element->scaled.position.y != element->oldScale[frame].position.y || element->scaled.size.x != element->oldScale[frame].size.x || element->scaled.size.y != element->oldScale[frame].size.y || element->redraw[frame]) { - clipRects.emplace_back(std::max(element->scaled.position.x, std::int16_t(0)), std::min(std::int16_t(element->scaled.position.x + element->scaled.size.x), std::int16_t(this->sizeX)), std::max(element->scaled.position.y, std::int16_t(0)), std::min(std::int16_t(element->scaled.position.y + element->scaled.size.y), std::int16_t(this->sizeY))); - clipRects.emplace_back(std::max(element->oldScale[frame].position.x, std::int16_t(0)), std::min(std::int16_t(element->oldScale[frame].position.x + element->oldScale[frame].size.x), std::int16_t(this->sizeX)), std::max(element->oldScale[frame].position.y, std::int16_t(0)), std::min(std::int16_t(element->oldScale[frame].position.y + element->oldScale[frame].size.y), std::int16_t(this->sizeY))); - element->oldScale[frame] = element->scaled; - element->redraw[frame] = false; - } else if(element->redraw[frame]) { - clipRects.emplace_back(std::max(element->scaled.position.x, std::int16_t(0)), std::min(std::int16_t(element->scaled.position.x + element->scaled.size.x), std::int16_t(this->sizeX)), std::max(element->scaled.position.y, std::int16_t(0)), std::min(std::int16_t(element->scaled.position.y + element->scaled.size.y), std::int16_t(this->sizeY))); - element->oldScale[frame] = element->scaled; - element->redraw[frame] = false; - } - } - for(Transform2D* child : elementTransform->children) { - AddOldRects(child, frame, clipRects); - } - } - - bool Render(std::uint8_t frame) { - std::sort(this->transform.children.begin(), this->transform.children.end(), [](Transform2D* a, Transform2D* b){ return a->anchor.z < b->anchor.z; }); - std::vector clipRects; - for(Transform2D* child : this->transform.children) { - AddOldRects(child, frame, clipRects); - } - - //std::vector newClip; - // for (std::uint32_t i = 0; i < dirtyRects.size(); i++) { - // ClipRect rect = dirtyRects[i]; - // for (std::uint32_t i2 = i + 1; i2 < dirtyRects.size(); i2++) { - // ClipRect existing = dirtyRects[i2]; - // if(rect.bottom >= existing.top && rect.top <= existing.top) { - // newClip.push_back({ - // .left = rect.left, - // .right = rect.right, - // .top = rect.top, - // .bottom = existing.top, - // }); - // //-| shape - // if(rect.right > existing.right) { - // newClip.push_back({ - // .left = existing.right, - // .right = rect.right, - // .top = existing.top, - // .bottom = existing.bottom, - // }); - // } - // //|- shape - // if(rect.left < existing.left) { - // newClip.push_back({ - // .left = rect.left, - // .right = existing.left, - // .top = existing.top, - // .bottom = existing.bottom, - // }); - // } - // //-| or |- shape where rect extends further down - // if(rect.bottom > existing.bottom) { - // newClip.push_back({ - // .left = rect.left, - // .right = rect.right, - // .top = existing.bottom, - // .bottom = rect.bottom, - // }); - // } - // goto inner; - // } - // if (rect.left <= existing.right && rect.right >= existing.left) { - // newClip.push_back({ - // .left = rect.left, - // .right = existing.left, - // .top = rect.top, - // .bottom = rect.bottom, - // }); - // if (rect.right > existing.right) { - // newClip.push_back({ - // .left = existing.right, - // .right = rect.right, - // .top = rect.top, - // .bottom = rect.bottom, - // }); - // } - // goto inner; - // } - // } - // newClip.push_back(rect); - // inner:; - // } - - //dirtyRects = std::move(newClip); - - // std::memset(buffer, 0, width*height*4); - - // std::cout << dirtyRects.size() << std::endl; - // // Color palette - // static const std::vector> colors = { - // {255, 0, 0, 255}, // red - // { 0, 255, 0, 255}, // green - // { 0, 0, 255, 255}, // blue - // {255, 255, 0, 255}, // yellow - // {255, 0, 255, 255}, // magenta - // { 0, 255, 255, 255}, // cyan - // }; - - // std::size_t rectIndex = 0; - - // for (const ClipRect& rect : dirtyRects) { - // const Vector& color = colors[rectIndex % colors.size()]; - - // std::cout << std::format( - // "ClipRect {}: [{}, {}, {}, {}] Color = RGBA({}, {}, {}, {})", - // rectIndex, - // rect.left, rect.top, rect.right, rect.bottom, - // color.r, color.g, color.b, color.a - // ) << std::endl; - - // for (std::int32_t y = rect.top; y < rect.bottom; ++y) { - // for (std::int32_t x = rect.left; x < rect.right; ++x) { - // buffer[y * width + x] = color; - // } - // } - - // ++rectIndex; - // } - - if (!clipRects.empty()) { - for (ClipRect rect : clipRects) { - for (std::int32_t y = rect.top; y < rect.bottom; y++) { - for (std::int32_t x = rect.left; x < rect.right; x++) { - this->buffer[frame][y * this->sizeX + x] = {0, 0, 0, 0}; - } - } - } - - for(Transform2D* child : this->transform.children) { - RenderElement(child, frame, std::move(clipRects)); - } - return true; - } else { - return false; - } - } - }; -} \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-SamplerVulkan.cppm b/interfaces/Crafter.Graphics-SamplerVulkan.cppm index ea688f1..79b5dec 100644 --- a/interfaces/Crafter.Graphics-SamplerVulkan.cppm +++ b/interfaces/Crafter.Graphics-SamplerVulkan.cppm @@ -19,9 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA module; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN #include "vulkan/vulkan.h" -#endif export module Crafter.Graphics:SamplerVulkan; import std; @@ -29,7 +27,6 @@ import :VulkanBuffer; import :ImageVulkan; export namespace Crafter { - #ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN template class SamplerVulkan { public: @@ -60,5 +57,4 @@ export namespace Crafter { }; } }; - #endif } \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-ShaderBindingTableVulkan.cppm b/interfaces/Crafter.Graphics-ShaderBindingTableVulkan.cppm index bff2f7b..e709775 100644 --- a/interfaces/Crafter.Graphics-ShaderBindingTableVulkan.cppm +++ b/interfaces/Crafter.Graphics-ShaderBindingTableVulkan.cppm @@ -18,11 +18,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN #include "vulkan/vulkan.h" -#endif export module Crafter.Graphics:ShaderBindingTableVulkan; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN import std; import :Device; import :VulkanBuffer; @@ -40,6 +37,4 @@ export namespace Crafter { } } }; -} - -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-ShaderVulkan.cppm b/interfaces/Crafter.Graphics-ShaderVulkan.cppm index 8f71926..7e0342c 100644 --- a/interfaces/Crafter.Graphics-ShaderVulkan.cppm +++ b/interfaces/Crafter.Graphics-ShaderVulkan.cppm @@ -18,11 +18,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN #include "vulkan/vulkan.h" -#endif export module Crafter.Graphics:ShaderVulkan; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN import std; import :Device; import :Types; @@ -62,6 +59,4 @@ export namespace Crafter { Device::CheckVkResult(vkCreateShaderModule(Device::device, &module_info, nullptr, &shader)); } }; -} - -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-Transform2D.cppm b/interfaces/Crafter.Graphics-Transform2D.cppm deleted file mode 100644 index f24bb6a..0000000 --- a/interfaces/Crafter.Graphics-Transform2D.cppm +++ /dev/null @@ -1,77 +0,0 @@ -/* -Crafter®.Graphics -Copyright (C) 2026 Catcrafts® -catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License version 3.0 as published by the Free Software Foundation; - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -export module Crafter.Graphics:Transform2D; -import std; -import :Types; -import :ForwardDeclarations; - -export namespace Crafter { - struct Anchor2D { - float x; - float y; - float width; - float height; - float offsetX; - float offsetY; - std::uint8_t z; - bool maintainAspectRatio; - Anchor2D() = default; - Anchor2D(float x, float y, float width, float height, float offsetX, float offsetY, std::uint8_t z, bool maintainAspectRatio = false); - }; - - struct Transform2D { - Anchor2D anchor; - ScaleData2D scaled; - std::vector children; - Transform2D() = default; - Transform2D(Anchor2D anchor) : anchor(anchor) { - - } - Transform2D(Transform2D&) = delete; - Transform2D(Transform2D&&) = delete; - Transform2D& operator=(Transform2D&) = delete; - virtual ~Transform2D() = default; - - virtual void UpdatePosition(RendertargetBase& window, Transform2D& parent) { - ScaleElement(parent); - for(Transform2D* child : children) { - child->UpdatePosition(window, *this); - } - } - - void ScaleElement(Transform2D& parent) { - if(anchor.maintainAspectRatio) { - if(parent.scaled.size.x > parent.scaled.size.y) { - scaled.size.x = anchor.width * parent.scaled.size.y; - scaled.size.y = anchor.height * parent.scaled.size.y; - } else { - scaled.size.x = anchor.width * parent.scaled.size.x; - scaled.size.y = anchor.height * parent.scaled.size.x; - } - } else { - scaled.size.x = anchor.width * parent.scaled.size.x; - scaled.size.y = anchor.height * parent.scaled.size.y; - } - - scaled.position.x = parent.scaled.position.x + (anchor.x * parent.scaled.size.x - anchor.offsetX * scaled.size.x); - scaled.position.y = parent.scaled.position.y + (anchor.y * parent.scaled.size.y - anchor.offsetY * scaled.size.y); - } - }; -} \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-Types.cppm b/interfaces/Crafter.Graphics-Types.cppm index 5ae0b8d..eaef82a 100644 --- a/interfaces/Crafter.Graphics-Types.cppm +++ b/interfaces/Crafter.Graphics-Types.cppm @@ -18,9 +18,7 @@ License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; -#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN #include "vulkan/vulkan.h" -#endif export module Crafter.Graphics:Types; import std; import Crafter.Math; @@ -247,10 +245,8 @@ export namespace Crafter { return std::tan(fov * std::numbers::pi / 360.0); } - #ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN struct DescriptorBinding { VkDescriptorType type; std::uint32_t slot; }; - #endif } diff --git a/interfaces/Crafter.Graphics-UI.cppm b/interfaces/Crafter.Graphics-UI.cppm new file mode 100644 index 0000000..7e1c53e --- /dev/null +++ b/interfaces/Crafter.Graphics-UI.cppm @@ -0,0 +1,30 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +export module Crafter.Graphics:UI; + +export import :UILength; +export import :UIWidget; +export import :UILayout; +export import :UIDrawList; +export import :UIAtlas; +export import :UIWidgets; +export import :UITheme; +export import :UIHit; +export import :UIRenderer; +export import :UIScene; diff --git a/interfaces/Crafter.Graphics-UIAtlas.cppm b/interfaces/Crafter.Graphics-UIAtlas.cppm new file mode 100644 index 0000000..05036eb --- /dev/null +++ b/interfaces/Crafter.Graphics-UIAtlas.cppm @@ -0,0 +1,100 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +module; +#include "vulkan/vulkan.h" +export module Crafter.Graphics:UIAtlas; +import std; +import :Font; +import :ImageVulkan; +import :Device; + +export namespace Crafter::UI { + // Per-glyph metrics. UVs are 0..1 in atlas space; on-screen sizes / + // offsets / advance are in *atlas pixels at the base size* and scale + // linearly with the requested font size at draw time. + struct Glyph { + float u0 = 0, v0 = 0; // top-left UV in the atlas + float u1 = 0, v1 = 0; // bottom-right UV in the atlas + float w = 0, h = 0; // glyph quad size in atlas px (= the bitmap size) + float xoff = 0, yoff = 0; // glyph bearing relative to baseline cursor + float advance = 0; // horizontal advance at base size, in atlas px + }; + + // Single-channel SDF atlas. Glyphs are rasterised with stb_truetype's + // GetGlyphSDF at a fixed `kBaseSize` resolution and packed via a simple + // shelf allocator. Drawing scales the glyph quad linearly; the shader + // resolves edge AA via screen-space derivatives, so a single atlas + // serves all sizes and DPI scales without re-bake. + class FontAtlas { + public: + // Build-time constants. Tweak in one place if needed; values picked + // to give crisp text from ~10pt to ~96pt and leave headroom in the + // SDF distance band so smoothstep is in the linear regime. + static constexpr int kAtlasSize = 1024; + static constexpr float kBaseSize = 32.0f; // pixel-height at which we rasterise + static constexpr int kPadding = 4; // distance-field padding around each glyph + static constexpr int kOnEdgeValue = 128; // 8-bit value mapped to "0 distance" + static constexpr float kPixelDistScale = 32.0f; // how many distance units per pixel — wider = softer AA range + + ImageVulkan image; + bool dirty = false; // staging has unflushed writes + + // Allocate the GPU image and zero-clear it. Must be called once + // with a one-shot init command buffer. + void Initialize(VkCommandBuffer cmd); + + // Rasterise + pack the glyph if it isn't cached yet. Returns + // false only if the atlas is out of space (V2: grow). After a + // successful Ensure the bitmap lives in `image.buffer.value` and + // `dirty` is true; call Update(cmd) before reading on the GPU. + bool Ensure(Font& font, std::uint32_t codepoint); + + // Lookup is cheap (hash-table). Returns nullptr if the glyph + // hasn't been Ensured. + const Glyph* Lookup(Font& font, std::uint32_t codepoint) const; + + // If `dirty`, flushes staging into the GPU image and transitions + // it back to SHADER_READ_ONLY_OPTIMAL. No-op if not dirty. + void Update(VkCommandBuffer cmd); + + private: + // Shelf packer state. + struct Shelf { int y = 0; int height = 0; int cursorX = 0; }; + std::vector shelves_; + int nextShelfY_ = 0; + + // (font*, codepoint) → Glyph cache. + struct Key { + const Font* font; + std::uint32_t cp; + bool operator==(const Key&) const = default; + }; + struct KeyHash { + std::size_t operator()(const Key& k) const noexcept { + std::size_t h1 = std::hash{}(k.font); + std::size_t h2 = std::hash{}(k.cp); + return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2)); + } + }; + std::unordered_map cache_; + + // Place a wxh glyph; returns true + writes top-left into outX/outY. + bool ShelfPlace(int w, int h, int& outX, int& outY); + }; +} diff --git a/interfaces/Crafter.Graphics-UIDrawList.cppm b/interfaces/Crafter.Graphics-UIDrawList.cppm new file mode 100644 index 0000000..0a0da01 --- /dev/null +++ b/interfaces/Crafter.Graphics-UIDrawList.cppm @@ -0,0 +1,167 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +export module Crafter.Graphics:UIDrawList; +import std; +import :UILength; +import :UIWidget; + +export namespace Crafter::UI { + class FontAtlas; // forward decl (full def in :UIAtlas) + + // Item type tags. Must match the shader-side constants exactly. + enum class ItemType : std::uint32_t { + Rect = 0, + RoundRect = 1, + Glyph = 2, + Image = 3, + ClipPush = 5, + ClipPop = 6, + }; + + // GPU-bound draw item. Layout matches the shader's UIItem struct under + // GL_EXT_scalar_block_layout (no std140/std430 padding). Keep this in + // sync with shaders/ui.comp.glsl. + // + // Field meanings by ItemType: + // Rect: posPx, sizePx, color (alpha-premultiplied). + // RoundRect: same as Rect + cornerRadiusPx. + // Glyph: posPx/sizePx = on-screen quad; uvRect = atlas region; + // color tints the SDF sample; cornerRadiusPx unused. + // Image: posPx/sizePx = quad; uvRect = source rect (0..1); + // imageIdx = bindless slot offset; color tints. + // ClipPush: posPx/sizePx = clip rect to push (intersected with current). + // ClipPop: fields ignored. + struct UIItem { + std::uint32_t type; // ItemType + std::uint32_t flags; + float posPx[2]; + float sizePx[2]; + float color[4]; + float colorB[4]; + float uvRect[4]; + std::uint32_t imageIdx; + std::uint32_t cornerRadiusPx; + float reserved[2]; + }; + static_assert(sizeof(UIItem) == 88, "UIItem size must match shader-side struct"); + + // CPU-side accumulator. Widgets call `Add(...)` (or convenience helpers) + // during their Emit pass; the renderer copies the resulting buffer into + // the per-frame mapped SSBO and dispatches the compute shader. + class DrawList { + public: + std::vector items; + + // Set by the renderer before EmitTree(). Widgets that draw text or + // images consult these — without an atlas, glyph emission is a + // no-op (useful for layout-only debug dumps). + FontAtlas* atlas = nullptr; + std::uint32_t bindlessBaseHeapIdx = 0; // base heap slot for Image widgets + float scale = 1.0f; // device scale (mirrors LayoutContext::scale) + + void Reset() { items.clear(); } + + void Add(const UIItem& it) { items.push_back(it); } + + // Convenience constructors for common items. These keep widget + // Emit code short and self-documenting. + void AddRect(Rect r, Color c) { + UIItem it{}; + it.type = static_cast(ItemType::Rect); + it.posPx[0] = r.x; it.posPx[1] = r.y; + it.sizePx[0] = r.w; it.sizePx[1] = r.h; + // Premultiply alpha so the shader's "OVER" operator works without + // a per-pixel multiply. + it.color[0] = c.r * c.a; + it.color[1] = c.g * c.a; + it.color[2] = c.b * c.a; + it.color[3] = c.a; + items.push_back(it); + } + + void AddRoundRect(Rect r, Color c, float radiusPx) { + UIItem it{}; + it.type = static_cast(ItemType::RoundRect); + it.posPx[0] = r.x; it.posPx[1] = r.y; + it.sizePx[0] = r.w; it.sizePx[1] = r.h; + it.color[0] = c.r * c.a; + it.color[1] = c.g * c.a; + it.color[2] = c.b * c.a; + it.color[3] = c.a; + it.cornerRadiusPx = static_cast(radiusPx); + items.push_back(it); + } + + // Glyph item: `quad` is the glyph's on-screen rect, `atlasUV` is + // its (x, y, w, h) region in 0..1 atlas-UV space. + void AddGlyph(Rect quad, Color color, std::array atlasUV) { + UIItem it{}; + it.type = static_cast(ItemType::Glyph); + it.posPx[0] = quad.x; it.posPx[1] = quad.y; + it.sizePx[0] = quad.w; it.sizePx[1] = quad.h; + it.color[0] = color.r * color.a; + it.color[1] = color.g * color.a; + it.color[2] = color.b * color.a; + it.color[3] = color.a; + it.uvRect[0] = atlasUV[0]; it.uvRect[1] = atlasUV[1]; + it.uvRect[2] = atlasUV[2]; it.uvRect[3] = atlasUV[3]; + items.push_back(it); + } + + // Image item: `imageHeapOffset` is added to the renderer's + // bindless-base slot at draw time to find the right descriptor. + void AddImage(Rect quad, Color tint, std::uint32_t imageHeapOffset, + std::array sourceUV = {0, 0, 1, 1}) { + UIItem it{}; + it.type = static_cast(ItemType::Image); + it.posPx[0] = quad.x; it.posPx[1] = quad.y; + it.sizePx[0] = quad.w; it.sizePx[1] = quad.h; + it.color[0] = tint.r * tint.a; + it.color[1] = tint.g * tint.a; + it.color[2] = tint.b * tint.a; + it.color[3] = tint.a; + it.uvRect[0] = sourceUV[0]; it.uvRect[1] = sourceUV[1]; + it.uvRect[2] = sourceUV[2]; it.uvRect[3] = sourceUV[3]; + it.imageIdx = imageHeapOffset; + items.push_back(it); + } + + // Clip stack — emit a ClipPush at the start of the clipped region + // and a matching ClipPop at the end. The shader maintains a small + // fixed-size stack and intersects pushes with the existing clip. + void PushClip(Rect r) { + UIItem it{}; + it.type = static_cast(ItemType::ClipPush); + it.posPx[0] = r.x; it.posPx[1] = r.y; + it.sizePx[0] = r.w; it.sizePx[1] = r.h; + items.push_back(it); + } + + void PopClip() { + UIItem it{}; + it.type = static_cast(ItemType::ClipPop); + items.push_back(it); + } + }; + + // Walk the laid-out tree and emit every widget's items. + inline void EmitTree(const Widget& root, DrawList& dl) { + root.Emit(dl); + } +} diff --git a/interfaces/Crafter.Graphics-UIHit.cppm b/interfaces/Crafter.Graphics-UIHit.cppm new file mode 100644 index 0000000..272f4c7 --- /dev/null +++ b/interfaces/Crafter.Graphics-UIHit.cppm @@ -0,0 +1,50 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +export module Crafter.Graphics:UIHit; +import std; +import :UILength; +import :UIWidget; + +export namespace Crafter::UI { + // Find the topmost widget whose computedRect contains (x, y). + // Children are visited in reverse order so later children (drawn on + // top) win ties. Returns nullptr if the point is outside `root`. + inline Widget* HitTest(Widget& root, float x, float y) { + if (!root.computedRect.Contains(x, y)) return nullptr; + + // Search children in reverse — the last-added child is on top in + // our draw order, so it wins overlapping hits. + for (auto it = root.children_.rbegin(); it != root.children_.rend(); ++it) { + if (Widget* hit = HitTest(**it, x, y); hit) return hit; + } + return &root; + } + + // Dispatch a click at (x, y) to the topmost widget under the cursor, + // bubbling to ancestors until one returns true (handled). The default + // Widget::OnMouseClick returns false, so leaf widgets that don't care + // automatically defer to their parents. + inline void DispatchClick(Widget& root, float x, float y) { + Widget* target = HitTest(root, x, y); + while (target) { + if (target->OnMouseClick(x, y)) return; + target = target->parent; + } + } +} diff --git a/interfaces/Crafter.Graphics-UILayout.cppm b/interfaces/Crafter.Graphics-UILayout.cppm new file mode 100644 index 0000000..124bcaf --- /dev/null +++ b/interfaces/Crafter.Graphics-UILayout.cppm @@ -0,0 +1,73 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +export module Crafter.Graphics:UILayout; +import std; +import :UILength; +import :UIWidget; + +export namespace Crafter::UI { + // Convert a Length to device pixels. `parentExtent` is the parent's + // available extent on the same axis (already in device px). `autoFn` + // produces the size to use for `Auto` and `Frac` modes — for Auto this + // is the desired-content size, for Frac it's the same fallback (Frac + // is meaningful only inside a stack container, which resolves it + // separately; everywhere else it's just "fill what's available", same + // as Auto). + template + constexpr float ResolveLength(Length len, float parentExtent, float scale, AutoFn&& autoFn) { + switch (len.mode) { + case Length::Mode::Px: return len.value * scale; + case Length::Mode::Pct: return len.value * 0.01f * parentExtent; + case Length::Mode::Auto: return static_cast(autoFn()); + case Length::Mode::Frac: return static_cast(autoFn()); + } + return 0.0f; + } + + // Edges resolved into device pixels (no Length involvement; Edges are + // already plain floats in logical px). + struct EdgesPx { + float top = 0, right = 0, bottom = 0, left = 0; + constexpr float Horiz() const { return left + right; } + constexpr float Vert() const { return top + bottom; } + }; + + constexpr EdgesPx ResolveEdges(Edges e, float scale) { + return { e.top * scale, e.right * scale, e.bottom * scale, e.left * scale }; + } + + // Rect minus padding — yields the content rect. + constexpr Rect ShrinkBy(Rect r, EdgesPx p) { + return { + r.x + p.left, + r.y + p.top, + std::max(0.0f, r.w - p.Horiz()), + std::max(0.0f, r.h - p.Vert()), + }; + } + + // Run the two-pass measure/arrange on a root widget bound to a surface + // of `surfacePx` device pixels at `scale`. The root receives the full + // surface as its arrange rect. + inline void RunLayout(Widget& root, Size surfacePx, float scale) { + LayoutContext ctx{ .scale = scale, .surfaceSize = surfacePx }; + root.Measure(surfacePx, ctx); + root.Arrange({0, 0, surfacePx.w, surfacePx.h}, ctx); + } +} diff --git a/interfaces/Crafter.Graphics-UILength.cppm b/interfaces/Crafter.Graphics-UILength.cppm new file mode 100644 index 0000000..c910818 --- /dev/null +++ b/interfaces/Crafter.Graphics-UILength.cppm @@ -0,0 +1,98 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +export module Crafter.Graphics:UILength; +import std; + +export namespace Crafter::UI { + struct Length { + enum class Mode : std::uint8_t { Px, Pct, Auto, Frac }; + Mode mode = Mode::Auto; + float value = 0.0f; + + static constexpr Length Px(float v) { return {Mode::Px, v}; } + static constexpr Length Pct(float v) { return {Mode::Pct, v}; } + static constexpr Length Auto() { return {Mode::Auto, 0.0f}; } + static constexpr Length Frac(float v) { return {Mode::Frac, v}; } + }; + + enum class Anchor : std::uint8_t { + TopLeft, Top, TopRight, + Left, Center, Right, + BottomLeft, Bottom, BottomRight, + }; + + struct Edges { + float top = 0, right = 0, bottom = 0, left = 0; + + constexpr Edges() = default; + constexpr explicit Edges(float all) : top(all), right(all), bottom(all), left(all) {} + constexpr Edges(float vert, float horiz) : top(vert), right(horiz), bottom(vert), left(horiz) {} + constexpr Edges(float t, float r, float b, float l) : top(t), right(r), bottom(b), left(l) {} + }; + + struct Color { + float r = 0, g = 0, b = 0, a = 1; + + constexpr Color() = default; + constexpr Color(float r, float g, float b, float a = 1.0f) : r(r), g(g), b(b), a(a) {} + + // 0xRRGGBB, alpha = 1.0 + static constexpr Color rgb(std::uint32_t hex) { + return { + ((hex >> 16) & 0xFF) / 255.0f, + ((hex >> 8) & 0xFF) / 255.0f, + ( hex & 0xFF) / 255.0f, + 1.0f + }; + } + // 0xRRGGBBAA + static constexpr Color rgba(std::uint32_t hex) { + return { + ((hex >> 24) & 0xFF) / 255.0f, + ((hex >> 16) & 0xFF) / 255.0f, + ((hex >> 8) & 0xFF) / 255.0f, + ( hex & 0xFF) / 255.0f + }; + } + }; + + struct Size { + float w = 0, h = 0; + }; + + struct Rect { + float x = 0, y = 0, w = 0, h = 0; + + constexpr float Right() const { return x + w; } + constexpr float Bottom() const { return y + h; } + + constexpr bool Contains(float px, float py) const { + return px >= x && px < x + w && py >= y && py < y + h; + } + + constexpr Rect Intersect(Rect o) const { + float l = std::max(x, o.x); + float t = std::max(y, o.y); + float r = std::min(Right(), o.Right()); + float b = std::min(Bottom(), o.Bottom()); + if (r <= l || b <= t) return {0, 0, 0, 0}; + return {l, t, r - l, b - t}; + } + }; +} diff --git a/interfaces/Crafter.Graphics-UIRenderer.cppm b/interfaces/Crafter.Graphics-UIRenderer.cppm new file mode 100644 index 0000000..8561b88 --- /dev/null +++ b/interfaces/Crafter.Graphics-UIRenderer.cppm @@ -0,0 +1,110 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +module; +#include "vulkan/vulkan.h" +export module Crafter.Graphics:UIRenderer; +import std; +import :Device; +import :Window; +import :RenderPass; +import :DescriptorHeapVulkan; +import :VulkanBuffer; +import :SamplerVulkan; +import :ShaderVulkan; +import :ImageVulkan; +import :UIDrawList; +import :UIAtlas; + +export namespace Crafter::UI { + // The compute-pass-side renderer. Owns the compute pipeline, per-frame + // item buffers, the SDF glyph atlas, and the descriptor-heap slot + // allocations. Implements RenderPass so it plugs into Window::passes. + // + // Lifecycle: + // - Initialize(window, shaderPath) — once, after the window has a + // descriptor heap. Allocates slots, creates pipeline, atlas image. + // - SetItems(span) per frame, before Window::Render runs. + // - Record(...) — invoked by Window::Render's pass loop. + class UIRenderer : public RenderPass { + public: + // Defaulted bindless slot capacity — covers most game UIs without + // descriptor heap pressure. Override in Initialize. + static constexpr std::uint16_t kDefaultBindlessImageCount = 256; + + FontAtlas atlas; + + // Initialize. `initCmd` must be a command buffer in recording + // state — used to transition the atlas image. Window must already + // have a non-null descriptorHeap with enough free slots for + // (numFrames + 1 + bindlessImageCount) images, numFrames buffers, + // and 1 sampler. + void Initialize(Window& window, + VkCommandBuffer initCmd, + const std::filesystem::path& spvPath = "ui.comp.spv", + std::uint16_t bindlessImageCount = kDefaultBindlessImageCount); + + // Stage `items` into the next-frame mapped buffer. Must be called + // BEFORE Window::Render so the buffer is flushed before the + // dispatch reads it. + void SetItems(std::span items); + + // RenderPass impl — invoked from Window::Render's pass loop. + void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override; + + // Heap slot accessors — UIScene reads these to populate DrawList. + std::uint32_t BindlessBaseHeapIdx() const { return bindlessBase_; } + FontAtlas& Atlas() { return atlas; } + + // The frame currently being staged. Window::Render advances + // `currentBuffer` before passes record; SetItems writes to + // (currentBuffer + 1) so the previous frame's buffer is still in + // flight on the GPU. For V1 we ride on Window's currentBuffer + // directly since vkQueueWaitIdle gates each frame. + std::uint32_t pendingItemCount = 0; + + private: + Window* window_ = nullptr; + + VkPipeline pipeline_ = VK_NULL_HANDLE; + + VulkanBuffer itemBufs_[Window::numFrames]; + std::uint16_t itemCapacity_ = 0; + + // Heap slot allocations (resource heap unless noted). + std::uint16_t outImageBase_ = 0; // images[outImageBase_ + frame] = swapchain view + std::uint16_t atlasImageSlot_ = 0; // sampled atlas image slot + std::uint16_t bindlessBase_ = 0; // first user-image slot + std::uint16_t bindlessCount_ = 0; // user-image slot count + std::uint16_t itemBufBase_ = 0; // SSBO slot base; per-frame at base + i + std::uint16_t linearSamplerSlot_ = 0; // sampler heap + + // Stable VkImageViewCreateInfo for the atlas — descriptor heap + // writes need a pointer to one, so we keep it on the renderer. + VkImageViewCreateInfo atlasViewCreateInfo_{}; + + // Helpers. + void GrowItemBuffersIfNeeded(std::uint32_t needed); + void WriteSwapchainDescriptors(); + void WriteAtlasDescriptor(); + void WriteSamplerDescriptors(); + void WriteItemBufferDescriptors(); + void CreatePipeline(const std::filesystem::path& spvPath); + void CreateLinearSampler(); + }; +} diff --git a/interfaces/Crafter.Graphics-UIScene.cppm b/interfaces/Crafter.Graphics-UIScene.cppm new file mode 100644 index 0000000..071ac25 --- /dev/null +++ b/interfaces/Crafter.Graphics-UIScene.cppm @@ -0,0 +1,111 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +module; +#include "vulkan/vulkan.h" +export module Crafter.Graphics:UIScene; +import std; +import :Window; +import :Types; +import :DescriptorHeapVulkan; +import Crafter.Event; +import :UIWidget; +import :UIWidgets; +import :UILayout; +import :UIDrawList; +import :UIRenderer; +import :UIHit; + +export namespace Crafter::UI { + // The single user-facing wrapper that ties the widget tree to the + // window's frame loop. Owns the renderer + draw list, optionally + // owns a default descriptor heap, registers itself as a RenderPass on + // the window, and routes mouse clicks through the hit tester. + // + // Typical usage: + // + // Crafter::Window window(1280, 720, "Demo"); + // window.StartInit(); window.FinishInit(); + // Crafter::UI::UIScene scene; + // scene.Initialize(window); + // scene.Root(VStack{}.children( + // Button{"Play"}.onClick([&]{ ... }), + // ... + // )); + // window.Render(); + // window.StartUpdate(); // continuous rendering + // window.StartSync(); + class UIScene { + public: + UIRenderer renderer; + DrawList drawList; + + UIScene() = default; + UIScene(const UIScene&) = delete; + UIScene& operator=(const UIScene&) = delete; + ~UIScene(); + + void Initialize(Window& window, + const std::filesystem::path& spvPath = "ui.comp.spv"); + + // Replace the widget tree. Takes ownership and clears focus + // (the previously-focused widget will be destroyed with the + // old tree). + template + requires std::derived_from, Widget> + void Root(W&& root) { + SetFocus(nullptr); + using T = std::remove_cvref_t; + auto p = std::make_unique(std::move(root)); + p->parent = nullptr; + root_ = std::move(p); + } + + // Focus management. Calling with nullptr blurs whatever was focused. + void SetFocus(Widget* w); + Widget* Focused() const { return focused_; } + + // Optional surface-clearing colour. The swapchain image is + // STORAGE-only (can't be vkCmdClearColorImage'd), so we paint a + // full-surface rect at the start of every frame's draw list when + // this is set. + UIScene& background(Color c) { background_ = c; return *this; } + + Widget* root() { return root_.get(); } + const Widget* root() const { return root_.get(); } + + private: + Window* window_ = nullptr; + std::unique_ptr root_; + std::optional background_; + + // Auto-allocated heap for UI-only apps. If the user already attached + // a heap to the window, we leave it alone and don't own one. + DescriptorHeapVulkan ownedHeap_; + bool ownsHeap_ = false; + + std::unique_ptr> mouseListener_; + std::unique_ptr> updateListener_; + std::unique_ptr> textListener_; + std::unique_ptr> keyListener_; + Widget* focused_ = nullptr; + + float WindowScale() const; + void RebuildFrame(); + }; +} diff --git a/interfaces/Crafter.Graphics-UITheme.cppm b/interfaces/Crafter.Graphics-UITheme.cppm new file mode 100644 index 0000000..466fe3e --- /dev/null +++ b/interfaces/Crafter.Graphics-UITheme.cppm @@ -0,0 +1,80 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +export module Crafter.Graphics:UITheme; +import std; +import :UILength; +import :UIWidgets; +import :Font; + +export namespace Crafter::UI { + // Flat theme — named slots, no cascading. Users keep one Theme value + // (typically as a member of their scene) and reference its slots on + // each widget via `.style(theme.primary)` etc. No automatic + // propagation: per-widget overrides win. + struct Theme { + // Buttons + ButtonStyle primary; // default action ("Save", "Play") + ButtonStyle secondary; // neutral action ("Cancel", "Back") + ButtonStyle danger; // destructive ("Delete", "Quit") + ButtonStyle disabled; // greyed out + + // Inputs + InputFieldStyle input; + + // Generic palette + Color text {0.95f, 0.95f, 0.95f, 1.0f}; + Color textMuted {0.65f, 0.65f, 0.65f, 1.0f}; + Color panel {0.10f, 0.11f, 0.13f, 1.0f}; + Color panelElevated {0.14f, 0.15f, 0.17f, 1.0f}; + Color border {0.30f, 0.30f, 0.30f, 1.0f}; + Color focusRing {0.40f, 0.70f, 1.00f, 1.0f}; + + // Typography. Optional: not every widget requires the theme's font; + // builder methods can override per-instance. + Font* defaultFont = nullptr; + float defaultFontSize = 16.0f; + }; + + namespace themes { + // A balanced dark-mode theme — matches the kind of game-menu palette + // 3DForts uses. Users can copy + tweak. + inline Theme default_dark() { + Theme t; + + t.primary.background = Color{0.22f, 0.45f, 0.78f, 1.0f}; + t.primary.hoverBackground = Color{0.28f, 0.55f, 0.92f, 1.0f}; + t.primary.pressedBackground = Color{0.16f, 0.36f, 0.66f, 1.0f}; + t.primary.textColor = Color{1.0f, 1.0f, 1.0f, 1.0f}; + + t.secondary.background = Color{0.20f, 0.20f, 0.20f, 1.0f}; + t.secondary.hoverBackground = Color{0.28f, 0.28f, 0.28f, 1.0f}; + t.secondary.pressedBackground = Color{0.14f, 0.14f, 0.14f, 1.0f}; + + t.danger.background = Color{0.62f, 0.20f, 0.20f, 1.0f}; + t.danger.hoverBackground = Color{0.78f, 0.26f, 0.26f, 1.0f}; + t.danger.pressedBackground = Color{0.46f, 0.14f, 0.14f, 1.0f}; + t.danger.textColor = Color{1.0f, 0.95f, 0.95f, 1.0f}; + + t.disabled.background = Color{0.15f, 0.15f, 0.15f, 1.0f}; + t.disabled.textColor = Color{0.50f, 0.50f, 0.50f, 1.0f}; + + return t; + } + } +} diff --git a/interfaces/Crafter.Graphics-UIWidget.cppm b/interfaces/Crafter.Graphics-UIWidget.cppm new file mode 100644 index 0000000..8c3a5d0 --- /dev/null +++ b/interfaces/Crafter.Graphics-UIWidget.cppm @@ -0,0 +1,189 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +export module Crafter.Graphics:UIWidget; +import std; +import :UILength; +import :Types; // for CrafterKeys + +export namespace Crafter::UI { + struct DrawList; // forward decl (full def in :UIDrawList) + + // Threaded through layout. Holds anything every widget needs from the + // surrounding scene at layout time (DPI scale, root surface size, …). + struct LayoutContext { + float scale = 1.0f; // device scale (Window::scale) + Size surfaceSize{}; // root surface in device px + }; + + struct Widget { + Length width_ = Length::Auto(); + Length height_ = Length::Auto(); + Edges padding_; + Edges margin_; + std::optional anchor_; + + // Layout output, filled by the engine. + Rect computedRect{}; + Size desiredSize{}; + bool dirty = true; + + // Tree. + Widget* parent = nullptr; + std::vector> children_; + + Widget() = default; + Widget(const Widget&) = delete; + Widget& operator=(const Widget&) = delete; + Widget(Widget&&) = default; + Widget& operator=(Widget&&) = default; + virtual ~Widget() = default; + + // Layout protocol — Measure returns the size this widget wants given + // the available space; engine then calls Arrange with the final rect. + virtual Size Measure(Size avail, const LayoutContext& ctx) = 0; + virtual void Arrange(Rect rect, const LayoutContext& ctx) = 0; + + // Interaction protocol — return true if the event was handled and + // should NOT bubble to the parent. Default: not handled. + virtual bool OnMouseClick(float /*x*/, float /*y*/) { return false; } + + // Focus protocol. Widgets that opt in (e.g. InputField) return + // true from IsFocusable; UIScene tracks the currently-focused + // widget and routes keyboard events to it. + virtual bool IsFocusable() const { return false; } + virtual void OnFocus() {} + virtual void OnBlur() {} + + // Keyboard input. Both default to "not handled". OnTextInput + // receives a UTF-8 substring (typically one codepoint per call). + // OnKeyDown receives non-character keys (Backspace, arrows, …). + virtual bool OnTextInput(std::string_view /*text*/) { return false; } + virtual bool OnKeyDown (CrafterKeys /*key*/) { return false; } + + // Drawing protocol — emit GPU-bound draw items into `dl`. Default + // implementation is "container behaviour": just descend into + // children. Leaf widgets override to emit their own primitives; + // containers that also draw (Button background, ScrollView clip + // push/pop, TabView bar) override and explicitly recurse into + // children where appropriate. + // + // The body just forwards to children, so the forward-declared + // DrawList is enough — no member access here. + virtual void Emit(DrawList& dl) const { + for (auto& c : children_) c->Emit(dl); + } + + // Walk all descendants in pre-order. + template + void ForEach(F&& f) { + f(*this); + for (auto& c : children_) c->ForEach(f); + } + }; + + // CRTP base providing fluent setters that return the concrete widget type. + template + struct WidgetBuilder : Widget { + Self& self() { return static_cast(*this); } + + Self& width(Length l) { width_ = l; return self(); } + Self& height(Length l) { height_ = l; return self(); } + Self& size(Length w, Length h) { width_ = w; height_ = h; return self(); } + Self& padding(Edges e) { padding_ = e; return self(); } + Self& padding(float all) { padding_ = Edges(all); return self(); } + Self& padding(float v, float h) { padding_ = Edges(v, h); return self(); } + Self& margin(Edges e) { margin_ = e; return self(); } + Self& margin(float all) { margin_ = Edges(all); return self(); } + Self& anchor(Anchor a) { anchor_ = a; return self(); } + Self& expand() { width_ = Length::Frac(1); height_ = Length::Frac(1); return self(); } + + // Take ownership of a parameter pack of widgets and append them as children. + template + requires (std::derived_from, Widget> && ...) + Self& children(Ws&&... ws) { + children_.reserve(children_.size() + sizeof...(Ws)); + (AppendChild(std::forward(ws)), ...); + return self(); + } + + private: + // .children(...) takes ownership of each widget argument unconditionally; + // builder chains like `Button{"X"}.font(f)` return Self& (lvalue ref to + // the temporary), so we always move rather than std::forward. + template + void AppendChild(W&& w) { + using T = std::remove_cvref_t; + auto p = std::make_unique(std::move(w)); + p->parent = this; + children_.push_back(std::move(p)); + } + }; + + // Stable typed handle into the scene; populated by the scene when a + // widget tree is mounted. + template + struct WidgetRef { + T* node = nullptr; + + T* operator->() const { return node; } + T& operator*() const { return *node; } + explicit operator bool() const { return node != nullptr; } + }; + + // Mutable observable value. Setting a new value invokes any registered + // watchers; widgets register watchers in their mount step to mark + // themselves dirty when the underlying value changes. + template + class Observable { + public: + Observable() = default; + Observable(T v) : value_(std::move(v)) {} + + Observable(const Observable&) = delete; + Observable& operator=(const Observable&) = delete; + + Observable& operator=(T v) { + if constexpr (std::equality_comparable) { + if (value_ == v) return *this; + } + value_ = std::move(v); + Notify(); + return *this; + } + + const T& Get() const { return value_; } + operator const T&() const { return value_; } + + // Register a watcher; returned token unregisters on destruction. + // For V1 there is no unsubscribe — watchers live as long as the + // Observable does. The scene clears watchers when widgets are torn + // down by destroying the Observable they were watching. + void Watch(std::function fn) { + watchers_.push_back(std::move(fn)); + } + + private: + T value_{}; + std::vector> watchers_; + + void Notify() { + for (auto& w : watchers_) w(); + } + }; +} diff --git a/interfaces/Crafter.Graphics-UIWidgets.cppm b/interfaces/Crafter.Graphics-UIWidgets.cppm new file mode 100644 index 0000000..27a0418 --- /dev/null +++ b/interfaces/Crafter.Graphics-UIWidgets.cppm @@ -0,0 +1,989 @@ +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +export module Crafter.Graphics:UIWidgets; +import std; +import :UILength; +import :UIWidget; +import :UILayout; +import :UIDrawList; +import :UIAtlas; +import :Font; + +export namespace Crafter::UI { + // ─────────────────── Text-emission helper ───────────────────────────── + // Walks an ASCII string, ensuring each codepoint in the atlas and + // emitting one Glyph item per non-whitespace glyph. Returns the final + // x-cursor position (useful when emitting multi-run text). + namespace detail { + inline float EmitText(DrawList& dl, Font* font, std::string_view text, + float fontSizePx, Color color, + float originX, float topY) { + if (!dl.atlas || !font) return originX; + FontAtlas& atlas = *dl.atlas; + + float scaleFactor = fontSizePx / FontAtlas::kBaseSize; + float baselineY = topY + font->AscentPx(fontSizePx); + float cursorX = originX; + + std::size_t i = 0; + while (i < text.size()) { + std::uint32_t cp = DecodeUtf8(text, i); + if (cp == 0) break; + atlas.Ensure(*font, cp); + const Glyph* g = atlas.Lookup(*font, cp); + if (!g) continue; + if (g->w > 0 && g->h > 0) { + Rect quad{ + cursorX + g->xoff * scaleFactor, + baselineY + g->yoff * scaleFactor, + g->w * scaleFactor, + g->h * scaleFactor, + }; + dl.AddGlyph(quad, color, {g->u0, g->v0, g->u1 - g->u0, g->v1 - g->v0}); + } + cursorX += g->advance * scaleFactor; + } + return cursorX; + } + } + + // ─────────────────────────── Spacer ─────────────────────────────────── + // Takes up flex space along whichever axis its parent stacks on. Has + // no minimum size of its own; an HStack treats it as a horizontal gap, + // a VStack as vertical. + struct Spacer : WidgetBuilder { + Spacer() { + width_ = Length::Frac(1); + height_ = Length::Frac(1); + } + + Size Measure(Size /*avail*/, const LayoutContext& /*ctx*/) override { + desiredSize = {0, 0}; + return desiredSize; + } + + void Arrange(Rect rect, const LayoutContext& /*ctx*/) override { + computedRect = rect; + } + }; + + // ─────────────────────── Stack-axis helpers ─────────────────────────── + namespace detail { + // Pick a child's cross-axis size (the axis the stack does NOT lay + // children along). For Auto, defer to the child's measured size; + // for Px/Pct/Frac, resolve against the available content size. + inline float ResolveCrossAxis(const Widget& c, Length len, float contentExtent, + float scale, float autoSize) { + return ResolveLength(len, contentExtent, scale, [&]{ return autoSize; }); + } + } + + // ─────────────────────────── VStack ─────────────────────────────────── + struct VStack : WidgetBuilder { + float spacing_ = 0; + + VStack& spacing(float s) { spacing_ = s; return *this; } + + Size Measure(Size avail, const LayoutContext& ctx) override { + EdgesPx p = ResolveEdges(padding_, ctx.scale); + float spacingPx = spacing_ * ctx.scale; + + // If our own width/height is fixed, that bounds children; if Auto, + // children may use the full available extent. + float ownW = ResolveLength(width_, avail.w, ctx.scale, [&]{ return avail.w; }); + float ownH = ResolveLength(height_, avail.h, ctx.scale, [&]{ return avail.h; }); + float contentW = std::max(0.0f, ownW - p.Horiz()); + float contentH = std::max(0.0f, ownH - p.Vert()); + + float maxChildW = 0; + float totalH = 0; + float remainingH = contentH; + for (std::size_t i = 0; i < children_.size(); ++i) { + auto& c = *children_[i]; + Size childAvail = { contentW, std::max(0.0f, remainingH) }; + Size cs = c.Measure(childAvail, ctx); + maxChildW = std::max(maxChildW, cs.w); + totalH += cs.h; + remainingH -= cs.h; + if (i + 1 < children_.size()) { + totalH += spacingPx; + remainingH -= spacingPx; + } + } + + desiredSize = { + (width_.mode == Length::Mode::Auto) ? maxChildW + p.Horiz() : ownW, + (height_.mode == Length::Mode::Auto) ? totalH + p.Vert() : ownH, + }; + return desiredSize; + } + + void Arrange(Rect rect, const LayoutContext& ctx) override { + computedRect = rect; + EdgesPx p = ResolveEdges(padding_, ctx.scale); + Rect content = ShrinkBy(rect, p); + float spacingPx = spacing_ * ctx.scale; + + // First pass: sum fixed heights + count Frac weight. + float fracWeight = 0; + float fixedH = 0; + for (auto& c : children_) { + if (c->height_.mode == Length::Mode::Frac) { + fracWeight += c->height_.value; + } else { + fixedH += c->desiredSize.h; + } + } + if (children_.size() > 1) fixedH += spacingPx * (children_.size() - 1); + float leftover = std::max(0.0f, content.h - fixedH); + float fracUnit = (fracWeight > 0) ? (leftover / fracWeight) : 0; + + // Second pass: arrange. + float y = content.y; + for (std::size_t i = 0; i < children_.size(); ++i) { + auto& c = *children_[i]; + float h = (c.height_.mode == Length::Mode::Frac) + ? c.height_.value * fracUnit + : c.desiredSize.h; + float w = detail::ResolveCrossAxis(c, c.width_, content.w, ctx.scale, c.desiredSize.w); + if (c.width_.mode == Length::Mode::Frac) w = content.w * c.width_.value; + if (w > content.w) w = content.w; + + // Cross-axis (horizontal) alignment: honor the child's anchor + // for the horizontal half (Left/Center/Right); default Left. + float x = content.x; + if (c.anchor_) { + switch (*c.anchor_) { + case Anchor::Top: case Anchor::Center: case Anchor::Bottom: + x = content.x + (content.w - w) / 2; break; + case Anchor::TopRight: case Anchor::Right: case Anchor::BottomRight: + x = content.x + content.w - w; break; + default: break; + } + } + c.Arrange({x, y, w, h}, ctx); + y += h + spacingPx; + } + } + }; + + // ─────────────────────────── HStack ─────────────────────────────────── + struct HStack : WidgetBuilder { + float spacing_ = 0; + + HStack& spacing(float s) { spacing_ = s; return *this; } + + Size Measure(Size avail, const LayoutContext& ctx) override { + EdgesPx p = ResolveEdges(padding_, ctx.scale); + float spacingPx = spacing_ * ctx.scale; + + float ownW = ResolveLength(width_, avail.w, ctx.scale, [&]{ return avail.w; }); + float ownH = ResolveLength(height_, avail.h, ctx.scale, [&]{ return avail.h; }); + float contentW = std::max(0.0f, ownW - p.Horiz()); + float contentH = std::max(0.0f, ownH - p.Vert()); + + float maxChildH = 0; + float totalW = 0; + float remainingW = contentW; + for (std::size_t i = 0; i < children_.size(); ++i) { + auto& c = *children_[i]; + Size childAvail = { std::max(0.0f, remainingW), contentH }; + Size cs = c.Measure(childAvail, ctx); + maxChildH = std::max(maxChildH, cs.h); + totalW += cs.w; + remainingW -= cs.w; + if (i + 1 < children_.size()) { + totalW += spacingPx; + remainingW -= spacingPx; + } + } + + desiredSize = { + (width_.mode == Length::Mode::Auto) ? totalW + p.Horiz() : ownW, + (height_.mode == Length::Mode::Auto) ? maxChildH + p.Vert() : ownH, + }; + return desiredSize; + } + + void Arrange(Rect rect, const LayoutContext& ctx) override { + computedRect = rect; + EdgesPx p = ResolveEdges(padding_, ctx.scale); + Rect content = ShrinkBy(rect, p); + float spacingPx = spacing_ * ctx.scale; + + float fracWeight = 0; + float fixedW = 0; + for (auto& c : children_) { + if (c->width_.mode == Length::Mode::Frac) { + fracWeight += c->width_.value; + } else { + fixedW += c->desiredSize.w; + } + } + if (children_.size() > 1) fixedW += spacingPx * (children_.size() - 1); + float leftover = std::max(0.0f, content.w - fixedW); + float fracUnit = (fracWeight > 0) ? (leftover / fracWeight) : 0; + + float x = content.x; + for (std::size_t i = 0; i < children_.size(); ++i) { + auto& c = *children_[i]; + float w = (c.width_.mode == Length::Mode::Frac) + ? c.width_.value * fracUnit + : c.desiredSize.w; + float h = detail::ResolveCrossAxis(c, c.height_, content.h, ctx.scale, c.desiredSize.h); + if (c.height_.mode == Length::Mode::Frac) h = content.h * c.height_.value; + if (h > content.h) h = content.h; + + float y = content.y; + if (c.anchor_) { + switch (*c.anchor_) { + case Anchor::Left: case Anchor::Center: case Anchor::Right: + y = content.y + (content.h - h) / 2; break; + case Anchor::BottomLeft: case Anchor::Bottom: case Anchor::BottomRight: + y = content.y + content.h - h; break; + default: break; + } + } + c.Arrange({x, y, w, h}, ctx); + x += w + spacingPx; + } + } + }; + + // ──────────────────── Stack (anchored layering) ─────────────────────── + // Children stack on top of each other inside the parent's content rect. + // Each child positions itself by its own `.anchor(...)`; default is + // TopLeft. Children with Auto sizes are sized to their measured needs. + struct Stack : WidgetBuilder { + Size Measure(Size avail, const LayoutContext& ctx) override { + EdgesPx p = ResolveEdges(padding_, ctx.scale); + float ownW = ResolveLength(width_, avail.w, ctx.scale, [&]{ return avail.w; }); + float ownH = ResolveLength(height_, avail.h, ctx.scale, [&]{ return avail.h; }); + float contentW = std::max(0.0f, ownW - p.Horiz()); + float contentH = std::max(0.0f, ownH - p.Vert()); + + float maxW = 0, maxH = 0; + for (auto& c : children_) { + Size cs = c->Measure({contentW, contentH}, ctx); + maxW = std::max(maxW, cs.w); + maxH = std::max(maxH, cs.h); + } + + desiredSize = { + (width_.mode == Length::Mode::Auto) ? maxW + p.Horiz() : ownW, + (height_.mode == Length::Mode::Auto) ? maxH + p.Vert() : ownH, + }; + return desiredSize; + } + + void Arrange(Rect rect, const LayoutContext& ctx) override { + computedRect = rect; + EdgesPx p = ResolveEdges(padding_, ctx.scale); + Rect content = ShrinkBy(rect, p); + + for (auto& cp : children_) { + auto& c = *cp; + // Resolve child width/height. Frac fills parent. + float w = (c.width_.mode == Length::Mode::Auto) + ? c.desiredSize.w + : (c.width_.mode == Length::Mode::Frac + ? content.w * c.width_.value + : ResolveLength(c.width_, content.w, ctx.scale, [&]{return c.desiredSize.w;})); + float h = (c.height_.mode == Length::Mode::Auto) + ? c.desiredSize.h + : (c.height_.mode == Length::Mode::Frac + ? content.h * c.height_.value + : ResolveLength(c.height_, content.h, ctx.scale, [&]{return c.desiredSize.h;})); + w = std::min(w, content.w); + h = std::min(h, content.h); + + Anchor a = c.anchor_.value_or(Anchor::TopLeft); + float x = content.x, y = content.y; + switch (a) { + case Anchor::TopLeft: break; + case Anchor::Top: x = content.x + (content.w - w) / 2; break; + case Anchor::TopRight: x = content.x + content.w - w; break; + case Anchor::Left: y = content.y + (content.h - h) / 2; break; + case Anchor::Center: x = content.x + (content.w - w) / 2; + y = content.y + (content.h - h) / 2; break; + case Anchor::Right: x = content.x + content.w - w; + y = content.y + (content.h - h) / 2; break; + case Anchor::BottomLeft: y = content.y + content.h - h; break; + case Anchor::Bottom: x = content.x + (content.w - w) / 2; + y = content.y + content.h - h; break; + case Anchor::BottomRight: x = content.x + content.w - w; + y = content.y + content.h - h; break; + } + c.Arrange({x, y, w, h}, ctx); + } + } + }; + + // Overlay is functionally identical to Stack today; it exists so user + // code can spell intent ("layered HUD" vs "anchored content"). + using Overlay = Stack; + + // ─────────────────────────── TextRun ────────────────────────────────── + // A styled span inside a Text widget. Per-run overrides win over the + // Text's base style; unset fields fall back to the parent Text. + struct TextRun { + std::string text; + std::optional size_; + std::optional color_; + bool bold_ = false; + bool italic_ = false; + bool underline_ = false; + bool strikethrough_ = false; + + TextRun() = default; + TextRun(std::string s) : text(std::move(s)) {} + TextRun(const char* s) : text(s) {} + + TextRun& size(float s) { size_ = s; return *this; } + TextRun& color(Color c) { color_ = c; return *this; } + TextRun& bold() { bold_ = true; return *this; } + TextRun& italic() { italic_ = true; return *this; } + TextRun& underline() { underline_ = true; return *this; } + TextRun& strikethrough() { strikethrough_ = true; return *this; } + }; + + // ─────────────────────────── Text ───────────────────────────────────── + // Static text. V1 supports a single line, single font, with per-run + // styling (color, size, weight, italics, underline, strikethrough). + // Wrap, multi-font, BiDi etc. are V2+. + struct Text : WidgetBuilder { + // Bring back the layout `size(Length, Length)` overload — our own + // `size(float)` would otherwise hide it. + using WidgetBuilder::size; + + std::vector runs_; + Font* font_ = nullptr; + float size_ = 16.0f; + Color color_{1, 1, 1, 1}; + + Text() = default; + Text(std::string s) { runs_.emplace_back(std::move(s)); } + Text(const char* s) { runs_.emplace_back(std::string(s)); } + + Text& font(Font& f) { font_ = &f; return *this; } + Text& size(float s) { size_ = s; return *this; } + Text& color(Color c) { color_ = c; return *this; } + + // Replace the run list with a parameter pack of styled runs. + template + requires (std::convertible_to, TextRun> && ...) + Text& runs(Rs&&... rs) { + runs_.clear(); + runs_.reserve(sizeof...(Rs)); + (runs_.emplace_back(std::forward(rs)), ...); + return *this; + } + + Size Measure(Size avail, const LayoutContext& ctx) override { + float ownW = ResolveLength(width_, avail.w, ctx.scale, [&]{ + if (!font_) return 0.0f; + float w = 0; + for (auto& r : runs_) { + float rs = (r.size_.value_or(size_)) * ctx.scale; + w += font_->GetLineWidth(r.text, rs); + } + return w; + }); + float ownH = ResolveLength(height_, avail.h, ctx.scale, [&]{ + if (!font_) return 0.0f; + // Tallest run dictates the line height. + float h = 0; + for (auto& r : runs_) { + float rs = (r.size_.value_or(size_)) * ctx.scale; + h = std::max(h, font_->LineHeight(rs)); + } + return h; + }); + desiredSize = {ownW, ownH}; + return desiredSize; + } + + void Arrange(Rect rect, const LayoutContext& /*ctx*/) override { + computedRect = rect; + } + + void Emit(DrawList& dl) const override { + if (!font_) return; + float cursorX = computedRect.x; + for (auto& r : runs_) { + float rs = r.size_.value_or(size_) * dl.scale; + Color c = r.color_.value_or(color_); + cursorX = detail::EmitText(dl, font_, r.text, rs, c, cursorX, computedRect.y); + } + } + }; + + // ─────────────────────────── Image ──────────────────────────────────── + // A texture-or-asset reference. V1 stores just the source path; the + // renderer will resolve it. If `sourceSize_` is set, an Auto axis paired + // with a fixed axis preserves aspect ratio. + struct Image : WidgetBuilder { + std::filesystem::path source_; + Size sourceSize_{}; + Color tint_{1, 1, 1, 1}; + + Image() = default; + Image(std::filesystem::path p) : source_(std::move(p)) {} + Image(const char* p) : source_(p) {} + + Image& source(std::filesystem::path p) { source_ = std::move(p); return *this; } + Image& sourceSize(Size s) { sourceSize_ = s; return *this; } + Image& tint(Color c) { tint_ = c; return *this; } + + Size Measure(Size avail, const LayoutContext& ctx) override { + float w = ResolveLength(width_, avail.w, ctx.scale, + [&]{ return sourceSize_.w * ctx.scale; }); + float h = ResolveLength(height_, avail.h, ctx.scale, + [&]{ return sourceSize_.h * ctx.scale; }); + + // If we know the source aspect AND only one axis is Auto, derive + // the missing axis to preserve aspect ratio. + if (sourceSize_.w > 0 && sourceSize_.h > 0) { + bool autoW = (width_.mode == Length::Mode::Auto); + bool autoH = (height_.mode == Length::Mode::Auto); + if (autoW && !autoH && h > 0) w = h * (sourceSize_.w / sourceSize_.h); + else if (autoH && !autoW && w > 0) h = w * (sourceSize_.h / sourceSize_.w); + } + desiredSize = {w, h}; + return desiredSize; + } + + void Arrange(Rect rect, const LayoutContext& /*ctx*/) override { + computedRect = rect; + } + + std::uint32_t bindlessSlot_ = 0; // assigned by UIScene when source is loaded + + void Emit(DrawList& dl) const override { + if (bindlessSlot_ == 0) return; // texture not loaded yet + dl.AddImage(computedRect, tint_, bindlessSlot_); + } + }; + + // ─────────────────────────── ProgressBar ────────────────────────────── + // Horizontal bar showing `value_` ∈ [min_, max_]. `bindValue` registers + // an Observable so external state drives the bar without rebuilding. + struct ProgressBar : WidgetBuilder { + // Bring back the layout `size(Length, Length)` overload — the + // single-arg `value(...)` sets a float; layout sizing uses Length. + using WidgetBuilder::size; + + float value_ = 0.0f; + float min_ = 0.0f; + float max_ = 1.0f; + Color background_{0.20f, 0.20f, 0.20f, 1.0f}; + Color foreground_{0.40f, 0.70f, 1.00f, 1.0f}; + Observable* boundValue_ = nullptr; + + ProgressBar() { height_ = Length::Px(8); } + + ProgressBar& value(float v) { value_ = v; return *this; } + ProgressBar& range(float lo, float hi) { min_ = lo; max_ = hi; return *this; } + ProgressBar& background(Color c) { background_ = c; return *this; } + ProgressBar& foreground(Color c) { foreground_ = c; return *this; } + ProgressBar& bindValue(Observable& v, float lo = 0.0f, float hi = 1.0f) { + boundValue_ = &v; min_ = lo; max_ = hi; return *this; + } + + // Normalised progress in [0, 1]. + float Progress() const { + float v = boundValue_ ? boundValue_->Get() : value_; + if (max_ <= min_) return 0.0f; + return std::clamp((v - min_) / (max_ - min_), 0.0f, 1.0f); + } + + Size Measure(Size avail, const LayoutContext& ctx) override { + float w = ResolveLength(width_, avail.w, ctx.scale, [&]{ return avail.w; }); + float h = ResolveLength(height_, avail.h, ctx.scale, [&]{ return 8.0f * ctx.scale; }); + desiredSize = {w, h}; + return desiredSize; + } + + void Arrange(Rect rect, const LayoutContext& /*ctx*/) override { + computedRect = rect; + } + + void Emit(DrawList& dl) const override { + dl.AddRect(computedRect, background_); + float p = Progress(); + if (p > 0.0f) { + Rect fg{computedRect.x, computedRect.y, computedRect.w * p, computedRect.h}; + dl.AddRect(fg, foreground_); + } + } + }; + + // ─────────────────────────── ButtonStyle ───────────────────────────── + // Reusable visual style for Buttons. Lives in UIWidgets (not UITheme) + // so Button::style(...) can take it by const-ref without a circular + // module dependency. + struct ButtonStyle { + Color background {0.20f, 0.20f, 0.20f, 1.0f}; + Color hoverBackground {0.28f, 0.28f, 0.28f, 1.0f}; + Color pressedBackground{0.14f, 0.14f, 0.14f, 1.0f}; + Color textColor {0.95f, 0.95f, 0.95f, 1.0f}; + Color borderColor {0.30f, 0.30f, 0.30f, 1.0f}; + float fontSize = 16.0f; + Edges padding{8.0f, 12.0f}; + }; + + // ─────────────────────────── InputFieldStyle ───────────────────────── + struct InputFieldStyle { + Color background {0.10f, 0.10f, 0.10f, 1.0f}; + Color textColor {0.95f, 0.95f, 0.95f, 1.0f}; + Color borderColor {0.40f, 0.40f, 0.40f, 1.0f}; + Color focusBorderColor {0.40f, 0.70f, 1.00f, 1.0f}; + float fontSize = 16.0f; + Edges padding{6.0f, 8.0f}; + }; + + // ─────────────────────────── InputField ────────────────────────────── + // Single-line text editor. V1 stores a std::string; the renderer draws + // the background, border, text, and a focus-cursor caret. Keyboard + // events are wired in by UI-Hit/UI-Scene; for now the widget owns the + // data + visual config and exposes onChange/onSubmit callbacks. + struct InputField : WidgetBuilder { + std::string text_; + Font* font_ = nullptr; + float fontSize_ = 16.0f; + Color textColor_{0.95f, 0.95f, 0.95f, 1.0f}; + Color background_{0.10f, 0.10f, 0.10f, 1.0f}; + Color borderColor_{0.40f, 0.40f, 0.40f, 1.0f}; + Color focusBorderColor_{0.40f, 0.70f, 1.00f, 1.0f}; + bool focused_ = false; + std::size_t cursor_ = 0; // codepoint index within text_ + std::string placeholder_; + std::function onChange_; + std::function onSubmit_; + + InputField() { + padding_ = Edges(6, 8); + width_ = Length::Px(160); + } + InputField(std::string initial) : InputField() { + text_ = std::move(initial); + cursor_ = text_.size(); + } + + InputField& text(std::string s) { text_ = std::move(s); cursor_ = text_.size(); return *this; } + InputField& placeholder(std::string s) { placeholder_ = std::move(s); return *this; } + InputField& font(Font& f) { font_ = &f; return *this; } + InputField& fontSize(float s) { fontSize_ = s; return *this; } + InputField& textColor(Color c) { textColor_ = c; return *this; } + InputField& background(Color c) { background_ = c; return *this; } + InputField& borderColor(Color c) { borderColor_ = c; return *this; } + InputField& focusBorderColor(Color c) { focusBorderColor_ = c; return *this; } + InputField& onChange(std::function f) { onChange_ = std::move(f); return *this; } + InputField& onSubmit(std::function f) { onSubmit_ = std::move(f); return *this; } + + InputField& style(const InputFieldStyle& s) { + background_ = s.background; + textColor_ = s.textColor; + borderColor_ = s.borderColor; + focusBorderColor_ = s.focusBorderColor; + fontSize_ = s.fontSize; + padding_ = s.padding; + return *this; + } + + Size Measure(Size avail, const LayoutContext& ctx) override { + EdgesPx p = ResolveEdges(padding_, ctx.scale); + float devSize = fontSize_ * ctx.scale; + float lineH = font_ ? font_->LineHeight(devSize) : devSize; + + float w = ResolveLength(width_, avail.w, ctx.scale, [&]{ return 160.0f * ctx.scale; }); + float h = ResolveLength(height_, avail.h, ctx.scale, [&]{ return lineH + p.Vert(); }); + desiredSize = {w, h}; + return desiredSize; + } + + void Arrange(Rect rect, const LayoutContext& /*ctx*/) override { + computedRect = rect; + } + + // Interaction. UIScene's focus manager flips `focused_` via + // OnFocus/OnBlur; mouse clicks just need to be claimed so the + // bubble stops here (and so a non-focusable parent doesn't eat + // the event). The text-edit ops are deliberately tiny: insert at + // cursor, backspace, delete, enter; arrow keys move the caret. + bool IsFocusable() const override { return true; } + void OnFocus() override { focused_ = true; } + void OnBlur() override { focused_ = false; } + bool OnMouseClick(float, float) override { return true; } + + bool OnTextInput(std::string_view text) override { + text_.insert(cursor_, text); + cursor_ += text.size(); + if (onChange_) onChange_(text_); + return true; + } + + bool OnKeyDown(CrafterKeys key) override { + switch (key) { + case CrafterKeys::Backspace: { + if (cursor_ > 0) { + // Remove a full UTF-8 codepoint, not a byte. + std::size_t back = 1; + while (back < cursor_ && + (static_cast(text_[cursor_ - back]) & 0xC0) == 0x80) { + ++back; + } + text_.erase(cursor_ - back, back); + cursor_ -= back; + if (onChange_) onChange_(text_); + } + return true; + } + case CrafterKeys::Delete: { + if (cursor_ < text_.size()) { + std::size_t fwd = 1; + while (cursor_ + fwd < text_.size() && + (static_cast(text_[cursor_ + fwd]) & 0xC0) == 0x80) { + ++fwd; + } + text_.erase(cursor_, fwd); + if (onChange_) onChange_(text_); + } + return true; + } + case CrafterKeys::Left: { + while (cursor_ > 0) { + --cursor_; + if ((static_cast(text_[cursor_]) & 0xC0) != 0x80) break; + } + return true; + } + case CrafterKeys::Right: { + while (cursor_ < text_.size()) { + ++cursor_; + if (cursor_ == text_.size() || + (static_cast(text_[cursor_]) & 0xC0) != 0x80) break; + } + return true; + } + case CrafterKeys::Home: cursor_ = 0; return true; + case CrafterKeys::End: cursor_ = text_.size(); return true; + case CrafterKeys::Enter: + if (onSubmit_) onSubmit_(text_); + return true; + default: return false; + } + } + + void Emit(DrawList& dl) const override { + // Background. + dl.AddRect(computedRect, background_); + + // Border: 4 thin rects so the user can see focus state without + // a stencil pass. 1-device-pixel-wide outline. + float t = std::max(1.0f, dl.scale); + Color border = focused_ ? focusBorderColor_ : borderColor_; + dl.AddRect({computedRect.x, computedRect.y, computedRect.w, t}, border); // top + dl.AddRect({computedRect.x, computedRect.y + computedRect.h - t, computedRect.w, t}, border); // bottom + dl.AddRect({computedRect.x, computedRect.y, t, computedRect.h}, border); // left + dl.AddRect({computedRect.x + computedRect.w - t, computedRect.y, t, computedRect.h}, border); // right + + if (font_) { + EdgesPx p = ResolveEdges(padding_, dl.scale); + float devSize = fontSize_ * dl.scale; + float originX = computedRect.x + p.left; + float originY = computedRect.y + p.top; + std::string_view show = !text_.empty() ? std::string_view(text_) + : std::string_view(placeholder_); + Color col = !text_.empty() ? textColor_ + : Color{textColor_.r, textColor_.g, textColor_.b, textColor_.a * 0.5f}; + detail::EmitText(dl, font_, show, devSize, col, originX, originY); + + // Caret bar at the cursor position when focused. + if (focused_) { + std::string_view before(text_.data(), cursor_); + float prefixW = static_cast(font_->GetLineWidth(before, devSize)); + float caretX = originX + prefixW; + dl.AddRect({caretX, originY, t, font_->LineHeight(devSize)}, focusBorderColor_); + } + } + } + }; + + // ─────────────────────────── ScrollView ────────────────────────────── + // Clipping viewport. Children are laid out vertically (treated like a + // VStack with no spacing) and translated by scrollY_; horizontal scroll + // is opt-in via .horizontal(true). Hit/wheel/drag interaction is wired + // by UI-Hit/UI-Scene. + struct ScrollView : WidgetBuilder { + bool scrollVertical_ = true; + bool scrollHorizontal_ = false; + float scrollX_ = 0.0f; + float scrollY_ = 0.0f; + Size contentSize_{}; // total laid-out children size (for scroll bounds) + + ScrollView& vertical(bool b) { scrollVertical_ = b; return *this; } + ScrollView& horizontal(bool b) { scrollHorizontal_ = b; return *this; } + + Size Measure(Size avail, const LayoutContext& ctx) override { + EdgesPx p = ResolveEdges(padding_, ctx.scale); + + // Viewport size — defaults to filling available. + float w = ResolveLength(width_, avail.w, ctx.scale, [&]{ return avail.w; }); + float h = ResolveLength(height_, avail.h, ctx.scale, [&]{ return avail.h; }); + + // Children measure with effectively-unbounded space along the + // scroll axis, allowing them to grow beyond the viewport. + constexpr float kInf = std::numeric_limits::max(); + Size childAvail = { + scrollHorizontal_ ? kInf : std::max(0.0f, w - p.Horiz()), + scrollVertical_ ? kInf : std::max(0.0f, h - p.Vert()), + }; + float totalH = 0; + float maxW = 0; + for (auto& c : children_) { + Size cs = c->Measure(childAvail, ctx); + totalH += cs.h; + maxW = std::max(maxW, cs.w); + } + contentSize_ = {maxW, totalH}; + + desiredSize = {w, h}; + return desiredSize; + } + + void Arrange(Rect rect, const LayoutContext& ctx) override { + computedRect = rect; + EdgesPx p = ResolveEdges(padding_, ctx.scale); + Rect content = ShrinkBy(rect, p); + + // Clamp scroll to valid range. + float maxScrollY = std::max(0.0f, contentSize_.h - content.h); + float maxScrollX = std::max(0.0f, contentSize_.w - content.w); + scrollY_ = std::clamp(scrollY_, 0.0f, maxScrollY); + scrollX_ = std::clamp(scrollX_, 0.0f, maxScrollX); + + float y = content.y - scrollY_; + for (auto& c : children_) { + float w = (c->width_.mode == Length::Mode::Auto) + ? c->desiredSize.w + : ResolveLength(c->width_, content.w, ctx.scale, + [&]{ return c->desiredSize.w; }); + float h = c->desiredSize.h; + c->Arrange({content.x - scrollX_, y, w, h}, ctx); + y += h; + } + } + + void Emit(DrawList& dl) const override { + // Wrap children in a clip rect equal to our viewport. + dl.PushClip(computedRect); + for (auto& c : children_) c->Emit(dl); + dl.PopClip(); + } + }; + + // ─────────────────────────── TabView ────────────────────────────────── + // A tab bar at the top + the selected tab's content below. Each `.tab( + // name, widget)` adds a child widget; the selected index drives which + // child is laid out and rendered. Tab clicks are routed by UI-Hit later. + struct TabView : WidgetBuilder { + std::vector tabNames_; + int selected_ = 0; + Font* font_ = nullptr; + float tabFontSize_ = 14.0f; + Color tabBackground_{0.10f, 0.10f, 0.10f, 1.0f}; + Color tabActiveBackground_{0.18f, 0.18f, 0.18f, 1.0f}; + Color tabTextColor_{0.85f, 0.85f, 0.85f, 1.0f}; + Color tabActiveTextColor_{1.0f, 1.0f, 1.0f, 1.0f}; + float tabBarHeight_ = 32.0f; + float tabBarBottomYPx_ = 0.0f; // cached after Arrange for hit-testing + + TabView& font(Font& f) { font_ = &f; return *this; } + TabView& tabFontSize(float s) { tabFontSize_ = s; return *this; } + TabView& tabBarHeight(float h) { tabBarHeight_ = h; return *this; } + TabView& selected(int i) { selected_ = i; return *this; } + + // Appends one tab (name + content widget). Each content widget + // is owned via children_; tabNames_[i] mirrors children_[i]'s name. + template + requires std::derived_from, Widget> + TabView& tab(std::string name, W&& content) { + tabNames_.push_back(std::move(name)); + auto p = std::make_unique>(std::move(content)); + p->parent = this; + children_.push_back(std::move(p)); + return *this; + } + + Size Measure(Size avail, const LayoutContext& ctx) override { + EdgesPx p = ResolveEdges(padding_, ctx.scale); + float tbh = tabBarHeight_ * ctx.scale; + + float ownW = ResolveLength(width_, avail.w, ctx.scale, [&]{ return avail.w; }); + float ownH = ResolveLength(height_, avail.h, ctx.scale, [&]{ return avail.h; }); + Size contentAvail = { + std::max(0.0f, ownW - p.Horiz()), + std::max(0.0f, ownH - p.Vert() - tbh), + }; + + // Measure ALL tab contents (so a switch doesn't trigger a + // re-measure). Cheap for typical tab counts. + for (auto& c : children_) c->Measure(contentAvail, ctx); + + desiredSize = {ownW, ownH}; + return desiredSize; + } + + void Arrange(Rect rect, const LayoutContext& ctx) override { + computedRect = rect; + EdgesPx p = ResolveEdges(padding_, ctx.scale); + Rect content = ShrinkBy(rect, p); + float tbh = tabBarHeight_ * ctx.scale; + tabBarBottomYPx_ = content.y + tbh; + + // The active tab gets the area below the tab bar. + Rect contentArea = { + content.x, + content.y + tbh, + content.w, + std::max(0.0f, content.h - tbh), + }; + + for (std::size_t i = 0; i < children_.size(); ++i) { + if (static_cast(i) == selected_) { + children_[i]->Arrange(contentArea, ctx); + } else { + // Off-screen so nothing draws / hit-tests for inactive tabs. + children_[i]->Arrange({0, 0, 0, 0}, ctx); + } + } + } + + bool OnMouseClick(float x, float y) override { + // Only the tab bar consumes clicks; let content-area clicks bubble. + if (y < computedRect.y || y >= tabBarBottomYPx_) return false; + if (tabNames_.empty()) return false; + float tabW = computedRect.w / static_cast(tabNames_.size()); + int idx = static_cast((x - computedRect.x) / tabW); + idx = std::clamp(idx, 0, static_cast(tabNames_.size()) - 1); + selected_ = idx; + return true; + } + + void Emit(DrawList& dl) const override { + if (tabNames_.empty()) return; + + float tbh = tabBarBottomYPx_ - computedRect.y; + // Whole tab-bar background. + dl.AddRect({computedRect.x, computedRect.y, computedRect.w, tbh}, tabBackground_); + + // Active-tab highlight + labels. + float tabW = computedRect.w / static_cast(tabNames_.size()); + for (std::size_t i = 0; i < tabNames_.size(); ++i) { + float tx = computedRect.x + tabW * static_cast(i); + bool active = (static_cast(i) == selected_); + if (active) { + dl.AddRect({tx, computedRect.y, tabW, tbh}, tabActiveBackground_); + } + if (font_) { + float devSize = tabFontSize_ * dl.scale; + float labelW = static_cast(font_->GetLineWidth(tabNames_[i], devSize)); + float labelX = tx + (tabW - labelW) * 0.5f; + float labelY = computedRect.y + (tbh - font_->LineHeight(devSize)) * 0.5f; + Color col = active ? tabActiveTextColor_ : tabTextColor_; + detail::EmitText(dl, font_, tabNames_[i], devSize, col, labelX, labelY); + } + } + + // Active tab content only. + if (selected_ >= 0 && selected_ < static_cast(children_.size())) { + children_[selected_]->Emit(dl); + } + } + }; + + // ─────────────────────────── Button ─────────────────────────────────── + // Clickable rectangle with a label. Default padding makes the click + // target comfortable; users can override via .padding(...). + struct Button : WidgetBuilder