UI rewrite 3rd attempt
This commit is contained in:
parent
c9fd1b1585
commit
1f5697326c
48 changed files with 2155 additions and 6190 deletions
182
README.md
182
README.md
|
|
@ -1,111 +1,105 @@
|
||||||
# Crafter.Graphics
|
# Crafter.Graphics
|
||||||
|
|
||||||
Catcrafts' Vulkan-based graphics + UI library. C++20 modules, ray-traced 3D, compute-shader UI, fully bindless via `VK_EXT_descriptor_heap`.
|
Vulkan-based graphics library built around C++20 modules and the bindless
|
||||||
|
`VK_EXT_descriptor_heap` extension. Provides window management, ray
|
||||||
|
tracing, and a compute-shader-driven UI on a single, opinionated stack.
|
||||||
|
|
||||||
This is **V2** of the library — a from-scratch rewrite that replaces the
|
## What's in here
|
||||||
old `RenderingElement2D`-style UI (verbose, no batching, per-element
|
|
||||||
descriptor surgery) with a declarative widget tree rendered through a
|
|
||||||
single compute dispatch.
|
|
||||||
|
|
||||||
## Capabilities
|
- **Window** — Wayland and Win32 backends, swapchain ring, frame pacing,
|
||||||
|
input events. Pick a backend at build time via the target triple.
|
||||||
|
- **Device** — single-Vulkan-instance bring-up. The library targets
|
||||||
|
`VK_EXT_descriptor_heap` exclusively; pipelines are created with
|
||||||
|
`VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT` so there are no
|
||||||
|
descriptor-set layouts and push constants travel via
|
||||||
|
`vkCmdPushDataEXT`.
|
||||||
|
- **DescriptorHeapVulkan** — bindless slot allocator. `AllocateImageSlots`
|
||||||
|
/ `AllocateBufferSlots` / `AllocateSamplerSlots`, with byte-offset
|
||||||
|
helpers for direct descriptor writes.
|
||||||
|
- **VulkanBuffer\<T, Mapped\>** — typed buffer with optional host mapping
|
||||||
|
and a `FlushDevice` that issues the right host-write barrier.
|
||||||
|
- **ImageVulkan\<Pixel\>** — image + staging buffer, mip-chain support,
|
||||||
|
one-shot uploads via a command buffer.
|
||||||
|
- **PipelineRTVulkan / ShaderBindingTableVulkan / RTPass** — ray-tracing
|
||||||
|
pipeline, SBT, and a `RenderPass` that dispatches it.
|
||||||
|
- **ComputeShader** — the Tier 1 wrapper used by the UI system. Loads a
|
||||||
|
`.spv`, builds a heap-bound compute pipeline, dispatches with
|
||||||
|
`vkCmdPushDataEXT`. Use it directly for any custom compute.
|
||||||
|
- **UI** — three-tier UI system; see below.
|
||||||
|
- **FontAtlas** — single-channel SDF atlas (1024×1024, 32pt base,
|
||||||
|
shelf-packed, lazy `Ensure` per codepoint, dirty-flush via `Update`).
|
||||||
|
- **Mesh / RenderingElement3D / Animation** — BLAS/TLAS construction
|
||||||
|
and 3D scene plumbing for the ray-tracing path.
|
||||||
|
|
||||||
- **3D rendering** through `VK_KHR_ray_tracing_pipeline`. `RTPass` is the
|
## UI system (three tiers)
|
||||||
reusable wrapper; `Mesh` builds BLAS, `RenderingElement3D` builds TLAS.
|
|
||||||
- **2D / UI rendering** through one compute shader per frame. Widgets
|
|
||||||
emit `UIItem`s into a per-frame mapped SSBO; the shader scans it and
|
|
||||||
composites onto the swapchain image. SDF glyph atlas means one
|
|
||||||
texture covers all sizes / DPI scales.
|
|
||||||
- **Bindless descriptor model** via `VK_EXT_descriptor_heap` — one
|
|
||||||
resource heap + one sampler heap, bound once per frame. RT and UI
|
|
||||||
passes share the same heap.
|
|
||||||
- **`Window::passes`** — render passes are pluggable (`RenderPass*`
|
|
||||||
vector). Add `RTPass`, `UIScene`, your own pass, in any order. Window
|
|
||||||
inserts storage→storage barriers between consecutive passes.
|
|
||||||
- **Cross-platform window backend** — Wayland (with fractional scale +
|
|
||||||
XKB keyboard) or Win32, picked at compile time from the target triple.
|
|
||||||
|
|
||||||
## Quick start
|
The UI is *deliberately* layered to balance no-boilerplate against
|
||||||
|
no-lock-in:
|
||||||
|
|
||||||
|
- **Tier 1 — `ComputeShader`.** Load any `.spv`, dispatch with push
|
||||||
|
constants, library inserts inter-dispatch barriers. The escape hatch:
|
||||||
|
if the standard shaders don't fit, write your own compute and
|
||||||
|
dispatch it next to them.
|
||||||
|
- **Tier 2 — `UIRenderer` + standard shaders.** Four shipped compute
|
||||||
|
shaders (`drawQuads`, `drawCircles`, `drawImages`, `drawText`), POD
|
||||||
|
item structs (`QuadItem`, `CircleItem`, `ImageItem`, `GlyphItem`), a
|
||||||
|
shared GLSL contract in [shaders/ui-shared.glsl](shaders/ui-shared.glsl),
|
||||||
|
and helpers (`RegisterBuffer`, `RegisterImage`, `RegisterSampler`,
|
||||||
|
`FillHeader`, `Dispatch*`, `ShapeText`). You build your own per-shader
|
||||||
|
SSBOs (manual batching) and call one `Dispatch*` per shader type per
|
||||||
|
frame. Item array order = draw order.
|
||||||
|
- **Tier 3 — stateless presentation functions.** `DrawButton`,
|
||||||
|
`DrawCheckbox`, `DrawSlider`, `DrawProgressBar`. Each is a small
|
||||||
|
function that *appends* items to your buffers — they don't dispatch.
|
||||||
|
Colors come in as small inline `*Colors` aggregates, no library
|
||||||
|
`Theme` type. **The source is the customization API**: if a
|
||||||
|
component doesn't fit, copy its body and edit it. No virtual hooks,
|
||||||
|
no extension points.
|
||||||
|
|
||||||
|
What's *not* in the UI: widget tree, layout engine (just a `Rect::SubRect`
|
||||||
|
carving helper), theming, hit-testing, focus management. State for
|
||||||
|
interactive components (hover, drag, focus) lives in user-owned POD
|
||||||
|
structs, not the library.
|
||||||
|
|
||||||
|
### UI dispatch model
|
||||||
|
|
||||||
|
Standard shaders dispatch one workgroup per 8×8 *screen tile* — each
|
||||||
|
thread iterates every item in the SSBO in array order, accumulating
|
||||||
|
into a local `dst`, and stores once. Total cost is `O(W·H·N)`; works
|
||||||
|
well up to a few hundred items at 1080p. Splitting one buffer into
|
||||||
|
multiple dispatches doesn't help — the same total work plus barrier
|
||||||
|
overhead. If you need to render thousands of UI items, you want a
|
||||||
|
different shader (tile binning, per-item-list resolve), not more
|
||||||
|
dispatches.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
The repository is built with `crafter-build` (a project-config based
|
||||||
|
build system; the project description lives in `project.cpp`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build the library
|
crafter-build # build the library
|
||||||
cd Crafter.Graphics2
|
crafter-build -r # build and run (in an example directory)
|
||||||
crafter-build
|
|
||||||
|
|
||||||
# Build + run an example
|
|
||||||
cd examples/VulkanUI
|
|
||||||
crafter-build -r
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Build dependencies (cloned automatically): `Vulkan-Headers`,
|
The build picks the window backend automatically: Wayland on Linux,
|
||||||
`Vulkan-Utility-Libraries`, `Crafter.Event`, `Crafter.Math`,
|
Win32 on Windows / mingw. Cross-compile via the standard `--target=...`
|
||||||
`Crafter.Asset`. System dependencies: `clang++` with C++20 modules and
|
flag.
|
||||||
`libstd` PCM, `libvulkan`, `libwayland-client` + `xkbcommon`
|
|
||||||
(or `kernel32/user32/gdi32` on Windows).
|
|
||||||
|
|
||||||
## Module layout
|
|
||||||
|
|
||||||
The library is one C++20 module, `Crafter.Graphics`, with partitions
|
|
||||||
grouped by concern:
|
|
||||||
|
|
||||||
| Partition family | Purpose |
|
|
||||||
|------------------------|------------------------------------------------------------|
|
|
||||||
| `:Window`, `:Device` | Window + Vulkan instance/device |
|
|
||||||
| `:RenderPass`, `:RTPass` | Pluggable pass interface + ray-tracing helper |
|
|
||||||
| `:DescriptorHeapVulkan`, `:VulkanBuffer`, `:ImageVulkan`, `:SamplerVulkan` | Bindless heap + GPU buffers / images / samplers |
|
|
||||||
| `:PipelineRTVulkan`, `:ShaderVulkan`, `:ShaderBindingTableVulkan` | RT pipeline plumbing |
|
|
||||||
| `:Mesh`, `:RenderingElement3D` | BLAS / TLAS for ray tracing |
|
|
||||||
| `:Font` | TTF loading + UTF-8 metrics |
|
|
||||||
| `:UI*` | Widget tree, layout, hit-testing, theme, draw list, atlas, renderer, scene |
|
|
||||||
|
|
||||||
The umbrella `import Crafter.Graphics;` re-exports everything.
|
|
||||||
|
|
||||||
## UI architecture (one paragraph)
|
|
||||||
|
|
||||||
Widgets are value types with a fluent builder API. Composite containers
|
|
||||||
(`VStack`, `HStack`, `Stack`, `Overlay`, `TabView`, `ScrollView`) take
|
|
||||||
children as `&&` parameter packs and own them inside a `UIScene` arena.
|
|
||||||
Layout is two-pass measure/arrange (WPF / Avalonia / Flutter style)
|
|
||||||
with `Length::Px` / `Pct` / `Auto` / `Frac` units and DPI scaling
|
|
||||||
threaded through. Each frame, `UIScene` walks the tree, emits a flat
|
|
||||||
`UIItem` array into a mapped SSBO, and the compute shader scans it
|
|
||||||
front-to-back compositing rectangles, rounded rectangles, SDF glyphs,
|
|
||||||
and bindless images. Mouse clicks bubble through `OnMouseClick`; focus
|
|
||||||
+ `OnTextInput` / `OnKeyDown` route to the focused widget. Themes are
|
|
||||||
flat structs (`ButtonStyle`, `InputFieldStyle`) applied per-instance via
|
|
||||||
`.style(theme.primary)`.
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
- [`examples/VulkanTriangle`](examples/VulkanTriangle/) — minimal
|
See [examples/](examples/). Quick map:
|
||||||
ray-traced triangle. The reference for `RTPass` + descriptor heap
|
|
||||||
setup with no UI.
|
|
||||||
- [`examples/VulkanUI`](examples/VulkanUI/) — the full Phase 2/3
|
|
||||||
surface: stacks, themed buttons, progress bar, tab view, focusable
|
|
||||||
input fields with caret blink + key repeat.
|
|
||||||
- [`examples/VulkanAnimated`](examples/VulkanAnimated/) — `Observable<T>`
|
|
||||||
+ per-frame ticks driving live HUD bars and labels with no manual
|
|
||||||
invalidation.
|
|
||||||
|
|
||||||
## V1 known limitations
|
- [HelloWindow](examples/HelloWindow/) — minimal window, no rendering.
|
||||||
|
- [VulkanTriangle](examples/VulkanTriangle/) — ray-traced triangle, the
|
||||||
- Single-font, LTR-only text. No bold / italic / kerning beyond stb's
|
smallest test of the bindless + ray-tracing path.
|
||||||
default. No multi-line wrap or BiDi.
|
- [HelloUI](examples/HelloUI/) — UI smoke test using all three tiers
|
||||||
- No tile-binning in the UI compute shader; the naive front-to-back
|
(background quad, slider, progress bar, button with text label,
|
||||||
per-pixel scan handles a few hundred items effortlessly. Past
|
cursor-tracking circle).
|
||||||
~5,000 items the data layout supports tile-binning without an API
|
- [CustomShader](examples/CustomShader/) — Tier 1 demo: a user-authored
|
||||||
change.
|
compute shader inverting RGB under a list of item-circles, dispatched
|
||||||
- No render-target-to-texture for world-space UI yet.
|
alongside the standard `drawQuads`. The "could attempt #2 do this?" test.
|
||||||
- No animation primitives in the UI module — drive `Observable<T>`s
|
|
||||||
yourself from `onUpdate`.
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Phase 1, 2, and 3 V1 of the rewrite are complete:
|
|
||||||
ray-traced 3D path migrated to `RenderPass`, full UI widget set
|
|
||||||
rendering through compute, focus + keyboard input + Wayland key repeat
|
|
||||||
working end-to-end. Verified on NVIDIA GeForce RTX 4090 with
|
|
||||||
`VK_EXT_descriptor_heap` and `VK_LAYER_KHRONOS_validation` clean.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
LGPL v3 — see [LICENSE](LICENSE).
|
LGPL 3.0. See per-file headers and `LICENSE`.
|
||||||
|
|
|
||||||
61
examples/CustomShader/inverse-circle.comp.glsl
Normal file
61
examples/CustomShader/inverse-circle.comp.glsl
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Custom UI compute shader. Demonstrates the Tier 1 dispatch path:
|
||||||
|
// the user defines their own item struct, writes their own GLSL alongside
|
||||||
|
// the standard shaders (sharing the same UIDispatchHeader contract via
|
||||||
|
// ui-shared.glsl), and dispatches it via UIRenderer::Dispatch.
|
||||||
|
//
|
||||||
|
// What it does: each item is a circle. For every pixel the workgroup tile
|
||||||
|
// owns, if the pixel falls inside any item-circle, the pixel's RGB is
|
||||||
|
// inverted (1 - rgb). Composes naturally with whatever previous dispatches
|
||||||
|
// drew into the swapchain image — works on top of standard quads, custom
|
||||||
|
// effects, anything.
|
||||||
|
#version 460
|
||||||
|
#extension GL_GOOGLE_include_directive : enable
|
||||||
|
#include "ui-shared.glsl"
|
||||||
|
|
||||||
|
// Application-defined item: just (cx, cy, radius, _pad). Layout matches the
|
||||||
|
// C++ InverseCircleItem struct in main.cpp byte-for-byte under std430.
|
||||||
|
struct InverseCircleItem {
|
||||||
|
vec4 centerRadius;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(descriptor_heap, std430) readonly buffer InvCircleBuf {
|
||||||
|
InverseCircleItem items[];
|
||||||
|
} invCircleHeap[];
|
||||||
|
|
||||||
|
// NVIDIA workaround — same per-member-load pattern the library shaders use
|
||||||
|
// for the descriptor-heap'd SSBO composite-load issue.
|
||||||
|
InverseCircleItem LoadInverseCircleItem(uint heap, uint i) {
|
||||||
|
InverseCircleItem it;
|
||||||
|
it.centerRadius = invCircleHeap[heap].items[i].centerRadius;
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout(push_constant) uniform PC {
|
||||||
|
UIDispatchHeader hdr;
|
||||||
|
} pc;
|
||||||
|
|
||||||
|
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
ivec2 screenPx;
|
||||||
|
if (!uiResolveScreenPixel(pc.hdr, screenPx)) return;
|
||||||
|
|
||||||
|
vec2 sp = vec2(screenPx) + 0.5;
|
||||||
|
|
||||||
|
// Find the strongest circle coverage at this pixel — using max instead
|
||||||
|
// of literal per-item invert so overlapping circles don't double-invert
|
||||||
|
// back to the original.
|
||||||
|
float coverage = 0.0;
|
||||||
|
for (uint i = 0u; i < pc.hdr.itemCount; ++i) {
|
||||||
|
InverseCircleItem it = LoadInverseCircleItem(pc.hdr.itemBuffer, i);
|
||||||
|
vec2 c = it.centerRadius.xy;
|
||||||
|
float r = it.centerRadius.z;
|
||||||
|
float d = length(sp - c) - r;
|
||||||
|
coverage = max(coverage, clamp(0.5 - d, 0.0, 1.0));
|
||||||
|
}
|
||||||
|
if (coverage <= 0.0) return;
|
||||||
|
|
||||||
|
vec4 dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||||
|
dst.rgb = mix(dst.rgb, vec3(1.0) - dst.rgb, coverage);
|
||||||
|
imageStore(uiImages[pc.hdr.outImage], screenPx, dst);
|
||||||
|
}
|
||||||
105
examples/CustomShader/main.cpp
Normal file
105
examples/CustomShader/main.cpp
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
// Tier 1 demo: a user-authored compute shader dispatched alongside the
|
||||||
|
// standard ones. The custom shader inverts RGB in the area covered by a
|
||||||
|
// list of circles. The mouse-tracking circle moves; two static ones sit
|
||||||
|
// on a striped background drawn with the standard DrawQuads shader.
|
||||||
|
|
||||||
|
#include "vulkan/vulkan.h"
|
||||||
|
|
||||||
|
import Crafter.Graphics;
|
||||||
|
import Crafter.Event;
|
||||||
|
import std;
|
||||||
|
using namespace Crafter;
|
||||||
|
|
||||||
|
// Application-side item POD. Matches `struct InverseCircleItem { vec4
|
||||||
|
// centerRadius; }` in inverse-circle.comp.glsl byte-for-byte.
|
||||||
|
struct InverseCircleItem {
|
||||||
|
float cx, cy, radius, _pad;
|
||||||
|
};
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
Device::Initialize();
|
||||||
|
Window window(1280, 720, "Custom Shader");
|
||||||
|
|
||||||
|
VkCommandBuffer init = window.StartInit();
|
||||||
|
|
||||||
|
DescriptorHeapVulkan heap;
|
||||||
|
heap.Initialize(/*images*/ 8, /*buffers*/ 8, /*samplers*/ 4);
|
||||||
|
window.descriptorHeap = &heap;
|
||||||
|
|
||||||
|
UIRenderer ui;
|
||||||
|
ui.Initialize(window, heap, init);
|
||||||
|
window.passes.push_back(&ui);
|
||||||
|
|
||||||
|
// Load the user-authored shader. Same wrapper as the four shipped with
|
||||||
|
// the library — there is no privileged path.
|
||||||
|
ComputeShader inverseCircle;
|
||||||
|
inverseCircle.Load("inverse-circle.comp.spv");
|
||||||
|
|
||||||
|
// User-owned buffers.
|
||||||
|
VulkanBuffer<QuadItem, true> quadsBuf;
|
||||||
|
VulkanBuffer<InverseCircleItem, true> invBuf;
|
||||||
|
quadsBuf.Create(
|
||||||
|
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, 64);
|
||||||
|
invBuf.Create(
|
||||||
|
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, 16);
|
||||||
|
|
||||||
|
auto quadsSlot = ui.RegisterBuffer(quadsBuf);
|
||||||
|
auto invSlot = ui.RegisterBuffer(invBuf);
|
||||||
|
|
||||||
|
EventListener<UIBuildArgs> buildSub(&ui.onBuild, [&](UIBuildArgs a) {
|
||||||
|
VkCommandBuffer cmd = a.cmd;
|
||||||
|
|
||||||
|
Rect canvas = Rect::FromWindow(window);
|
||||||
|
|
||||||
|
// Six vertical stripes covering the canvas — gives the inverse
|
||||||
|
// circles something visibly different to invert.
|
||||||
|
std::array<std::array<float, 4>, 6> palette = {{
|
||||||
|
{0.95f, 0.30f, 0.30f, 1.0f},
|
||||||
|
{0.95f, 0.65f, 0.20f, 1.0f},
|
||||||
|
{0.95f, 0.95f, 0.20f, 1.0f},
|
||||||
|
{0.30f, 0.85f, 0.30f, 1.0f},
|
||||||
|
{0.20f, 0.55f, 0.95f, 1.0f},
|
||||||
|
{0.65f, 0.30f, 0.95f, 1.0f},
|
||||||
|
}};
|
||||||
|
std::uint32_t qc = 0;
|
||||||
|
float stripeW = canvas.w / 6.0f;
|
||||||
|
for (int i = 0; i < 6; ++i) {
|
||||||
|
quadsBuf.value[qc++] = QuadItem{
|
||||||
|
i * stripeW, 0, stripeW, canvas.h,
|
||||||
|
palette[i][0], palette[i][1], palette[i][2], palette[i][3],
|
||||||
|
0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Three inverse circles: one tracking the mouse, two stationary.
|
||||||
|
std::uint32_t ic = 0;
|
||||||
|
invBuf.value[ic++] = { window.currentMousePos.x, window.currentMousePos.y, 100.0f, 0.0f };
|
||||||
|
invBuf.value[ic++] = { canvas.w * 0.25f, canvas.h * 0.5f, 60.0f, 0.0f };
|
||||||
|
invBuf.value[ic++] = { canvas.w * 0.75f, canvas.h * 0.5f, 80.0f, 0.0f };
|
||||||
|
|
||||||
|
// Standard dispatch first — paints the stripes.
|
||||||
|
if (qc > 0) {
|
||||||
|
quadsBuf.FlushDevice(cmd, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
|
||||||
|
ui.DispatchQuads(cmd, quadsSlot, qc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom dispatch second — reads the stripes, inverts under
|
||||||
|
// circles, writes back. The library inserts the inter-dispatch
|
||||||
|
// SHADER_WRITE → SHADER_READ|WRITE barrier automatically.
|
||||||
|
if (ic > 0) {
|
||||||
|
invBuf.FlushDevice(cmd, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
|
||||||
|
struct PC { UIDispatchHeader hdr; } pc { ui.FillHeader(invSlot, ic) };
|
||||||
|
std::uint32_t gx = (window.width + 7) / 8;
|
||||||
|
std::uint32_t gy = (window.height + 7) / 8;
|
||||||
|
ui.Dispatch(cmd, inverseCircle, &pc, sizeof(pc), gx, gy, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.FinishInit();
|
||||||
|
window.Render();
|
||||||
|
window.StartUpdate();
|
||||||
|
window.StartSync();
|
||||||
|
}
|
||||||
|
|
@ -4,16 +4,15 @@ namespace fs = std::filesystem;
|
||||||
using namespace Crafter;
|
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());
|
|
||||||
Configuration* graphics = LocalProject({
|
Configuration* graphics = LocalProject({
|
||||||
.projectFile = "../../project.cpp",
|
.projectFile = "../../project.cpp",
|
||||||
.args = graphicsArgs,
|
.args = std::vector<std::string>(args.begin(), args.end()),
|
||||||
});
|
});
|
||||||
|
|
||||||
Configuration cfg;
|
Configuration cfg;
|
||||||
cfg.path = "./";
|
cfg.path = "./";
|
||||||
cfg.name = "VulkanAnimated";
|
cfg.name = "CustomShader";
|
||||||
cfg.outputName = "VulkanAnimated";
|
cfg.outputName = "CustomShader";
|
||||||
ApplyStandardArgs(cfg, args);
|
ApplyStandardArgs(cfg, args);
|
||||||
cfg.dependencies = { graphics };
|
cfg.dependencies = { graphics };
|
||||||
|
|
||||||
|
|
@ -21,7 +20,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
std::array<fs::path, 1> impls = { "main" };
|
std::array<fs::path, 1> impls = { "main" };
|
||||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||||
|
|
||||||
cfg.files.push_back("Inter.ttf");
|
cfg.shaders.emplace_back(fs::path("inverse-circle.comp.glsl"), std::string("main"), ShaderType::Compute);
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
|
@ -1,27 +0,0 @@
|
||||||
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 = "Forts3DMainMenu";
|
|
||||||
cfg.outputName = "Forts3DMainMenu";
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
BIN
examples/HelloUI/font.ttf
Normal file
BIN
examples/HelloUI/font.ttf
Normal file
Binary file not shown.
180
examples/HelloUI/main.cpp
Normal file
180
examples/HelloUI/main.cpp
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
// Smoke test for the Tier 1+2+3 UI architecture. Opens a window, draws a
|
||||||
|
// background, a button (Tier 3 component), a slider (Tier 3 component), a
|
||||||
|
// progress bar (Tier 3 component), and a circle that follows the mouse
|
||||||
|
// (Tier 2 standard shader, dispatched directly). Hit-testing for the button
|
||||||
|
// and slider is the user's responsibility — see the onMouseMove listener.
|
||||||
|
|
||||||
|
#include "vulkan/vulkan.h"
|
||||||
|
|
||||||
|
import Crafter.Graphics;
|
||||||
|
import Crafter.Event;
|
||||||
|
import std;
|
||||||
|
using namespace Crafter;
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
Device::Initialize();
|
||||||
|
Window window(1280, 720, "Hello UI");
|
||||||
|
|
||||||
|
VkCommandBuffer init = window.StartInit();
|
||||||
|
|
||||||
|
DescriptorHeapVulkan heap;
|
||||||
|
heap.Initialize(/*images*/ 16, /*buffers*/ 16, /*samplers*/ 4);
|
||||||
|
window.descriptorHeap = &heap;
|
||||||
|
|
||||||
|
Font font("font.ttf");
|
||||||
|
|
||||||
|
FontAtlas atlas;
|
||||||
|
atlas.Initialize(init);
|
||||||
|
|
||||||
|
UIRenderer ui;
|
||||||
|
ui.fontAtlas = &atlas;
|
||||||
|
ui.Initialize(window, heap, init);
|
||||||
|
window.passes.push_back(&ui);
|
||||||
|
|
||||||
|
// User-owned per-shader buffers. Mapped, written each frame, dispatched
|
||||||
|
// by the user. Capacity is up to the user; resize means re-Register.
|
||||||
|
VulkanBuffer<QuadItem, true> quadsBuf;
|
||||||
|
VulkanBuffer<CircleItem, true> circlesBuf;
|
||||||
|
VulkanBuffer<GlyphItem, true> glyphsBuf;
|
||||||
|
quadsBuf.Create(
|
||||||
|
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, 256);
|
||||||
|
circlesBuf.Create(
|
||||||
|
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, 64);
|
||||||
|
glyphsBuf.Create(
|
||||||
|
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, 4096);
|
||||||
|
|
||||||
|
auto quadsSlot = ui.RegisterBuffer(quadsBuf);
|
||||||
|
auto circlesSlot = ui.RegisterBuffer(circlesBuf);
|
||||||
|
auto glyphsSlot = ui.RegisterBuffer(glyphsBuf);
|
||||||
|
|
||||||
|
// Application-side state. Library doesn't track this; the user owns it.
|
||||||
|
Rect btnRect{};
|
||||||
|
Rect sliderRect{};
|
||||||
|
bool hovered = false;
|
||||||
|
bool sliderHovered = false;
|
||||||
|
bool dragging = false;
|
||||||
|
float sliderT = 0.42f;
|
||||||
|
float progress = 0.0f;
|
||||||
|
|
||||||
|
// Hit-testing — purely user code. EventListener objects hold the
|
||||||
|
// subscription; their destructors unregister at scope exit.
|
||||||
|
EventListener<void> moveSub(&window.onMouseMove, [&]() {
|
||||||
|
float mx = window.currentMousePos.x;
|
||||||
|
float my = window.currentMousePos.y;
|
||||||
|
hovered = btnRect.Contains(mx, my);
|
||||||
|
sliderHovered = sliderRect.Contains(mx, my);
|
||||||
|
if (dragging && sliderRect.w > 0) {
|
||||||
|
sliderT = std::clamp((mx - sliderRect.x) / sliderRect.w, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
EventListener<void> clickSub(&window.onMouseLeftClick, [&]() {
|
||||||
|
if (sliderHovered) dragging = true;
|
||||||
|
});
|
||||||
|
EventListener<void> releaseSub(&window.onMouseLeftRelease, [&]() {
|
||||||
|
dragging = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Application color palette. No library Theme — just a struct.
|
||||||
|
ButtonColors btnPalette{
|
||||||
|
.bg = {0.20f, 0.22f, 0.28f, 1.0f},
|
||||||
|
.bgHover = {0.30f, 0.55f, 0.95f, 1.0f},
|
||||||
|
.bgPressed = {0.18f, 0.40f, 0.78f, 1.0f},
|
||||||
|
.text = {1.0f, 1.0f, 1.0f, 1.0f},
|
||||||
|
.border = {0.0f, 0.0f, 0.0f, 0.0f},
|
||||||
|
.cornerRadius = 8.0f,
|
||||||
|
.borderThickness = 0.0f,
|
||||||
|
};
|
||||||
|
SliderColors sliderPalette{
|
||||||
|
.track = {0.20f, 0.20f, 0.25f, 1.0f},
|
||||||
|
.trackFilled = {0.30f, 0.55f, 0.95f, 1.0f},
|
||||||
|
.thumb = {0.85f, 0.85f, 0.90f, 1.0f},
|
||||||
|
.thumbHover = {1.00f, 1.00f, 1.00f, 1.0f},
|
||||||
|
.trackHeight = 6.0f,
|
||||||
|
.thumbRadius = 10.0f,
|
||||||
|
};
|
||||||
|
ProgressColors progressPalette{
|
||||||
|
.bg = {0.15f, 0.15f, 0.18f, 1.0f},
|
||||||
|
.fill = {0.40f, 0.85f, 0.50f, 1.0f},
|
||||||
|
.cornerRadius = 4.0f,
|
||||||
|
};
|
||||||
|
|
||||||
|
EventListener<UIBuildArgs> buildSub(&ui.onBuild, [&](UIBuildArgs a) {
|
||||||
|
VkCommandBuffer cmd = a.cmd;
|
||||||
|
|
||||||
|
// Update demo progress.
|
||||||
|
progress = std::fmod(progress + 0.005f, 1.0f);
|
||||||
|
|
||||||
|
// Layout via SubRect — resize-safe.
|
||||||
|
Rect canvas = Rect::FromWindow(window);
|
||||||
|
Rect topBar = canvas.SubRect(80, Rect::Anchor::Top).Inset(20, 20, 10, 20);
|
||||||
|
btnRect = topBar.SubRect(160, Rect::Anchor::Left);
|
||||||
|
sliderRect = canvas.Inset(60).SubRect(20, Rect::Anchor::Top);
|
||||||
|
sliderRect.y = canvas.h * 0.5f;
|
||||||
|
sliderRect.x = 60.0f;
|
||||||
|
sliderRect.w = canvas.w - 120.0f;
|
||||||
|
Rect progressRect{ 60.0f, canvas.h * 0.5f + 60.0f, canvas.w - 120.0f, 16.0f };
|
||||||
|
|
||||||
|
// Reset per-frame counters.
|
||||||
|
std::uint32_t qc = 0, cc = 0, gc = 0;
|
||||||
|
|
||||||
|
UIBuffer buf{
|
||||||
|
.quads = quadsBuf.value,
|
||||||
|
.quadCount = &qc,
|
||||||
|
.quadCap = 256,
|
||||||
|
.glyphs = glyphsBuf.value,
|
||||||
|
.glyphCount = &gc,
|
||||||
|
.glyphCap = 4096,
|
||||||
|
.atlas = &atlas,
|
||||||
|
.renderer = &ui,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Background quad — required because the swapchain is not cleared
|
||||||
|
// (no TRANSFER_DST_BIT on the swapchain image).
|
||||||
|
QuadItem bg{
|
||||||
|
canvas.x, canvas.y, canvas.w, canvas.h,
|
||||||
|
0.f, 0, 0, 1.f,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
};
|
||||||
|
if (qc < buf.quadCap) buf.quads[qc++] = bg;
|
||||||
|
|
||||||
|
// Tier 3 components.
|
||||||
|
DrawButton(buf, btnRect,
|
||||||
|
hovered ? "Hovered!" : "Hover me",
|
||||||
|
hovered, /*pressed*/ false,
|
||||||
|
font, 18.0f, btnPalette);
|
||||||
|
|
||||||
|
DrawSlider(buf, sliderRect, sliderT, dragging, sliderPalette);
|
||||||
|
|
||||||
|
DrawProgressBar(buf, progressRect, progress, progressPalette);
|
||||||
|
|
||||||
|
// Tier 2 standard shader, used directly: a circle following the mouse.
|
||||||
|
circlesBuf.value[cc++] = CircleItem{
|
||||||
|
window.currentMousePos.x, window.currentMousePos.y, 6.0f, 0.0f,
|
||||||
|
1.0f, 1.0f, 1.0f, 0.85f,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Flush + dispatch. The library inserts the inter-dispatch barriers.
|
||||||
|
if (qc > 0) {
|
||||||
|
quadsBuf.FlushDevice(cmd, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
|
||||||
|
ui.DispatchQuads(cmd, quadsSlot, qc);
|
||||||
|
}
|
||||||
|
if (cc > 0) {
|
||||||
|
circlesBuf.FlushDevice(cmd, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
|
||||||
|
ui.DispatchCircles(cmd, circlesSlot, cc);
|
||||||
|
}
|
||||||
|
if (gc > 0) {
|
||||||
|
glyphsBuf.FlushDevice(cmd, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
|
||||||
|
ui.DispatchText(cmd, glyphsSlot, gc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.FinishInit();
|
||||||
|
window.Render();
|
||||||
|
window.StartUpdate();
|
||||||
|
window.StartSync();
|
||||||
|
}
|
||||||
|
|
@ -4,16 +4,15 @@ namespace fs = std::filesystem;
|
||||||
using namespace Crafter;
|
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());
|
|
||||||
Configuration* graphics = LocalProject({
|
Configuration* graphics = LocalProject({
|
||||||
.projectFile = "../../project.cpp",
|
.projectFile = "../../project.cpp",
|
||||||
.args = graphicsArgs,
|
.args = std::vector<std::string>(args.begin(), args.end()),
|
||||||
});
|
});
|
||||||
|
|
||||||
Configuration cfg;
|
Configuration cfg;
|
||||||
cfg.path = "./";
|
cfg.path = "./";
|
||||||
cfg.name = "VulkanUI";
|
cfg.name = "HelloUI";
|
||||||
cfg.outputName = "VulkanUI";
|
cfg.outputName = "HelloUI";
|
||||||
ApplyStandardArgs(cfg, args);
|
ApplyStandardArgs(cfg, args);
|
||||||
cfg.dependencies = { graphics };
|
cfg.dependencies = { graphics };
|
||||||
|
|
||||||
|
|
@ -21,7 +20,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
std::array<fs::path, 1> impls = { "main" };
|
std::array<fs::path, 1> impls = { "main" };
|
||||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||||
|
|
||||||
cfg.files.push_back("Inter.ttf");
|
cfg.files.push_back("font.ttf");
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
63
examples/README.md
Normal file
63
examples/README.md
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
Each example is a self-contained `crafter-build` project that depends on
|
||||||
|
the parent `Crafter.Graphics` via `LocalProject`. To build and run any
|
||||||
|
of them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/<name>
|
||||||
|
crafter-build -r
|
||||||
|
```
|
||||||
|
|
||||||
|
## Index
|
||||||
|
|
||||||
|
### [HelloWindow](HelloWindow/)
|
||||||
|
Minimum viable program: open a window, run the event loop. No Vulkan
|
||||||
|
rendering. Useful as a smoke test for `Device::Initialize` + `Window` +
|
||||||
|
the platform backend.
|
||||||
|
|
||||||
|
### [VulkanTriangle](VulkanTriangle/)
|
||||||
|
Ray-traced single triangle through `vkCmdTraceRaysKHR`. Shows the full
|
||||||
|
ray-tracing setup: `DescriptorHeapVulkan` with image and buffer slots,
|
||||||
|
`PipelineRTVulkan` from raygen / miss / closesthit SPIR-V, BLAS via
|
||||||
|
`Mesh::Build`, TLAS via `RenderingElement3D::BuildTLAS`, direct
|
||||||
|
`vkWriteResourceDescriptorsEXT` for swapchain views, `RTPass` on
|
||||||
|
`window.passes`. Smallest test of the bindless ray-tracing path.
|
||||||
|
|
||||||
|
### [HelloUI](HelloUI/)
|
||||||
|
Compute-shader UI demo using all three UI tiers:
|
||||||
|
|
||||||
|
- **Tier 3** components: `DrawButton`, `DrawSlider`, `DrawProgressBar`,
|
||||||
|
composed via `Rect::SubRect` for resize-safe layout.
|
||||||
|
- **Tier 2** standard shaders: `DispatchQuads` for the background and
|
||||||
|
components, `DispatchCircles` for a cursor-tracking dot,
|
||||||
|
`DispatchText` for the button label (with the FontAtlas wired up to
|
||||||
|
`UIRenderer`).
|
||||||
|
- **Tier 1** is available too — any custom `ComputeShader` registered
|
||||||
|
on the same heap can be dispatched alongside the standard ones.
|
||||||
|
|
||||||
|
Hit-testing and animation are user code (see the `EventListener`
|
||||||
|
subscriptions on `window.onMouseMove` / `onMouseLeftClick`); the
|
||||||
|
library does not track widgets or focus.
|
||||||
|
|
||||||
|
Drop a TTF in this directory as `font.ttf` before running (the example
|
||||||
|
loads it via `Font("font.ttf")`).
|
||||||
|
|
||||||
|
### [CustomShader](CustomShader/)
|
||||||
|
Tier 1 demo: a user-authored compute shader (`inverse-circle.comp.glsl`)
|
||||||
|
running alongside the shipped `drawQuads`. The custom shader inverts RGB
|
||||||
|
under each item-circle — exactly the kind of effect attempt #2's closed
|
||||||
|
shader couldn't express. Shows:
|
||||||
|
|
||||||
|
- Defining your own item POD struct in C++ + matching `std430` struct
|
||||||
|
in GLSL.
|
||||||
|
- `#include "../../shaders/ui-shared.glsl"` for the bindless heap
|
||||||
|
declarations + `UIDispatchHeader` push-constant contract.
|
||||||
|
- `ComputeShader::Load` for the `.spv`, `UIRenderer::RegisterBuffer`
|
||||||
|
for your SSBO, `FillHeader` to populate the standard prefix, and
|
||||||
|
`UIRenderer::Dispatch` to launch — the same pattern the standard
|
||||||
|
shaders use under the hood.
|
||||||
|
- The inter-dispatch SHADER_WRITE → SHADER_READ|WRITE barrier is
|
||||||
|
inserted automatically, so the custom shader sees the colored stripes
|
||||||
|
drawn by the prior `DispatchQuads` and reads/writes the swapchain
|
||||||
|
image safely.
|
||||||
Binary file not shown.
|
|
@ -1,55 +0,0 @@
|
||||||
# VulkanAnimated
|
|
||||||
|
|
||||||
A live HUD demo: three `Observable<float>`s drive `ProgressBar`s, three
|
|
||||||
`Observable<std::string>`s drive `Text` labels, and an FPS readout in
|
|
||||||
the corner ticks every frame. Everything updates from a single
|
|
||||||
`onUpdate` listener — no `Invalidate()` / `Redraw()` calls.
|
|
||||||
|
|
||||||
## What it shows
|
|
||||||
|
|
||||||
- **`Observable<T>` data flow**: change a value in `onUpdate`, the
|
|
||||||
next frame's `RebuildFrame` re-emits the draw list with the new value
|
|
||||||
automatically. No tree rebuild.
|
|
||||||
- **`ProgressBar::bindValue(obs, lo, hi)`** — the bar fill normalises the
|
|
||||||
observable's current value into 0..1 each frame.
|
|
||||||
- **`Text::bind(observable)`** — the displayed string is sourced from
|
|
||||||
the observable each frame, replacing any baked-in runs.
|
|
||||||
- **Composition pattern**: a small lambda helper builds one HP/MP-style
|
|
||||||
row (`Text` + `ProgressBar` inside an `HStack`) given the observables
|
|
||||||
and a colour, then the row is dropped into the parent `VStack` like
|
|
||||||
any other widget.
|
|
||||||
- **Different update rates per observable**: `health` oscillates at 0.7
|
|
||||||
rad/s, `mana` at 1.3, `charge` advances linearly modulo 1 — visible
|
|
||||||
proof that each observable updates independently.
|
|
||||||
|
|
||||||
## Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd examples/VulkanAnimated
|
|
||||||
crafter-build -r
|
|
||||||
```
|
|
||||||
|
|
||||||
You should see "Animated HUD" with three coloured bars (red HP, blue MP,
|
|
||||||
yellow Charge) all moving at different rates, with the FPS readout in
|
|
||||||
the top-right ticking once per frame.
|
|
||||||
|
|
||||||
Click `Quit` to exit.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
The whole tick handler is just:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
EventListener<FrameTime> tick(&window.onUpdate, [&](FrameTime ft){
|
|
||||||
t += ft.delta.count();
|
|
||||||
health = 0.5f + 0.5f * std::sin(t * 0.7f);
|
|
||||||
mana = std::abs(std::sin(t * 1.3f));
|
|
||||||
charge = std::fmod(t * 0.3f, 1.0f);
|
|
||||||
healthLabel = std::format("HP {:>3.0f} / 100", health.Get() * 100);
|
|
||||||
// …
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
That's the entire animation system. There is deliberately no
|
|
||||||
`Animation<T>` / tween primitive in the library — drive observables
|
|
||||||
from any source you like.
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
#include "vulkan/vulkan.h"
|
|
||||||
|
|
||||||
import Crafter.Graphics;
|
|
||||||
import Crafter.Event;
|
|
||||||
import Crafter.Math;
|
|
||||||
import std;
|
|
||||||
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
// A simple "game HUD" demo: three Observable<float> drive ProgressBars at
|
|
||||||
// different rates / colours, while Observable<std::string> labels feed the
|
|
||||||
// Text widgets next to them. UIScene::RebuildFrame re-emits each frame, so
|
|
||||||
// the only application code needed is "update the observables in
|
|
||||||
// onUpdate" — no manual Invalidate / Redraw calls.
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
Device::Initialize();
|
|
||||||
Window window(1280, 720, "VulkanAnimated");
|
|
||||||
window.StartInit();
|
|
||||||
window.FinishInit();
|
|
||||||
|
|
||||||
Font font("Inter.ttf");
|
|
||||||
UI::Theme theme = UI::themes::default_dark();
|
|
||||||
|
|
||||||
UI::UIScene scene;
|
|
||||||
scene.Initialize(window, "ui.comp.spv");
|
|
||||||
scene.background(UI::Color{0.06f, 0.07f, 0.10f, 1.0f});
|
|
||||||
|
|
||||||
// ─── Observables ─────────────────────────────────────────────────────
|
|
||||||
UI::Observable<float> health{1.0f};
|
|
||||||
UI::Observable<float> mana {0.5f};
|
|
||||||
UI::Observable<float> charge{0.0f};
|
|
||||||
UI::Observable<std::string> healthLabel;
|
|
||||||
UI::Observable<std::string> manaLabel;
|
|
||||||
UI::Observable<std::string> chargeLabel;
|
|
||||||
UI::Observable<std::string> fpsLabel;
|
|
||||||
|
|
||||||
// ─── Per-frame tick: drive the observables. UIScene re-emits on
|
|
||||||
// onUpdate, so any read of these values is automatically picked
|
|
||||||
// up in the next frame's draw list.
|
|
||||||
float t = 0.0f;
|
|
||||||
EventListener<FrameTime> tick(&window.onUpdate, [&](FrameTime ft) {
|
|
||||||
const float dt = static_cast<float>(ft.delta.count());
|
|
||||||
t += dt;
|
|
||||||
|
|
||||||
// Three offset waves at different rates / phases.
|
|
||||||
health = 0.5f + 0.5f * std::sin(t * 0.7f);
|
|
||||||
mana = std::abs(std::sin(t * 1.3f));
|
|
||||||
charge = std::fmod(t * 0.3f, 1.0f);
|
|
||||||
|
|
||||||
healthLabel = std::format("HP {:>3.0f} / 100", health.Get() * 100.0f);
|
|
||||||
manaLabel = std::format("MP {:>3.0f} / 100", mana.Get() * 100.0f);
|
|
||||||
chargeLabel = std::format("Charge {:>3.0f}%", charge.Get() * 100.0f);
|
|
||||||
fpsLabel = (dt > 0.0f) ? std::format("{:>5.1f} fps", 1.0f / dt) : std::string{"---.- fps"};
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Helper: one HP/MP-style row (label on the left, bar on the right).
|
|
||||||
// Build into a local (avoids returning a reference to a temporary
|
|
||||||
// that dies at the end of the chain expression).
|
|
||||||
auto bar = [&](UI::Observable<std::string>& label,
|
|
||||||
UI::Observable<float>& value,
|
|
||||||
UI::Color fg) -> UI::HStack {
|
|
||||||
UI::HStack h;
|
|
||||||
h.width(UI::Length::Frac(1))
|
|
||||||
.spacing(12)
|
|
||||||
.children(
|
|
||||||
UI::Text{}.bind(label).font(font).size(16)
|
|
||||||
.width(UI::Length::Px(160)),
|
|
||||||
UI::ProgressBar{}
|
|
||||||
.bindValue(value, 0.0f, 1.0f)
|
|
||||||
.foreground(fg)
|
|
||||||
.size(UI::Length::Frac(1), UI::Length::Px(20))
|
|
||||||
);
|
|
||||||
return h;
|
|
||||||
};
|
|
||||||
|
|
||||||
scene.Root(
|
|
||||||
UI::VStack{}
|
|
||||||
.padding(28)
|
|
||||||
.spacing(16)
|
|
||||||
.children(
|
|
||||||
UI::HStack{}
|
|
||||||
.width(UI::Length::Frac(1))
|
|
||||||
.children(
|
|
||||||
UI::Text{"Animated HUD"}.font(font).size(28),
|
|
||||||
UI::Spacer{},
|
|
||||||
UI::Text{}.bind(fpsLabel).font(font).size(16)
|
|
||||||
.color(UI::Color{0.55f, 0.85f, 1.0f, 1.0f})
|
|
||||||
),
|
|
||||||
|
|
||||||
UI::Text{"Three Observable<float>s drive the bars; "
|
|
||||||
"Observable<std::string>s drive the labels."}
|
|
||||||
.font(font).size(14).color(UI::Color{0.65f, 0.65f, 0.65f, 1}),
|
|
||||||
|
|
||||||
bar(healthLabel, health, UI::Color{0.90f, 0.30f, 0.30f, 1.0f}),
|
|
||||||
bar(manaLabel, mana, UI::Color{0.30f, 0.55f, 0.95f, 1.0f}),
|
|
||||||
bar(chargeLabel, charge, UI::Color{0.95f, 0.85f, 0.30f, 1.0f}),
|
|
||||||
|
|
||||||
UI::Spacer{},
|
|
||||||
|
|
||||||
UI::HStack{}
|
|
||||||
.width(UI::Length::Frac(1))
|
|
||||||
.spacing(8)
|
|
||||||
.children(
|
|
||||||
UI::Spacer{},
|
|
||||||
UI::Button{"Quit"}.font(font).style(theme.danger)
|
|
||||||
.onClick([]{ std::_Exit(0); })
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
window.Render();
|
|
||||||
window.StartUpdate();
|
|
||||||
window.StartSync();
|
|
||||||
}
|
|
||||||
|
|
@ -34,17 +34,17 @@ void main() {
|
||||||
1.0
|
1.0
|
||||||
));
|
));
|
||||||
|
|
||||||
traceRayEXT(
|
// traceRayEXT(
|
||||||
topLevelAS[bufferStart],
|
// topLevelAS[bufferStart],
|
||||||
gl_RayFlagsNoneEXT,
|
// gl_RayFlagsNoneEXT,
|
||||||
0xff,
|
// 0xff,
|
||||||
0, 0, 0,
|
// 0, 0, 0,
|
||||||
origin,
|
// origin,
|
||||||
0.001,
|
// 0.001,
|
||||||
direction,
|
// direction,
|
||||||
10000.0,
|
// 10000.0,
|
||||||
0
|
// 0
|
||||||
);
|
// );
|
||||||
|
|
||||||
imageStore(image[0], ivec2(pixel), vec4(hitValue, 1));
|
imageStore(image[0], ivec2(pixel), vec4(hitValue, 1));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,52 +0,0 @@
|
||||||
# VulkanUI
|
|
||||||
|
|
||||||
A walking tour of the V1 widget set. UI-only (no 3D pass), so the entire
|
|
||||||
visible image comes from one compute dispatch per frame.
|
|
||||||
|
|
||||||
## What it shows
|
|
||||||
|
|
||||||
- **Layout**: nested `VStack` / `HStack` / `Spacer` / `TabView`, fluent
|
|
||||||
builder API, `Length::Px` / `Pct` / `Frac` / `Auto` units, DPI scaling
|
|
||||||
(your `Window::scale` flows through automatically).
|
|
||||||
- **Theming**: `themes::default_dark()` with `theme.primary` / `secondary`
|
|
||||||
/ `danger` / `input` styles applied per-widget via `.style(...)`.
|
|
||||||
- **Text**: per-run colour styling via `TextRun`, an em-dash in the
|
|
||||||
header to confirm UTF-8 decoding works end-to-end.
|
|
||||||
- **Buttons**: rounded background (SDF in the shader), centred SDF
|
|
||||||
glyphs, `onClick` callbacks. Quit calls `_Exit(0)` so a working click
|
|
||||||
visibly closes the window.
|
|
||||||
- **Progress bar**: a `ProgressBar` at 42 %.
|
|
||||||
- **TabView**: three tabs (Graphics / Input / Audio); clicking the
|
|
||||||
tab bar swaps content.
|
|
||||||
- **InputField**: focusable text edits with caret blink, UTF-8 typing,
|
|
||||||
Backspace / Delete / arrow keys / Home / End, key repeat, horizontal
|
|
||||||
scrolling that keeps the caret visible, clipping that prevents
|
|
||||||
overflow from drawing past the field's bounds.
|
|
||||||
|
|
||||||
## Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd examples/VulkanUI
|
|
||||||
crafter-build -r
|
|
||||||
```
|
|
||||||
|
|
||||||
## Interactions to try
|
|
||||||
|
|
||||||
| Action | Expected |
|
|
||||||
|---|---|
|
|
||||||
| Click `Play` / `Options` | Prints `[click] ...` to stderr |
|
|
||||||
| Click `Quit` | App exits |
|
|
||||||
| Click a tab label (Graphics / Input / Audio) | Tab body swaps |
|
|
||||||
| Click an `InputField` | Border turns blue, caret appears and blinks |
|
|
||||||
| Type | Characters appear at the caret, including multi-byte UTF-8 |
|
|
||||||
| Hold a letter | After ~500 ms the character starts repeating at ~25 Hz |
|
|
||||||
| Backspace / Delete | Removes one full UTF-8 codepoint |
|
|
||||||
| ← / → / Home / End | Moves the caret |
|
|
||||||
| Type past the right edge | Text scrolls left, caret stays visible |
|
|
||||||
| Click outside any input | Caret disappears (focus cleared) |
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
The shader (`shaders/ui.comp.glsl` in the library) is compiled to
|
|
||||||
`ui.comp.spv` next to the binary by the build system.
|
|
||||||
The font (`Inter.ttf`) is bundled via `cfg.files.push_back`.
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
#include "vulkan/vulkan.h"
|
|
||||||
|
|
||||||
import Crafter.Graphics;
|
|
||||||
import Crafter.Event;
|
|
||||||
import Crafter.Math;
|
|
||||||
import std;
|
|
||||||
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
Device::Initialize();
|
|
||||||
Window window(1280, 720, "VulkanUI");
|
|
||||||
window.StartInit();
|
|
||||||
window.FinishInit();
|
|
||||||
|
|
||||||
Font font("Inter.ttf");
|
|
||||||
|
|
||||||
UI::Theme theme = UI::themes::default_dark();
|
|
||||||
theme.defaultFont = &font;
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
|
||||||
// Wire the scene: it auto-creates a descriptor heap, plugs into the
|
|
||||||
// window's pass list, hooks mouse + update events, and drives a
|
|
||||||
// compute-shader UI pass per frame.
|
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
|
||||||
UI::UIScene scene;
|
|
||||||
scene.Initialize(window, "ui.comp.spv");
|
|
||||||
scene.background(UI::Color{0.06f, 0.07f, 0.10f, 1.0f});
|
|
||||||
|
|
||||||
scene.Root(
|
|
||||||
UI::VStack{}
|
|
||||||
.padding(20)
|
|
||||||
.spacing(12)
|
|
||||||
.children(
|
|
||||||
UI::Text{"Crafter UI — V1"}.font(font).size(28),
|
|
||||||
|
|
||||||
UI::Text{}.font(font).size(14).runs(
|
|
||||||
UI::TextRun{"Click "},
|
|
||||||
UI::TextRun{"Quit"}.color(UI::Color{1.0f, 0.55f, 0.55f}).bold(),
|
|
||||||
UI::TextRun{" to close the window. Tabs switch on click. "},
|
|
||||||
UI::TextRun{"Have fun!"}.color(UI::Color{0.55f, 0.85f, 1.0f})
|
|
||||||
),
|
|
||||||
|
|
||||||
UI::HStack{}
|
|
||||||
.width(UI::Length::Frac(1))
|
|
||||||
.spacing(8)
|
|
||||||
.children(
|
|
||||||
UI::Button{"Play"} .font(font).style(theme.primary) .onClick([]{ std::println(std::cerr, "[click] Play"); }),
|
|
||||||
UI::Button{"Options"}.font(font).style(theme.secondary).onClick([]{ std::println(std::cerr, "[click] Options"); }),
|
|
||||||
UI::Spacer{},
|
|
||||||
UI::Button{"Quit"} .font(font).style(theme.danger) .onClick([]{ std::println(std::cerr, "[click] Quit"); std::_Exit(0); })
|
|
||||||
),
|
|
||||||
|
|
||||||
UI::ProgressBar{}
|
|
||||||
.value(0.42f)
|
|
||||||
.size(UI::Length::Frac(1), UI::Length::Px(20))
|
|
||||||
.foreground(theme.primary.background),
|
|
||||||
|
|
||||||
UI::TabView{}
|
|
||||||
.font(font)
|
|
||||||
.width(UI::Length::Frac(1))
|
|
||||||
.height(UI::Length::Px(220))
|
|
||||||
.tab("Graphics", UI::VStack{}.padding(8).spacing(8).children(
|
|
||||||
UI::Text{"Resolution"}.font(font).size(14),
|
|
||||||
UI::InputField{"1920x1080"}.font(font).style(theme.input),
|
|
||||||
UI::Text{"Max lights"}.font(font).size(14),
|
|
||||||
UI::InputField{"32"}.font(font).style(theme.input)
|
|
||||||
))
|
|
||||||
.tab("Input", UI::VStack{}.padding(8).spacing(8).children(
|
|
||||||
UI::Text{"Mouse sensitivity"}.font(font).size(14),
|
|
||||||
UI::InputField{"1.0"}.font(font).style(theme.input)
|
|
||||||
))
|
|
||||||
.tab("Audio", UI::VStack{}.padding(8).spacing(8).children(
|
|
||||||
UI::Text{"Master volume"}.font(font).size(14),
|
|
||||||
UI::InputField{"80"}.font(font).style(theme.input)
|
|
||||||
))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
window.Render();
|
|
||||||
window.SaveFrame("frame.png");
|
|
||||||
|
|
||||||
window.StartUpdate(); // continuous rendering — UIScene re-emits per frame
|
|
||||||
window.StartSync();
|
|
||||||
}
|
|
||||||
91
implementations/Crafter.Graphics-ComputeShader.cpp
Normal file
91
implementations/Crafter.Graphics-ComputeShader.cpp
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
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:ComputeShader_impl;
|
||||||
|
import :ComputeShader;
|
||||||
|
import :ShaderVulkan;
|
||||||
|
import :Device;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using namespace Crafter;
|
||||||
|
|
||||||
|
ComputeShader::ComputeShader(ComputeShader&& other) noexcept : pipeline(other.pipeline) {
|
||||||
|
other.pipeline = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ComputeShader& ComputeShader::operator=(ComputeShader&& other) noexcept {
|
||||||
|
if (this != &other) {
|
||||||
|
if (pipeline != VK_NULL_HANDLE) {
|
||||||
|
vkDestroyPipeline(Device::device, pipeline, nullptr);
|
||||||
|
}
|
||||||
|
pipeline = other.pipeline;
|
||||||
|
other.pipeline = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ComputeShader::~ComputeShader() {
|
||||||
|
if (pipeline != VK_NULL_HANDLE) {
|
||||||
|
vkDestroyPipeline(Device::device, pipeline, nullptr);
|
||||||
|
pipeline = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ComputeShader::Load(const std::filesystem::path& spvPath) {
|
||||||
|
VulkanShader shader(spvPath, "main", VK_SHADER_STAGE_COMPUTE_BIT, nullptr);
|
||||||
|
|
||||||
|
// Spec: with VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT, layout MUST be
|
||||||
|
// VK_NULL_HANDLE — bindings come from the bound descriptor heap and push
|
||||||
|
// constants are pushed via vkCmdPushDataEXT instead of vkCmdPushConstants.
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ComputeShader::Dispatch(VkCommandBuffer cmd,
|
||||||
|
const void* push, std::uint32_t pushBytes,
|
||||||
|
std::uint32_t gx,
|
||||||
|
std::uint32_t gy,
|
||||||
|
std::uint32_t gz) const {
|
||||||
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
||||||
|
if (push != nullptr && pushBytes > 0) {
|
||||||
|
VkPushDataInfoEXT pushInfo {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PUSH_DATA_INFO_EXT,
|
||||||
|
.offset = 0,
|
||||||
|
.data = { .address = const_cast<void*>(push), .size = pushBytes },
|
||||||
|
};
|
||||||
|
Device::vkCmdPushDataEXT(cmd, &pushInfo);
|
||||||
|
}
|
||||||
|
vkCmdDispatch(cmd, gx, gy, gz);
|
||||||
|
}
|
||||||
|
|
@ -386,8 +386,10 @@ void Device::PointerListenerHandleEnter(void* data, wl_pointer* wl_pointer, std:
|
||||||
Device::wlPointer = wl_pointer;
|
Device::wlPointer = wl_pointer;
|
||||||
for(Window* window : windows) {
|
for(Window* window : windows) {
|
||||||
if(window->surface == surface) {
|
if(window->surface == surface) {
|
||||||
|
window->lastPointerSerial_ = serial;
|
||||||
if(window->cursorSurface != nullptr) {
|
if(window->cursorSurface != nullptr) {
|
||||||
wl_pointer_set_cursor(wl_pointer, serial, window->cursorSurface, 0, 0);
|
wl_pointer_set_cursor(wl_pointer, serial, window->cursorSurface,
|
||||||
|
window->cursorHotspotX_, window->cursorHotspotY_);
|
||||||
}
|
}
|
||||||
focusedWindow = window;
|
focusedWindow = window;
|
||||||
window->onMouseEnter.Invoke();
|
window->onMouseEnter.Invoke();
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,14 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
module;
|
module;
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
#include "../lib/stb_truetype.h"
|
#include "../lib/stb_truetype.h"
|
||||||
module Crafter.Graphics:UIAtlas_impl;
|
module Crafter.Graphics:FontAtlas_impl;
|
||||||
import :UIAtlas;
|
import :FontAtlas;
|
||||||
import :Font;
|
import :Font;
|
||||||
import :ImageVulkan;
|
import :ImageVulkan;
|
||||||
import :Device;
|
import :Device;
|
||||||
import std;
|
import std;
|
||||||
|
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
using namespace Crafter::UI;
|
|
||||||
|
|
||||||
void FontAtlas::Initialize(VkCommandBuffer cmd) {
|
void FontAtlas::Initialize(VkCommandBuffer cmd) {
|
||||||
image.Create(
|
image.Create(
|
||||||
393
implementations/Crafter.Graphics-UI.cpp
Normal file
393
implementations/Crafter.Graphics-UI.cpp
Normal file
|
|
@ -0,0 +1,393 @@
|
||||||
|
/*
|
||||||
|
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:UI_impl;
|
||||||
|
import :UI;
|
||||||
|
import :ComputeShader;
|
||||||
|
import :Device;
|
||||||
|
import :Window;
|
||||||
|
import :DescriptorHeapVulkan;
|
||||||
|
import :ImageVulkan;
|
||||||
|
import :VulkanBuffer;
|
||||||
|
import :FontAtlas;
|
||||||
|
import :Font;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using namespace Crafter;
|
||||||
|
|
||||||
|
// ─── Initialize ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void UIRenderer::Initialize(Window& window, DescriptorHeapVulkan& heap, VkCommandBuffer initCmd,
|
||||||
|
std::filesystem::path quadsSpv,
|
||||||
|
std::filesystem::path circlesSpv,
|
||||||
|
std::filesystem::path imagesSpv,
|
||||||
|
std::filesystem::path textSpv) {
|
||||||
|
window_ = &window;
|
||||||
|
heap_ = &heap;
|
||||||
|
|
||||||
|
// Load the four standard pipelines.
|
||||||
|
drawQuads.Load(quadsSpv);
|
||||||
|
drawCircles.Load(circlesSpv);
|
||||||
|
drawImages.Load(imagesSpv);
|
||||||
|
drawText.Load(textSpv);
|
||||||
|
|
||||||
|
// Allocate one image slot for the swapchain output. Each per-frame heap
|
||||||
|
// copy will hold ITS frame's image at this slot.
|
||||||
|
auto outRange = heap_->AllocateImageSlots(1);
|
||||||
|
outImageSlot_ = outRange.firstElement;
|
||||||
|
|
||||||
|
WriteSwapchainDescriptors();
|
||||||
|
|
||||||
|
// Optional font-atlas registration (user must have called atlas->Initialize
|
||||||
|
// already before reaching here, so atlas->image is live).
|
||||||
|
if (fontAtlas != nullptr) {
|
||||||
|
auto atlasImg = heap_->AllocateImageSlots(1);
|
||||||
|
fontAtlasImageSlot_ = atlasImg.firstElement;
|
||||||
|
fontAtlasSamplerSlot_ = RegisterLinearClampSampler();
|
||||||
|
WriteFontAtlasDescriptor();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the host-mapped descriptor heaps so the GPU sees what we wrote.
|
||||||
|
for (auto& h : heap_->resourceHeap) h.FlushDevice();
|
||||||
|
for (auto& h : heap_->samplerHeap) h.FlushDevice();
|
||||||
|
|
||||||
|
(void)initCmd; // reserved for future image-layout tweaks
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── per-frame Record ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void UIRenderer::Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) {
|
||||||
|
// Reset per-frame state.
|
||||||
|
firstDispatchThisFrame_ = true;
|
||||||
|
|
||||||
|
// If text is in use, flush any glyphs that user-side ShapeText calls
|
||||||
|
// produced during a previous frame's onBuild. (Ensure() during the
|
||||||
|
// current onBuild also marks the atlas dirty; that's flushed on the
|
||||||
|
// NEXT Record. For v1 this is fine because the current frame's text
|
||||||
|
// dispatch reads whatever's already been uploaded, and brand-new glyphs
|
||||||
|
// missing from the atlas this frame will simply render blank for one
|
||||||
|
// frame and resolve next frame. To get them this frame, the user can
|
||||||
|
// call atlas->Update(cmd) themselves at the top of onBuild.)
|
||||||
|
if (fontAtlas != nullptr && fontAtlas->dirty) {
|
||||||
|
fontAtlas->Update(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBuild.Invoke({cmd, frameIdx});
|
||||||
|
|
||||||
|
(void)window;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── header builder ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
UIDispatchHeader UIRenderer::FillHeader(std::uint32_t itemBufferSlot,
|
||||||
|
std::uint32_t itemCount,
|
||||||
|
std::array<float,4> clipRectPx,
|
||||||
|
std::uint32_t flags) const noexcept {
|
||||||
|
UIDispatchHeader h{};
|
||||||
|
h.outImage = outImageSlot_;
|
||||||
|
h.itemBuffer = itemBufferSlot;
|
||||||
|
h.surfaceWidth = window_->width;
|
||||||
|
h.surfaceHeight = window_->height;
|
||||||
|
h.clipX = clipRectPx[0];
|
||||||
|
h.clipY = clipRectPx[1];
|
||||||
|
h.clipW = clipRectPx[2];
|
||||||
|
h.clipH = clipRectPx[3];
|
||||||
|
h.itemCount = itemCount;
|
||||||
|
h.frameIdx = window_->currentBuffer;
|
||||||
|
h.flags = flags;
|
||||||
|
h._pad = 0;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── group-count helper ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Number of 8-pixel tiles needed to cover `dim` pixels (rounded up).
|
||||||
|
inline std::uint32_t TilesFor(std::uint32_t dim) {
|
||||||
|
return (dim + 7u) / 8u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── standard-shader convenience dispatches ─────────────────────────────
|
||||||
|
//
|
||||||
|
// All four standard shaders use the same pixel-tile dispatch model: one
|
||||||
|
// workgroup per 8×8 screen tile, each thread iterates every item in order
|
||||||
|
// inside the workgroup, accumulating into a local register. This guarantees
|
||||||
|
// "items in the buffer render in order" (later items overdraw earlier ones)
|
||||||
|
// without inter-workgroup races on imageLoad/imageStore — the bug that the
|
||||||
|
// per-item dispatch model had.
|
||||||
|
|
||||||
|
void UIRenderer::DispatchQuads(VkCommandBuffer cmd, std::uint32_t bufferSlot,
|
||||||
|
std::uint32_t itemCount,
|
||||||
|
std::array<float,4> clipRectPx) {
|
||||||
|
if (itemCount == 0) return;
|
||||||
|
struct PC { UIDispatchHeader hdr; } pc { FillHeader(bufferSlot, itemCount, clipRectPx) };
|
||||||
|
Dispatch(cmd, drawQuads, &pc, sizeof(pc),
|
||||||
|
TilesFor(window_->width), TilesFor(window_->height), 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIRenderer::DispatchCircles(VkCommandBuffer cmd, std::uint32_t bufferSlot,
|
||||||
|
std::uint32_t itemCount,
|
||||||
|
std::array<float,4> clipRectPx) {
|
||||||
|
if (itemCount == 0) return;
|
||||||
|
struct PC { UIDispatchHeader hdr; } pc { FillHeader(bufferSlot, itemCount, clipRectPx) };
|
||||||
|
Dispatch(cmd, drawCircles, &pc, sizeof(pc),
|
||||||
|
TilesFor(window_->width), TilesFor(window_->height), 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIRenderer::DispatchImages(VkCommandBuffer cmd, std::uint32_t bufferSlot,
|
||||||
|
std::uint32_t itemCount,
|
||||||
|
std::array<float,4> clipRectPx) {
|
||||||
|
if (itemCount == 0) return;
|
||||||
|
struct PC { UIDispatchHeader hdr; } pc { FillHeader(bufferSlot, itemCount, clipRectPx) };
|
||||||
|
Dispatch(cmd, drawImages, &pc, sizeof(pc),
|
||||||
|
TilesFor(window_->width), TilesFor(window_->height), 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIRenderer::DispatchText(VkCommandBuffer cmd, std::uint32_t bufferSlot,
|
||||||
|
std::uint32_t itemCount,
|
||||||
|
std::array<float,4> clipRectPx) {
|
||||||
|
if (itemCount == 0) return;
|
||||||
|
if (fontAtlasImageSlot_ == 0xFFFF) {
|
||||||
|
throw std::runtime_error("UIRenderer::DispatchText: no FontAtlas registered (set fontAtlas before Initialize)");
|
||||||
|
}
|
||||||
|
// Flush any glyphs that ShapeText calls (during this onBuild) just
|
||||||
|
// rasterised, so the dispatch below sees them.
|
||||||
|
if (fontAtlas != nullptr && fontAtlas->dirty) {
|
||||||
|
fontAtlas->Update(cmd);
|
||||||
|
}
|
||||||
|
struct PC {
|
||||||
|
UIDispatchHeader hdr;
|
||||||
|
std::uint32_t fontTextureSlot;
|
||||||
|
std::uint32_t fontSamplerSlot;
|
||||||
|
std::uint32_t _p0;
|
||||||
|
std::uint32_t _p1;
|
||||||
|
} pc {
|
||||||
|
FillHeader(bufferSlot, itemCount, clipRectPx),
|
||||||
|
fontAtlasImageSlot_,
|
||||||
|
fontAtlasSamplerSlot_,
|
||||||
|
0, 0
|
||||||
|
};
|
||||||
|
Dispatch(cmd, drawText, &pc, sizeof(pc),
|
||||||
|
TilesFor(window_->width), TilesFor(window_->height), 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── generic Dispatch (with barrier) ────────────────────────────────────
|
||||||
|
|
||||||
|
void UIRenderer::Dispatch(VkCommandBuffer cmd, const ComputeShader& shader,
|
||||||
|
const void* push, std::uint32_t pushBytes,
|
||||||
|
std::uint32_t gx, std::uint32_t gy, std::uint32_t gz) {
|
||||||
|
if (!firstDispatchThisFrame_) {
|
||||||
|
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(cmd,
|
||||||
|
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||||
|
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||||
|
0, 1, &mb, 0, nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
firstDispatchThisFrame_ = false;
|
||||||
|
shader.Dispatch(cmd, push, pushBytes, gx, gy, gz);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── descriptor writes ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void UIRenderer::WriteSwapchainDescriptors() {
|
||||||
|
// Each per-frame heap holds ITS swapchain image at outImageSlot_.
|
||||||
|
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 = &window_->imageViews[f],
|
||||||
|
.layout = VK_IMAGE_LAYOUT_GENERAL,
|
||||||
|
};
|
||||||
|
resources[f] = {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||||
|
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
||||||
|
.data = { .pImage = &infos[f] },
|
||||||
|
};
|
||||||
|
destinations[f] = {
|
||||||
|
.address = heap_->resourceHeap[f].value
|
||||||
|
+ heap_->ImageByteOffset(outImageSlot_),
|
||||||
|
.size = Device::descriptorHeapProperties.imageDescriptorSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Device::vkWriteResourceDescriptorsEXT(
|
||||||
|
Device::device, Window::numFrames, resources.data(), destinations.data()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIRenderer::WriteFontAtlasDescriptor() {
|
||||||
|
atlasViewCreateInfo_ = {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||||
|
.image = fontAtlas->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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
WriteSampledImageDescriptor(fontAtlasImageSlot_,
|
||||||
|
atlasViewCreateInfo_,
|
||||||
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIRenderer::WriteSampledImageDescriptor(std::uint16_t slot,
|
||||||
|
const VkImageViewCreateInfo& viewInfo,
|
||||||
|
VkImageLayout layout) {
|
||||||
|
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 = &viewInfo,
|
||||||
|
.layout = layout,
|
||||||
|
};
|
||||||
|
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(slot),
|
||||||
|
.size = Device::descriptorHeapProperties.imageDescriptorSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Device::vkWriteResourceDescriptorsEXT(
|
||||||
|
Device::device, Window::numFrames, resources.data(), destinations.data()
|
||||||
|
);
|
||||||
|
for (auto& h : heap_->resourceHeap) h.FlushDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIRenderer::WriteBufferDescriptor(std::uint16_t slot, VkDeviceAddress address, std::uint32_t size) {
|
||||||
|
std::array<VkDeviceAddressRangeEXT, Window::numFrames> ranges{};
|
||||||
|
std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
|
||||||
|
std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
|
||||||
|
|
||||||
|
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
||||||
|
ranges[f] = { .address = address, .size = size };
|
||||||
|
resources[f] = {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
||||||
|
.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||||
|
.data = { .pAddressRange = &ranges[f] },
|
||||||
|
};
|
||||||
|
destinations[f] = {
|
||||||
|
.address = heap_->resourceHeap[f].value + heap_->BufferByteOffset(slot),
|
||||||
|
.size = Device::descriptorHeapProperties.bufferDescriptorSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Device::vkWriteResourceDescriptorsEXT(
|
||||||
|
Device::device, Window::numFrames, resources.data(), destinations.data()
|
||||||
|
);
|
||||||
|
for (auto& h : heap_->resourceHeap) h.FlushDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint16_t UIRenderer::RegisterSampler(const VkSamplerCreateInfo& info) {
|
||||||
|
auto range = heap_->AllocateSamplerSlots(1);
|
||||||
|
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(range.firstElement),
|
||||||
|
.size = Device::descriptorHeapProperties.samplerDescriptorSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Device::vkWriteSamplerDescriptorsEXT(
|
||||||
|
Device::device, Window::numFrames, infos.data(), destinations.data()
|
||||||
|
);
|
||||||
|
for (auto& h : heap_->samplerHeap) h.FlushDevice();
|
||||||
|
return range.firstElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint16_t UIRenderer::RegisterLinearClampSampler() {
|
||||||
|
VkSamplerCreateInfo s {
|
||||||
|
.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,
|
||||||
|
};
|
||||||
|
return RegisterSampler(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── ShapeText ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
std::uint32_t UIRenderer::ShapeText(Font& font, float pxSize,
|
||||||
|
float x, float baselineY,
|
||||||
|
std::string_view utf8,
|
||||||
|
std::array<float,4> color,
|
||||||
|
GlyphItem* out, std::uint32_t outCapacity,
|
||||||
|
float* outAdvance) {
|
||||||
|
if (fontAtlas == nullptr) {
|
||||||
|
throw std::runtime_error("UIRenderer::ShapeText: no FontAtlas (set fontAtlas before Initialize)");
|
||||||
|
}
|
||||||
|
|
||||||
|
const float scale = pxSize / FontAtlas::kBaseSize;
|
||||||
|
float cursor = x;
|
||||||
|
std::uint32_t written = 0;
|
||||||
|
|
||||||
|
std::size_t i = 0;
|
||||||
|
while (i < utf8.size() && written < outCapacity) {
|
||||||
|
std::uint32_t cp = DecodeUtf8(utf8, i);
|
||||||
|
if (cp == 0) break;
|
||||||
|
if (cp == '\n') { /* single-line shaper — ignore */ continue; }
|
||||||
|
|
||||||
|
fontAtlas->Ensure(font, cp);
|
||||||
|
const Glyph* g = fontAtlas->Lookup(font, cp);
|
||||||
|
if (g == nullptr) continue;
|
||||||
|
|
||||||
|
// Empty glyph (whitespace) — advance only.
|
||||||
|
if (g->w > 0 && g->h > 0) {
|
||||||
|
GlyphItem& gi = out[written++];
|
||||||
|
gi.x = cursor + g->xoff * scale;
|
||||||
|
gi.y = baselineY + g->yoff * scale;
|
||||||
|
gi.w = g->w * scale;
|
||||||
|
gi.h = g->h * scale;
|
||||||
|
gi.u0 = g->u0; gi.v0 = g->v0;
|
||||||
|
gi.u1 = g->u1; gi.v1 = g->v1;
|
||||||
|
gi.r = color[0]; gi.g = color[1]; gi.b = color[2]; gi.a = color[3];
|
||||||
|
}
|
||||||
|
cursor += g->advance * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outAdvance) *outAdvance = cursor - x;
|
||||||
|
return written;
|
||||||
|
}
|
||||||
186
implementations/Crafter.Graphics-UIComponents.cpp
Normal file
186
implementations/Crafter.Graphics-UIComponents.cpp
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
/*
|
||||||
|
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;
|
||||||
|
module Crafter.Graphics:UIComponents_impl;
|
||||||
|
import :UIComponents;
|
||||||
|
import :UI;
|
||||||
|
import :Font;
|
||||||
|
import :FontAtlas;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using namespace Crafter;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Push one QuadItem into buf if there's room. No-op if full.
|
||||||
|
inline void PushQuad(UIBuffer& buf, const QuadItem& q) {
|
||||||
|
if (buf.quads == nullptr || buf.quadCount == nullptr) return;
|
||||||
|
if (*buf.quadCount >= buf.quadCap) return;
|
||||||
|
buf.quads[(*buf.quadCount)++] = q;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::array<float, 4> Pick(const std::array<float, 4>& a,
|
||||||
|
const std::array<float, 4>& b, bool useB) {
|
||||||
|
return useB ? b : a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Centered single-line text emit. Appends glyphs at the buffer's tail,
|
||||||
|
// then offsets them so the run is horizontally centered around `centerX`.
|
||||||
|
// Vertical baseline is placed at the centerY plus a coarse ascent estimate
|
||||||
|
// (fontSize * 0.32). Returns the advance width that was written.
|
||||||
|
float EmitCenteredLabel(UIBuffer& buf, std::string_view label,
|
||||||
|
Font& font, float fontSize,
|
||||||
|
float centerX, float centerY,
|
||||||
|
std::array<float, 4> color)
|
||||||
|
{
|
||||||
|
if (label.empty() || buf.atlas == nullptr || buf.renderer == nullptr) return 0.0f;
|
||||||
|
if (buf.glyphs == nullptr || buf.glyphCount == nullptr) return 0.0f;
|
||||||
|
|
||||||
|
std::uint32_t before = *buf.glyphCount;
|
||||||
|
std::uint32_t cap = (buf.glyphCap > before) ? (buf.glyphCap - before) : 0;
|
||||||
|
if (cap == 0) return 0.0f;
|
||||||
|
|
||||||
|
GlyphItem* writePos = buf.glyphs + before;
|
||||||
|
float baseline = centerY + fontSize * 0.32f;
|
||||||
|
float advance = 0.0f;
|
||||||
|
std::uint32_t n = buf.renderer->ShapeText(
|
||||||
|
font, fontSize, centerX, baseline,
|
||||||
|
label, color, writePos, cap, &advance
|
||||||
|
);
|
||||||
|
*buf.glyphCount = before + n;
|
||||||
|
|
||||||
|
// Center: shift each glyph's x by -advance/2.
|
||||||
|
float shift = -advance * 0.5f;
|
||||||
|
for (std::uint32_t i = 0; i < n; ++i) {
|
||||||
|
writePos[i].x += shift;
|
||||||
|
}
|
||||||
|
return advance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── DrawButton ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void Crafter::DrawButton(UIBuffer& buf, Rect r, std::string_view label,
|
||||||
|
bool hovered, bool pressed,
|
||||||
|
Font& font, float fontSize,
|
||||||
|
const ButtonColors& c)
|
||||||
|
{
|
||||||
|
auto bg = pressed ? c.bgPressed : (hovered ? c.bgHover : c.bg);
|
||||||
|
PushQuad(buf, QuadItem{
|
||||||
|
r.x, r.y, r.w, r.h,
|
||||||
|
bg[0], bg[1], bg[2], bg[3],
|
||||||
|
c.cornerRadius, c.cornerRadius, c.cornerRadius, c.cornerRadius,
|
||||||
|
c.borderThickness, c.border[0], c.border[1], c.border[2],
|
||||||
|
});
|
||||||
|
|
||||||
|
EmitCenteredLabel(buf, label, font, fontSize,
|
||||||
|
r.x + r.w * 0.5f, r.y + r.h * 0.5f, c.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── DrawCheckbox ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void Crafter::DrawCheckbox(UIBuffer& buf, Rect r, bool checked, bool hovered,
|
||||||
|
const CheckboxColors& c)
|
||||||
|
{
|
||||||
|
auto bg = Pick(c.bg, c.bgHover, hovered);
|
||||||
|
PushQuad(buf, QuadItem{
|
||||||
|
r.x, r.y, r.w, r.h,
|
||||||
|
bg[0], bg[1], bg[2], bg[3],
|
||||||
|
c.cornerRadius, c.cornerRadius, c.cornerRadius, c.cornerRadius,
|
||||||
|
c.borderThickness, c.border[0], c.border[1], c.border[2],
|
||||||
|
});
|
||||||
|
if (checked) {
|
||||||
|
Rect inner = r.Inset(c.checkInset);
|
||||||
|
if (inner.w > 0 && inner.h > 0) {
|
||||||
|
float innerR = std::max(0.0f, c.cornerRadius - c.checkInset);
|
||||||
|
PushQuad(buf, QuadItem{
|
||||||
|
inner.x, inner.y, inner.w, inner.h,
|
||||||
|
c.check[0], c.check[1], c.check[2], c.check[3],
|
||||||
|
innerR, innerR, innerR, innerR,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── DrawSlider ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void Crafter::DrawSlider(UIBuffer& buf, Rect r, float t01, bool dragging,
|
||||||
|
const SliderColors& c)
|
||||||
|
{
|
||||||
|
t01 = std::clamp(t01, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
// Track is a thin centered horizontal strip.
|
||||||
|
float trackY = r.y + (r.h - c.trackHeight) * 0.5f;
|
||||||
|
float trackR = c.trackHeight * 0.5f;
|
||||||
|
float fillW = r.w * t01;
|
||||||
|
|
||||||
|
if (fillW > 0.0f) {
|
||||||
|
PushQuad(buf, QuadItem{
|
||||||
|
r.x, trackY, fillW, c.trackHeight,
|
||||||
|
c.trackFilled[0], c.trackFilled[1], c.trackFilled[2], c.trackFilled[3],
|
||||||
|
trackR, trackR, trackR, trackR,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (fillW < r.w) {
|
||||||
|
PushQuad(buf, QuadItem{
|
||||||
|
r.x + fillW, trackY, r.w - fillW, c.trackHeight,
|
||||||
|
c.track[0], c.track[1], c.track[2], c.track[3],
|
||||||
|
trackR, trackR, trackR, trackR,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumb: a quad with cornerRadius = thumbRadius (= a circle).
|
||||||
|
auto thumbColor = Pick(c.thumb, c.thumbHover, dragging);
|
||||||
|
float thumbCx = r.x + r.w * t01;
|
||||||
|
float thumbCy = r.y + r.h * 0.5f;
|
||||||
|
float d = c.thumbRadius * 2.0f;
|
||||||
|
PushQuad(buf, QuadItem{
|
||||||
|
thumbCx - c.thumbRadius, thumbCy - c.thumbRadius, d, d,
|
||||||
|
thumbColor[0], thumbColor[1], thumbColor[2], thumbColor[3],
|
||||||
|
c.thumbRadius, c.thumbRadius, c.thumbRadius, c.thumbRadius,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── DrawProgressBar ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void Crafter::DrawProgressBar(UIBuffer& buf, Rect r, float t01,
|
||||||
|
const ProgressColors& c)
|
||||||
|
{
|
||||||
|
t01 = std::clamp(t01, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
PushQuad(buf, QuadItem{
|
||||||
|
r.x, r.y, r.w, r.h,
|
||||||
|
c.bg[0], c.bg[1], c.bg[2], c.bg[3],
|
||||||
|
c.cornerRadius, c.cornerRadius, c.cornerRadius, c.cornerRadius,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
float fillW = r.w * t01;
|
||||||
|
if (fillW > 0.0f) {
|
||||||
|
PushQuad(buf, QuadItem{
|
||||||
|
r.x, r.y, fillW, r.h,
|
||||||
|
c.fill[0], c.fill[1], c.fill[2], c.fill[3],
|
||||||
|
c.cornerRadius, c.cornerRadius, c.cornerRadius, c.cornerRadius,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,354 +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 "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).
|
|
||||||
}
|
|
||||||
|
|
@ -1,171 +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 "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. We capture FrameTime here
|
|
||||||
// so we can advance the scene's clock (caret blink, animations).
|
|
||||||
updateListener_ = std::make_unique<EventListener<FrameTime>>(
|
|
||||||
&window.onUpdate,
|
|
||||||
[this](FrameTime ft) {
|
|
||||||
elapsedSec_ += static_cast<float>(ft.delta.count());
|
|
||||||
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;
|
|
||||||
drawList.time = elapsedSec_;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
@ -516,109 +516,113 @@ void Window::SetTitle(const std::string_view title) {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void Window::SetCusorImage(std::uint16_t sizeX, std::uint16_t sizeY) {
|
void Window::SetCursorImage(std::uint16_t width, std::uint16_t height,
|
||||||
|
std::uint16_t hotspotX, std::uint16_t hotspotY,
|
||||||
|
const std::uint8_t* pixels) {
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||||
|
if (width == 0 || height == 0 || pixels == nullptr) {
|
||||||
|
SetDefaultCursor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (cursorSurface == nullptr) {
|
if (cursorSurface == nullptr) {
|
||||||
cursorSurface = wl_compositor_create_surface(Device::compositor);
|
cursorSurface = wl_compositor_create_surface(Device::compositor);
|
||||||
} else {
|
|
||||||
wl_buffer_destroy(cursorWlBuffer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int stride = sizeX * 4;
|
int stride = width * 4;
|
||||||
int size = stride * sizeY;
|
int size = stride * height;
|
||||||
cursorBufferOldSize = size;
|
|
||||||
|
// Reuse the existing mmap+buffer if the size is unchanged; otherwise
|
||||||
|
// tear down and re-allocate.
|
||||||
|
if (cursorWlBuffer != nullptr &&
|
||||||
|
cursorBufferOldSize == static_cast<std::uint32_t>(size)) {
|
||||||
|
// size unchanged — keep the buffer and mmap.
|
||||||
|
} else {
|
||||||
|
if (cursorMmap_) {
|
||||||
|
munmap(cursorMmap_, cursorBufferOldSize);
|
||||||
|
cursorMmap_ = nullptr;
|
||||||
|
}
|
||||||
|
if (cursorWlBuffer) {
|
||||||
|
wl_buffer_destroy(cursorWlBuffer);
|
||||||
|
cursorWlBuffer = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// Allocate a shared memory file with the right size
|
|
||||||
int fd = create_shm_file(size);
|
int fd = create_shm_file(size);
|
||||||
if (fd < 0) {
|
if (fd < 0) {
|
||||||
throw std::runtime_error(std::format("creating a buffer file for {}B failed", size));
|
throw std::runtime_error(std::format(
|
||||||
|
"Window::SetCursorImage: shm allocation for {}B failed", size));
|
||||||
}
|
}
|
||||||
|
void* mapped = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||||
|
if (mapped == MAP_FAILED) {
|
||||||
|
close(fd);
|
||||||
|
throw std::runtime_error("Window::SetCursorImage: mmap failed");
|
||||||
|
}
|
||||||
|
cursorMmap_ = static_cast<std::uint8_t*>(mapped);
|
||||||
|
|
||||||
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, width, height, stride, WL_SHM_FORMAT_ARGB8888);
|
||||||
wl_shm_pool_destroy(pool);
|
wl_shm_pool_destroy(pool);
|
||||||
|
|
||||||
close(fd);
|
close(fd);
|
||||||
|
|
||||||
|
cursorBufferOldSize = static_cast<std::uint32_t>(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the user's straight-alpha RGBA8 pixels into the compositor's
|
||||||
|
// expected premultiplied BGRA8 (= ARGB8888 little-endian byte order).
|
||||||
|
for (int i = 0; i < width * height; ++i) {
|
||||||
|
std::uint8_t r = pixels[i * 4 + 0];
|
||||||
|
std::uint8_t g = pixels[i * 4 + 1];
|
||||||
|
std::uint8_t b = pixels[i * 4 + 2];
|
||||||
|
std::uint8_t a = pixels[i * 4 + 3];
|
||||||
|
cursorMmap_[i * 4 + 0] = static_cast<std::uint8_t>((b * a) / 255);
|
||||||
|
cursorMmap_[i * 4 + 1] = static_cast<std::uint8_t>((g * a) / 255);
|
||||||
|
cursorMmap_[i * 4 + 2] = static_cast<std::uint8_t>((r * a) / 255);
|
||||||
|
cursorMmap_[i * 4 + 3] = a;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursorHotspotX_ = hotspotX;
|
||||||
|
cursorHotspotY_ = hotspotY;
|
||||||
|
|
||||||
wl_surface_attach(cursorSurface, cursorWlBuffer, 0, 0);
|
wl_surface_attach(cursorSurface, cursorWlBuffer, 0, 0);
|
||||||
wl_surface_damage(cursorSurface, 0, 0, sizeX, sizeY);
|
wl_surface_damage(cursorSurface, 0, 0, width, height);
|
||||||
wl_surface_commit(cursorSurface);
|
wl_surface_commit(cursorSurface);
|
||||||
|
|
||||||
|
// If the pointer is currently inside our window, re-apply the cursor
|
||||||
|
// so the new hotspot takes effect immediately. Otherwise the next
|
||||||
|
// pointer-enter event will pick it up.
|
||||||
|
if (Device::wlPointer && Device::focusedWindow == this && lastPointerSerial_) {
|
||||||
|
wl_pointer_set_cursor(Device::wlPointer, lastPointerSerial_,
|
||||||
|
cursorSurface, hotspotX, hotspotY);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
||||||
if (cursorBitmap) {
|
// Win32 cursor support is not implemented for the v2 Window.
|
||||||
DeleteObject(cursorBitmap);
|
(void)width; (void)height; (void)hotspotX; (void)hotspotY; (void)pixels;
|
||||||
}
|
|
||||||
if (cursorHandle) {
|
|
||||||
DestroyCursor(cursorHandle);
|
|
||||||
cursorHandle = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
BITMAPINFO bmi = {};
|
|
||||||
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
|
||||||
bmi.bmiHeader.biWidth = sizeX;
|
|
||||||
bmi.bmiHeader.biHeight = -(int)sizeY; // top-down
|
|
||||||
bmi.bmiHeader.biPlanes = 1;
|
|
||||||
bmi.bmiHeader.biBitCount = 32;
|
|
||||||
bmi.bmiHeader.biCompression = BI_RGB;
|
|
||||||
|
|
||||||
HDC hdc = GetDC(nullptr);
|
|
||||||
cursorBitmap = CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, reinterpret_cast<void**>(&cursorRenderer.buffer[0]), nullptr, 0);
|
|
||||||
ReleaseDC(nullptr, hdc);
|
|
||||||
|
|
||||||
if (!cursorBitmap) {
|
|
||||||
throw std::runtime_error("CreateDIBSection failed for cursor");
|
|
||||||
}
|
|
||||||
|
|
||||||
cursorSizeX = sizeX;
|
|
||||||
cursorSizeY = sizeY;
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void Window::SetCusorImageDefault() {
|
void Window::SetDefaultCursor() {
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||||
|
if (cursorMmap_) {
|
||||||
|
munmap(cursorMmap_, cursorBufferOldSize);
|
||||||
|
cursorMmap_ = nullptr;
|
||||||
|
}
|
||||||
|
if (cursorWlBuffer) {
|
||||||
wl_buffer_destroy(cursorWlBuffer);
|
wl_buffer_destroy(cursorWlBuffer);
|
||||||
|
cursorWlBuffer = nullptr;
|
||||||
|
}
|
||||||
|
if (cursorSurface) {
|
||||||
wl_surface_destroy(cursorSurface);
|
wl_surface_destroy(cursorSurface);
|
||||||
cursorSurface = nullptr;
|
cursorSurface = nullptr;
|
||||||
#endif
|
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
|
||||||
if (cursorHandle) {
|
|
||||||
DestroyCursor(cursorHandle);
|
|
||||||
cursorHandle = nullptr;
|
|
||||||
}
|
}
|
||||||
if (cursorBitmap) {
|
cursorBufferOldSize = 0;
|
||||||
DeleteObject(cursorBitmap);
|
cursorHotspotX_ = 0;
|
||||||
cursorBitmap = nullptr;
|
cursorHotspotY_ = 0;
|
||||||
}
|
// Tell the compositor to drop our cursor surface — passing nullptr
|
||||||
// Setting nullptr will make WM_SETCURSOR fall through to the default
|
// makes it fall back to the system default.
|
||||||
#endif
|
if (Device::wlPointer && Device::focusedWindow == this && lastPointerSerial_) {
|
||||||
}
|
wl_pointer_set_cursor(Device::wlPointer, lastPointerSerial_, nullptr, 0, 0);
|
||||||
|
|
||||||
void Window::UpdateCursorImage() {
|
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
||||||
wl_surface_attach(cursorSurface, cursorWlBuffer, 0, 0);
|
|
||||||
wl_surface_damage(cursorSurface, 0, 0, 9999999, 99999999);
|
|
||||||
wl_surface_commit(cursorSurface);
|
|
||||||
#endif
|
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
|
||||||
|
|
||||||
// Create a mask bitmap (all zeros = fully opaque, alpha comes from color bitmap)
|
|
||||||
HBITMAP hMask = CreateBitmap(cursorSizeX, cursorSizeY, 1, 1, nullptr);
|
|
||||||
|
|
||||||
ICONINFO ii = {};
|
|
||||||
ii.fIcon = FALSE;
|
|
||||||
ii.xHotspot = 0;
|
|
||||||
ii.yHotspot = 0;
|
|
||||||
ii.hbmMask = hMask;
|
|
||||||
ii.hbmColor = cursorBitmap;
|
|
||||||
|
|
||||||
if (cursorHandle) {
|
|
||||||
DestroyCursor(cursorHandle);
|
|
||||||
}
|
|
||||||
cursorHandle = (HCURSOR)CreateIconIndirect(&ii);
|
|
||||||
DeleteObject(hMask);
|
|
||||||
|
|
||||||
if (cursorHandle) {
|
|
||||||
SetCursor(cursorHandle);
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
||||||
56
interfaces/Crafter.Graphics-ComputeShader.cppm
Normal file
56
interfaces/Crafter.Graphics-ComputeShader.cppm
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
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:ComputeShader;
|
||||||
|
import std;
|
||||||
|
import :Device;
|
||||||
|
|
||||||
|
export namespace Crafter {
|
||||||
|
// Tier 1: thin compute-pipeline wrapper. Owns one VkPipeline created with
|
||||||
|
// VK_PIPELINE_CREATE_2_DESCRIPTOR_HEAP_BIT_EXT (no pipeline layout — the
|
||||||
|
// bindless heap supplies all bindings, push constants travel via
|
||||||
|
// vkCmdPushDataEXT). Use this to dispatch the four standard UI shaders
|
||||||
|
// and any user-authored compute shader that follows the ui-shared.glsl
|
||||||
|
// contract.
|
||||||
|
class ComputeShader {
|
||||||
|
public:
|
||||||
|
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||||
|
|
||||||
|
ComputeShader() = default;
|
||||||
|
ComputeShader(const ComputeShader&) = delete;
|
||||||
|
ComputeShader& operator=(const ComputeShader&) = delete;
|
||||||
|
ComputeShader(ComputeShader&& other) noexcept;
|
||||||
|
ComputeShader& operator=(ComputeShader&& other) noexcept;
|
||||||
|
~ComputeShader();
|
||||||
|
|
||||||
|
// Loads a SPIR-V compute shader from disk and creates a pipeline that
|
||||||
|
// uses the bindless descriptor-heap binding model.
|
||||||
|
void Load(const std::filesystem::path& spvPath);
|
||||||
|
|
||||||
|
// Bind, push constants (if any), dispatch. Caller computes group counts
|
||||||
|
// and is responsible for any inter-dispatch barriers (UIRenderer::Dispatch
|
||||||
|
// wraps this with the standard write-after-write barrier).
|
||||||
|
void Dispatch(VkCommandBuffer cmd,
|
||||||
|
const void* push, std::uint32_t pushBytes,
|
||||||
|
std::uint32_t gx,
|
||||||
|
std::uint32_t gy = 1,
|
||||||
|
std::uint32_t gz = 1) const;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -18,13 +18,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
*/
|
*/
|
||||||
module;
|
module;
|
||||||
#include "vulkan/vulkan.h"
|
#include "vulkan/vulkan.h"
|
||||||
export module Crafter.Graphics:UIAtlas;
|
export module Crafter.Graphics:FontAtlas;
|
||||||
import std;
|
import std;
|
||||||
import :Font;
|
import :Font;
|
||||||
import :ImageVulkan;
|
import :ImageVulkan;
|
||||||
import :Device;
|
import :Device;
|
||||||
|
|
||||||
export namespace Crafter::UI {
|
export namespace Crafter {
|
||||||
// Per-glyph metrics. UVs are 0..1 in atlas space; on-screen sizes /
|
// 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
|
// offsets / advance are in *atlas pixels at the base size* and scale
|
||||||
// linearly with the requested font size at draw time.
|
// linearly with the requested font size at draw time.
|
||||||
|
|
@ -16,15 +16,308 @@ 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;
|
||||||
|
#include "vulkan/vulkan.h"
|
||||||
export module Crafter.Graphics:UI;
|
export module Crafter.Graphics:UI;
|
||||||
|
import std;
|
||||||
|
import Crafter.Event;
|
||||||
|
import :Device;
|
||||||
|
import :Window;
|
||||||
|
import :RenderPass;
|
||||||
|
import :DescriptorHeapVulkan;
|
||||||
|
import :ImageVulkan;
|
||||||
|
import :VulkanBuffer;
|
||||||
|
import :ComputeShader;
|
||||||
|
import :FontAtlas;
|
||||||
|
import :Font;
|
||||||
|
|
||||||
export import :UILength;
|
export namespace Crafter {
|
||||||
export import :UIWidget;
|
// ─── push-constant header ───────────────────────────────────────────
|
||||||
export import :UILayout;
|
// Mirrors shaders/ui-shared.glsl::UIDispatchHeader byte-for-byte. User
|
||||||
export import :UIDrawList;
|
// shaders MUST embed this as the first member of their push-constant
|
||||||
export import :UIAtlas;
|
// struct so UIRenderer::FillHeader works.
|
||||||
export import :UIWidgets;
|
struct UIDispatchHeader {
|
||||||
export import :UITheme;
|
std::uint32_t outImage;
|
||||||
export import :UIHit;
|
std::uint32_t itemBuffer;
|
||||||
export import :UIRenderer;
|
std::uint32_t surfaceWidth;
|
||||||
export import :UIScene;
|
std::uint32_t surfaceHeight;
|
||||||
|
float clipX, clipY, clipW, clipH;
|
||||||
|
std::uint32_t itemCount;
|
||||||
|
std::uint32_t frameIdx;
|
||||||
|
std::uint32_t flags;
|
||||||
|
std::uint32_t _pad;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(UIDispatchHeader) == 48);
|
||||||
|
|
||||||
|
// ─── standard item PODs (match GLSL std430) ─────────────────────────
|
||||||
|
struct QuadItem {
|
||||||
|
float x, y, w, h;
|
||||||
|
float r, g, b, a;
|
||||||
|
float cTL, cTR, cBR, cBL; // per-corner radius in px
|
||||||
|
float outline, oR, oG, oB; // outline thickness + RGB
|
||||||
|
};
|
||||||
|
static_assert(sizeof(QuadItem) == 64);
|
||||||
|
|
||||||
|
struct CircleItem {
|
||||||
|
float cx, cy, radius, _p0;
|
||||||
|
float r, g, b, a;
|
||||||
|
float outline, oR, oG, oB;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(CircleItem) == 48);
|
||||||
|
|
||||||
|
struct ImageItem {
|
||||||
|
float x, y, w, h;
|
||||||
|
float u0, v0, u1, v1;
|
||||||
|
float tR, tG, tB, tA;
|
||||||
|
std::uint32_t texSlot, sampSlot, _p1, _p2;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(ImageItem) == 64);
|
||||||
|
|
||||||
|
struct GlyphItem {
|
||||||
|
float x, y, w, h;
|
||||||
|
float u0, v0, u1, v1;
|
||||||
|
float r, g, b, a;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(GlyphItem) == 48);
|
||||||
|
|
||||||
|
// ─── tiny rect-carving helper ───────────────────────────────────────
|
||||||
|
// Pure value semantics. No engine, just convenience. Skip if you'd rather
|
||||||
|
// compute pixels yourself.
|
||||||
|
struct Rect {
|
||||||
|
float x = 0, y = 0, w = 0, h = 0;
|
||||||
|
|
||||||
|
enum class Anchor { Top, Bottom, Left, Right };
|
||||||
|
|
||||||
|
// Returns a sub-rect of `size` along the given anchor edge of self.
|
||||||
|
// Does not modify `*this`. (Use `.Inset(...)` to drop a margin first.)
|
||||||
|
Rect SubRect(float size, Anchor a) const noexcept {
|
||||||
|
switch (a) {
|
||||||
|
case Anchor::Top: return { x, y, w, std::min(size, h) };
|
||||||
|
case Anchor::Bottom: return { x, y + h - std::min(size, h), w, std::min(size, h) };
|
||||||
|
case Anchor::Left: return { x, y, std::min(size, w), h };
|
||||||
|
case Anchor::Right: return { x + w - std::min(size, w), y, std::min(size, w), h };
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect Inset(float padding) const noexcept {
|
||||||
|
return Inset(padding, padding, padding, padding);
|
||||||
|
}
|
||||||
|
Rect Inset(float top, float right, float bottom, float left) const noexcept {
|
||||||
|
float nw = std::max(0.0f, w - left - right);
|
||||||
|
float nh = std::max(0.0f, h - top - bottom);
|
||||||
|
return { x + left, y + top, nw, nh };
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Contains(float px, float py) const noexcept {
|
||||||
|
return px >= x && px < x + w && py >= y && py < y + h;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Rect FromWindow(const Window& win) noexcept {
|
||||||
|
return { 0, 0, static_cast<float>(win.width), static_cast<float>(win.height) };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── per-frame callback args ────────────────────────────────────────
|
||||||
|
struct UIBuildArgs {
|
||||||
|
VkCommandBuffer cmd;
|
||||||
|
std::uint32_t frameIdx;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── UIRenderer ─────────────────────────────────────────────────────
|
||||||
|
// One per Window (typically). Owns the four standard compute shaders,
|
||||||
|
// pre-allocates heap slots for the swapchain images, and exposes a thin
|
||||||
|
// dispatch helper for both the standard shaders and user-supplied ones.
|
||||||
|
//
|
||||||
|
// Workflow:
|
||||||
|
// 1. Construct, configure (set fontAtlas if drawing text).
|
||||||
|
// 2. Initialize(window, heap, initCmd) — once, after window.descriptorHeap
|
||||||
|
// is set and before window.FinishInit().
|
||||||
|
// 3. window.passes.push_back(&ui).
|
||||||
|
// 4. Listen on `onBuild`. Inside the callback, fill your item buffers,
|
||||||
|
// flush them, and call DispatchQuads / DispatchCircles / DispatchImages
|
||||||
|
// / DispatchText / Dispatch as needed. Library inserts a SHADER_WRITE
|
||||||
|
// → SHADER_READ|WRITE memory barrier between consecutive dispatches.
|
||||||
|
class UIRenderer : public RenderPass {
|
||||||
|
public:
|
||||||
|
// Pre-loaded standard shaders (public so users can call Dispatch
|
||||||
|
// directly with them if they want to embed extra push-constant fields
|
||||||
|
// beyond the standard header).
|
||||||
|
ComputeShader drawQuads;
|
||||||
|
ComputeShader drawCircles;
|
||||||
|
ComputeShader drawImages;
|
||||||
|
ComputeShader drawText;
|
||||||
|
|
||||||
|
// Optional. If set before Initialize, the atlas is registered into a
|
||||||
|
// sampled-image slot + linear sampler slot, and Update(cmd) is called
|
||||||
|
// at the top of every Record() so any glyphs ensured during onBuild
|
||||||
|
// make it to the GPU before the text dispatch reads them.
|
||||||
|
FontAtlas* fontAtlas = nullptr;
|
||||||
|
|
||||||
|
// User callback. Subscribe by holding a Crafter::EventListener<UIBuildArgs>:
|
||||||
|
// EventListener<UIBuildArgs> sub(&ui.onBuild, [&](UIBuildArgs a) { ... });
|
||||||
|
// Listener lifetime governs the subscription.
|
||||||
|
Crafter::Event<UIBuildArgs> onBuild;
|
||||||
|
|
||||||
|
UIRenderer() = default;
|
||||||
|
UIRenderer(const UIRenderer&) = delete;
|
||||||
|
UIRenderer& operator=(const UIRenderer&) = delete;
|
||||||
|
|
||||||
|
// Default shader paths assume Crafter.Build placed the .spv files
|
||||||
|
// alongside the consumer binary (this is what cfg.shaders does).
|
||||||
|
void Initialize(Window& window, DescriptorHeapVulkan& heap, VkCommandBuffer initCmd,
|
||||||
|
std::filesystem::path quadsSpv = "ui-quads.comp.spv",
|
||||||
|
std::filesystem::path circlesSpv = "ui-circles.comp.spv",
|
||||||
|
std::filesystem::path imagesSpv = "ui-images.comp.spv",
|
||||||
|
std::filesystem::path textSpv = "ui-text.comp.spv");
|
||||||
|
|
||||||
|
// RenderPass interface — invoked from Window::Render.
|
||||||
|
void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override;
|
||||||
|
|
||||||
|
// ─── helpers used inside `onBuild` ─────────────────────────────
|
||||||
|
|
||||||
|
// Builds a populated header. `clipRectPx` defaults to "no clip".
|
||||||
|
UIDispatchHeader FillHeader(std::uint32_t itemBufferSlot,
|
||||||
|
std::uint32_t itemCount,
|
||||||
|
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f},
|
||||||
|
std::uint32_t flags = 0) const noexcept;
|
||||||
|
|
||||||
|
// Convenience: dispatches the named standard shader. Group count is
|
||||||
|
// computed from the window's surface size — the standard shaders
|
||||||
|
// dispatch one workgroup per 8×8 screen tile and iterate every item
|
||||||
|
// in the buffer in order, so item ORDER in the buffer == draw order
|
||||||
|
// on screen (later items overdraw earlier ones, race-free).
|
||||||
|
void DispatchQuads(VkCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
|
||||||
|
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f});
|
||||||
|
void DispatchCircles(VkCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
|
||||||
|
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f});
|
||||||
|
void DispatchImages(VkCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
|
||||||
|
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f});
|
||||||
|
// For DispatchText, the font atlas image+sampler slots are taken from
|
||||||
|
// UIRenderer's Initialize-time registration (see fontAtlasImageSlot()
|
||||||
|
// / fontAtlasSamplerSlot()). Set `fontAtlas` before Initialize.
|
||||||
|
void DispatchText(VkCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
|
||||||
|
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f});
|
||||||
|
|
||||||
|
// Generic dispatch — for user-authored shaders. Inserts the standard
|
||||||
|
// pre-dispatch barrier (skipped on the first call per frame).
|
||||||
|
void Dispatch(VkCommandBuffer cmd, const ComputeShader& shader,
|
||||||
|
const void* push, std::uint32_t pushBytes,
|
||||||
|
std::uint32_t gx, std::uint32_t gy = 1, std::uint32_t gz = 1);
|
||||||
|
|
||||||
|
// Allocates a heap slot for the buffer and writes its descriptor into
|
||||||
|
// every per-frame heap. The user's mapped buffer is shared across
|
||||||
|
// frames — fine because Window::Render currently waits idle before
|
||||||
|
// submitting the next frame. Returns the slot index for use in headers.
|
||||||
|
template<typename T, bool Mapped>
|
||||||
|
std::uint16_t RegisterBuffer(VulkanBuffer<T, Mapped>& buffer);
|
||||||
|
|
||||||
|
// Same for an ImageVulkan-managed sampled image (e.g. a user texture).
|
||||||
|
// Caller specifies the layout the image will be sampled in (typically
|
||||||
|
// VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL).
|
||||||
|
template<typename Pixel>
|
||||||
|
std::uint16_t RegisterImage(ImageVulkan<Pixel>& image, VkFormat format,
|
||||||
|
VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||||
|
|
||||||
|
// Allocates a sampler slot and writes a VkSamplerCreateInfo into
|
||||||
|
// every per-frame sampler heap. v1 takes the create-info inline.
|
||||||
|
std::uint16_t RegisterSampler(const VkSamplerCreateInfo& info);
|
||||||
|
|
||||||
|
// Convenience: a linear-filter, clamp-to-edge sampler. Returns the
|
||||||
|
// slot. Useful for the FontAtlas and most plain image sampling.
|
||||||
|
std::uint16_t RegisterLinearClampSampler();
|
||||||
|
|
||||||
|
// Shapes a UTF-8 string into glyph quads at (x, y) baseline. Calls
|
||||||
|
// FontAtlas::Ensure for each codepoint (rasterising on first use),
|
||||||
|
// emits one GlyphItem per visible glyph, returns the count written.
|
||||||
|
// Use this to fill a GlyphItem buffer that you then dispatch.
|
||||||
|
// Cursor advances along +X. No line-wrap, no kerning — single line.
|
||||||
|
std::uint32_t ShapeText(Font& font, float pxSize,
|
||||||
|
float x, float baselineY,
|
||||||
|
std::string_view utf8,
|
||||||
|
std::array<float,4> color,
|
||||||
|
GlyphItem* out, std::uint32_t outCapacity,
|
||||||
|
float* outAdvance = nullptr);
|
||||||
|
|
||||||
|
// Read after Initialize: the slot the font atlas was registered into.
|
||||||
|
// 0xFFFF means "no atlas" (set fontAtlas before Initialize).
|
||||||
|
std::uint16_t FontAtlasImageSlot() const noexcept { return fontAtlasImageSlot_; }
|
||||||
|
std::uint16_t FontAtlasSamplerSlot() const noexcept { return fontAtlasSamplerSlot_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
Window* window_ = nullptr;
|
||||||
|
DescriptorHeapVulkan* heap_ = nullptr;
|
||||||
|
|
||||||
|
// One image slot used for the swapchain output. In each per-frame
|
||||||
|
// heap, that slot points at THAT frame's swapchain image. So the
|
||||||
|
// shader's `uiImages[hdr.outImage]` is always the current frame's
|
||||||
|
// swapchain image regardless of which heap is bound.
|
||||||
|
std::uint16_t outImageSlot_ = 0;
|
||||||
|
|
||||||
|
// Stable VkImageViewCreateInfos for the descriptor heap to ingest.
|
||||||
|
// These must outlive the write call.
|
||||||
|
VkImageViewCreateInfo atlasViewCreateInfo_{};
|
||||||
|
|
||||||
|
std::uint16_t fontAtlasImageSlot_ = 0xFFFF;
|
||||||
|
std::uint16_t fontAtlasSamplerSlot_ = 0xFFFF;
|
||||||
|
|
||||||
|
bool firstDispatchThisFrame_ = true;
|
||||||
|
|
||||||
|
void WriteSwapchainDescriptors();
|
||||||
|
void WriteFontAtlasDescriptor();
|
||||||
|
|
||||||
|
// Helper used by RegisterBuffer template (defined in impl). Writes the
|
||||||
|
// address-range descriptor at `slot` into all per-frame heaps.
|
||||||
|
void WriteBufferDescriptor(std::uint16_t slot, VkDeviceAddress address, std::uint32_t size);
|
||||||
|
|
||||||
|
// Helper used by RegisterImage template — writes a sampled-image at
|
||||||
|
// `slot` referring to a stable VkImageViewCreateInfo (caller stores).
|
||||||
|
void WriteSampledImageDescriptor(std::uint16_t slot,
|
||||||
|
const VkImageViewCreateInfo& viewInfo,
|
||||||
|
VkImageLayout layout);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── template-method implementations ────────────────────────────────
|
||||||
|
template<typename T, bool Mapped>
|
||||||
|
std::uint16_t UIRenderer::RegisterBuffer(VulkanBuffer<T, Mapped>& buffer) {
|
||||||
|
auto range = heap_->AllocateBufferSlots(1);
|
||||||
|
WriteBufferDescriptor(range.firstElement, buffer.address, buffer.size);
|
||||||
|
// GLSL `descriptor_heap` indexes buffer-typed views in buffer-descriptor
|
||||||
|
// units from heap byte 0; the actual buffer region starts past the
|
||||||
|
// image region at `bufferStartElement`. Return the absolute index so
|
||||||
|
// the user just hands it to FillHeader without thinking about it.
|
||||||
|
return static_cast<std::uint16_t>(heap_->bufferStartElement + range.firstElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Pixel>
|
||||||
|
std::uint16_t UIRenderer::RegisterImage(ImageVulkan<Pixel>& image, VkFormat format,
|
||||||
|
VkImageLayout layout) {
|
||||||
|
auto range = heap_->AllocateImageSlots(1);
|
||||||
|
|
||||||
|
// Build a stable view-create-info that lives as long as the heap reads
|
||||||
|
// it. We co-locate it on the renderer for the font atlas; for arbitrary
|
||||||
|
// user images we lean on the fact that vkWriteResourceDescriptorsEXT
|
||||||
|
// copies the view descriptor immediately. (Validated by the heap spec:
|
||||||
|
// the descriptor is materialised at write time, the create-info need
|
||||||
|
// not persist past the call.)
|
||||||
|
VkImageViewCreateInfo info {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||||
|
.image = image.image,
|
||||||
|
.viewType = VK_IMAGE_VIEW_TYPE_2D,
|
||||||
|
.format = format,
|
||||||
|
.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 = image.mipLevels,
|
||||||
|
.baseArrayLayer = 0,
|
||||||
|
.layerCount = 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
WriteSampledImageDescriptor(range.firstElement, info, layout);
|
||||||
|
return range.firstElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
115
interfaces/Crafter.Graphics-UIComponents.cppm
Normal file
115
interfaces/Crafter.Graphics-UIComponents.cppm
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
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:UIComponents;
|
||||||
|
import std;
|
||||||
|
import :UI;
|
||||||
|
import :Font;
|
||||||
|
import :FontAtlas;
|
||||||
|
|
||||||
|
// Tier 3: stateless presentation functions. These append items to the user's
|
||||||
|
// QuadItem / GlyphItem buffers — they do NOT dispatch. The user dispatches
|
||||||
|
// via UIRenderer::DispatchQuads / DispatchText after their onBuild fills
|
||||||
|
// everything, so a frame stays one quads dispatch + one text dispatch
|
||||||
|
// regardless of how many components were drawn.
|
||||||
|
//
|
||||||
|
// State for components that need it (hovered, pressed, dragging, t01) is the
|
||||||
|
// USER's responsibility — these functions are pure presentation.
|
||||||
|
//
|
||||||
|
// EXTENSION MODEL: each function below is short on purpose. If you want a
|
||||||
|
// hexagon button, an icon-with-label button, a tristate checkbox — copy the
|
||||||
|
// function body into your code and modify it. There is no override hook.
|
||||||
|
|
||||||
|
export namespace Crafter {
|
||||||
|
// Aggregate for the two item buffers + the optional text-shaping deps.
|
||||||
|
// Build one per frame in onBuild and pass it to component calls.
|
||||||
|
struct UIBuffer {
|
||||||
|
QuadItem* quads = nullptr;
|
||||||
|
std::uint32_t* quadCount = nullptr;
|
||||||
|
std::uint32_t quadCap = 0;
|
||||||
|
|
||||||
|
GlyphItem* glyphs = nullptr;
|
||||||
|
std::uint32_t* glyphCount = nullptr;
|
||||||
|
std::uint32_t glyphCap = 0;
|
||||||
|
|
||||||
|
FontAtlas* atlas = nullptr; // for text-emitting components
|
||||||
|
UIRenderer* renderer = nullptr; // for ShapeText
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── per-component color blocks ─────────────────────────────────────
|
||||||
|
// Inline POD aggregates. Users compose their own application-level theme
|
||||||
|
// by holding a few of these together; the library has no Theme type.
|
||||||
|
|
||||||
|
struct ButtonColors {
|
||||||
|
std::array<float, 4> bg;
|
||||||
|
std::array<float, 4> bgHover;
|
||||||
|
std::array<float, 4> bgPressed;
|
||||||
|
std::array<float, 4> text;
|
||||||
|
std::array<float, 4> border = {0, 0, 0, 0};
|
||||||
|
float cornerRadius = 0;
|
||||||
|
float borderThickness = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CheckboxColors {
|
||||||
|
std::array<float, 4> bg;
|
||||||
|
std::array<float, 4> bgHover;
|
||||||
|
std::array<float, 4> check;
|
||||||
|
std::array<float, 4> border = {0, 0, 0, 0};
|
||||||
|
float cornerRadius = 4;
|
||||||
|
float borderThickness = 1;
|
||||||
|
float checkInset = 4; // px on each side
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SliderColors {
|
||||||
|
std::array<float, 4> track;
|
||||||
|
std::array<float, 4> trackFilled;
|
||||||
|
std::array<float, 4> thumb;
|
||||||
|
std::array<float, 4> thumbHover;
|
||||||
|
float trackHeight = 4;
|
||||||
|
float thumbRadius = 8;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ProgressColors {
|
||||||
|
std::array<float, 4> bg;
|
||||||
|
std::array<float, 4> fill;
|
||||||
|
float cornerRadius = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── component functions ───────────────────────────────────────────
|
||||||
|
|
||||||
|
// Background quad (color depends on state) + centered label glyphs.
|
||||||
|
void DrawButton(UIBuffer& buf, Rect r, std::string_view label,
|
||||||
|
bool hovered, bool pressed,
|
||||||
|
Font& font, float fontSize,
|
||||||
|
const ButtonColors& c);
|
||||||
|
|
||||||
|
// Outlined quad + a smaller filled inset quad when `checked`.
|
||||||
|
void DrawCheckbox(UIBuffer& buf, Rect r, bool checked, bool hovered,
|
||||||
|
const CheckboxColors& c);
|
||||||
|
|
||||||
|
// Thin track quad split at `t01` into filled/empty + a circular thumb
|
||||||
|
// (drawn as a quad with cornerRadius = thumbRadius).
|
||||||
|
void DrawSlider(UIBuffer& buf, Rect r, float t01, bool dragging,
|
||||||
|
const SliderColors& c);
|
||||||
|
|
||||||
|
// Background quad + a filled quad clipped to t01 of the inner width.
|
||||||
|
void DrawProgressBar(UIBuffer& buf, Rect r, float t01,
|
||||||
|
const ProgressColors& c);
|
||||||
|
}
|
||||||
|
|
@ -1,168 +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: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)
|
|
||||||
float time = 0.0f; // seconds since scene init (drives blink etc.)
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +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: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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +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: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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,98 +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: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};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,110 +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 "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();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,112 +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 "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 elapsedSec_ = 0.0f;
|
|
||||||
|
|
||||||
float WindowScale() const;
|
|
||||||
void RebuildFrame();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,80 +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: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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,189 +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: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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -106,9 +106,19 @@ export namespace Crafter {
|
||||||
void Resize(std::uint32_t width, std::uint32_t height);
|
void Resize(std::uint32_t width, std::uint32_t height);
|
||||||
void Render();
|
void Render();
|
||||||
void Update();
|
void Update();
|
||||||
void UpdateCursorImage();
|
// Replace the system cursor with a custom image. `pixels` is
|
||||||
void SetCusorImage(std::uint16_t sizeX, std::uint16_t sizeY);
|
// `width*height*4` bytes in R8G8B8A8 memory order (matching
|
||||||
void SetCusorImageDefault();
|
// stb_image's STBI_rgb_alpha output) with straight (non-premultiplied)
|
||||||
|
// alpha — the conversion to the compositor's expected format is
|
||||||
|
// handled internally. The hotspot is in image-pixel coordinates.
|
||||||
|
// Re-callable at any time.
|
||||||
|
void SetCursorImage(std::uint16_t width, std::uint16_t height,
|
||||||
|
std::uint16_t hotspotX, std::uint16_t hotspotY,
|
||||||
|
const std::uint8_t* pixels);
|
||||||
|
|
||||||
|
// Restore the default system cursor (releases any previously-uploaded
|
||||||
|
// cursor pixel buffer).
|
||||||
|
void SetDefaultCursor();
|
||||||
|
|
||||||
#ifdef CRAFTER_TIMING
|
#ifdef CRAFTER_TIMING
|
||||||
std::chrono::nanoseconds totalUpdate;
|
std::chrono::nanoseconds totalUpdate;
|
||||||
|
|
@ -139,6 +149,15 @@ export namespace Crafter {
|
||||||
wl_surface* cursorSurface = nullptr;
|
wl_surface* cursorSurface = nullptr;
|
||||||
wl_buffer* cursorWlBuffer = nullptr;
|
wl_buffer* cursorWlBuffer = nullptr;
|
||||||
std::uint32_t cursorBufferOldSize = 0;
|
std::uint32_t cursorBufferOldSize = 0;
|
||||||
|
// mmap'd view of the SHM cursor buffer — the user-supplied pixels
|
||||||
|
// are written here in BGRA8888 order. Lifetime matches cursorWlBuffer.
|
||||||
|
std::uint8_t* cursorMmap_ = nullptr;
|
||||||
|
std::uint16_t cursorHotspotX_ = 0;
|
||||||
|
std::uint16_t cursorHotspotY_ = 0;
|
||||||
|
// Most recent serial from a wl_pointer.enter on this window's surface.
|
||||||
|
// Needed so `SetCursorImage` can re-issue `wl_pointer_set_cursor`
|
||||||
|
// mid-session (the hotspot only updates when set_cursor is recalled).
|
||||||
|
std::uint32_t lastPointerSerial_ = 0;
|
||||||
|
|
||||||
static void xdg_surface_handle_preferred_scale(void* data, wp_fractional_scale_v1*, std::uint32_t scale);
|
static void xdg_surface_handle_preferred_scale(void* data, wp_fractional_scale_v1*, std::uint32_t scale);
|
||||||
static void wl_surface_frame_done(void *data, wl_callback *cb, uint32_t time);
|
static void wl_surface_frame_done(void *data, wl_callback *cb, uint32_t time);
|
||||||
|
|
|
||||||
|
|
@ -38,4 +38,7 @@ export import :SamplerVulkan;
|
||||||
export import :DescriptorHeapVulkan;
|
export import :DescriptorHeapVulkan;
|
||||||
export import :RenderPass;
|
export import :RenderPass;
|
||||||
export import :RTPass;
|
export import :RTPass;
|
||||||
|
export import :FontAtlas;
|
||||||
|
export import :ComputeShader;
|
||||||
export import :UI;
|
export import :UI;
|
||||||
|
export import :UIComponents;
|
||||||
30
project.cpp
30
project.cpp
|
|
@ -63,12 +63,14 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
|
|
||||||
if (opts.Has("--timing")) cfg.defines.push_back({"CRAFTER_TIMING", ""});
|
if (opts.Has("--timing")) cfg.defines.push_back({"CRAFTER_TIMING", ""});
|
||||||
|
|
||||||
std::array<fs::path, 30> ifaces = {
|
std::array<fs::path, 23> ifaces = {
|
||||||
"interfaces/Crafter.Graphics",
|
"interfaces/Crafter.Graphics",
|
||||||
"interfaces/Crafter.Graphics-Animation",
|
"interfaces/Crafter.Graphics-Animation",
|
||||||
|
"interfaces/Crafter.Graphics-ComputeShader",
|
||||||
"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-FontAtlas",
|
||||||
"interfaces/Crafter.Graphics-ForwardDeclarations",
|
"interfaces/Crafter.Graphics-ForwardDeclarations",
|
||||||
"interfaces/Crafter.Graphics-ImageVulkan",
|
"interfaces/Crafter.Graphics-ImageVulkan",
|
||||||
"interfaces/Crafter.Graphics-Mesh",
|
"interfaces/Crafter.Graphics-Mesh",
|
||||||
|
|
@ -81,33 +83,29 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
"interfaces/Crafter.Graphics-ShaderVulkan",
|
"interfaces/Crafter.Graphics-ShaderVulkan",
|
||||||
"interfaces/Crafter.Graphics-Types",
|
"interfaces/Crafter.Graphics-Types",
|
||||||
"interfaces/Crafter.Graphics-UI",
|
"interfaces/Crafter.Graphics-UI",
|
||||||
"interfaces/Crafter.Graphics-UIAtlas",
|
"interfaces/Crafter.Graphics-UIComponents",
|
||||||
"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",
|
||||||
};
|
};
|
||||||
std::array<fs::path, 8> impls = {
|
std::array<fs::path, 9> impls = {
|
||||||
|
"implementations/Crafter.Graphics-ComputeShader",
|
||||||
"implementations/Crafter.Graphics-Device",
|
"implementations/Crafter.Graphics-Device",
|
||||||
"implementations/Crafter.Graphics-Font",
|
"implementations/Crafter.Graphics-Font",
|
||||||
|
"implementations/Crafter.Graphics-FontAtlas",
|
||||||
"implementations/Crafter.Graphics-Mesh",
|
"implementations/Crafter.Graphics-Mesh",
|
||||||
"implementations/Crafter.Graphics-RenderingElement3D",
|
"implementations/Crafter.Graphics-RenderingElement3D",
|
||||||
"implementations/Crafter.Graphics-UIAtlas",
|
"implementations/Crafter.Graphics-UI",
|
||||||
"implementations/Crafter.Graphics-UIRenderer",
|
"implementations/Crafter.Graphics-UIComponents",
|
||||||
"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);
|
cfg.shaders.emplace_back(fs::path("shaders/ui-quads.comp.glsl"), std::string("main"), ShaderType::Compute);
|
||||||
|
cfg.shaders.emplace_back(fs::path("shaders/ui-circles.comp.glsl"), std::string("main"), ShaderType::Compute);
|
||||||
|
cfg.shaders.emplace_back(fs::path("shaders/ui-images.comp.glsl"), std::string("main"), ShaderType::Compute);
|
||||||
|
cfg.shaders.emplace_back(fs::path("shaders/ui-text.comp.glsl"), std::string("main"), ShaderType::Compute);
|
||||||
|
cfg.buildFiles.emplace_back(fs::path("shaders/ui-shared.glsl"));
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
48
shaders/ui-circles.comp.glsl
Normal file
48
shaders/ui-circles.comp.glsl
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
#version 460
|
||||||
|
#extension GL_GOOGLE_include_directive : enable
|
||||||
|
#include "ui-shared.glsl"
|
||||||
|
|
||||||
|
layout(push_constant) uniform PC {
|
||||||
|
UIDispatchHeader hdr;
|
||||||
|
} pc;
|
||||||
|
|
||||||
|
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
ivec2 screenPx;
|
||||||
|
if (!uiResolveScreenPixel(pc.hdr, screenPx)) return;
|
||||||
|
|
||||||
|
vec4 dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||||
|
vec2 sp = vec2(screenPx) + 0.5;
|
||||||
|
|
||||||
|
for (uint i = 0u; i < pc.hdr.itemCount; ++i) {
|
||||||
|
CircleItem it = LoadCircleItem(pc.hdr.itemBuffer, i);
|
||||||
|
|
||||||
|
vec2 center = it.centerRadius.xy;
|
||||||
|
float radius = it.centerRadius.z;
|
||||||
|
if (radius <= 0.0) continue;
|
||||||
|
|
||||||
|
// Cheap bounding-box reject.
|
||||||
|
if (abs(sp.x - center.x) > radius + 1.0) continue;
|
||||||
|
if (abs(sp.y - center.y) > radius + 1.0) continue;
|
||||||
|
|
||||||
|
float d = length(sp - center) - radius;
|
||||||
|
|
||||||
|
float bodyA = clamp(0.5 - d, 0.0, 1.0);
|
||||||
|
if (bodyA <= 0.0 && it.outline.x <= 0.0) continue;
|
||||||
|
|
||||||
|
vec4 src = vec4(it.color.rgb, it.color.a * bodyA);
|
||||||
|
|
||||||
|
if (it.outline.x > 0.0) {
|
||||||
|
float t = abs(d + it.outline.x * 0.5) - it.outline.x * 0.5;
|
||||||
|
float outlineA = clamp(0.5 - t, 0.0, 1.0);
|
||||||
|
src.rgb = mix(src.rgb, it.outline.yzw, outlineA);
|
||||||
|
src.a = max(src.a, outlineA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src.a <= 0.0) continue;
|
||||||
|
dst = uiBlendOver(dst, src);
|
||||||
|
}
|
||||||
|
|
||||||
|
imageStore(uiImages[pc.hdr.outImage], screenPx, dst);
|
||||||
|
}
|
||||||
43
shaders/ui-images.comp.glsl
Normal file
43
shaders/ui-images.comp.glsl
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
#version 460
|
||||||
|
#extension GL_GOOGLE_include_directive : enable
|
||||||
|
#include "ui-shared.glsl"
|
||||||
|
|
||||||
|
layout(push_constant) uniform PC {
|
||||||
|
UIDispatchHeader hdr;
|
||||||
|
} pc;
|
||||||
|
|
||||||
|
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
ivec2 screenPx;
|
||||||
|
if (!uiResolveScreenPixel(pc.hdr, screenPx)) return;
|
||||||
|
|
||||||
|
vec4 dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||||
|
vec2 sp = vec2(screenPx) + 0.5;
|
||||||
|
|
||||||
|
for (uint i = 0u; i < pc.hdr.itemCount; ++i) {
|
||||||
|
ImageItem it = LoadImageItem(pc.hdr.itemBuffer, i);
|
||||||
|
|
||||||
|
vec2 lo = it.rect.xy;
|
||||||
|
vec2 hi = it.rect.xy + it.rect.zw;
|
||||||
|
if (sp.x < lo.x || sp.y < lo.y) continue;
|
||||||
|
if (sp.x >= hi.x || sp.y >= hi.y) continue;
|
||||||
|
|
||||||
|
vec2 t = (sp - it.rect.xy) / it.rect.zw;
|
||||||
|
vec2 uv = mix(it.uv.xy, it.uv.zw, t);
|
||||||
|
|
||||||
|
uint texSlot = it.slots.x;
|
||||||
|
uint sampSlot = it.slots.y;
|
||||||
|
|
||||||
|
vec4 sampled = texture(
|
||||||
|
sampler2D(uiTextures[nonuniformEXT(texSlot)],
|
||||||
|
uiSamplers[nonuniformEXT(sampSlot)]),
|
||||||
|
uv
|
||||||
|
);
|
||||||
|
vec4 src = sampled * it.tint;
|
||||||
|
if (src.a <= 0.0) continue;
|
||||||
|
dst = uiBlendOver(dst, src);
|
||||||
|
}
|
||||||
|
|
||||||
|
imageStore(uiImages[pc.hdr.outImage], screenPx, dst);
|
||||||
|
}
|
||||||
51
shaders/ui-quads.comp.glsl
Normal file
51
shaders/ui-quads.comp.glsl
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
#version 460
|
||||||
|
#extension GL_GOOGLE_include_directive : enable
|
||||||
|
#include "ui-shared.glsl"
|
||||||
|
|
||||||
|
// One workgroup per 8×8 screen tile. Each thread owns one pixel and iterates
|
||||||
|
// every QuadItem in order, accumulating into a local dst register, so item
|
||||||
|
// order in the buffer == draw order on screen (later items overdraw earlier).
|
||||||
|
layout(push_constant) uniform PC {
|
||||||
|
UIDispatchHeader hdr;
|
||||||
|
} pc;
|
||||||
|
|
||||||
|
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
ivec2 screenPx;
|
||||||
|
if (!uiResolveScreenPixel(pc.hdr, screenPx)) return;
|
||||||
|
|
||||||
|
vec4 dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||||
|
vec2 sp = vec2(screenPx) + 0.5;
|
||||||
|
|
||||||
|
for (uint i = 0u; i < pc.hdr.itemCount; ++i) {
|
||||||
|
QuadItem it = LoadQuadItem(pc.hdr.itemBuffer, i);
|
||||||
|
|
||||||
|
// Cheap pre-test against the item's axis-aligned rect.
|
||||||
|
vec2 lo = it.rect.xy;
|
||||||
|
vec2 hi = it.rect.xy + it.rect.zw;
|
||||||
|
if (sp.x < lo.x || sp.y < lo.y) continue;
|
||||||
|
if (sp.x >= hi.x || sp.y >= hi.y) continue;
|
||||||
|
|
||||||
|
vec2 halfSize = it.rect.zw * 0.5;
|
||||||
|
vec2 p = sp - (it.rect.xy + halfSize);
|
||||||
|
float d = uiSdRoundRect(p, halfSize, it.corners);
|
||||||
|
|
||||||
|
float bodyA = clamp(0.5 - d, 0.0, 1.0);
|
||||||
|
if (bodyA <= 0.0 && it.outline.x <= 0.0) continue;
|
||||||
|
|
||||||
|
vec4 src = vec4(it.color.rgb, it.color.a * bodyA);
|
||||||
|
|
||||||
|
if (it.outline.x > 0.0) {
|
||||||
|
float t = abs(d + it.outline.x * 0.5) - it.outline.x * 0.5;
|
||||||
|
float outlineA = clamp(0.5 - t, 0.0, 1.0);
|
||||||
|
src.rgb = mix(src.rgb, it.outline.yzw, outlineA);
|
||||||
|
src.a = max(src.a, outlineA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src.a <= 0.0) continue;
|
||||||
|
dst = uiBlendOver(dst, src);
|
||||||
|
}
|
||||||
|
|
||||||
|
imageStore(uiImages[pc.hdr.outImage], screenPx, dst);
|
||||||
|
}
|
||||||
150
shaders/ui-shared.glsl
Normal file
150
shaders/ui-shared.glsl
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
// Crafter.Graphics UI shader contract — shared by every standard UI compute
|
||||||
|
// shader and intended to be #included by user-authored shaders that want to
|
||||||
|
// dispatch alongside them. Layouts here are FROZEN: only additive changes
|
||||||
|
// (using the reserved `flags` bits or `_pad`).
|
||||||
|
|
||||||
|
#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_buffer_reference : enable
|
||||||
|
|
||||||
|
// ─── bindless heap declarations ─────────────────────────────────────────
|
||||||
|
// The same heap slot can be read as either uiImages[] (storage image) or
|
||||||
|
// uiTextures[] (sampled image) depending on which descriptor was written
|
||||||
|
// at that slot. Samplers live in a separate sampler heap.
|
||||||
|
layout(descriptor_heap) uniform image2D uiImages[];
|
||||||
|
layout(descriptor_heap) uniform texture2D uiTextures[];
|
||||||
|
layout(descriptor_heap) uniform sampler uiSamplers[];
|
||||||
|
|
||||||
|
// ─── push-constant header ───────────────────────────────────────────────
|
||||||
|
// Every UI dispatch's push-constant struct begins with this. User shaders
|
||||||
|
// MUST embed it as the first member so UIRenderer::FillHeader works.
|
||||||
|
struct UIDispatchHeader {
|
||||||
|
uint outImage; // heap slot of the swapchain image (this frame)
|
||||||
|
uint itemBuffer; // heap slot of the item SSBO
|
||||||
|
uvec2 surfaceSize; // window pixel size
|
||||||
|
vec4 clipRectPx; // (xy, wh) — every standard shader honors this
|
||||||
|
uint itemCount;
|
||||||
|
uint frameIdx;
|
||||||
|
uint flags; // user-defined feature bits
|
||||||
|
uint _pad; // reserved — keep zeroed
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── standard item structs ──────────────────────────────────────────────
|
||||||
|
// These match the C++ Crafter::QuadItem / CircleItem / ImageItem / GlyphItem
|
||||||
|
// byte-for-byte under std430.
|
||||||
|
struct QuadItem { vec4 rect; vec4 color; vec4 corners; vec4 outline; };
|
||||||
|
// rect = (x, y, w, h) in pixels
|
||||||
|
// color = filled body RGBA (premultiplied alpha not assumed)
|
||||||
|
// corners = per-corner radius in px (TL, TR, BR, BL); 0 = sharp
|
||||||
|
// outline = (thickness, R, G, B); thickness > 0 paints an outline of given color
|
||||||
|
|
||||||
|
struct CircleItem { vec4 centerRadius; vec4 color; vec4 outline; };
|
||||||
|
// centerRadius = (cx, cy, radius, _)
|
||||||
|
// outline.x = thickness (0 = filled), .yzw = outline RGB
|
||||||
|
|
||||||
|
struct ImageItem { vec4 rect; vec4 uv; vec4 tint; uvec4 slots; };
|
||||||
|
// rect = (x, y, w, h)
|
||||||
|
// uv = (u0, v0, u1, v1) into the source texture
|
||||||
|
// tint = multiplied with the sampled color
|
||||||
|
// slots = (textureHeapSlot, samplerHeapSlot, _, _)
|
||||||
|
|
||||||
|
struct GlyphItem { vec4 rect; vec4 uv; vec4 color; };
|
||||||
|
// rect = (x, y, w, h) on screen
|
||||||
|
// uv = (u0, v0, u1, v1) into the SDF font atlas
|
||||||
|
// color = glyph color (alpha modulated by SDF)
|
||||||
|
|
||||||
|
// ─── SSBO heap views ────────────────────────────────────────────────────
|
||||||
|
// One declaration per item type; each shader uses the one matching its
|
||||||
|
// dispatch. Indexed by hdr.itemBuffer.
|
||||||
|
layout(descriptor_heap, std430) readonly buffer UIQuadBuf { QuadItem items[]; } uiQuadHeap[];
|
||||||
|
layout(descriptor_heap, std430) readonly buffer UICircleBuf { CircleItem items[]; } uiCircleHeap[];
|
||||||
|
layout(descriptor_heap, std430) readonly buffer UIImageBuf { ImageItem items[]; } uiImageHeap[];
|
||||||
|
layout(descriptor_heap, std430) readonly buffer UIGlyphBuf { GlyphItem items[]; } uiGlyphHeap[];
|
||||||
|
|
||||||
|
|
||||||
|
// ──── 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.
|
||||||
|
|
||||||
|
ImageItem LoadImageItem(uint heap, uint i) {
|
||||||
|
ImageItem it;
|
||||||
|
it.rect = uiImageHeap[heap].items[i].rect;
|
||||||
|
it.uv = uiImageHeap[heap].items[i].uv;
|
||||||
|
it.tint = uiImageHeap[heap].items[i].tint;
|
||||||
|
it.slots = uiImageHeap[heap].items[i].slots;
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
|
||||||
|
GlyphItem LoadGlpyhtem(uint heap, uint i) {
|
||||||
|
GlyphItem it;
|
||||||
|
it.rect = uiGlyphHeap[heap].items[i].rect;
|
||||||
|
it.uv = uiGlyphHeap[heap].items[i].uv;
|
||||||
|
it.color = uiGlyphHeap[heap].items[i].color;
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
|
||||||
|
CircleItem LoadCircleItem(uint heap, uint i) {
|
||||||
|
CircleItem it;
|
||||||
|
it.centerRadius = uiCircleHeap[heap].items[i].centerRadius;
|
||||||
|
it.color = uiCircleHeap[heap].items[i].color;
|
||||||
|
it.outline = uiCircleHeap[heap].items[i].outline;
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
|
||||||
|
QuadItem LoadQuadItem(uint heap, uint i) {
|
||||||
|
QuadItem it;
|
||||||
|
it.rect = uiQuadHeap[heap].items[i].rect;
|
||||||
|
it.color = uiQuadHeap[heap].items[i].color;
|
||||||
|
it.corners = uiQuadHeap[heap].items[i].corners;
|
||||||
|
it.outline = uiQuadHeap[heap].items[i].outline;
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── pixel-tile dispatch model ─────────────────────────────────────────
|
||||||
|
// Standard shaders dispatch one workgroup per 8×8 screen tile. Each thread
|
||||||
|
// owns ONE pixel and iterates ALL items in order, accumulating the result
|
||||||
|
// in a local register, then stores once at the end. This guarantees correct
|
||||||
|
// z-order within a single dispatch (no inter-workgroup race on imageLoad/
|
||||||
|
// imageStore) and gives the user simple semantics: "items render in array
|
||||||
|
// order, later items overdraw earlier ones".
|
||||||
|
//
|
||||||
|
// Caller dispatches `(ceil(W/8), ceil(H/8), 1)` — no need to know the max
|
||||||
|
// item size.
|
||||||
|
|
||||||
|
// Returns the screen pixel and validates against the surface and clip rect.
|
||||||
|
bool uiResolveScreenPixel(UIDispatchHeader hdr, out ivec2 screenPx) {
|
||||||
|
uvec2 px = gl_GlobalInvocationID.xy;
|
||||||
|
if (px.x >= hdr.surfaceSize.x || px.y >= hdr.surfaceSize.y) return false;
|
||||||
|
if (float(px.x) < hdr.clipRectPx.x || float(px.y) < hdr.clipRectPx.y) return false;
|
||||||
|
if (float(px.x) >= hdr.clipRectPx.x + hdr.clipRectPx.z) return false;
|
||||||
|
if (float(px.y) >= hdr.clipRectPx.y + hdr.clipRectPx.w) return false;
|
||||||
|
screenPx = ivec2(px);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-premultiplied "src over dst" blend. Both operands and result are
|
||||||
|
// straight-alpha vec4. Use this when iterating items in a loop with a local
|
||||||
|
// accumulator.
|
||||||
|
vec4 uiBlendOver(vec4 dst, vec4 src) {
|
||||||
|
float a = clamp(src.a, 0.0, 1.0);
|
||||||
|
vec3 outRGB = mix(dst.rgb, src.rgb, a);
|
||||||
|
float outA = a + dst.a * (1.0 - a);
|
||||||
|
return vec4(outRGB, outA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SDF for a rounded rect with per-corner radius. p is the point relative to
|
||||||
|
// the rect's center; halfSize is the rect half-extents; r is per-corner
|
||||||
|
// (TL, TR, BR, BL). Returns signed distance (negative inside).
|
||||||
|
float uiSdRoundRect(vec2 p, vec2 halfSize, vec4 r) {
|
||||||
|
// Pick the radius for the quadrant p is in.
|
||||||
|
r.xy = (p.x > 0.0) ? r.zy : r.wx; // pick TR/BR vs TL/BL
|
||||||
|
r.x = (p.y > 0.0) ? r.x : r.y;
|
||||||
|
vec2 q = abs(p) - halfSize + r.x;
|
||||||
|
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x;
|
||||||
|
}
|
||||||
65
shaders/ui-text.comp.glsl
Normal file
65
shaders/ui-text.comp.glsl
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
#version 460
|
||||||
|
#extension GL_GOOGLE_include_directive : enable
|
||||||
|
#include "ui-shared.glsl"
|
||||||
|
|
||||||
|
// One workgroup per 8×8 screen tile. Iterates every glyph in order; each
|
||||||
|
// pixel keeps a local accumulator so order in the buffer == draw order.
|
||||||
|
layout(push_constant) uniform PC {
|
||||||
|
UIDispatchHeader hdr;
|
||||||
|
uint fontTextureSlot;
|
||||||
|
uint fontSamplerSlot;
|
||||||
|
uint _p0;
|
||||||
|
uint _p1;
|
||||||
|
} pc;
|
||||||
|
|
||||||
|
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
||||||
|
|
||||||
|
// SDF tuning — must match Crafter::FontAtlas::kOnEdgeValue / kPixelDistScale.
|
||||||
|
const float ON_EDGE = 128.0 / 255.0;
|
||||||
|
const float DIST_SCALE = 32.0;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
ivec2 screenPx;
|
||||||
|
if (!uiResolveScreenPixel(pc.hdr, screenPx)) return;
|
||||||
|
|
||||||
|
vec4 dst = imageLoad(uiImages[pc.hdr.outImage], screenPx);
|
||||||
|
vec2 sp = vec2(screenPx) + 0.5;
|
||||||
|
|
||||||
|
for (uint i = 0u; i < pc.hdr.itemCount; ++i) {
|
||||||
|
GlyphItem it = LoadGlpyhtem(pc.hdr.itemBuffer, i);
|
||||||
|
|
||||||
|
vec2 lo = it.rect.xy;
|
||||||
|
vec2 hi = it.rect.xy + it.rect.zw;
|
||||||
|
if (sp.x < lo.x || sp.y < lo.y) continue;
|
||||||
|
if (sp.x >= hi.x || sp.y >= hi.y) continue;
|
||||||
|
|
||||||
|
vec2 t = (sp - it.rect.xy) / it.rect.zw;
|
||||||
|
vec2 uv = mix(it.uv.xy, it.uv.zw, t);
|
||||||
|
|
||||||
|
float sdf = texture(
|
||||||
|
sampler2D(uiTextures[nonuniformEXT(pc.fontTextureSlot)],
|
||||||
|
uiSamplers[nonuniformEXT(pc.fontSamplerSlot)]),
|
||||||
|
uv
|
||||||
|
).r;
|
||||||
|
|
||||||
|
// Distance in atlas-pixels (negative inside the glyph).
|
||||||
|
float dAtlas = (ON_EDGE - sdf) * DIST_SCALE;
|
||||||
|
|
||||||
|
// Atlas-px per screen-px along this glyph's transform — keeps AA crisp
|
||||||
|
// at any rendering size. uvSpan * atlasSize / screenSpan.
|
||||||
|
vec2 uvSpan = it.uv.zw - it.uv.xy;
|
||||||
|
// FontAtlas::kAtlasSize = 1024.
|
||||||
|
vec2 atlasPerScreen = (uvSpan * 1024.0) / it.rect.zw;
|
||||||
|
float scalePx = max(atlasPerScreen.x, atlasPerScreen.y);
|
||||||
|
// 1-screen-px AA band, expressed in atlas-pixel units of dAtlas.
|
||||||
|
float band = max(scalePx, 0.0001);
|
||||||
|
|
||||||
|
float a = clamp(0.5 - dAtlas / band, 0.0, 1.0);
|
||||||
|
if (a <= 0.0) continue;
|
||||||
|
|
||||||
|
vec4 src = vec4(it.color.rgb, it.color.a * a);
|
||||||
|
dst = uiBlendOver(dst, src);
|
||||||
|
}
|
||||||
|
|
||||||
|
imageStore(uiImages[pc.hdr.outImage], screenPx, dst);
|
||||||
|
}
|
||||||
|
|
@ -1,192 +0,0 @@
|
||||||
#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