UI rewrite 3rd attempt
This commit is contained in:
parent
c9fd1b1585
commit
1f5697326c
48 changed files with 2155 additions and 6190 deletions
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;
|
||||
|
||||
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,
|
||||
.args = std::vector<std::string>(args.begin(), args.end()),
|
||||
});
|
||||
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "VulkanAnimated";
|
||||
cfg.outputName = "VulkanAnimated";
|
||||
cfg.name = "CustomShader";
|
||||
cfg.outputName = "CustomShader";
|
||||
ApplyStandardArgs(cfg, args);
|
||||
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" };
|
||||
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;
|
||||
}
|
||||
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;
|
||||
|
||||
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,
|
||||
.args = std::vector<std::string>(args.begin(), args.end()),
|
||||
});
|
||||
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "VulkanUI";
|
||||
cfg.outputName = "VulkanUI";
|
||||
cfg.name = "HelloUI";
|
||||
cfg.outputName = "HelloUI";
|
||||
ApplyStandardArgs(cfg, args);
|
||||
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" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
|
||||
cfg.files.push_back("Inter.ttf");
|
||||
|
||||
cfg.files.push_back("font.ttf");
|
||||
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
|
||||
));
|
||||
|
||||
traceRayEXT(
|
||||
topLevelAS[bufferStart],
|
||||
gl_RayFlagsNoneEXT,
|
||||
0xff,
|
||||
0, 0, 0,
|
||||
origin,
|
||||
0.001,
|
||||
direction,
|
||||
10000.0,
|
||||
0
|
||||
);
|
||||
// traceRayEXT(
|
||||
// topLevelAS[bufferStart],
|
||||
// gl_RayFlagsNoneEXT,
|
||||
// 0xff,
|
||||
// 0, 0, 0,
|
||||
// origin,
|
||||
// 0.001,
|
||||
// direction,
|
||||
// 10000.0,
|
||||
// 0
|
||||
// );
|
||||
|
||||
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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue