new UI system

This commit is contained in:
Jorijn van der Graaf 2026-05-01 23:35:37 +02:00
commit 216972e73a
82 changed files with 4837 additions and 3243 deletions

435
claude-rewrite-plan.md Normal file
View file

@ -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<T>` (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<T>::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<RenderPass*> passes;
DescriptorHeapVulkan* descriptorHeap; // still here, but now SHARED across all passes
std::optional<Vector<float, 4>> 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<Resolution>{}.bind(temp.resX, temp.resY)),
OptionRow{"Max lights"}.right(InputField<uint16_t>{}.bind(temp.maxLights))
))
.tab("Input", BuildInputPage())
.tab("Audio", BuildAudioPage())
));
}
};
```
`InputField<T>{}.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<float> fps;
Observable<int> 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<T>` 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<std::vector<TextRun>>
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<UIItem, true> 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<R8>` 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<T>`, `Observable<T>`. |
| `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<RenderPass*> passes;` and `std::optional<Vector<float, 4>> 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<float>` 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<T>` is not woven into the UI API; users may drive `Observable<T>` from any source (including `Animation<T>` if they want). No `Animated<T>` 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`.

View file

@ -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
```

View file

@ -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<std::tuple<std::int_fast32_t>> anim({
{std::chrono::seconds(5), FractionalToMapped(-0.5), FractionalToMapped(1.5)},
});
anim.Start(std::chrono::high_resolution_clock::now());
EventListener<FrameTime> updateListener(&window.onUpdate, [&](FrameTime time){
std::tuple<std::int_fast32_t> 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();
}

View file

@ -1,15 +0,0 @@
{
"name": "crafter-graphics",
"configurations": [
{
"name": "executable",
"implementations": ["main"],
"dependencies": [
{
"path":"../../project.json",
"configuration":"lib-wayland-timing"
}
]
}
]
}

View file

@ -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
```

View file

@ -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();
}

View file

@ -1,16 +0,0 @@
{
"name": "crafter-graphics",
"configurations": [
{
"name": "executable",
"implementations": ["main"],
"dependencies": [
{
"path":"../../project.json",
"configuration":"lib-wayland-debug"
}
],
"debug": true
}
]
}

View file

@ -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.

View file

@ -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();
}

View file

@ -1,15 +0,0 @@
{
"name": "crafter-graphics",
"configurations": [
{
"name": "executable",
"implementations": ["main"],
"dependencies": [
{
"path":"../../project.json",
"configuration":"lib-wayland"
}
]
}
]
}

View file

@ -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<T>` 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<MousePoint> 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<void> keyAListener(&window.onKeyDown['a'], [](){
std::cout << std::format("Pressed specifically the a key!");
});
EventListener<char> anyKeyListener(&window.onAnyKeyDown, [](char key){
std::cout << std::format("Pressed the {} key!", key);
});
```
## How to Run
```bash
crafter-build build executable -r
```

View file

@ -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<MousePoint> 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<void> 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<char> 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();
}

View file

@ -1,15 +0,0 @@
{
"name": "crafter-graphics",
"configurations": [
{
"name": "executable",
"implementations": ["main"],
"dependencies": [
{
"path":"../../project.json",
"configuration":"lib-wayland"
}
]
}
]
}

View file

@ -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<FrameTime> updateListener(&window.onUpdate, [&](FrameTime time){
element.rotation += 50000000000000000;
std::cout << element.rotation << std::endl;
element.UpdatePosition(window);
window.LogTiming();
});
window.StartUpdate();
window.StartSync();
}

View file

@ -1,16 +0,0 @@
{
"name": "crafter-graphics",
"configurations": [
{
"name": "executable",
"implementations": ["main"],
"dependencies": [
{
"path":"../../project.json",
"configuration":"lib-wayland-timing"
}
],
"debug": true
}
]
}

View file

@ -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
```

View file

@ -1,33 +0,0 @@
import Crafter.Event;
import Crafter.Graphics;
import std;
using namespace Crafter;
int main() {
WindowWayland window(1280, 720, "Hello Input!");
RenderingElement<false, false, false> element(
{
FractionalToMapped<std::int32_t>(0), //anchorX: relative position where this elements x anchor (top-left) is placed to its parent x anchor
FractionalToMapped<std::int32_t>(0.5), //anchorY: relative position where this elements y anchor (top-left) is placed to its parent y anchor
FractionalToMapped<std::int32_t>(0.1), //relativeSizeX: the relative x size this element should be scaled to compared to its parent
FractionalToMapped<std::int32_t>(1), //relativeSizeY: the relative y size this element should be scaled to compared to its parent
FractionalToMapped<std::int32_t>(0), //anchorOffsetX: the amount this element's anchor should be offset from the top left corner (0.5 to in the middle)
FractionalToMapped<std::int32_t>(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<std::string_view> 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();
}

View file

@ -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"]
}
]
}

View file

@ -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
```

View file

@ -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<true, true, false> 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<void> clickListener(&mouse.onMouseLeftClick, [&window]() {
std::println("Clicked on X:{} Y:{}!",
window.currentMousePos.x, window.currentMousePos.y
);
});
window.Render();
window.StartSync();
}

View file

@ -1,16 +0,0 @@
{
"name": "crafter-graphics",
"configurations": [
{
"name": "executable",
"implementations": ["main"],
"dependencies": [
{
"path":"../../project.json",
"configuration":"lib-wayland-debug"
}
],
"debug": true
}
]
}

View file

@ -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
```

View file

@ -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;
}

View file

@ -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<Raygenspv, Misspv, Closesthitspv> 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<VK_SHADER_UNUSED_KHR, 2, VK_SHADER_UNUSED_KHR, VK_SHADER_UNUSED_KHR>
> ShaderGroups;
typedef PipelineRTVulkanConst<AllShaders, ShaderGroups> 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<AllShaders>::Init();
descriptorSetLayoutTlas::Init();
descriptorSetLayoutImage::Init();
std::array<VkDescriptorSetLayout, 4> layouts {{descriptorSetLayoutTlas::layout, descriptorSetLayoutImage::layout, descriptorSetLayoutImage::layout, descriptorSetLayoutImage::layout}};
DescriptorPool pool;
pool.sets.resize(4);
pool.BuildPool(DescriptorPool::GetPoolSizes<descriptorSetLayoutTlas, descriptorSetLayoutImage, descriptorSetLayoutImage, descriptorSetLayoutImage>(), layouts);
Pipeline::Init(cmd, layouts);
window.SetPipelineRT<Pipeline>();
Mesh triangleMesh;
std::array<Vector<float, 3, 3>, 3> verts {{{-150, -150, 100}, {0, 150, 100}, {150, -150, 100}}};
std::array<std::uint32_t, 3> 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<float, 4, 3, 1> transform = MatrixRowMajor<float, 4, 3, 1>::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<std::tuple<float>> anim({
{std::chrono::seconds(3), -600, 600},
});
anim.Start(std::chrono::high_resolution_clock::now());
EventListener<void> 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<float, 4, 3, 1> transform = MatrixRowMajor<float, 4, 3, 1>::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();
}

View file

@ -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);
}

View file

@ -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"
}
]
}
]
}

View file

@ -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));
}

View file

@ -96,6 +96,9 @@ int main() {
window.FinishInit(); window.FinishInit();
auto imgSlots = descriptorHeap.AllocateImageSlots(1);
auto bufSlots = descriptorHeap.AllocateBufferSlots(1);
VkDeviceAddressRangeKHR tlasRange0 = { VkDeviceAddressRangeKHR tlasRange0 = {
.address = RenderingElement3D::tlases[0].address, .address = RenderingElement3D::tlases[0].address,
}; };
@ -161,27 +164,27 @@ int main() {
VkHostAddressRangeEXT destinations[6] = { VkHostAddressRangeEXT destinations[6] = {
{ {
.address = descriptorHeap.resourceHeap[0].value + descriptorHeap.bufferStartOffset, .address = descriptorHeap.resourceHeap[0].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
.size = Device::descriptorHeapProperties.bufferDescriptorSize .size = Device::descriptorHeapProperties.bufferDescriptorSize
}, },
{ {
.address = descriptorHeap.resourceHeap[0].value, .address = descriptorHeap.resourceHeap[0].value + descriptorHeap.ImageByteOffset(imgSlots.firstElement),
.size = Device::descriptorHeapProperties.imageDescriptorSize .size = Device::descriptorHeapProperties.imageDescriptorSize
}, },
{ {
.address = descriptorHeap.resourceHeap[1].value + descriptorHeap.bufferStartOffset, .address = descriptorHeap.resourceHeap[1].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
.size = Device::descriptorHeapProperties.bufferDescriptorSize .size = Device::descriptorHeapProperties.bufferDescriptorSize
}, },
{ {
.address = descriptorHeap.resourceHeap[1].value, .address = descriptorHeap.resourceHeap[1].value + descriptorHeap.ImageByteOffset(imgSlots.firstElement),
.size = Device::descriptorHeapProperties.imageDescriptorSize .size = Device::descriptorHeapProperties.imageDescriptorSize
}, },
{ {
.address = descriptorHeap.resourceHeap[2].value + descriptorHeap.bufferStartOffset, .address = descriptorHeap.resourceHeap[2].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
.size = Device::descriptorHeapProperties.bufferDescriptorSize .size = Device::descriptorHeapProperties.bufferDescriptorSize
}, },
{ {
.address = descriptorHeap.resourceHeap[2].value, .address = descriptorHeap.resourceHeap[2].value + descriptorHeap.ImageByteOffset(imgSlots.firstElement),
.size = Device::descriptorHeapProperties.imageDescriptorSize .size = Device::descriptorHeapProperties.imageDescriptorSize
}, },
}; };
@ -192,8 +195,9 @@ int main() {
descriptorHeap.resourceHeap[1].FlushDevice(); descriptorHeap.resourceHeap[1].FlushDevice();
descriptorHeap.resourceHeap[2].FlushDevice(); descriptorHeap.resourceHeap[2].FlushDevice();
window.pipeline = &pipeline;
window.descriptorHeap = &descriptorHeap; window.descriptorHeap = &descriptorHeap;
RTPass rtPass(&pipeline);
window.passes.push_back(&rtPass);
window.Render(); window.Render();
window.StartSync(); window.StartSync();

View file

@ -5,7 +5,6 @@ using namespace Crafter;
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) { extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
std::vector<std::string> graphicsArgs(args.begin(), args.end()); std::vector<std::string> graphicsArgs(args.begin(), args.end());
graphicsArgs.push_back("--vulkan");
Configuration* graphics = LocalProject({ Configuration* graphics = LocalProject({
.projectFile = "../../project.cpp", .projectFile = "../../project.cpp",
.args = graphicsArgs, .args = graphicsArgs,

View file

@ -1,133 +1,85 @@
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#include <cassert>
import Crafter.Graphics; import Crafter.Graphics;
using namespace Crafter;
import std;
import Crafter.Event; import Crafter.Event;
import Crafter.Math; import Crafter.Math;
import std;
using namespace Crafter;
int main() { int main() {
Device::Initialize(); Device::Initialize();
Window window(1280, 720, "HelloVulkan"); Window window(1280, 720, "VulkanUI");
VkCommandBuffer cmd = window.StartInit(); 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<VulkanShader, 1> shaders{{
{"raygen.spv", "main", VK_SHADER_STAGE_RAYGEN_BIT_KHR, &specilizationInfo}
}};
ShaderBindingTableVulkan shaderTable;
shaderTable.Init(shaders);
std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> 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<VkRayTracingShaderGroupCreateInfoKHR, 0> missGroups;
std::array<VkRayTracingShaderGroupCreateInfoKHR, 0> hitGroups;
PipelineRTVulkan pipeline;
pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, shaderTable);
window.FinishInit(); window.FinishInit();
RenderingElement2DVulkan<true, true> element( Font font("Inter.ttf");
{
0.5, //anchorX: relative position where this elements x anchor (top-left) is placed to its parent x anchor UI::Theme theme = UI::themes::default_dark();
0.5, //anchorY: relative position where this elements y anchor (top-left) is placed to its parent y anchor theme.defaultFont = &font;
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) // Wire the scene: it auto-creates a descriptor heap, plugs into the
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) // window's pass list, hooks mouse + update events, and drives a
0 //z: this elements Z position // compute-shader UI pass per frame.
}, // ─────────────────────────────────────────────────────────────────────
2, UI::UIScene scene;
1 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<VulkanBuffer<Vector<_Float16, 4, 4>, true>*>(element.buffers[i])->value[0] = {1, 0, 0, 1};
reinterpret_cast<VulkanBuffer<Vector<_Float16, 4, 4>, true>*>(element.buffers[i])->value[1] = {0, 1, 0, 1};
reinterpret_cast<VulkanBuffer<Vector<_Float16, 4, 4>, 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<VkResourceDescriptorInfoEXT, 9> 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<VkHostAddressRangeEXT, 9> 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.Render();
window.SaveFrame("frame.png");
window.StartUpdate(); // continuous rendering — UIScene re-emits per frame
window.StartSync(); window.StartSync();
} }

View file

@ -0,0 +1,27 @@
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
std::vector<std::string> 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<fs::path, 0> ifaces = {};
std::array<fs::path, 1> impls = { "main" };
cfg.GetInterfacesAndImplementations(ifaces, impls);
cfg.files.push_back("Inter.ttf");
return cfg;
}

View file

@ -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"
}
]
}
]
}

View file

@ -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);
}

View file

@ -20,11 +20,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
module; module;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#include "vulkan/vk_enum_string_helper.h" #include "vulkan/vk_enum_string_helper.h"
#define GET_EXTENSION_FUNCTION(_id) ((PFN_##_id)(vkGetInstanceProcAddr(instance, #_id))) #define GET_EXTENSION_FUNCTION(_id) ((PFN_##_id)(vkGetInstanceProcAddr(instance, #_id)))
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
#include <linux/input-event-codes.h> #include <linux/input-event-codes.h>
@ -45,12 +43,10 @@ module;
module Crafter.Graphics:Device_impl; module Crafter.Graphics:Device_impl;
import :Device; import :Device;
import :Window; import :Window;
import :MouseElement;
import :Types; import :Types;
import std; import std;
using namespace Crafter; using namespace Crafter;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
const char* const instanceExtensionNames[] = { const char* const instanceExtensionNames[] = {
"VK_EXT_debug_utils", "VK_EXT_debug_utils",
"VK_KHR_surface", "VK_KHR_surface",
@ -175,7 +171,6 @@ VkBool32 onError(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMe
return 0; return 0;
} }
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #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) { if(state == WL_POINTER_BUTTON_STATE_PRESSED) {
Device::focusedWindow->mouseLeftHeld = true; Device::focusedWindow->mouseLeftHeld = true;
Device::focusedWindow->onMouseLeftClick.Invoke(); 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 { } else {
Device::focusedWindow->mouseLeftHeld = false; Device::focusedWindow->mouseLeftHeld = false;
Device::focusedWindow->onMouseLeftRelease.Invoke(); 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){ } else if(button == BTN_RIGHT){
if(state == WL_POINTER_BUTTON_STATE_PRESSED) { if(state == WL_POINTER_BUTTON_STATE_PRESSED) {
Device::focusedWindow->mouseRightHeld = true; Device::focusedWindow->mouseRightHeld = true;
Device::focusedWindow->onMouseRightClick.Invoke(); 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 { } else {
Device::focusedWindow->mouseRightHeld = false; Device::focusedWindow->mouseRightHeld = false;
Device::focusedWindow->onMouseRightRelease.Invoke(); 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->mouseElements.erase(std::remove(Device::focusedWindow->mouseElements.begin(), Device::focusedWindow->mouseElements.end(), static_cast<MouseElement*>(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) { 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->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->mouseDelta = {Device::focusedWindow->currentMousePos.x-Device::focusedWindow->lastMousePos.x, Device::focusedWindow->currentMousePos.y-Device::focusedWindow->lastMousePos.y};
Device::focusedWindow->onMouseMove.Invoke(); 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<MouseElement*>(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) { 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 #endif
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
app.pApplicationName = ""; app.pApplicationName = "";
app.pEngineName = "Crafter.Graphics"; app.pEngineName = "Crafter.Graphics";
@ -727,8 +675,15 @@ void Device::Initialize() {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES,
.pNext = &bit16, .pNext = &bit16,
.shaderFloat16 = VK_TRUE, .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, .runtimeDescriptorArray = VK_TRUE,
.bufferDeviceAddress = VK_TRUE .scalarBlockLayout = VK_TRUE,
.bufferDeviceAddress = VK_TRUE
}; };
VkPhysicalDeviceRayTracingPipelineFeaturesKHR physicalDeviceRayTracingPipelineFeatures{ VkPhysicalDeviceRayTracingPipelineFeaturesKHR physicalDeviceRayTracingPipelineFeatures{
@ -747,8 +702,17 @@ void Device::Initialize() {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
.pNext = &deviceAccelerationStructureFeature, .pNext = &deviceAccelerationStructureFeature,
.features = { .features = {
.samplerAnisotropy = VK_TRUE, // Order matches VkPhysicalDeviceFeatures declaration so the
.shaderInt16 = VK_TRUE // 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<PFN_vkCmdBindResourceHeapEXT>(vkGetInstanceProcAddr(instance, "vkCmdBindResourceHeapEXT")); vkCmdBindResourceHeapEXT = reinterpret_cast<PFN_vkCmdBindResourceHeapEXT>(vkGetInstanceProcAddr(instance, "vkCmdBindResourceHeapEXT"));
vkCmdBindSamplerHeapEXT = reinterpret_cast<PFN_vkCmdBindSamplerHeapEXT>(vkGetInstanceProcAddr(instance, "vkCmdBindSamplerHeapEXT")); vkCmdBindSamplerHeapEXT = reinterpret_cast<PFN_vkCmdBindSamplerHeapEXT>(vkGetInstanceProcAddr(instance, "vkCmdBindSamplerHeapEXT"));
vkWriteResourceDescriptorsEXT = reinterpret_cast<PFN_vkWriteResourceDescriptorsEXT>(vkGetInstanceProcAddr(instance, "vkWriteResourceDescriptorsEXT")); vkWriteResourceDescriptorsEXT = reinterpret_cast<PFN_vkWriteResourceDescriptorsEXT>(vkGetInstanceProcAddr(instance, "vkWriteResourceDescriptorsEXT"));
vkWriteSamplerDescriptorsEXT = reinterpret_cast<PFN_vkWriteSamplerDescriptorsEXT>(vkGetInstanceProcAddr(instance, "vkWriteSamplerDescriptorsEXT"));
vkCmdPushDataEXT = reinterpret_cast<PFN_vkCmdPushDataEXT>(vkGetInstanceProcAddr(instance, "vkCmdPushDataEXT"));
vkGetPhysicalDeviceDescriptorSizeEXT = reinterpret_cast<PFN_vkGetPhysicalDeviceDescriptorSizeEXT>(vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceDescriptorSizeEXT")); vkGetPhysicalDeviceDescriptorSizeEXT = reinterpret_cast<PFN_vkGetPhysicalDeviceDescriptorSizeEXT>(vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceDescriptorSizeEXT"));
vkGetDeviceFaultInfoEXT = reinterpret_cast<PFN_vkGetDeviceFaultInfoEXT>(vkGetInstanceProcAddr(instance, "vkGetDeviceFaultInfoEXT")); vkGetDeviceFaultInfoEXT = reinterpret_cast<PFN_vkGetDeviceFaultInfoEXT>(vkGetInstanceProcAddr(instance, "vkGetDeviceFaultInfoEXT"));
#endif
} }
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
std::uint32_t Device::GetMemoryType(uint32_t typeBits, VkMemoryPropertyFlags properties) { std::uint32_t Device::GetMemoryType(uint32_t typeBits, VkMemoryPropertyFlags properties) {
for (uint32_t i = 0; i < memoryProperties.memoryTypeCount; i++) 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"); throw std::runtime_error("Could not find a matching memory type");
} }
#endif

View file

@ -61,10 +61,26 @@ Font::Font(const std::filesystem::path& fontFilePath) {
std::uint32_t Font::GetLineWidth(const std::string_view text, float size) { std::uint32_t Font::GetLineWidth(const std::string_view text, float size) {
float scale = stbtt_ScaleForPixelHeight(&font, size); float scale = stbtt_ScaleForPixelHeight(&font, size);
std::uint32_t lineWidth = 0; 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; int advance, lsb;
stbtt_GetCodepointHMetrics(&font, c, &advance, &lsb); stbtt_GetCodepointHMetrics(&font, static_cast<int>(cp), &advance, &lsb);
lineWidth += (int)(advance * scale); lineWidth += (int)(advance * scale);
} }
return lineWidth; 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);
} }

View file

@ -18,9 +18,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
module; module;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#endif
module Crafter.Graphics:Mesh_impl; module Crafter.Graphics:Mesh_impl;
import Crafter.Math; import Crafter.Math;
import :Mesh; import :Mesh;
@ -30,8 +28,6 @@ import std;
using namespace Crafter; using namespace Crafter;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd) { void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> 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()); 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()); 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<Vector<float, 3, 3>> verticies, std::span<std::uint32
.accelerationStructure = accelerationStructure .accelerationStructure = accelerationStructure
}; };
blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo); blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
} }
#endif

View file

@ -1,44 +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 as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.
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 Crafter.Graphics:MouseElement_impl;
import :MouseElement;
import :Window;
import :Types;
import :Font;
import std;
using namespace Crafter;
MouseElement::MouseElement(Anchor2D anchor, Window& window) : Transform2D(anchor) {
window.mouseElements.push_back(this);
}
MouseElement::MouseElement(Anchor2D anchor) : Transform2D(anchor) {
}
MouseElement::MouseElement(Window& window) : Transform2D({0, 0, 1, 1, 0, 0, 0}) {
window.mouseElements.push_back(this);
}
MouseElement::MouseElement() : Transform2D({0, 0, 1, 1, 0, 0, 0}) {
}

View file

@ -18,14 +18,11 @@ License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
module; module;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include <vulkan/vulkan_core.h> #include <vulkan/vulkan_core.h>
#endif
module Crafter.Graphics:RenderingElement3D_impl; module Crafter.Graphics:RenderingElement3D_impl;
import :RenderingElement3D; import :RenderingElement3D;
import std; import std;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
using namespace Crafter; using namespace Crafter;
@ -132,6 +129,4 @@ void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index) {
.accelerationStructure = tlases[index].accelerationStructure .accelerationStructure = tlases[index].accelerationStructure
}; };
tlases[index].address = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo); tlases[index].address = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
} }
#endif

View file

@ -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 <vulkan/vulkan.h>
#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<RenderingElement2DVulkanTransformInfo*>(reinterpret_cast<char*>(transformBuffer[frame].value) + sizeof(RenderingElement2DVulkanTransformInfo));
std::uint16_t* sizePtr = reinterpret_cast<std::uint16_t*>(transformBuffer[frame].value);
*sizePtr = static_cast<std::uint16_t>(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<RenderingElement2DVulkanTransformInfo*>(reinterpret_cast<char*>(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<VkResourceDescriptorInfoEXT> infos, std::span<VkHostAddressRangeEXT> 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<VkDeviceAddressRangeKHR> 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<RenderingElement2DVulkanBase*>(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

View file

@ -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 <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <time.h>
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;
}

View file

@ -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<int>(codepoint), &advanceUnits, &lsb);
int sw = 0, sh = 0, xoff = 0, yoff = 0;
unsigned char* sdf = stbtt_GetCodepointSDF(
&font.font, fontScale, static_cast<int>(codepoint),
kPadding, static_cast<unsigned char>(kOnEdgeValue), kPixelDistScale,
&sw, &sh, &xoff, &yoff
);
Glyph g{};
g.advance = advanceUnits * fontScale;
g.xoff = static_cast<float>(xoff);
g.yoff = static_cast<float>(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<std::size_t>(sw)
);
}
stbtt_FreeSDF(sdf, nullptr);
g.w = static_cast<float>(sw);
g.h = static_cast<float>(sh);
g.u0 = static_cast<float>(px) / kAtlasSize;
g.v0 = static_cast<float>(py) / kAtlasSize;
g.u1 = static_cast<float>(px + sw) / kAtlasSize;
g.v1 = static_cast<float>(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;
}

View file

@ -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::uint16_t>(std::min<std::uint32_t>(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<const UIItem> items) {
if (items.size() > itemCapacity_) {
GrowItemBuffersIfNeeded(static_cast<std::uint32_t>(items.size()));
}
pendingItemCount = static_cast<std::uint32_t>(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<float>(window.width);
pc.surfaceSize[1] = static_cast<float>(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<VkImageDescriptorInfoEXT, Window::numFrames * Window::numFrames> infos{};
std::array<VkResourceDescriptorInfoEXT, Window::numFrames * Window::numFrames> resources{};
std::array<VkHostAddressRangeEXT, Window::numFrames * Window::numFrames> 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<std::uint16_t>(outImageBase_ + imgFrame)),
.size = Device::descriptorHeapProperties.imageDescriptorSize,
};
++k;
}
}
Device::vkWriteResourceDescriptorsEXT(
Device::device, static_cast<std::uint32_t>(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<VkImageDescriptorInfoEXT, Window::numFrames> infos{};
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
std::array<VkHostAddressRangeEXT, Window::numFrames> 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<VkSamplerCreateInfo, Window::numFrames> infos{};
std::array<VkHostAddressRangeEXT, Window::numFrames> 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<VkDeviceAddressRangeEXT, Window::numFrames * Window::numFrames> ranges{};
std::array<VkResourceDescriptorInfoEXT, Window::numFrames * Window::numFrames> resources{};
std::array<VkHostAddressRangeEXT, Window::numFrames * Window::numFrames> 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<std::uint16_t>(itemBufBase_ + bufFrame)),
.size = Device::descriptorHeapProperties.bufferDescriptorSize,
};
++k;
}
}
Device::vkWriteResourceDescriptorsEXT(
Device::device, static_cast<std::uint32_t>(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).
}

View file

@ -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<RenderPass*>(&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<EventListener<void>>(
&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<EventListener<const std::string_view>>(
&window.onTextInput,
[this](std::string_view t) {
if (focused_) focused_->OnTextInput(t);
}
);
// Non-character keys (Backspace, arrows, Enter, …).
keyListener_ = std::make_unique<EventListener<CrafterKeys>>(
&window.onAnyKeyDown,
[this](CrafterKeys key) {
if (focused_) focused_->OnKeyDown(key);
}
);
// Per-frame: re-layout, emit, push items.
updateListener_ = std::make_unique<EventListener<FrameTime>>(
&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<float>(window_->width), static_cast<float>(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<float>(window_->width), static_cast<float>(window_->height) },
*background_
);
}
UI::EmitTree(*root_, drawList);
// Stage to GPU.
renderer.SetItems(drawList.items);
}

View file

@ -37,16 +37,13 @@ module;
#include <print> #include <print>
#include <wayland-client.h> #include <wayland-client.h>
#include <wayland-client-protocol.h> #include <wayland-client-protocol.h>
#ifdef CRAFTER_GRAPHICS_RENDERER_SOFTWARE
#include <sys/stat.h> #include <sys/stat.h>
#include <time.h> #include <time.h>
#endif #endif
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32 #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
#include <windows.h> #include <windows.h>
#include <cassert> #include <cassert>
#endif #endif
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
#include "vulkan/vulkan_wayland.h" #include "vulkan/vulkan_wayland.h"
@ -54,17 +51,14 @@ module;
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32 #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
#include "vulkan/vulkan_win32.h" #include "vulkan/vulkan_win32.h"
#endif #endif
#endif #define STB_IMAGE_WRITE_IMPLEMENTATION
#include "../lib/stb_image_write.h"
module Crafter.Graphics:Window_impl; module Crafter.Graphics:Window_impl;
import :Window; import :Window;
import :Transform2D;
import :MouseElement;
import :Device; import :Device;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
import :VulkanTransition; import :VulkanTransition;
import :DescriptorHeapVulkan; import :DescriptorHeapVulkan;
import :PipelineRTVulkan; import :RenderPass;
#endif
import std; import std;
using namespace Crafter; using namespace Crafter;
@ -336,52 +330,24 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
case WM_LBUTTONDOWN: { case WM_LBUTTONDOWN: {
window->mouseLeftHeld = true; window->mouseLeftHeld = true;
window->onMouseLeftClick.Invoke(); 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; break;
} }
case WM_LBUTTONUP: { case WM_LBUTTONUP: {
window->mouseLeftHeld = false; window->mouseLeftHeld = false;
window->onMouseLeftRelease.Invoke(); 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; break;
} }
case WM_RBUTTONDOWN: { case WM_RBUTTONDOWN: {
window->mouseRightHeld = true; window->mouseRightHeld = true;
window->onMouseRightClick.Invoke(); 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; break;
} }
case WM_RBUTTONUP: { case WM_RBUTTONUP: {
window->mouseRightHeld = false; window->mouseRightHeld = false;
window->onMouseRightRelease.Invoke(); 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; break;
} }
@ -405,11 +371,7 @@ Window::Window(std::uint32_t width, std::uint32_t height, const std::string_view
SetTitle(title); 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) { Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height(height) {
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
Device::windows.push_back(this); Device::windows.push_back(this);
surface = wl_compositor_create_surface(Device::compositor); 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)); wp_viewport_set_destination(wpViewport, std::ceil(width/scale), std::ceil(height/scale));
wl_surface_commit(surface); 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<Vector<std::uint8_t, 4, 4>*>(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
#endif
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32 #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
// Initialize the window class // 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.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = &semaphores.renderComplete; submitInfo.pSignalSemaphores = &semaphores.renderComplete;
submitInfo.pNext = VK_NULL_HANDLE; submitInfo.pNext = VK_NULL_HANDLE;
#endif
lastMousePos = {0,0}; lastMousePos = {0,0};
mouseDelta = {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) { void Window::SetCusorImage(std::uint16_t sizeX, std::uint16_t sizeY) {
new (&cursorRenderer) Rendertarget<std::uint8_t, 4, 4, 1>(sizeX, sizeY);
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
if(cursorSurface == nullptr) { if(cursorSurface == nullptr) {
cursorSurface = wl_compositor_create_surface(Device::compositor); cursorSurface = wl_compositor_create_surface(Device::compositor);
} else { } else {
wl_buffer_destroy(cursorWlBuffer); wl_buffer_destroy(cursorWlBuffer);
munmap(cursorRenderer.buffer[0], cursorBufferOldSize);
} }
int stride = sizeX * 4; 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)); throw std::runtime_error(std::format("creating a buffer file for {}B failed", size));
} }
cursorRenderer.buffer[0] = reinterpret_cast<Vector<std::uint8_t, 4, 4>*>(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); 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); cursorWlBuffer = wl_shm_pool_create_buffer(pool, 0, sizeX, sizeY, stride, WL_SHM_FORMAT_ARGB8888);
wl_shm_pool_destroy(pool); wl_shm_pool_destroy(pool);
@ -673,21 +595,11 @@ void Window::SetCusorImageDefault() {
void Window::UpdateCursorImage() { void Window::UpdateCursorImage() {
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #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_attach(cursorSurface, cursorWlBuffer, 0, 0);
wl_surface_damage(cursorSurface, 0, 0, 9999999, 99999999); wl_surface_damage(cursorSurface, 0, 0, 9999999, 99999999);
wl_surface_commit(cursorSurface); wl_surface_commit(cursorSurface);
#endif #endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32 #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) // Create a mask bitmap (all zeros = fully opaque, alpha comes from color bitmap)
HBITMAP hMask = CreateBitmap(cursorSizeX, cursorSizeY, 1, 1, nullptr); HBITMAP hMask = CreateBitmap(cursorSizeX, cursorSizeY, 1, 1, nullptr);
@ -781,15 +693,6 @@ void Window::Update() {
} }
void Window::Render() { 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 // Acquire the next image from the swap chain
Device::CheckVkResult(vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX, semaphores.presentComplete, (VkFence)nullptr, &currentBuffer)); Device::CheckVkResult(vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX, semaphores.presentComplete, (VkFence)nullptr, &currentBuffer));
submitInfo.commandBufferCount = 1; submitInfo.commandBufferCount = 1;
@ -810,7 +713,7 @@ void Window::Render() {
VkImageMemoryBarrier image_memory_barrier { VkImageMemoryBarrier image_memory_barrier {
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.srcAccessMask = 0, .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, .oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
.newLayout = VK_IMAGE_LAYOUT_GENERAL, .newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
@ -819,7 +722,7 @@ void Window::Render() {
.subresourceRange = range .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}); onUpdate.Invoke({startTime, startTime-lastFrameBegin});
#ifdef CRAFTER_TIMING #ifdef CRAFTER_TIMING
@ -831,31 +734,48 @@ void Window::Render() {
} }
#endif #endif
vkCmdBindPipeline(drawCmdBuffers[currentBuffer], VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR, pipeline->pipeline); if (descriptorHeap) {
VkBindHeapInfoEXT resourceHeapInfo = {
VkBindHeapInfoEXT resourceHeapInfo = { .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT,
.sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, .heapRange = {
.heapRange = { .address = descriptorHeap->resourceHeap[currentBuffer].address,
.address = descriptorHeap->resourceHeap[currentBuffer].address, .size = static_cast<std::uint32_t>(descriptorHeap->resourceHeap[currentBuffer].size)
.size = static_cast<std::uint32_t>(descriptorHeap->resourceHeap[currentBuffer].size) },
}, .reservedRangeOffset = (descriptorHeap->resourceHeap[currentBuffer].size - Device::descriptorHeapProperties.minResourceHeapReservedRange) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1),
.reservedRangeOffset = (descriptorHeap->resourceHeap[currentBuffer].size - Device::descriptorHeapProperties.minResourceHeapReservedRange) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1), .reservedRangeSize = Device::descriptorHeapProperties.minResourceHeapReservedRange
.reservedRangeSize = Device::descriptorHeapProperties.minResourceHeapReservedRange };
}; Device::vkCmdBindResourceHeapEXT(drawCmdBuffers[currentBuffer], &resourceHeapInfo);
Device::vkCmdBindResourceHeapEXT(drawCmdBuffers[currentBuffer], &resourceHeapInfo);
VkBindHeapInfoEXT samplerHeapInfo = { VkBindHeapInfoEXT samplerHeapInfo = {
.sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT, .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT,
.heapRange = { .heapRange = {
.address = descriptorHeap->samplerHeap[currentBuffer].address, .address = descriptorHeap->samplerHeap[currentBuffer].address,
.size = static_cast<std::uint32_t>(descriptorHeap->samplerHeap[currentBuffer].size) .size = static_cast<std::uint32_t>(descriptorHeap->samplerHeap[currentBuffer].size)
}, },
.reservedRangeOffset = descriptorHeap->samplerHeap[currentBuffer].size - Device::descriptorHeapProperties.minSamplerHeapReservedRange, .reservedRangeOffset = descriptorHeap->samplerHeap[currentBuffer].size - Device::descriptorHeapProperties.minSamplerHeapReservedRange,
.reservedRangeSize = Device::descriptorHeapProperties.minSamplerHeapReservedRange .reservedRangeSize = Device::descriptorHeapProperties.minSamplerHeapReservedRange
}; };
Device::vkCmdBindSamplerHeapEXT(drawCmdBuffers[currentBuffer], &samplerHeapInfo); 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 { VkImageMemoryBarrier image_memory_barrier2 {
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
@ -869,7 +789,7 @@ void Window::Render() {
.subresourceRange = range .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])); Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer]));
@ -894,7 +814,6 @@ void Window::Render() {
Device::CheckVkResult(result); Device::CheckVkResult(result);
} }
Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
#endif
} }
#ifdef CRAFTER_TIMING #ifdef CRAFTER_TIMING
@ -935,7 +854,6 @@ void Window::LogTiming() {
} }
#endif #endif
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
void Window::CreateSwapchain() void Window::CreateSwapchain()
{ {
// Store the current swap chain handle so we can use it later on to ease up recreation // 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.imageFormat = colorFormat;
swapchainCI.imageColorSpace = colorSpace; swapchainCI.imageColorSpace = colorSpace;
swapchainCI.imageExtent = { swapchainExtent.width, swapchainExtent.height }; 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.preTransform = (VkSurfaceTransformFlagBitsKHR)preTransform;
swapchainCI.imageArrayLayers = 1; swapchainCI.imageArrayLayers = 1;
swapchainCI.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; swapchainCI.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
@ -1120,8 +1038,6 @@ void Window::EndCmd(VkCommandBuffer cmd) {
Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
} }
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
void Window::wl_surface_frame_done(void* data, struct wl_callback *cb, uint32_t time) { void Window::wl_surface_frame_done(void* data, struct wl_callback *cb, uint32_t time) {
wl_callback_destroy(cb); 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; window->scale = scale / 120.0f;
} }
#endif #endif
void Window::SaveFrame(const std::filesystem::path& path) {
// Staging buffer big enough for one RGBA frame.
VkDeviceSize bufSize = static_cast<VkDeviceSize>(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, &region);
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<const std::uint8_t*>(mapped);
std::vector<std::uint8_t> rgba(static_cast<std::size_t>(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<int>(width), static_cast<int>(height),
4, rgba.data(), static_cast<int>(width) * 4);
vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd);
vkDestroyBuffer(Device::device, stagingBuf, nullptr);
vkFreeMemory(Device::device, stagingMem, nullptr);
}

View file

@ -18,11 +18,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
module; module;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#endif
export module Crafter.Graphics:DescriptorHeapVulkan; export module Crafter.Graphics:DescriptorHeapVulkan;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
import std; import std;
import :Device; import :Device;
import :Window; import :Window;
@ -30,12 +27,23 @@ import :Types;
import :VulkanBuffer; import :VulkanBuffer;
export namespace Crafter { 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 { struct DescriptorHeapVulkan {
VulkanBuffer<std::uint8_t, true> resourceHeap[Window::numFrames]; VulkanBuffer<std::uint8_t, true> resourceHeap[Window::numFrames];
VulkanBuffer<std::uint8_t, true> samplerHeap[Window::numFrames]; VulkanBuffer<std::uint8_t, true> samplerHeap[Window::numFrames];
std::uint32_t bufferStartOffset; std::uint32_t bufferStartOffset;
std::uint16_t bufferStartElement; 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) { 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 descriptorRegion = images * Device::descriptorHeapProperties.imageDescriptorSize + buffers * Device::descriptorHeapProperties.bufferDescriptorSize;
std::uint32_t alignedDescriptorRegion = (descriptorRegion + Device::descriptorHeapProperties.imageDescriptorAlignment - 1) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1); std::uint32_t alignedDescriptorRegion = (descriptorRegion + Device::descriptorHeapProperties.imageDescriptorAlignment - 1) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1);
@ -47,11 +55,57 @@ export namespace Crafter {
bufferStartElement = 1; bufferStartElement = 1;
} }
bufferStartOffset = bufferStartElement * Device::descriptorHeapProperties.bufferDescriptorSize; 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++) { 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); 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); 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) { 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; std::uint32_t bufferStartElement = images * Device::descriptorHeapProperties.imageDescriptorSize / Device::descriptorHeapProperties.bufferDescriptorSize;
@ -66,10 +120,8 @@ export namespace Crafter {
if(images > 0 && bufferStartElement == 0) { if(images > 0 && bufferStartElement == 0) {
bufferStartElement = 1; bufferStartElement = 1;
} }
return bufferStartElement; return bufferStartElement;
} }
}; };
} }
#endif

View file

@ -18,9 +18,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
module; module;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
#include <wayland-client.h> #include <wayland-client.h>
#include <wayland-client-protocol.h> #include <wayland-client-protocol.h>
@ -98,7 +96,6 @@ export namespace Crafter {
}; };
#endif #endif
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
inline static VkInstance instance = VK_NULL_HANDLE; inline static VkInstance instance = VK_NULL_HANDLE;
inline static VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE; inline static VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE;
inline static VkPhysicalDevice physDevice = 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_vkCmdBindResourceHeapEXT vkCmdBindResourceHeapEXT;
inline static PFN_vkCmdBindSamplerHeapEXT vkCmdBindSamplerHeapEXT; inline static PFN_vkCmdBindSamplerHeapEXT vkCmdBindSamplerHeapEXT;
inline static PFN_vkWriteResourceDescriptorsEXT vkWriteResourceDescriptorsEXT; inline static PFN_vkWriteResourceDescriptorsEXT vkWriteResourceDescriptorsEXT;
inline static PFN_vkWriteSamplerDescriptorsEXT vkWriteSamplerDescriptorsEXT;
inline static PFN_vkCmdPushDataEXT vkCmdPushDataEXT;
inline static PFN_vkGetPhysicalDeviceDescriptorSizeEXT vkGetPhysicalDeviceDescriptorSizeEXT; inline static PFN_vkGetPhysicalDeviceDescriptorSizeEXT vkGetPhysicalDeviceDescriptorSizeEXT;
inline static PFN_vkGetDeviceFaultInfoEXT vkGetDeviceFaultInfoEXT; inline static PFN_vkGetDeviceFaultInfoEXT vkGetDeviceFaultInfoEXT;
@ -132,6 +131,5 @@ export namespace Crafter {
static void CheckVkResult(VkResult result); static void CheckVkResult(VkResult result);
static std::uint32_t GetMemoryType(std::uint32_t typeBits, VkMemoryPropertyFlags properties); static std::uint32_t GetMemoryType(std::uint32_t typeBits, VkMemoryPropertyFlags properties);
#endif
}; };
} }

View file

@ -26,14 +26,45 @@ export module Crafter.Graphics:Font;
import std; import std;
namespace Crafter { 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<std::uint8_t>(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<std::uint8_t>(text[i]);
if ((b & 0xC0) != 0x80) return 0xFFFD; // missing continuation
cp = (cp << 6) | (b & 0x3Fu);
++i;
}
return cp;
}
export class Font { export class Font {
public: public:
std::vector<unsigned char> fontBuffer; std::vector<unsigned char> fontBuffer;
std::int_fast32_t ascent; std::int_fast32_t ascent;
std::int_fast32_t descent; std::int_fast32_t descent;
std::int_fast32_t lineGap; std::int_fast32_t lineGap;
stbtt_fontinfo font; stbtt_fontinfo font;
Font(const std::filesystem::path& font); Font(const std::filesystem::path& font);
std::uint32_t GetLineWidth(const std::string_view text, float size); 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
}; };
} }

View file

@ -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++;
}
}
}
};
}

View file

@ -19,16 +19,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 0215-1301 USA
module; module;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#endif
export module Crafter.Graphics:ImageVulkan; export module Crafter.Graphics:ImageVulkan;
import std; import std;
import :VulkanBuffer; import :VulkanBuffer;
export namespace Crafter { export namespace Crafter {
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
template <typename PixelType> template <typename PixelType>
class ImageVulkan { class ImageVulkan {
public: public:
@ -45,7 +42,11 @@ export namespace Crafter {
this->width = width; this->width = width;
this->height = height; this->height = height;
this->mipLevels = mipLevels; 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 = {}; VkImageCreateInfo imageInfo = {};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; 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); vkCmdPipelineBarrier(cmd, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
} }
}; };
#endif
} }

View file

@ -19,9 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
module; module;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#endif
export module Crafter.Graphics:Mesh; export module Crafter.Graphics:Mesh;
import std; import std;
@ -29,7 +27,6 @@ import Crafter.Math;
import :VulkanBuffer; import :VulkanBuffer;
export namespace Crafter { export namespace Crafter {
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
class Mesh { class Mesh {
public: public:
VulkanBuffer<char, false> scratchBuffer; VulkanBuffer<char, false> scratchBuffer;
@ -43,5 +40,4 @@ export namespace Crafter {
bool opaque; bool opaque;
void Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd); void Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd);
}; };
#endif
} }

View file

@ -18,11 +18,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
module; module;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#endif
export module Crafter.Graphics:PipelineRTVulkan; export module Crafter.Graphics:PipelineRTVulkan;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
import std; import std;
import :Device; import :Device;
import :VulkanBuffer; import :VulkanBuffer;
@ -115,6 +112,4 @@ export namespace Crafter {
vkDestroyPipeline(Device::device, pipeline, nullptr); vkDestroyPipeline(Device::device, pipeline, nullptr);
} }
}; };
} }
#endif

View file

@ -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 License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
module;
export module Crafter.Graphics:MouseElement; #include "vulkan/vulkan.h"
export module Crafter.Graphics:RTPass;
import std; import std;
import Crafter.Event; import :RenderPass;
import :Transform2D; import :Window;
import :ForwardDeclarations; import :Device;
import :PipelineRTVulkan;
export namespace Crafter { export namespace Crafter {
struct MouseElement : Transform2D { struct RTPass : RenderPass {
Event<void> onMouseMove; PipelineRTVulkan* pipeline;
Event<void> onMouseEnter;
Event<void> onMouseLeave;
Event<void> onMouseRightClick;
Event<void> onMouseLeftClick;
Event<void> onMouseRightHold;
Event<void> onMouseLeftHold;
Event<void> onMouseRightRelease;
Event<void> onMouseLeftRelease;
bool mouseHover = false;
MouseElement(); RTPass(PipelineRTVulkan* p) : pipeline(p) {}
MouseElement(Window& window);
MouseElement(Anchor2D anchor); void Record(VkCommandBuffer cmd, std::uint32_t /*frameIdx*/, Window& window) override {
MouseElement(Anchor2D anchor, Window& window); 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);
}
}; };
} }

View file

@ -1,12 +1,11 @@
/* /*
Crafter®.Graphics Crafter®.Graphics
Copyright (C) 2026 Catcrafts® Copyright (C) 2026 Catcrafts®
Catcrafts.net catcrafts.net
This library is free software; you can redistribute it and/or This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License version 3.0 as published by the Free Software Foundation;
version 3.0 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful, This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of 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 License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
module;
module Crafter.Graphics:Transform2D_impl; #include "vulkan/vulkan.h"
import :Transform2D; export module Crafter.Graphics:RenderPass;
import :Rendertarget;
import :Types;
import :Font;
import std; import std;
using namespace Crafter; export namespace Crafter {
struct Window;
struct RenderPass {
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) { virtual void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) = 0;
virtual ~RenderPass() = default;
} };
}

View file

@ -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<typename T, bool Scaling, bool Owning, bool Rotating, std::uint8_t Alignment = 0, std::uint8_t Frames = 1> requires ((!Rotating || Scaling) && (!Owning || Scaling))
struct RenderingElement2D : RenderingElement2DBase<T, Frames>, ScalingBase<T, Scaling, Owning, Alignment>, RotatingBase<Rotating> {
RenderingElement2D() = default;
RenderingElement2D(Anchor2D anchor, OpaqueType opaque) : RenderingElement2DBase<T, Frames>(anchor, opaque) {
}
RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t rotation) requires(Rotating) : RenderingElement2DBase<T, Frames>(anchor, opaque), RotatingBase<Rotating>(rotation) {
}
RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, Vector<T, 4, Alignment>* scalingBuffer) requires(Scaling && !Owning) : RenderingElement2DBase<T, Frames>(anchor, opaque), ScalingBase<T, Scaling, Owning, Alignment>(bufferWidth, bufferHeight, scalingBuffer) {
}
RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, Vector<T, 4, Alignment>* scalingBuffer, std::uint32_t rotation) requires(Scaling && !Owning && Rotating) : RenderingElement2DBase<T, Frames>(anchor, opaque), ScalingBase<T, Scaling, Owning, Alignment>(bufferWidth, bufferHeight, scalingBuffer), RotatingBase<Rotating>(rotation) {
}
RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight) requires(Owning) : RenderingElement2DBase<T, Frames>(anchor, opaque), ScalingBase<T, Scaling, Owning, Alignment>(bufferWidth, bufferHeight) {
}
RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, std::uint32_t rotation) requires(Owning && Rotating) : RenderingElement2DBase<T, Frames>(anchor, opaque), ScalingBase<T, Scaling, Owning, Alignment>(bufferWidth, bufferHeight) , RotatingBase<Rotating>(rotation) {
}
RenderingElement2D(Anchor2D anchor, TextureAsset<Vector<T, 4, Alignment>>& texture) requires(!Owning && Scaling) : RenderingElement2DBase<T, Frames>(anchor, texture.opaque), ScalingBase<T, Scaling, Owning, Alignment>(texture.pixels.data(), texture.sizeX, texture.sizeY) {
}
RenderingElement2D(Anchor2D anchor, TextureAsset<Vector<T, 4, Alignment>>& texture, std::uint32_t rotation) requires(!Owning && Scaling && Rotating) : RenderingElement2DBase<T, Frames>(anchor, texture.opaque), ScalingBase<T, Scaling, Owning, Alignment>(texture.pixels.data(), texture.sizeX, texture.sizeY), RotatingBase<Rotating>(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<T, true, Owning, Alignment>::bufferHeight / this->scaled.size.y;
for (std::uint32_t x = 0; x < this->scaled.size.x; x++) {
std::uint32_t srcX = x * ScalingBase<T, true, Owning, Alignment>::bufferWidth / this->scaled.size.x;
this->buffer[y * this->scaled.size.x + x] = ScalingBase<T, true, Owning, Alignment>::scalingBuffer[srcY * ScalingBase<T, true, Owning, Alignment>::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<true>::rotation));
const float s2 = std::abs(std::sin(RotatingBase<true>::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<T, true, Owning, Alignment>::bufferWidth - 1.0) * 0.5;
const float srcCy = (ScalingBase<T, true, Owning, Alignment>::bufferHeight - 1.0) * 0.5;
const float c = std::cos(RotatingBase<true>::rotation);
const float s = std::sin(RotatingBase<true>::rotation);
// Scale factors (destination → source)
const float scaleX = static_cast<float>(ScalingBase<T, true, Owning, Alignment>::bufferWidth) / dstWidth;
const float scaleY = static_cast<float>(ScalingBase<T, true, Owning, Alignment>::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<float>(xB) - dstCx) * scaleX;
const float dy = (static_cast<float>(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::int32_t>(std::round(sx));
const std::int32_t srcY = static_cast<std::int32_t>(std::round(sy));
if (srcX >= 0 && srcX < ScalingBase<T, true, Owning, Alignment>::bufferWidth && srcY >= 0 && srcY < ScalingBase<T, true, Owning, Alignment>::bufferHeight) {
this->buffer[yB * this->scaled.size.x + xB] = ScalingBase<T, true, Owning, Alignment>::scalingBuffer[srcY * ScalingBase<T, true, Owning, Alignment>::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<std::string_view> 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<std::string_view> 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<T, true, Owning, Alignment>::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<T, true, Owning, Alignment>::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<const std::string_view> lines, float size, Vector<T, 4> 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<unsigned char> 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<T, true, Owning, Alignment>::bufferWidth && bufferY >= 0 && bufferY < ScalingBase<T, true, Owning, Alignment>::bufferHeight) {
ScalingBase<T, true, Owning, Alignment>::scalingBuffer[bufferY * ScalingBase<T, true, Owning, Alignment>::bufferWidth + bufferX] = {color.r, color.g, color.b, static_cast<T>(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<T>(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<T, true, Owning, Alignment>::bufferWidth && bufferY >= 0 && bufferY < ScalingBase<T, true, Owning, Alignment>::bufferHeight) {
ScalingBase<T, true, Owning, Alignment>::scalingBuffer[bufferY * ScalingBase<T, true, Owning, Alignment>::bufferWidth + bufferX] = {color.r, color.g, color.b, static_cast<T>(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<T, std::uint8_t>) {
std::uint8_t alpha = bitmap[j * w + i];
Vector<T, 4, Alignment> 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<std::uint8_t, 4, Alignment>(
static_cast<std::uint8_t>((color.r * srcA + dst.r * dstA * (1.0f - srcA)) / outA),
static_cast<std::uint8_t>((color.g * srcA + dst.g * dstA * (1.0f - srcA)) / outA),
static_cast<std::uint8_t>((color.b * srcA + dst.b * dstA * (1.0f - srcA)) / outA),
static_cast<std::uint8_t>(outA * 255)
);
} else if constexpr(std::same_as<T, _Float16>) {
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;
}
}
};
}

View file

@ -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<typename T, std::uint8_t Alignment = 0>
struct RenderElement2DScalingOwning {
std::vector<Vector<T, 4, Alignment>> 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<typename T, std::uint8_t Alignment = 0>
struct RenderElement2DScalingNonOwning {
Vector<T, 4, Alignment>* scalingBuffer;
std::uint32_t bufferWidth;
std::uint32_t bufferHeight;
RenderElement2DScalingNonOwning() = default;
RenderElement2DScalingNonOwning(Vector<T, 4, Alignment>* 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<typename T, bool Scaling, bool Owning, std::uint8_t Alignment = 0>
using ScalingBase =
std::conditional_t<
Scaling,
std::conditional_t<Owning,
RenderElement2DScalingOwning<T, Alignment>,
RenderElement2DScalingNonOwning<T, Alignment>>,
EmptyScalingBase
>;
template<bool Rotating>
using RotatingBase =
std::conditional_t<
Rotating,
RenderElement2DRotating,
EmptyRotatingBase
>;
template<typename T, std::uint8_t Frames = 1>
struct RenderingElement2DBase : Transform2D {
ScaleData2D oldScale[Frames];
bool redraw[Frames];
std::vector<Vector<T, 4, 4>> 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<T, 4>* 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];
}
}
}
};
}

View file

@ -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 <vulkan/vulkan.h>
#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<VulkanBufferBase*, Window::numFrames> 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<VulkanBufferBase*, Window::numFrames>&& buffers) : bufferX(bufferX), bufferY(bufferY), buffers(std::move(buffers)), Transform2D(anchor) {
}
};
template<bool Owning, bool Mapped, bool Single = false>
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<Vector<_Float16, 4, 4>, Mapped>();
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, 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<Vector<_Float16, 4, 4>, Mapped>();
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, 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<Vector<_Float16, 4, 4>, Mapped>();
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, 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<Vector<_Float16, 4, 4>, Mapped>();
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, 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<VulkanBufferBase*, Window::numFrames>&& 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<Vector<_Float16, 4, 4>, Mapped>();
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, 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<Vector<_Float16, 4, 4>>::Load(assetPath, static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[0])->value, this->bufferX, this->bufferY);
} else {
for(std::uint8_t i = 0; i < Window::numFrames; i++) {
buffers[i] = new VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>();
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, 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<Vector<_Float16, 4, 4>>::Load(assetPath, static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[0])->value, this->bufferX, this->bufferY);
for(std::uint8_t i = 1; i < Window::numFrames; i++) {
std::memcpy(static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[i])->value, static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[0])->value, this->bufferX * this->bufferY * sizeof(_Float16));
}
}
}
~RenderingElement2DVulkan() {
if constexpr(Owning) {
if constexpr(Single) {
delete static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, Mapped>*>(buffers[0]);
} else {
for(VulkanBufferBase* buffer : buffers) {
delete static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, 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<Vector<_Float16, 4, 4>, Mapped>();
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, 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<Vector<_Float16, 4, 4>, Mapped>();
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, 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<VulkanBuffer<Vector<_Float16, 4, 4>, 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<VulkanBuffer<Vector<_Float16, 4, 4>, 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<RenderingElement2DVulkanTransformInfo*>(reinterpret_cast<char*>(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<RendertargetVulkan&>(window2);
this->ScaleElement(parent);
RenderingElement2DVulkanTransformInfo* val = reinterpret_cast<RenderingElement2DVulkanTransformInfo*>(reinterpret_cast<char*>(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<const std::string_view> 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<unsigned char> 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<VulkanBuffer<Vector<_Float16, 4, 4>, 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<VulkanBuffer<Vector<_Float16, 4, 4>, true>*>(buffers[frame])->value[bufferY * this->bufferX + bufferX];
_Float16 outA = srcA + dst.a * (1.0f - srcA);
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, 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<const std::string_view> 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<unsigned char> 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<VulkanBuffer<Vector<_Float16, 4, 4>, 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<VulkanBuffer<Vector<_Float16, 4, 4>, true>*>(buffers[frame])->value[bufferY * this->bufferX + bufferX];
_Float16 outA = srcA + dst.a * (1.0f - srcA);
static_cast<VulkanBuffer<Vector<_Float16, 4, 4>, 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

View file

@ -18,11 +18,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
module; module;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#endif
export module Crafter.Graphics:RenderingElement3D; export module Crafter.Graphics:RenderingElement3D;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
import std; import std;
import :Mesh; import :Mesh;
import :VulkanBuffer; import :VulkanBuffer;
@ -45,5 +42,4 @@ export namespace Crafter {
inline static TlasWithBuffer tlases[Window::numFrames]; inline static TlasWithBuffer tlases[Window::numFrames];
static void BuildTLAS(VkCommandBuffer cmd, std::uint32_t index); static void BuildTLAS(VkCommandBuffer cmd, std::uint32_t index);
}; };
} }
#endif

View file

@ -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 <vulkan/vulkan.h>
#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<std::tuple<const Transform*, std::uint32_t, std::uint32_t, std::chrono::nanoseconds>> 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<RenderingElement2DVulkanBase*> elements;
VulkanBuffer<RenderingElement2DVulkanTransformInfo, true> 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<VkResourceDescriptorInfoEXT> infos, std::span<VkHostAddressRangeEXT> ranges, std::uint16_t start, std::uint32_t bufferOffset, DescriptorHeapVulkan& descriptorHeap);
void SetOrderResursive(Transform2D* elementTransform);
};
#endif
template<typename T, std::uint8_t Channels, std::uint8_t Alignment, std::uint8_t Frames>
struct Rendertarget : RendertargetBase {
Vector<T, Channels, Alignment>* 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<ClipRect>&& dirtyRects) {
RenderingElement2DBase<T, Frames>* element = dynamic_cast<RenderingElement2DBase<T, Frames>*>(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<T, 4, 4>* 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<T, Channels, Alignment>));
}
break;
}
case OpaqueType::SemiOpaque:
case OpaqueType::Transparent:
if constexpr(std::same_as<T, _Float16>) {
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<T, Channels, Alignment> src = src_buffer[src_y * src_width + src_x];
Vector<T, Channels, Alignment> dst = buffer[frame][y * this->sizeX + px];
_Float16 oneMinusSrcA = (_Float16)1.0f - src.a;
buffer[frame][y * this->sizeX + px] = Vector<T, Channels, Alignment>(
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<T, Channels, Alignment>));
}
}
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<ClipRect>& clipRects) {
RenderingElement2DBase<T, Frames>* element = dynamic_cast<RenderingElement2DBase<T, Frames>*>(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<ClipRect> clipRects;
for(Transform2D* child : this->transform.children) {
AddOldRects(child, frame, clipRects);
}
//std::vector<ClipRect> 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<Vector<std::uint8_t, 4>> 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<std::uint8_t, 4>& 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;
}
}
};
}

View file

@ -19,9 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
module; module;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#endif
export module Crafter.Graphics:SamplerVulkan; export module Crafter.Graphics:SamplerVulkan;
import std; import std;
@ -29,7 +27,6 @@ import :VulkanBuffer;
import :ImageVulkan; import :ImageVulkan;
export namespace Crafter { export namespace Crafter {
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
template <typename PixelType> template <typename PixelType>
class SamplerVulkan { class SamplerVulkan {
public: public:
@ -60,5 +57,4 @@ export namespace Crafter {
}; };
} }
}; };
#endif
} }

View file

@ -18,11 +18,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
module; module;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#endif
export module Crafter.Graphics:ShaderBindingTableVulkan; export module Crafter.Graphics:ShaderBindingTableVulkan;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
import std; import std;
import :Device; import :Device;
import :VulkanBuffer; import :VulkanBuffer;
@ -40,6 +37,4 @@ export namespace Crafter {
} }
} }
}; };
} }
#endif

View file

@ -18,11 +18,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
module; module;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#endif
export module Crafter.Graphics:ShaderVulkan; export module Crafter.Graphics:ShaderVulkan;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
import std; import std;
import :Device; import :Device;
import :Types; import :Types;
@ -62,6 +59,4 @@ export namespace Crafter {
Device::CheckVkResult(vkCreateShaderModule(Device::device, &module_info, nullptr, &shader)); Device::CheckVkResult(vkCreateShaderModule(Device::device, &module_info, nullptr, &shader));
} }
}; };
} }
#endif

View file

@ -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<Transform2D*> 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);
}
};
}

View file

@ -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 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
module; module;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#endif
export module Crafter.Graphics:Types; export module Crafter.Graphics:Types;
import std; import std;
import Crafter.Math; import Crafter.Math;
@ -247,10 +245,8 @@ export namespace Crafter {
return std::tan(fov * std::numbers::pi / 360.0); return std::tan(fov * std::numbers::pi / 360.0);
} }
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
struct DescriptorBinding { struct DescriptorBinding {
VkDescriptorType type; VkDescriptorType type;
std::uint32_t slot; std::uint32_t slot;
}; };
#endif
} }

View file

@ -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;

View file

@ -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<std::uint8_t> 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<Shelf> 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<const void*>{}(k.font);
std::size_t h2 = std::hash<std::uint32_t>{}(k.cp);
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
}
};
std::unordered_map<Key, Glyph, KeyHash> cache_;
// Place a wxh glyph; returns true + writes top-left into outX/outY.
bool ShelfPlace(int w, int h, int& outX, int& outY);
};
}

View file

@ -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<UIItem> 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<std::uint32_t>(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<std::uint32_t>(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<std::uint32_t>(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<float, 4> atlasUV) {
UIItem it{};
it.type = static_cast<std::uint32_t>(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<float, 4> sourceUV = {0, 0, 1, 1}) {
UIItem it{};
it.type = static_cast<std::uint32_t>(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<std::uint32_t>(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<std::uint32_t>(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);
}
}

View file

@ -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;
}
}
}

View file

@ -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<typename AutoFn>
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<float>(autoFn());
case Length::Mode::Frac: return static_cast<float>(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);
}
}

View file

@ -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};
}
};
}

View file

@ -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<UIItem>) 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<const UIItem> 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<UIItem, true> 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();
};
}

View file

@ -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<typename W>
requires std::derived_from<std::remove_cvref_t<W>, Widget>
void Root(W&& root) {
SetFocus(nullptr);
using T = std::remove_cvref_t<W>;
auto p = std::make_unique<T>(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<Widget> root_;
std::optional<Color> 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<EventListener<void>> mouseListener_;
std::unique_ptr<EventListener<FrameTime>> updateListener_;
std::unique_ptr<EventListener<const std::string_view>> textListener_;
std::unique_ptr<EventListener<CrafterKeys>> keyListener_;
Widget* focused_ = nullptr;
float WindowScale() const;
void RebuildFrame();
};
}

View file

@ -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;
}
}
}

View file

@ -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> anchor_;
// Layout output, filled by the engine.
Rect computedRect{};
Size desiredSize{};
bool dirty = true;
// Tree.
Widget* parent = nullptr;
std::vector<std::unique_ptr<Widget>> 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<typename F>
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<typename Self>
struct WidgetBuilder : Widget {
Self& self() { return static_cast<Self&>(*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<typename... Ws>
requires (std::derived_from<std::decay_t<Ws>, Widget> && ...)
Self& children(Ws&&... ws) {
children_.reserve(children_.size() + sizeof...(Ws));
(AppendChild(std::forward<Ws>(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<typename W>
void AppendChild(W&& w) {
using T = std::remove_cvref_t<W>;
auto p = std::make_unique<T>(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<typename T>
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<typename T>
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<T>) {
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<void()> fn) {
watchers_.push_back(std::move(fn));
}
private:
T value_{};
std::vector<std::function<void()>> watchers_;
void Notify() {
for (auto& w : watchers_) w();
}
};
}

View file

@ -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> {
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<VStack> {
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<HStack> {
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<Stack> {
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<float> size_;
std::optional<Color> 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<Text> {
// Bring back the layout `size(Length, Length)` overload — our own
// `size(float)` would otherwise hide it.
using WidgetBuilder<Text>::size;
std::vector<TextRun> 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<typename... Rs>
requires (std::convertible_to<std::decay_t<Rs>, TextRun> && ...)
Text& runs(Rs&&... rs) {
runs_.clear();
runs_.reserve(sizeof...(Rs));
(runs_.emplace_back(std::forward<Rs>(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<Image> {
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<ProgressBar> {
// Bring back the layout `size(Length, Length)` overload — the
// single-arg `value(...)` sets a float; layout sizing uses Length.
using WidgetBuilder<ProgressBar>::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<float>* 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<float>& 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<InputField> {
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<void(const std::string&)> onChange_;
std::function<void(const std::string&)> 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<void(const std::string&)> f) { onChange_ = std::move(f); return *this; }
InputField& onSubmit(std::function<void(const std::string&)> 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<unsigned char>(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<unsigned char>(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<unsigned char>(text_[cursor_]) & 0xC0) != 0x80) break;
}
return true;
}
case CrafterKeys::Right: {
while (cursor_ < text_.size()) {
++cursor_;
if (cursor_ == text_.size() ||
(static_cast<unsigned char>(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<float>(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<ScrollView> {
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<float>::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<TabView> {
std::vector<std::string> 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<typename W>
requires std::derived_from<std::remove_cvref_t<W>, Widget>
TabView& tab(std::string name, W&& content) {
tabNames_.push_back(std::move(name));
auto p = std::make_unique<std::remove_cvref_t<W>>(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<int>(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<float>(tabNames_.size());
int idx = static_cast<int>((x - computedRect.x) / tabW);
idx = std::clamp(idx, 0, static_cast<int>(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<float>(tabNames_.size());
for (std::size_t i = 0; i < tabNames_.size(); ++i) {
float tx = computedRect.x + tabW * static_cast<float>(i);
bool active = (static_cast<int>(i) == selected_);
if (active) {
dl.AddRect({tx, computedRect.y, tabW, tbh}, tabActiveBackground_);
}
if (font_) {
float devSize = tabFontSize_ * dl.scale;
float labelW = static_cast<float>(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<int>(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<Button> {
std::string label_;
std::function<void()> onClick_;
Color background_{0.2f, 0.2f, 0.2f, 1.0f};
Color textColor_{1, 1, 1, 1};
Font* font_ = nullptr;
float fontSize_ = 16.0f;
Button() { padding_ = Edges(8, 12); }
Button(std::string l) : Button() { label_ = std::move(l); }
Button(const char* l) : Button() { label_ = l; }
Button& text(std::string s) { label_ = std::move(s); return *this; }
Button& onClick(std::function<void()> f) { onClick_ = std::move(f); return *this; }
Button& background(Color c) { background_ = c; return *this; }
Button& textColor(Color c) { textColor_ = c; return *this; }
Button& font(Font& f) { font_ = &f; return *this; }
Button& fontSize(float s) { fontSize_ = s; return *this; }
Button& style(const ButtonStyle& s) {
background_ = s.background;
textColor_ = s.textColor;
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 textW = (font_ && !label_.empty()) ? font_->GetLineWidth(label_, devSize) : 0.0f;
float textH = font_ ? font_->LineHeight(devSize) : devSize;
float w = ResolveLength(width_, avail.w, ctx.scale, [&]{ return textW + p.Horiz(); });
float h = ResolveLength(height_, avail.h, ctx.scale, [&]{ return textH + p.Vert(); });
desiredSize = {w, h};
return desiredSize;
}
void Arrange(Rect rect, const LayoutContext& /*ctx*/) override {
computedRect = rect;
}
bool OnMouseClick(float /*x*/, float /*y*/) override {
if (onClick_) { onClick_(); return true; }
return false;
}
void Emit(DrawList& dl) const override {
// Rounded background — corner radius scales with DPI.
std::uint32_t radius = static_cast<std::uint32_t>(std::round(4.0f * dl.scale));
dl.AddRoundRect(computedRect, background_, static_cast<float>(radius));
// Centred label.
if (font_ && !label_.empty()) {
float devSize = fontSize_ * dl.scale;
float labelW = static_cast<float>(font_->GetLineWidth(label_, devSize));
float labelH = font_->LineHeight(devSize);
float originX = computedRect.x + (computedRect.w - labelW) * 0.5f;
float originY = computedRect.y + (computedRect.h - labelH) * 0.5f;
detail::EmitText(dl, font_, label_, devSize, textColor_, originX, originY);
}
}
};
}

View file

@ -19,12 +19,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
module; module;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#endif
export module Crafter.Graphics:VulkanBuffer; export module Crafter.Graphics:VulkanBuffer;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
import std; import std;
import :Device; import :Device;
@ -213,5 +210,4 @@ namespace Crafter {
VulkanBuffer(VulkanBuffer&) = delete; VulkanBuffer(VulkanBuffer&) = delete;
VulkanBuffer& operator=(const VulkanBuffer&) = delete; VulkanBuffer& operator=(const VulkanBuffer&) = delete;
}; };
} }
#endif

View file

@ -18,12 +18,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
module; module;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#include <assert.h> #include <assert.h>
#endif
export module Crafter.Graphics:VulkanTransition; export module Crafter.Graphics:VulkanTransition;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
import std; import std;
export namespace Crafter { export namespace Crafter {
@ -189,6 +186,4 @@ export namespace Crafter {
static_cast<uint32_t>(image_memory_barriers.size()), static_cast<uint32_t>(image_memory_barriers.size()),
image_memory_barriers.data()); image_memory_barriers.data());
} }
} }
#endif

View file

@ -38,9 +38,7 @@ module;
#include <wayland-client.h> #include <wayland-client.h>
#include <wayland-client-protocol.h> #include <wayland-client-protocol.h>
#endif #endif
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
#include "vulkan/vulkan.h" #include "vulkan/vulkan.h"
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32 #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
#include <windows.h> #include <windows.h>
#endif #endif
@ -48,23 +46,18 @@ module;
export module Crafter.Graphics:Window; export module Crafter.Graphics:Window;
import std; import std;
import :Types; import :Types;
import :Rendertarget;
import :Transform2D;
import Crafter.Event; import Crafter.Event;
export namespace Crafter { export namespace Crafter {
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
struct Semaphores { struct Semaphores {
// Swap chain image presentation // Swap chain image presentation
VkSemaphore presentComplete; VkSemaphore presentComplete;
// Command buffer submission and execution // Command buffer submission and execution
VkSemaphore renderComplete; VkSemaphore renderComplete;
}; };
struct PipelineRTVulkan; struct RenderPass;
struct DescriptorHeapVulkan; struct DescriptorHeapVulkan;
#endif
struct MouseElement;
struct Window { struct Window {
FrameTime currentFrameTime; FrameTime currentFrameTime;
std::uint32_t width; std::uint32_t width;
@ -98,9 +91,6 @@ export namespace Crafter {
Vector<float, 2> mouseDelta; Vector<float, 2> mouseDelta;
bool mouseLeftHeld = false; bool mouseLeftHeld = false;
bool mouseRightHeld = false; bool mouseRightHeld = false;
std::vector<MouseElement*> mouseElements;
std::vector<MouseElement*> pendingMouseElements;
Rendertarget<std::uint8_t, 4, 4, 1> cursorRenderer;
Window() = default; Window() = default;
Window(std::uint32_t width, std::uint32_t height); Window(std::uint32_t width, std::uint32_t height);
@ -139,16 +129,11 @@ export namespace Crafter {
#endif #endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
float scale; float scale = 1.0f;
#ifdef CRAFTER_GRAPHICS_RENDERER_SOFTWARE
Rendertarget<std::uint8_t, 4, 4, 1> renderer;
#endif
bool configured = false; bool configured = false;
xdg_toplevel* xdgToplevel = nullptr; xdg_toplevel* xdgToplevel = nullptr;
wp_viewport* wpViewport = nullptr; wp_viewport* wpViewport = nullptr;
wl_surface* surface = nullptr; wl_surface* surface = nullptr;
wl_buffer* buffer = nullptr;
wl_buffer* backBuffer = nullptr;
xdg_surface* xdgSurface = nullptr; xdg_surface* xdgSurface = nullptr;
wl_callback* cb = nullptr; wl_callback* cb = nullptr;
wl_surface* cursorSurface = nullptr; wl_surface* cursorSurface = nullptr;
@ -177,12 +162,17 @@ export namespace Crafter {
inline static wp_fractional_scale_v1* wp_scale = nullptr; inline static wp_fractional_scale_v1* wp_scale = nullptr;
#endif #endif
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
VkCommandBuffer StartInit(); VkCommandBuffer StartInit();
void FinishInit(); void FinishInit();
VkCommandBuffer GetCmd(); VkCommandBuffer GetCmd();
void EndCmd(VkCommandBuffer cmd); void EndCmd(VkCommandBuffer cmd);
void CreateSwapchain(); void CreateSwapchain();
// Save the current swapchain image (state after Render() returns) to
// a PNG file. Allocates a one-shot staging buffer + command buffer,
// copies image-to-buffer, waits idle, then writes PNG via stb. Useful
// for visual regression tests and screenshotting from headless code.
void SaveFrame(const std::filesystem::path& path);
static constexpr std::uint8_t numFrames = 3; static constexpr std::uint8_t numFrames = 3;
VkSurfaceKHR vulkanSurface = VK_NULL_HANDLE; VkSurfaceKHR vulkanSurface = VK_NULL_HANDLE;
VkSwapchainKHR swapChain = VK_NULL_HANDLE; VkSwapchainKHR swapChain = VK_NULL_HANDLE;
@ -196,8 +186,8 @@ export namespace Crafter {
Semaphores semaphores; Semaphores semaphores;
std::uint32_t currentBuffer = 0; std::uint32_t currentBuffer = 0;
VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
PipelineRTVulkan* pipeline; std::vector<RenderPass*> passes;
DescriptorHeapVulkan* descriptorHeap; DescriptorHeapVulkan* descriptorHeap = nullptr;
#endif std::optional<std::array<float, 4>> clearColor;
}; };
} }

View file

@ -21,21 +21,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
export module Crafter.Graphics; export module Crafter.Graphics;
export import :Window; export import :Window;
export import :Transform2D;
export import :RenderingElement2D;
export import :RenderingElement2DBase;
export import :MouseElement;
export import :GridElement;
export import :Types; export import :Types;
export import :Device; export import :Device;
export import :Font; export import :Font;
export import :Animation; export import :Animation;
export import :Mesh; export import :Mesh;
export import :Rendertarget;
export import :ForwardDeclarations; export import :ForwardDeclarations;
#ifdef CRAFTER_GRAPHICS_RENDERER_VULKAN
export import :Device;
export import :VulkanTransition; export import :VulkanTransition;
export import :VulkanBuffer; export import :VulkanBuffer;
export import :ShaderVulkan; export import :ShaderVulkan;
@ -45,5 +36,6 @@ export import :RenderingElement3D;
export import :ImageVulkan; export import :ImageVulkan;
export import :SamplerVulkan; export import :SamplerVulkan;
export import :DescriptorHeapVulkan; export import :DescriptorHeapVulkan;
export import :RenderingElement2DVulkan; export import :RenderPass;
#endif export import :RTPass;
export import :UI;

1048
lib/stb_image_write.h Normal file

File diff suppressed because it is too large Load diff

View file

@ -47,49 +47,50 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
cfg.cFiles.push_back("lib/viewporter"); cfg.cFiles.push_back("lib/viewporter");
} }
// Renderer: --vulkan opts in (clones Vulkan-Headers + Vulkan-Utility-Libraries // Vulkan is the only renderer. Software fallback is provided externally
// for headers and links libvulkan); default is the software path. // via the Vulkan loader (e.g. llvmpipe / lavapipe) — no separate code path.
if (opts.Has("--vulkan")) { ExternalDependency& vkHeaders = cfg.externalDependencies.emplace_back();
cfg.defines.push_back({"CRAFTER_GRAPHICS_RENDERER_VULKAN", ""}); vkHeaders.name = "Vulkan-Headers";
ExternalDependency& vkHeaders = cfg.externalDependencies.emplace_back(); vkHeaders.source.url = "https://github.com/KhronosGroup/Vulkan-Headers.git";
vkHeaders.name = "Vulkan-Headers"; vkHeaders.builder = ExternalBuilder::None;
vkHeaders.source.url = "https://github.com/KhronosGroup/Vulkan-Headers.git"; vkHeaders.includeDirs = { "include" };
vkHeaders.builder = ExternalBuilder::None; ExternalDependency& vkUtility = cfg.externalDependencies.emplace_back();
vkHeaders.includeDirs = { "include" }; vkUtility.name = "Vulkan-Utility-Libraries";
ExternalDependency& vkUtility = cfg.externalDependencies.emplace_back(); vkUtility.source.url = "https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git";
vkUtility.name = "Vulkan-Utility-Libraries"; vkUtility.builder = ExternalBuilder::None;
vkUtility.source.url = "https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git"; vkUtility.includeDirs = { "include" };
vkUtility.builder = ExternalBuilder::None; cfg.linkFlags.push_back(windows ? "-lvulkan-1" : "-lvulkan");
vkUtility.includeDirs = { "include" };
cfg.linkFlags.push_back(windows ? "-lvulkan-1" : "-lvulkan");
} else {
cfg.defines.push_back({"CRAFTER_GRAPHICS_RENDERER_SOFTWARE", ""});
}
if (opts.Has("--timing")) cfg.defines.push_back({"CRAFTER_TIMING", ""}); if (opts.Has("--timing")) cfg.defines.push_back({"CRAFTER_TIMING", ""});
std::array<fs::path, 24> ifaces = { std::array<fs::path, 30> ifaces = {
"interfaces/Crafter.Graphics", "interfaces/Crafter.Graphics",
"interfaces/Crafter.Graphics-Animation", "interfaces/Crafter.Graphics-Animation",
"interfaces/Crafter.Graphics-DescriptorHeapVulkan", "interfaces/Crafter.Graphics-DescriptorHeapVulkan",
"interfaces/Crafter.Graphics-Device", "interfaces/Crafter.Graphics-Device",
"interfaces/Crafter.Graphics-Font", "interfaces/Crafter.Graphics-Font",
"interfaces/Crafter.Graphics-ForwardDeclarations", "interfaces/Crafter.Graphics-ForwardDeclarations",
"interfaces/Crafter.Graphics-GridElement",
"interfaces/Crafter.Graphics-ImageVulkan", "interfaces/Crafter.Graphics-ImageVulkan",
"interfaces/Crafter.Graphics-Mesh", "interfaces/Crafter.Graphics-Mesh",
"interfaces/Crafter.Graphics-MouseElement",
"interfaces/Crafter.Graphics-PipelineRTVulkan", "interfaces/Crafter.Graphics-PipelineRTVulkan",
"interfaces/Crafter.Graphics-RenderingElement2D",
"interfaces/Crafter.Graphics-RenderingElement2DBase",
"interfaces/Crafter.Graphics-RenderingElement2DVulkan",
"interfaces/Crafter.Graphics-RenderingElement3D", "interfaces/Crafter.Graphics-RenderingElement3D",
"interfaces/Crafter.Graphics-Rendertarget", "interfaces/Crafter.Graphics-RenderPass",
"interfaces/Crafter.Graphics-RTPass",
"interfaces/Crafter.Graphics-SamplerVulkan", "interfaces/Crafter.Graphics-SamplerVulkan",
"interfaces/Crafter.Graphics-ShaderBindingTableVulkan", "interfaces/Crafter.Graphics-ShaderBindingTableVulkan",
"interfaces/Crafter.Graphics-ShaderVulkan", "interfaces/Crafter.Graphics-ShaderVulkan",
"interfaces/Crafter.Graphics-Transform2D",
"interfaces/Crafter.Graphics-Types", "interfaces/Crafter.Graphics-Types",
"interfaces/Crafter.Graphics-UI",
"interfaces/Crafter.Graphics-UIAtlas",
"interfaces/Crafter.Graphics-UIDrawList",
"interfaces/Crafter.Graphics-UIHit",
"interfaces/Crafter.Graphics-UILayout",
"interfaces/Crafter.Graphics-UILength",
"interfaces/Crafter.Graphics-UIRenderer",
"interfaces/Crafter.Graphics-UIScene",
"interfaces/Crafter.Graphics-UITheme",
"interfaces/Crafter.Graphics-UIWidget",
"interfaces/Crafter.Graphics-UIWidgets",
"interfaces/Crafter.Graphics-VulkanBuffer", "interfaces/Crafter.Graphics-VulkanBuffer",
"interfaces/Crafter.Graphics-VulkanTransition", "interfaces/Crafter.Graphics-VulkanTransition",
"interfaces/Crafter.Graphics-Window", "interfaces/Crafter.Graphics-Window",
@ -98,13 +99,15 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
"implementations/Crafter.Graphics-Device", "implementations/Crafter.Graphics-Device",
"implementations/Crafter.Graphics-Font", "implementations/Crafter.Graphics-Font",
"implementations/Crafter.Graphics-Mesh", "implementations/Crafter.Graphics-Mesh",
"implementations/Crafter.Graphics-MouseElement",
"implementations/Crafter.Graphics-Rendertarget",
"implementations/Crafter.Graphics-RenderingElement3D", "implementations/Crafter.Graphics-RenderingElement3D",
"implementations/Crafter.Graphics-Transform2D", "implementations/Crafter.Graphics-UIAtlas",
"implementations/Crafter.Graphics-UIRenderer",
"implementations/Crafter.Graphics-UIScene",
"implementations/Crafter.Graphics-Window", "implementations/Crafter.Graphics-Window",
}; };
cfg.GetInterfacesAndImplementations(ifaces, impls); cfg.GetInterfacesAndImplementations(ifaces, impls);
cfg.shaders.emplace_back(fs::path("shaders/ui.comp.glsl"), std::string("main"), ShaderType::Compute);
return cfg; return cfg;
} }

192
shaders/ui.comp.glsl Normal file
View file

@ -0,0 +1,192 @@
#version 460
#extension GL_EXT_descriptor_heap : enable
#extension GL_EXT_nonuniform_qualifier : enable
#extension GL_EXT_scalar_block_layout : enable
#extension GL_EXT_shader_image_load_formatted : enable
#extension GL_EXT_shader_explicit_arithmetic_types_int16 : enable
layout(local_size_x = 16, local_size_y = 16) in;
// ──── Item types — must match UI::ItemType in UIDrawList.cppm ────────────
const uint TYPE_RECT = 0u;
const uint TYPE_ROUND_RECT = 1u;
const uint TYPE_GLYPH = 2u;
const uint TYPE_IMAGE = 3u;
const uint TYPE_CLIP_PUSH = 5u;
const uint TYPE_CLIP_POP = 6u;
#define MAX_CLIP_DEPTH 8
// ──── Draw item — must match UI::UIItem layout (88 bytes, scalar) ────────
struct UIItem {
uint itype;
uint flags;
vec2 posPx;
vec2 sizePx;
vec4 color;
vec4 colorB;
vec4 uvRect;
uint imageIdx;
uint cornerRadiusPx;
vec2 reserved;
};
// ──── Bindless heap views — VK_EXT_descriptor_heap untyped model ─────────
// Each `layout(descriptor_heap)` declaration is a typed view over the same
// resource heap; indexing is in slot units (image-descriptor units for
// image2D, buffer-descriptor units for buffers, etc.). The application
// passes the absolute heap slot indices via push constants.
layout(descriptor_heap, scalar) readonly buffer UIItemBuf {
UIItem items[];
} itemHeap[];
layout(descriptor_heap) uniform image2D images[];
layout(descriptor_heap) uniform texture2D textures[];
layout(descriptor_heap) uniform sampler samplers[];
// ──── Push constants ─────────────────────────────────────────────────────
layout(push_constant) uniform PC {
uint itemCount;
vec2 surfaceSize;
float scale;
uint outImageHeapIdx; // storage-image slot of the current swapchain view
uint itemBufHeapIdx; // SSBO slot of the current frame's items
uint atlasTextureHeapIdx; // sampled-image slot of the SDF atlas
uint bindlessBaseHeapIdx; // base sampled-image slot for user images
uint linearSamplerHeapIdx; // sampler-heap slot
} pc;
// ──── Driver workaround: per-member SSBO load ────────────────────────────
// `UIItem it = itemHeap[idx].items[i]` emits an OpLoad of a composite type
// from a descriptor-heap'd SSBO, which crashes the GPU on the NVIDIA
// VK_EXT_descriptor_heap path (verified with a 1-float struct repro).
// Reading individual members works (each becomes OpAccessChain + scalar
// OpLoad). LoadItem reassembles the struct member-by-member into a local;
// the rest of the shader then operates on a regular local var.
UIItem LoadItem(uint i) {
UIItem it;
it.itype = itemHeap[pc.itemBufHeapIdx].items[i].itype;
it.flags = itemHeap[pc.itemBufHeapIdx].items[i].flags;
it.posPx = itemHeap[pc.itemBufHeapIdx].items[i].posPx;
it.sizePx = itemHeap[pc.itemBufHeapIdx].items[i].sizePx;
it.color = itemHeap[pc.itemBufHeapIdx].items[i].color;
it.colorB = itemHeap[pc.itemBufHeapIdx].items[i].colorB;
it.uvRect = itemHeap[pc.itemBufHeapIdx].items[i].uvRect;
it.imageIdx = itemHeap[pc.itemBufHeapIdx].items[i].imageIdx;
it.cornerRadiusPx = itemHeap[pc.itemBufHeapIdx].items[i].cornerRadiusPx;
it.reserved = itemHeap[pc.itemBufHeapIdx].items[i].reserved;
return it;
}
// ──── Shading helpers ────────────────────────────────────────────────────
// In-bounds sharp rectangle.
vec4 ShadeRect(UIItem it, vec2 fp) {
if (any(lessThan (fp, it.posPx)) ||
any(greaterThanEqual(fp, it.posPx + it.sizePx))) return vec4(0.0);
return it.color;
}
// SDF for a rounded rectangle. p is offset from rect centre.
float sdRoundRect(vec2 p, vec2 halfSize, float r) {
vec2 q = abs(p) - halfSize + vec2(r);
return length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - r;
}
vec4 ShadeRoundRect(UIItem it, vec2 fp) {
vec2 centre = it.posPx + it.sizePx * 0.5;
float r = float(it.cornerRadiusPx);
float d = sdRoundRect(fp - centre, it.sizePx * 0.5, r);
// 1-pixel AA band around the edge.
float a = clamp(0.5 - d, 0.0, 1.0);
return it.color * a;
}
vec4 ShadeGlyph(UIItem it, vec2 fp) {
if (any(lessThan (fp, it.posPx)) ||
any(greaterThanEqual(fp, it.posPx + it.sizePx))) return vec4(0.0);
vec2 localUV = (fp - it.posPx) / it.sizePx;
vec2 atlasUV = it.uvRect.xy + localUV * it.uvRect.zw;
// Inline sampler2D construction — GLSL doesn't allow sampler2D as a
// local variable, only as a function argument or uniform.
float dist = texture(
sampler2D(textures[pc.atlasTextureHeapIdx],
samplers[pc.linearSamplerHeapIdx]),
atlasUV
).r;
// SDF threshold (stored on-edge value = 128/255 ≈ 0.502). A small
// sample-units band gives ~1 screen pixel of AA at typical sizes.
float aa = 0.05;
float a = smoothstep(0.5 - aa, 0.5 + aa, dist);
return it.color * a;
}
vec4 ShadeImage(UIItem it, vec2 fp) {
if (any(lessThan (fp, it.posPx)) ||
any(greaterThanEqual(fp, it.posPx + it.sizePx))) return vec4(0.0);
vec2 localUV = (fp - it.posPx) / it.sizePx;
vec2 sourceUV = it.uvRect.xy + localUV * it.uvRect.zw;
uint slot = pc.bindlessBaseHeapIdx + it.imageIdx;
return texture(
sampler2D(textures[nonuniformEXT(slot)],
samplers[pc.linearSamplerHeapIdx]),
sourceUV
) * it.color;
}
// ──── Main ───────────────────────────────────────────────────────────────
void main() {
ivec2 ip = ivec2(gl_GlobalInvocationID.xy);
if (any(greaterThanEqual(ip, ivec2(pc.surfaceSize)))) return;
vec2 fp = vec2(ip) + 0.5; // pixel centre
// Composite over what's already in the swapchain (3D output, clear, …).
vec4 dst = imageLoad(images[pc.outImageHeapIdx], ip);
// Clip stack — current effective rect in (x, y, w, h).
vec4 clipStack[MAX_CLIP_DEPTH];
int clipTop = 0;
clipStack[0] = vec4(0.0, 0.0, pc.surfaceSize);
for (uint i = 0u; i < pc.itemCount; ++i) {
UIItem it = LoadItem(i);
if (it.itype == TYPE_CLIP_PUSH) {
vec4 outer = clipStack[clipTop];
vec2 a = max(outer.xy, it.posPx);
vec2 b = min(outer.xy + outer.zw, it.posPx + it.sizePx);
int next = min(clipTop + 1, MAX_CLIP_DEPTH - 1);
clipStack[next] = vec4(a, max(b - a, vec2(0.0)));
clipTop = next;
continue;
}
if (it.itype == TYPE_CLIP_POP) {
clipTop = max(clipTop - 1, 0);
continue;
}
// Skip if pixel is outside the current clip rect.
vec4 c = clipStack[clipTop];
if (any(lessThan(fp, c.xy)) || any(greaterThanEqual(fp, c.xy + c.zw))) continue;
vec4 src;
switch (it.itype) {
case TYPE_RECT: src = ShadeRect (it, fp); break;
case TYPE_ROUND_RECT: src = ShadeRoundRect (it, fp); break;
case TYPE_GLYPH: src = ShadeGlyph (it, fp); break;
case TYPE_IMAGE: src = ShadeImage (it, fp); break;
default: src = vec4(0.0);
}
// Premultiplied "OVER": dst = src + dst * (1 - src.a)
dst.rgb = src.rgb + dst.rgb * (1.0 - src.a);
dst.a = src.a + dst.a * (1.0 - src.a);
}
imageStore(images[pc.outImageHeapIdx], ip, dst);
}