Crafter.Graphics/interfaces/Crafter.Graphics-Mesh.cppm
Jorijn van der Graaf 079b6b0e1f
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
  compositor sends no frame callbacks (hidden, minimized): the Wayland loop
  is a poll() loop, and the Win32 loop sleeps until the next update. Both
  read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
  whose swapchain has other than numFrames images (Mesa's Mailbox has four),
  falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
  before the present, where SaveFrame read an image already presented. A
  frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
  stages the passes use.

Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
  their handles to the deletion queue from their destructors and moves, as
  a frame in flight may still use them; a static owner goes after the
  driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
  copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
  from any thread; Device::frameCounter is atomic. Both the queue and the
  staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
  a Window nothing else reclaims either, so an app without one calls it
  after waiting for its work.

Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
  same storage after a barrier, so the BLAS address stays put when it fits.
  The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
  and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
  of the same size, UploadDeviceLocalRange) copies on the GPU, after those
  reads, through a per-frame staging ring; on ReBAR it used to write the
  buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
  after the reads of frames in flight, and the hit shaders and ray queries
  wait for the decompress as well as the BLAS build. Its vertex buffer
  takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
  in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
  SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
  others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.

Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
  keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
  image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
  GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
  CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
  headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
  does for Math, and builds every test through one helper, projectArgs
  included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
  HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
  RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
  VulkanShaderLifetime, WindowUpdateClock; others extended.

Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 20:20:29 +02:00

366 lines
21 KiB
C++

//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
export module Crafter.Graphics:Mesh;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
import Crafter.Math;
import Crafter.Asset;
import :VulkanBuffer;
export namespace Crafter {
// One procedural primitive's axis-aligned bounding box, in object
// space. Layout-compatible with VkAabbPositionsKHR (24-byte stride),
// so a span of these uploads directly as a
// VK_GEOMETRY_TYPE_AABBS_KHR build input. Mirrors the DOM-side
// definition below so portable user code compiles unchanged.
struct RTAabb {
float min[3];
float max[3];
};
static_assert(sizeof(RTAabb) == sizeof(VkAabbPositionsKHR));
// Build-time tuning for a BLAS, mapped onto the preference bits of
// VkBuildAccelerationStructureFlagBitsKHR.
enum class RTBuildPreference {
// VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR — spend
// more time building for faster traversal. The right default for
// static geometry that is traced many times.
FastTrace,
// VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_BUILD_BIT_KHR — build
// quickly at the cost of traversal speed. For geometry rebuilt
// (or first-frame streamed) often enough that build time dominates.
FastBuild,
};
// Per-build options for Mesh::Build / BuildProcedural.
struct RTBuildOptions {
RTBuildPreference preference = RTBuildPreference::FastTrace;
// Set VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_UPDATE_BIT_KHR so the
// BLAS can later be refit in place via Refit()/RefitProcedural().
// Required for any subsequent refit; refitting a BLAS built without
// it falls back to a full rebuild. PREFER_FAST_TRACE and
// PREFER_FAST_BUILD are mutually exclusive, so `preference` selects
// exactly one — allowUpdate is layered on top of whichever is set.
bool allowUpdate = false;
// Set VK_GEOMETRY_NO_DUPLICATE_ANY_HIT_INVOCATION_BIT_KHR: an any-hit
// shader then runs at most once per primitive along a ray, which a
// shader that accumulates along it (transmittance through tinted
// glass) relies on. Without it the implementation may call it twice
// for one primitive. Can cost some trace speed.
bool noDuplicateAnyHit = false;
};
class Mesh {
public:
VulkanBuffer<char, false> scratchBuffer;
VulkanBuffer<char, false> blasBuffer;
// Non-mapped (device-local) RT geometry: VulkanBuffer::UploadDeviceLocal
// places these in device memory and picks direct-map vs staged-copy per
// the #89 upload strategy, so they live in VRAM for hit-shader fetch and
// BLAS-build/refit reads instead of being read from system RAM over PCIe
// every trace (#73). The compressed Build path allocates them pure
// DEVICE_LOCAL and lets the GPU decompressor fill them directly.
VulkanBuffer<Vector<float, 3, 3>, false> vertexBuffer;
VulkanBuffer<std::uint32_t, false> indexBuffer;
// AABB build input for the procedural path (BuildProcedural).
// Lifetime contract matches vertexBuffer/indexBuffer: must stay
// alive until the build submitted on `cmd` completes.
VulkanBuffer<RTAabb, false> aabbBuffer;
// Transient host-visible staging for the compressed Build path. Kept as
// a member only so the recorded vkCmdDecompressMemoryEXT has a stable
// address to reference; the compressed Build releases it via
// DeferredClear() right after recording the decompress, so it is freed
// by the fence-keyed deletion queue (#101/#102) once that submit's frame
// has cleared — no longer pinned for the mesh's life. Between Builds the
// handle is null; the next Build's Resize re-creates it.
VulkanBuffer<std::byte, true> compressedStaging;
VkAccelerationStructureKHR accelerationStructure = VK_NULL_HANDLE;
VkDeviceAddress blasAddr = 0;
// ─── Build-option / refit state ──────────────────────────────────
// The geometry's flags at the last (re)build: OPAQUE for triangles
// and for procedural geometry built opaque, plus
// NO_DUPLICATE_ANY_HIT_INVOCATION when asked for. Kept because an
// UPDATE must describe its geometry exactly as the build did.
VkGeometryFlagsKHR geometryFlags = VK_GEOMETRY_OPAQUE_BIT_KHR;
// Flags the BLAS was last (re)built with — carried so Refit can
// re-issue an identical-flags UPDATE build (the FAST_TRACE/FAST_BUILD
// preference plus, when opted in, ALLOW_UPDATE).
VkBuildAccelerationStructureFlagsKHR buildFlags = 0;
// Primitive count of the current build (triangles = indexCount/3,
// procedural = aabb count). An in-place UPDATE must preserve it.
std::uint32_t builtPrimitiveCount = 0;
// Element count the vertex / aabb input buffer was sized for. Refit
// re-uploads in place only when the new data matches this; a change
// forces a full rebuild (topology changed).
std::uint32_t builtInputCount = 0;
// Whether ALLOW_UPDATE was set on the last build, i.e. whether an
// in-place refit is possible at all.
bool allowUpdate = false;
Mesh() = default;
// Moving hands the BLAS over: the moved-from Mesh keeps no handle, so
// only the last owner releases it. Assigning queues the BLAS this one
// held, as the destructor would.
Mesh(Mesh&& other);
Mesh& operator=(Mesh&& other);
// Queues the BLAS for deferred deletion, ahead of the buffers it lives
// in (they queue themselves): a frame in flight may still trace it.
~Mesh();
// A rebuild (any Build on a Mesh that already has a BLAS) queues the
// old handle for deferred deletion, since this command buffer or a
// frame in flight may still use it, and builds into the same storage
// after a barrier when the new BLAS fits, so blasAddr normally stays
// the same. A BLAS that outgrows its storage moves, so read blasAddr
// again after a rebuild.
void Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd, RTBuildOptions options = {});
// GPU path: decompresses vertex (region 0) and index (region 1) streams
// from asset.blob into vertexBuffer / indexBuffer using
// VK_EXT_memory_decompression. Falls back to CPU decode + the
// uncompressed Build if Device::memoryDecompressionSupported is false.
// Region 2 (data) is not consumed here — the caller decompresses it
// into their own buffer if needed (or uses Compression::DecompressCPU).
void Build(const ::Crafter::CompressedMeshAsset& asset, VkCommandBuffer cmd, RTBuildOptions options = {});
// Build an AABB (procedural) BLAS from a list of object-space
// boxes (VK_GEOMETRY_TYPE_AABBS_KHR). The hit group bound to
// instances of this mesh must be a
// VK_RAY_TRACING_SHADER_GROUP_TYPE_PROCEDURAL_HIT_GROUP_KHR group
// carrying an intersection shader; that shader is invoked for
// each box the ray enters and reports the surface hit via
// reportIntersectionEXT. `opaque` maps to
// VK_GEOMETRY_OPAQUE_BIT_KHR: pass false (the WebGPU-path
// default) to let any-hit shaders run. Same scratch/lifetime
// handling as the triangle Build; instances reference blasAddr
// exactly like a triangle BLAS.
void BuildProcedural(std::span<const RTAabb> aabbs, bool opaque, VkCommandBuffer cmd, RTBuildOptions options = {});
// Zero-copy procedural build: take the AABB build input straight from
// an existing device buffer (by device address + primitive count)
// rather than memcpy-ing a host span through aabbBuffer. The intended
// producer is a GPU compute pass that writes the boxes itself — e.g.
// a GPU-resident particle system whose per-frame AABBs never touch the
// CPU. The buffer must carry
// VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR
// and VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, its boxes laid out as
// VkAabbPositionsKHR (`stride`, default sizeof(RTAabb) == 24), and the
// caller must pipeline-barrier the producing writes (e.g. a compute
// dispatch) before the build on `cmd` reads them, with
// VK_ACCESS_SHADER_READ_BIT at the acceleration-structure build stage:
// a build reads its geometry as a shader read, and the
// ACCELERATION_STRUCTURE access bits cover only the scratch and the
// acceleration structures themselves. The buffer's
// lifetime is the caller's responsibility — it is never copied, so it
// must outlive the build submitted on `cmd`. Everything else (opaque
// bit, hit-group contract, instance wiring) matches the host-span
// BuildProcedural above.
void BuildProcedural(VkDeviceAddress aabbAddress, std::uint32_t count, bool opaque, VkCommandBuffer cmd, RTBuildOptions options = {}, std::uint32_t stride = sizeof(RTAabb));
// Refit the triangle BLAS against new geometry of the *same
// topology* (same vertex count and index count as the original
// Build). When that Build set RTBuildOptions::allowUpdate this issues
// a hardware UPDATE-mode build — much cheaper than a rebuild, and the
// BLAS handle / blasAddr are preserved, so TLAS instances referencing
// it stay valid. A hardware update may only move vertex positions,
// not change connectivity, so on the UPDATE path only the vertex
// buffer is re-uploaded; the index buffer is left untouched (its
// contents are immutable for an UPDATE). The `indicies` span is read
// only for its count — a bitwise-different but topologically-equivalent
// index array is ignored on refit. Without allowUpdate (or if the
// counts changed) it falls back to a full rebuild, which does re-upload
// both buffers. Lifetime contract matches Build: the spans need only
// outlive this call. Call this per frame to track a deforming mesh.
void Refit(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd);
// Dirty-range refit: same as Refit above, but only the contiguous block
// of vertices in [dirtyVertexOffset, dirtyVertexOffset + dirtyVertexCount)
// actually moved, so on the in-place UPDATE path only that sub-range is
// re-uploaded — the staged copy scales with the moved vertices, not the
// whole array (#119). Use
// this for a deforming mesh that nudges a small, known window each frame.
// `verticies` / `indicies` are still the *full* arrays (same contract and
// topology rule as the full-span Refit — the index buffer is read only
// for its count on the UPDATE path); the dirty window simply tells the
// upload which slice changed, and is clamped to the array bounds. When an
// in-place UPDATE is not possible (allowUpdate was not set, or the counts
// changed) this falls back to the full-span Refit, which re-uploads
// everything from `verticies` / `indicies` — so passing the full arrays
// keeps that fallback correct. The AS handle / blasAddr are preserved on
// the UPDATE path exactly as in the full-span Refit.
void Refit(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, std::uint32_t dirtyVertexOffset, std::uint32_t dirtyVertexCount, VkCommandBuffer cmd);
// Procedural analog of Refit: new object-space boxes, same count.
void RefitProcedural(std::span<const RTAabb> aabbs, VkCommandBuffer cmd);
// Zero-copy procedural refit: the device-buffer counterpart of
// RefitProcedural above and the input-source pairing for the device
// BuildProcedural. The GPU producer rewrote the boxes in `aabbAddress`
// in place; this re-records the build straight from that address —
// an in-place hardware UPDATE when the original build set allowUpdate
// and `count` is unchanged (handle / blasAddr preserved), otherwise a
// full rebuild. Same buffer-usage / barrier / lifetime contract as the
// device BuildProcedural. Call per frame to track GPU-written boxes
// with zero host involvement.
void RefitProcedural(VkDeviceAddress aabbAddress, std::uint32_t count, VkCommandBuffer cmd, std::uint32_t stride = sizeof(RTAabb));
};
}
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
import Crafter.Math;
import Crafter.Asset;
import :WebGPU;
export namespace Crafter {
// Software-RT BLAS node, packed to 32 bytes. Matches the WGSL
// `BVHNode` struct in the RT WGSL prelude (additional/dom-webgpu.js,
// rtWgslPrelude) byte-for-byte.
//
// primCount == 0 → inner node, children at indices
// firstChildOrPrim and firstChildOrPrim+1.
// primCount > 0 → leaf, `primCount` primitives starting at
// primIndex `firstChildOrPrim` in the
// global primRemap heap.
//
// SAH-built BVH2; constructed CPU-side at Build() time, never refit.
struct BVHNode {
float aabbMin[3];
std::uint32_t firstChildOrPrim;
float aabbMax[3];
std::uint32_t primCount;
};
static_assert(sizeof(BVHNode) == 32);
// One procedural primitive's axis-aligned bounding box, in object
// space. The analog of VkAabbPositionsKHR — the BLAS stores these
// instead of triangles and an intersection shader (registered in the
// hit group as a ProceduralHitGroup) reports the actual surface hit
// for each AABB the ray enters.
struct RTAabb {
float min[3];
float max[3];
};
static_assert(sizeof(RTAabb) == 24);
// Mirror of the native build-option types so portable code compiles
// unchanged. The software-RT path has no hardware acceleration
// structure, so the FastTrace/FastBuild preference has no effect (the
// SAH BVH2 is always built the same way) and a "refit" simply rebuilds
// the BVH on the host. The types exist purely for API symmetry.
enum class RTBuildPreference {
FastTrace,
FastBuild,
};
struct RTBuildOptions {
RTBuildPreference preference = RTBuildPreference::FastTrace;
bool allowUpdate = false;
// No effect here either; for API symmetry like the rest.
bool noDuplicateAnyHit = false;
};
class Mesh {
public:
// BLAS "handle": opaque identity that goes into
// RTInstance::accelerationStructureReference. Set by Build() to a
// stable u32 (widened to u64 for Vulkan-struct layout parity), used
// by the WebGPU TLAS-build compute shader to look up the BLAS root
// AABB and per-mesh heap offsets. Handle 0 is the unassigned
// sentinel; never returned by Build().
std::uint64_t blasAddr = 0;
std::uint32_t triangleCount = 0;
std::uint32_t vertexCount = 0;
bool opaque = true;
// Build BLAS from raw triangle data. Runs the CPU SAH BVH2 builder
// and forwards vertex/index/BVH/remap arrays to the JS-side mesh
// heap (additional/dom-webgpu.js), which queue.writeBuffers them
// into the global heaps and records the per-mesh offsets keyed by
// the returned handle. The `cmd` parameter is unused on WebGPU —
// kept for API symmetry with the Vulkan signature.
void Build(std::span<Crafter::Vector<float, 3, 3>> vertices,
std::span<std::uint32_t> indices,
WebGPUCommandEncoderRef cmd = 0,
RTBuildOptions options = {});
// CPU-decompress the .cmesh blob (no VK_EXT_memory_decompression
// equivalent in WebGPU) and forward to the positions+indices path,
// plus push the optional `data` region into the per-vertex attribs
// heap so closest-hit shaders can sample UVs / normals / tangents.
// The data layout is example-defined — the heap is exposed in WGSL
// as `vertexAttribs : array<u32>` with a per-mesh u32-word offset.
void Build(const ::Crafter::CompressedMeshAsset& asset,
WebGPUCommandEncoderRef cmd = 0,
RTBuildOptions options = {});
// Build an AABB (procedural) BLAS from a list of object-space boxes
// — the WebGPU analog of a VK_GEOMETRY_TYPE_AABBS_KHR geometry. The
// hit group bound to instances of this mesh must be a
// ProceduralHitGroup carrying an intersection shader; that shader is
// invoked for each box the ray enters and reports the surface hit.
// `opaque` is the geometry's opaque bit: pass false to let any-hit
// shaders run (the default for procedural geometry, which is usually
// transparent / volumetric). The `cmd` parameter is unused on
// WebGPU — kept for API symmetry with the triangle path.
void BuildProcedural(std::span<const RTAabb> aabbs,
bool opaque = false,
WebGPUCommandEncoderRef cmd = 0,
RTBuildOptions options = {});
// Zero-copy procedural build: the boxes already live in an existing
// device buffer (`aabbBuffer`, a GPUBuffer a compute pass wrote), fed
// straight into the BLAS with no host copy — the WebGPU analog of the
// device-address Vulkan BuildProcedural. The software path has no
// hardware AS and builds its BVH on the host, but here there is no
// host copy to build over, so the device boxes are copied GPU→GPU into
// the mesh heap and wrapped in a single root leaf bounded by
// `worldBounds` (object-space min/max enclosing all `count` boxes —
// the producer supplies it since the CPU never sees the boxes). The
// traversal then linearly intersects every box. `opaque` is the
// geometry opaque bit (false lets any-hit run). `options` has no effect
// (no hardware AS to tune), kept for API symmetry.
void BuildProcedural(WebGPUBufferRef aabbBuffer,
std::uint32_t count,
RTAabb worldBounds,
bool opaque = false,
WebGPUCommandEncoderRef cmd = 0,
RTBuildOptions options = {});
// Refit analogs of the native API. With no hardware AS to update,
// these simply re-run the host BVH build over the new data, so they
// accept the full geometry (not just moved positions). Provided so
// portable code that calls Refit/RefitProcedural compiles and
// behaves correctly on the WebGPU backend.
void Refit(std::span<Crafter::Vector<float, 3, 3>> vertices,
std::span<std::uint32_t> indices,
WebGPUCommandEncoderRef cmd = 0);
// Dirty-range refit (#119). The software path has no hardware AS to
// update a sub-range of — it rebuilds the host BVH over the full
// geometry regardless — so the dirty window is ignored here and this
// behaves exactly like the full-span Refit above (a fresh build that
// re-publishes blasAddr). The overload exists so portable deforming-mesh
// code that passes a dirty window compiles and stays correct on WebGPU.
void Refit(std::span<Crafter::Vector<float, 3, 3>> vertices,
std::span<std::uint32_t> indices,
std::uint32_t dirtyVertexOffset,
std::uint32_t dirtyVertexCount,
WebGPUCommandEncoderRef cmd = 0);
void RefitProcedural(std::span<const RTAabb> aabbs,
WebGPUCommandEncoderRef cmd = 0);
// Zero-copy procedural refit: re-copy the boxes from the device buffer
// into this mesh's heap region (count unchanged) and update the root
// leaf bounds, keeping blasAddr stable — unlike the host-span
// RefitProcedural, which rebuilds and re-publishes a NEW handle. The
// device counterpart of the device-address Vulkan RefitProcedural;
// call per frame to track GPU-written boxes with no host copy.
void RefitProcedural(WebGPUBufferRef aabbBuffer,
std::uint32_t count,
RTAabb worldBounds,
WebGPUCommandEncoderRef cmd = 0);
};
}
#endif // CRAFTER_GRAPHICS_WINDOW_DOM