new UI system
This commit is contained in:
parent
d840a81448
commit
216972e73a
82 changed files with 4837 additions and 3243 deletions
435
claude-rewrite-plan.md
Normal file
435
claude-rewrite-plan.md
Normal 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`.
|
||||||
|
|
@ -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
|
|
||||||
```
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"name": "crafter-graphics",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "executable",
|
|
||||||
"implementations": ["main"],
|
|
||||||
"dependencies": [
|
|
||||||
{
|
|
||||||
"path":"../../project.json",
|
|
||||||
"configuration":"lib-wayland-timing"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
```
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"name": "crafter-graphics",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "executable",
|
|
||||||
"implementations": ["main"],
|
|
||||||
"dependencies": [
|
|
||||||
{
|
|
||||||
"path":"../../project.json",
|
|
||||||
"configuration":"lib-wayland-debug"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"debug": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"name": "crafter-graphics",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "executable",
|
|
||||||
"implementations": ["main"],
|
|
||||||
"dependencies": [
|
|
||||||
{
|
|
||||||
"path":"../../project.json",
|
|
||||||
"configuration":"lib-wayland"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
```
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"name": "crafter-graphics",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "executable",
|
|
||||||
"implementations": ["main"],
|
|
||||||
"dependencies": [
|
|
||||||
{
|
|
||||||
"path":"../../project.json",
|
|
||||||
"configuration":"lib-wayland"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"name": "crafter-graphics",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "executable",
|
|
||||||
"implementations": ["main"],
|
|
||||||
"dependencies": [
|
|
||||||
{
|
|
||||||
"path":"../../project.json",
|
|
||||||
"configuration":"lib-wayland-timing"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"debug": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
```
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -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"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
```
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"name": "crafter-graphics",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "executable",
|
|
||||||
"implementations": ["main"],
|
|
||||||
"dependencies": [
|
|
||||||
{
|
|
||||||
"path":"../../project.json",
|
|
||||||
"configuration":"lib-wayland-debug"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"debug": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
```
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
examples/VulkanUI/project.cpp
Normal file
27
examples/VulkanUI/project.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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,7 +675,14 @@ 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,
|
||||||
|
.scalarBlockLayout = VK_TRUE,
|
||||||
.bufferDeviceAddress = VK_TRUE
|
.bufferDeviceAddress = VK_TRUE
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -747,7 +702,16 @@ 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 = {
|
||||||
|
// Order matches VkPhysicalDeviceFeatures declaration so the
|
||||||
|
// designated-initializer-order warning stays quiet.
|
||||||
.samplerAnisotropy = VK_TRUE,
|
.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
|
.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++)
|
||||||
{
|
{
|
||||||
|
|
@ -827,4 +791,3 @@ 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
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
|
@ -130,4 +126,3 @@ void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32
|
||||||
};
|
};
|
||||||
blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
|
blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
@ -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}) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -133,5 +130,3 @@ void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index) {
|
||||||
};
|
};
|
||||||
tlases[index].address = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
|
tlases[index].address = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
130
implementations/Crafter.Graphics-UIAtlas.cpp
Normal file
130
implementations/Crafter.Graphics-UIAtlas.cpp
Normal 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;
|
||||||
|
}
|
||||||
354
implementations/Crafter.Graphics-UIRenderer.cpp
Normal file
354
implementations/Crafter.Graphics-UIRenderer.cpp
Normal 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).
|
||||||
|
}
|
||||||
166
implementations/Crafter.Graphics-UIScene.cpp
Normal file
166
implementations/Crafter.Graphics-UIScene.cpp
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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, ¤tBuffer));
|
Device::CheckVkResult(vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX, semaphores.presentComplete, (VkFence)nullptr, ¤tBuffer));
|
||||||
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,8 +734,7 @@ 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 = {
|
||||||
|
|
@ -854,8 +756,26 @@ void Window::Render() {
|
||||||
.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);
|
||||||
|
|
@ -1168,3 +1084,134 @@ void Window::xdg_surface_handle_preferred_scale(void* data, wp_fractional_scale_
|
||||||
}
|
}
|
||||||
|
|
||||||
#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, ®ion);
|
||||||
|
|
||||||
|
VkImageMemoryBarrier back{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
|
||||||
|
.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT,
|
||||||
|
.dstAccessMask = 0,
|
||||||
|
.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
|
.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
|
||||||
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||||
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||||
|
.image = images[currentBuffer],
|
||||||
|
.subresourceRange = range,
|
||||||
|
};
|
||||||
|
vkCmdPipelineBarrier(cmd,
|
||||||
|
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
|
||||||
|
0, 0, nullptr, 0, nullptr, 1, &back);
|
||||||
|
|
||||||
|
Device::CheckVkResult(vkEndCommandBuffer(cmd));
|
||||||
|
|
||||||
|
VkSubmitInfo si{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
|
||||||
|
.commandBufferCount = 1,
|
||||||
|
.pCommandBuffers = &cmd,
|
||||||
|
};
|
||||||
|
Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &si, VK_NULL_HANDLE));
|
||||||
|
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||||
|
|
||||||
|
// Read back, swizzle BGRA → RGBA if needed, write PNG.
|
||||||
|
void* mapped = nullptr;
|
||||||
|
Device::CheckVkResult(vkMapMemory(Device::device, stagingMem, 0, VK_WHOLE_SIZE, 0, &mapped));
|
||||||
|
const std::uint8_t* src = static_cast<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);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
@ -71,5 +125,3 @@ export namespace Crafter {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -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
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -26,6 +26,34 @@ 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;
|
||||||
|
|
@ -35,5 +63,8 @@ namespace Crafter {
|
||||||
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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -116,5 +113,3 @@ export namespace Crafter {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
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) {
|
|
||||||
|
|
||||||
|
struct RenderPass {
|
||||||
|
virtual void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) = 0;
|
||||||
|
virtual ~RenderPass() = default;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -46,4 +43,3 @@ export namespace Crafter {
|
||||||
static void BuildTLAS(VkCommandBuffer cmd, std::uint32_t index);
|
static void BuildTLAS(VkCommandBuffer cmd, std::uint32_t index);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -41,5 +38,3 @@ export namespace Crafter {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -63,5 +60,3 @@ export namespace Crafter {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
interfaces/Crafter.Graphics-UI.cppm
Normal file
30
interfaces/Crafter.Graphics-UI.cppm
Normal 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;
|
||||||
100
interfaces/Crafter.Graphics-UIAtlas.cppm
Normal file
100
interfaces/Crafter.Graphics-UIAtlas.cppm
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
167
interfaces/Crafter.Graphics-UIDrawList.cppm
Normal file
167
interfaces/Crafter.Graphics-UIDrawList.cppm
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
interfaces/Crafter.Graphics-UIHit.cppm
Normal file
50
interfaces/Crafter.Graphics-UIHit.cppm
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
interfaces/Crafter.Graphics-UILayout.cppm
Normal file
73
interfaces/Crafter.Graphics-UILayout.cppm
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
interfaces/Crafter.Graphics-UILength.cppm
Normal file
98
interfaces/Crafter.Graphics-UILength.cppm
Normal 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};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
110
interfaces/Crafter.Graphics-UIRenderer.cppm
Normal file
110
interfaces/Crafter.Graphics-UIRenderer.cppm
Normal 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();
|
||||||
|
};
|
||||||
|
}
|
||||||
111
interfaces/Crafter.Graphics-UIScene.cppm
Normal file
111
interfaces/Crafter.Graphics-UIScene.cppm
Normal 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();
|
||||||
|
};
|
||||||
|
}
|
||||||
80
interfaces/Crafter.Graphics-UITheme.cppm
Normal file
80
interfaces/Crafter.Graphics-UITheme.cppm
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
189
interfaces/Crafter.Graphics-UIWidget.cppm
Normal file
189
interfaces/Crafter.Graphics-UIWidget.cppm
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
989
interfaces/Crafter.Graphics-UIWidgets.cppm
Normal file
989
interfaces/Crafter.Graphics-UIWidgets.cppm
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
@ -214,4 +211,3 @@ namespace Crafter {
|
||||||
VulkanBuffer& operator=(const VulkanBuffer&) = delete;
|
VulkanBuffer& operator=(const VulkanBuffer&) = delete;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -190,5 +187,3 @@ export namespace Crafter {
|
||||||
image_memory_barriers.data());
|
image_memory_barriers.data());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -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
1048
lib/stb_image_write.h
Normal file
File diff suppressed because it is too large
Load diff
39
project.cpp
39
project.cpp
|
|
@ -47,10 +47,8 @@ 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")) {
|
|
||||||
cfg.defines.push_back({"CRAFTER_GRAPHICS_RENDERER_VULKAN", ""});
|
|
||||||
ExternalDependency& vkHeaders = cfg.externalDependencies.emplace_back();
|
ExternalDependency& vkHeaders = cfg.externalDependencies.emplace_back();
|
||||||
vkHeaders.name = "Vulkan-Headers";
|
vkHeaders.name = "Vulkan-Headers";
|
||||||
vkHeaders.source.url = "https://github.com/KhronosGroup/Vulkan-Headers.git";
|
vkHeaders.source.url = "https://github.com/KhronosGroup/Vulkan-Headers.git";
|
||||||
|
|
@ -62,34 +60,37 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
vkUtility.builder = ExternalBuilder::None;
|
vkUtility.builder = ExternalBuilder::None;
|
||||||
vkUtility.includeDirs = { "include" };
|
vkUtility.includeDirs = { "include" };
|
||||||
cfg.linkFlags.push_back(windows ? "-lvulkan-1" : "-lvulkan");
|
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
192
shaders/ui.comp.glsl
Normal 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);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue