Commit graph

40 commits

Author SHA1 Message Date
00b84dd301 Merge pull request 'perf(rt): high-water-mark growth for TLAS host-input buffers (#64)' (#98) from claude/issue-64 into master 2026-06-16 20:24:48 +02:00
catbot
8e6ba49743 perf(rt): high-water-mark growth for TLAS host-input buffers (#64)
BuildTLAS reallocated the host-visible instanceBuffer and metadataBuffer
on every topology change. They only ever need to hold at least
primitiveCount entries (the AS build reads exactly primitiveCount via
tlasRangeInfo, and the copy loop writes [0, primitiveCount)), so a
shrink — or any growth that still fits the previous capacity — can reuse
the existing allocation. Gate the two Resize calls on a high-water-mark
check, removing two of the four reallocations on a count change. The AS
storage + scratch rebuild below is unchanged: it is tied to the AS
itself and dominates this path regardless.

Adds tests/TLASHighWaterMark driving the real hardware AS-build path: it
asserts a shrink (and within-capacity growth) reuses the buffers while a
growth past the high-water reallocates, that builtInstanceCount still
tracks the live count, and that feeding an oversized instance buffer to
the build produces zero Vulkan validation errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 18:24:09 +00:00
bd8076769a Merge pull request 'perf(pipeline): shared, disk-persisted VkPipelineCache (#69)' (#97) from claude/issue-69 into master 2026-06-16 20:24:03 +02:00
catbot
a728482731 test(pipeline): cover pipeline-cache header validation (#69)
PipelineCacheValidation drives Device::PipelineCacheDataCompatible
directly (no GPU): synthetic device identities + 32-byte cache headers
assert that a matching header is accepted while foreign vendor/device,
a bumped UUID (driver update), unknown version, undersized headerSize,
and short/empty blobs are all rejected. Mirrors the GPU-free style of
MemoryTypeFallback / UploadStrategy. Also gitignore the serialized
pipeline_cache.bin a real-device test run drops in the working dir.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 18:23:25 +00:00
catbot
b6d0ec5cae perf(buffer): capacity-reuse in VulkanBuffer::Resize (#63)
Resize previously always destroyed + reallocated. It now reuses the
existing allocation in place when the new request still fits the created
capacity and the immutable-at-create properties match: usage flags are
fixed at create, and the chosen memory type must still satisfy the
required property flags. preferredPropertyFlags is a best-effort perf
hint and is intentionally not part of the guard. On reuse the buffer
handle, device address and mapped pointer are preserved; only `size`
shrinks to the new logical extent.

Tracks capacity and the created usage flags on VulkanBufferBase, set at
Create and carried through the move constructor.

Adds VulkanBufferResizeReuse, a device-free regression test that drives
the reuse guard directly and asserts the in-place path issues no Vulkan
call.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 18:22:09 +00:00
catbot
cfbe181d9e feat(device): runtime upload-strategy helper PreferDirectDeviceWrite (#89)
Add Device::PreferDirectDeviceWrite(size) so the memory-placement cluster
(#65/#72/#73/#75) no longer re-derives where a CPU-written, GPU-read buffer
should live. It picks the strategy at runtime from Device::memoryProperties:

  - No DEVICE_LOCAL|HOST_VISIBLE type (no resizable BAR) -> false (must stage).
  - ReBAR/UMA (BAR heap >= 90% of largest DEVICE_LOCAL heap) -> true: map and
    write directly; a staging copy would be pure overhead.
  - Small BAR window (BAR heap << VRAM) -> true only for buffers within a
    per-window budget (1/8 of the window); large buffers stage so they don't
    exhaust the window (#58).

Complements GetMemoryType (#59): #59 picks the memory *type*, this picks the
*strategy*. Upload-only — readback (#74) wants HOST_CACHED and stays separate.
A VK_EXT_memory_budget-driven remaining-space check is noted as a follow-up.

Tested by tests/UploadStrategy (pure CPU over synthetic ReBAR / UMA /
small-window / no-BAR layouts, no GPU device needed), mirroring
MemoryTypeFallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:23:27 +00:00
catbot
c670bc70b7 Merge remote-tracking branch 'origin/master' into claude/issue-47
# Conflicts:
#	project.cpp
2026-06-16 17:10:39 +00:00
catbot
e8f1c7cff9 Merge remote-tracking branch 'origin/master' into claude/issue-47
# Conflicts:
#	interfaces/Crafter.Graphics-UI.cppm
#	project.cpp
2026-06-16 17:09:06 +00:00
catbot
bdbe3bf568 Merge remote-tracking branch 'origin/master' into claude/issue-60
# Conflicts:
#	project.cpp
2026-06-16 17:07:32 +00:00
bda21979fd perf(vulkan-buffer): skip flush/invalidate on coherent memory (#60)
FlushDevice/FlushHost called vkFlushMappedMemoryRanges /
vkInvalidateMappedMemoryRanges unconditionally. Record the chosen
memory type's propertyFlags at Create time (via the index GetMemoryType
returns, not the requested flags) and early-return when HOST_COHERENT.

The cmd overload still always emits its pipeline barrier — only the
redundant host-side flush/invalidate is gated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:04:26 +00:00
catbot
574242dd30 perf(ui): add additive DispatchFused to collapse consecutive UI passes (#47)
Add a fifth UI compute path — DispatchFused — alongside the four
per-element Dispatch* calls. It composites quads → circles → images →
text (canonical back-to-front order) in ONE dispatch that loads the
destination image once and stores once, eliminating the per-pass
load+store and the inter-pass VkMemoryBarriers a run of consecutive
Dispatch* calls would pay. The win materialises at >= 2 non-empty
categories; an absent category is a zero-trip, push-constant-uniform
loop (~free).

The new uber-kernel (shaders/ui-fused.comp.glsl) reuses each category's
exact cooperative shared-memory tile-cull + per-pixel accumulate, so a
fused category is pixel-identical to its standalone pass. Categories run
sequentially per thread and share one set of shared-memory scratch, so
the VGPR/shared high-water mark stays at the per-category level, not the
sum.

Additive and non-breaking: the per-element Dispatch* calls and the
frozen 48-byte UIDispatchHeader are untouched. DispatchFused gets its own
128-byte push-constant layout (UIFusedHeader: four uvec4 headers + four
per-category clip vec4s). To interleave a custom ui.Dispatch() between
standard passes, bracket it with two DispatchFused calls — the dispatch
boundary is the explicit, app-declared flush point.

WebGPU (Vulkan-second): DispatchFused falls back to the per-element
Dispatch* calls in canonical order — one texture/sampler per dispatch
there can't feed the bindless image phase — so the result matches; only
the load/store fusion is Vulkan-only.

HelloUI now composites its background quads, mouse circle, and label
text in a single DispatchFused. Verified rendering identical on a live
Vulkan/Wayland session.

Resolves #47

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:03:24 +00:00
catbot
ce1eaf6366 Merge remote-tracking branch 'origin/master' into claude/issue-57
# Conflicts:
#	project.cpp
2026-06-16 17:02:48 +00:00
catbot
c169986835 perf(font): cache per-codepoint advances in font units (#57)
Font::GetLineWidth called stbtt_GetCodepointHMetrics for every glyph on
every invocation. InputField_HitTestCursor rescans the whole field once
per character, making caret hit-testing O(n²) in HMetrics lookups.

Memoise advances per Font via Font::AdvanceUnits: ASCII in a flat array,
the rest in a map. Advances are cached in unscaled font *units* and
rescaled per call — Glyph::advance in the FontAtlas is baked at kBaseSize
and is wrong at any other size, so it cannot be reused. The per-glyph
(int) truncation in GetLineWidth is preserved exactly.

Adds the FontAdvanceCache CPU-only regression test covering size
independence of the cached unit advance, the per-glyph-truncated rescale
pipeline, cache-hit determinism, width scaling with size, and the
non-ASCII map path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:00:48 +00:00
catbot
f2f5a770c0 Merge remote-tracking branch 'origin/master' into claude/issue-50
# Conflicts:
#	project.cpp
2026-06-16 16:59:05 +00:00
catbot
65c19ea84a perf(ui): gate per-pixel clip compares behind a flags bit (#50)
uiResolveScreenPixel ran four float compares against hdr.clipRectPx for
every pixel of every UI dispatch, even though the clip rect is the
full-surface default {0,0,1e9,1e9} in the overwhelmingly common case.

Reserve the high bit of the header flags word (UI_FLAG_CLIP) and have
FillHeader set it only when the clip rect is narrower than the surface
(extracted into the unit-testable UIRenderer::ClipFlags). The shader
gates the compares on the bit; the branch is push-constant-uniform so it
never diverges. When the rect covers the surface the skipped compares
could never reject an in-surface pixel, so output is pixel-identical.

Mirrored on the WebGPU/WGSL path (dom-webgpu.js) for parity. Adds the
UIClipFlag CPU test covering the gate decision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:57:07 +00:00
catbot
d712218f17 perf(input-field): O(n) cursor hit test via Font::NearestCursorByte (#56)
InputField_HitTestCursor mapped a click x to a cursor byte offset by
calling Font::GetLineWidth on every prefix value[0..i] for i=1..size.
Each call re-walks from byte 0, so the hit test was O(n^2) glyph-metric
lookups (each an uncached stbtt cmap binary search). It also iterated
raw byte boundaries, so on multibyte UTF-8 input it could return an
offset splitting a codepoint — an invalid position for the byte-based
cursorPos.

Add Font::NearestCursorByte: one left-to-right cumulative-advance pass
(O(n) metrics, scale computed once) over codepoint boundaries, returning
the byte offset of the boundary nearest the target. Cumulative advance is
monotonic, so the scan stops past the closest boundary. The hit test now
delegates to it.

Adds tests/InputFieldHitTest, a pure-CPU regression test (no Vulkan
device/window): a px sweep across ASCII and multibyte strings compared
against an independent brute-force nearest-boundary oracle, plus the
left/right/empty/end-of-text edge cases and a codepoint-boundary invariant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:55:03 +00:00
catbot
294c1378b5 fix(device): add preferred mask + required-only fallback to GetMemoryType (#59)
GetMemoryType was a bare first-superset match that threw whenever no
memory type satisfied the requested property flags. Callers combining a
mandatory flag with a perf-only one (HOST_VISIBLE | DEVICE_LOCAL for the
descriptor heaps) would then fail allocation on any device without a
host-visible device-local heap (no resizable BAR).

It now takes a `preferred` mask distinct from `required`: a type
satisfying both is chosen first, falling back to a required-only match
when the preference is unavailable, and throwing only when even
`required` cannot be met. VulkanBuffer::Create/Resize gain an optional
trailing `preferredPropertyFlags`, and the descriptor heaps now treat
DEVICE_LOCAL as a preference on top of mandatory HOST_VISIBLE.

This does not touch any flush/barrier behaviour — coherency is not
assumed anywhere here.

Adds tests/MemoryTypeFallback, a pure-CPU test that installs synthetic
memory layouts and exercises selection, preference, fallback, and the
unsatisfiable-required throw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:03:50 +00:00
991a39e094 Merge pull request 'perf(font): upload only the dirty sub-rect of the glyph atlas (#51)' (#83) from claude/issue-51 into master 2026-06-16 17:43:23 +02:00
catbot
e4f8b057f3 Merge remote-tracking branch 'origin/master' into claude/issue-51
# Conflicts:
#	project.cpp
2026-06-16 15:43:11 +00:00
1451e3aeb2 Merge pull request 'feat(window): multi-frame-in-flight frame pacing (#40)' (#81) from claude/issue-40 into master 2026-06-16 17:42:13 +02:00
catbot
ca48f79465 perf(text): cache shaped runs in ShapeText (#52)
ShapeText re-decoded UTF-8, re-looked-up every glyph, and recomputed
layout every frame for static strings (DrawText / EmitCenteredLabel run
inside per-frame onBuild). DrawText and EmitCenteredLabel also did a
second full pass over the glyphs to apply alignment.

Memoize the shaped run, keyed by (Font*, pxSize, color, utf8), as an
origin-relative vector<GlyphItem>. A cache hit skips decode/lookup/layout
and just translates the cached run into the output buffer by
(x + alignShift, baselineY). The alignment shift (Center = -advance/2,
Right = -advance) is folded into ShapeText's single pass, dropping the
second loop in both callers. TextAlign moves to the :UI partition so
ShapeText can take it.

Pure CPU memoization — the glyph buffer is fully rewritten and re-flushed
every frame, so a hit is byte-equivalent. A miss still calls Ensure so new
glyphs rasterise and dirty the atlas; hits don't (atlas entries are never
evicted). InvalidateFont drops a font's runs before it is destroyed (the
cache, like FontAtlas's glyph cache, keys on the Font pointer). A soft cap
bounds memory against pathological per-frame-unique text.

Adds the ShapeTextCache test (15 checks): hit == miss byte-for-byte, hits
don't touch the atlas, translate/alignment/truncation/InvalidateFont.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:40:52 +00:00
catbot
97c79c23ee perf(font): upload only the dirty sub-rect of the glyph atlas (#51)
FontAtlas::Update re-uploaded the entire 1024x1024 R8 atlas (1 MiB)
whenever a single glyph was rasterized, so warm-up / language-switch /
size-churn cost roughly one full-atlas copy per frame that added a
codepoint.

Accumulate a dirty bounding box (FontAtlas::DirtyRect) in MarkDirty as
glyphs are placed, and copy only that sub-rect. On Vulkan this is a new
ImageVulkan::UpdateRegion: the staging buffer keeps the full atlas row
stride, so bufferRowLength stays the atlas width and bufferOffset points
at the rect origin while imageOffset/imageExtent carve out the rect.
Layout transitions stay whole-image (cheap); only the copy extent
shrinks. WebGPU passes the rect straight to wgpuWriteAtlasRegion, which
already took x/y/w/h.

The freshly-zeroed atlas marks its whole extent dirty in Initialize so
the first Update clears the image once; thereafter every untouched texel
stays the zero it was uploaded as, keeping the image byte-identical to
the staging copy.

Tested: new FontAtlasDirtyRect unit test covers the accumulation /
clamp / lockstep-with-`dirty` math (no GPU needed); HelloUI renders text
correctly on both Vulkan (NVIDIA, validation-clean) and WebGPU.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:40:37 +00:00
catbot
621016f264 feat(window): multi-frame-in-flight frame pacing (#40)
The renderer was effectively single-buffered despite allocating
triple-buffered infrastructure: Render() ended with an unconditional
vkQueueWaitIdle, and a single (presentComplete, renderComplete)
semaphore pair with zero fences was shared for the Window's lifetime.

Rework the pacing model so up to numFrames frames overlap:
- Per-swapchain-image VkFence, signaled by the submit and waited+reset
  before that image's command buffer / descriptor-heap slot is
  re-recorded. Keyed by acquired image index (drawCmdBuffers, the heap
  slots, and the swapchain images are all image-indexed — see
  WriteSwapchainDescriptors). Created signaled so first use passes.
- Per-image render-finished (present) semaphore: the presentation engine
  holds it until the image is re-acquired, so per-image is the only safe
  key (per-CPU-frame trips VUID-vkQueueSubmit-pSignalSemaphores-00067).
- Per-CPU-frame acquire semaphores (image index unknown until acquire
  returns), sized numFrames+1: in-flight depth is bounded by the
  per-image fences, so numFrames+1 distinct acquire semaphores guarantee
  the reused one has no pending op (VUID-vkAcquireNextImageKHR-01779).
- Drop the steady-state vkQueueWaitIdle; keep it on resize / OUT_OF_DATE
  / teardown.

Add tests/FrameLoopSync: drives the real frame loop against a live
Wayland compositor for 60 frames (>> in-flight slots) and asserts the
CPU frame counter advanced, the swapchain rotated across multiple
images, and the validation layer stayed silent — the load-bearing check,
since the old single-pair design only avoided being an active race
because the wait-idle masked it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:37:11 +00:00
catbot
ce0114607a test(vulkan-rt): headless BLAS build-options + refit test (#36)
Exercises the real hardware AS-build path: builds and refits triangle
and procedural BLASes with fast-build/fast-trace + allow-update flags
via one-time command buffers, asserting the requested flags land, that
an allowUpdate refit keeps the same AS handle/blasAddr (in-place
UPDATE), that the no-update / topology-change fallback still produces a
valid BLAS, and that the validation layer reports zero errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 13:37:46 +00:00
catbot
419730f46b feat(input): wire native mouse-wheel scroll on Wayland + Win32, normalize all backends to ±1/detent (#32)
Window::onMouseScroll now fires on every backend and speaks one unit:
signed whole detents packed in the uint32 payload, +1 = wheel down
(DOM deltaY sign).

- Wayland: implement the previously-stubbed PointerListenerHandleAxis —
  vertical axis only, wl_fixed_to_double / 15 (libinput's units-per-
  detent), sub-detent remainder accumulated and reset on pointer leave.
- Win32: add the missing WM_MOUSEWHEEL case — -GET_WHEEL_DELTA_WPARAM /
  WHEEL_DELTA with a remainder accumulator for free-spinning wheels.
- DOM: dom-env.js normalizes WheelEvent.deltaY per deltaMode (100px /
  3 lines / 1 page per detent) with the same remainder scheme, so wasm
  delivers the identical unit instead of raw browser-specific deltas.
- Document the contract on Window::onMouseScroll.
- tests/MouseScroll: compositor-free regression test driving the
  Wayland axis handler directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 15:02:28 +00:00
catbot
471f480c5d test(vulkan-rt): spirv-val coverage for the push-constant rewrite (#18)
Adds tests/PushConstantRewrite, a host test that compiles representative
ray-generation shaders with glslang, runs the real WorkaroundNvidiaAS::Patch
over them, and asserts with spirv-val (the same invocation vkCreateShaderModule
uses) that the result is valid and contains exactly one push-constant block —
covering both the merge path (shaders that already declare a push constant,
including mat4/vec3/uint, a lone uint, and an array layout) and the synthesize
path, plus a no-op case (push constant but no AS read). It also checks the
published TLAS push offset for each layout.

The workaround namespace is exported so the test can drive Patch directly; both
go away with the rest of the workaround. project.cpp wires the test as an
executable that recompiles the module and requires glslang + spirv-val.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 02:28:09 +00:00
162d98cf5b got rid of --local 2026-05-27 04:38:30 +02:00
8347467e1e webgpu improvements 2026-05-24 13:32:08 +02:00
b5d0f52da0 webgpu sponza 2026-05-19 00:27:09 +02:00
5553ded476 webgpu triangle 2026-05-18 18:43:30 +02:00
64116cd980 custom shader webgpu 2026-05-18 05:39:17 +02:00
dedf6b0467 webgpu support 2026-05-18 04:58:52 +02:00
5352ef69a2 browser DOM support 2026-05-18 02:07:48 +02:00
ac2eb7fb0a new input system 2026-05-12 00:24:48 +02:00
c054f1e0b3 update 2026-05-03 02:45:38 +02:00
1f5697326c UI rewrite 3rd attempt 2026-05-02 21:08:20 +02:00
216972e73a new UI system 2026-05-01 23:35:37 +02:00
d29f5609cd fix 2026-04-30 02:05:16 +02:00
7f5297ca57 add Rendertarget and Shm implementations to project.cpp
Both were on disk but missing from the V2 port's implementations list;
Rendertarget is required for RendertargetVulkan linkage, Shm is the
Wayland shared-memory helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 02:00:17 +02:00
8a2fd33efc crafter-build V2 2026-04-30 01:29:17 +02:00