Skip to content

split/threedim#2079

Open
jcelerier wants to merge 115 commits into
masterfrom
split/threedim
Open

split/threedim#2079
jcelerier wants to merge 115 commits into
masterfrom
split/threedim

Conversation

@jcelerier

Copy link
Copy Markdown
Member
  • isf: TEX_DIMENSIONS_3D / IMG_SIZE_3D aliases for 3D samplers
  • isf: point3d_input AS_COLOR flag for color-swatch display
  • isf: parse-time warning on unknown csf_image_input FORMAT
  • isf: parser rework — top-level descriptors, uniform inputs, geometry AUXILIARY, vertex inputs
  • 3rdparty: add OffsetAllocator submodule
  • gfx: add AssetTable + TextureLoader for shared decoded-asset cache
  • gfx: add GpuResourceRegistry — slab-allocated GPU arenas backed by OffsetAllocator
  • gfx: add CameraMath / GpuTiming / VertexFallback helpers
  • gfx: add IsfBindingsBuilder + PipelineStateHelpers
  • gfx: add SceneGPUState — FlatScene + scene packer
  • gfx: add OffscreenDevice + RhiPreviewWidget + Metal buffer-copy backend
  • gfx: extend ISF / SimpleRenderedISF nodes for new ISF features
  • csf: rework for scene-aware compute pipeline
  • gfx: rework raw raster pipeline (MRT, AUXILIARY, EXECUTION_MODEL) and VSA
  • gfx: refresh shared graph plumbing for incremental scene pipeline
  • gfx: add ScenePreprocessorNode — scene_spec to flat draws + arena uploads
  • gfx: add SceneFilterNode + FlattenedSceneFilterNode + MergeGeometriesNode
  • avnd: split CpuFilter / CpuAnalysis lifecycle into init / initState / release
  • avnd: add scene_port concept
  • avnd: split GpuNode lifecycle and add scene_port storage helpers
  • gfx: extend ShaderProgram for new ISF features
  • gfx: refresh Filter / GeometryFilter / TexturePort for new pipeline
  • gfx: refresh WindowDevice / Spout / Syphon / WindowCapture
  • gfx: refresh GfxContext + window / screen / multiwindow nodes for incremental scene rebuild
  • js: gpu node lifecycle rework with deterministic teardown
  • wip: many bugfixes across the board
  • build: more build fixes
  • ci: fixes for older qt versions
  • rhi: populate caps
  • ci: try windows fixes
  • gfx: restore PhongNode, still used by score-vfx-template
  • gfx: keep TextureRenderTarget compatible with addons' aggregate init
  • gfx: do not include mmsystem.h in CommonUBOs.hpp
  • gfx: fix build with Qt < 6.9
  • gfx: fix designator order after TextureRenderTarget reorder
  • gfx: fix build with Qt 6.4
  • gfx: more Qt 6.4 compatibility guards
  • 3rdparty: add ufbx submodule
  • 3rdparty: add fastgltf submodule
  • threedim: refresh primitives, buffer/geometry helpers; rename ObjLoader to GeometryLoader; add ImageLoader / BufferInfo / TextureInfo / TangentUtils
  • threedim: add unified AssetLoader on top of FbxParser / GltfParser
  • threedim: add Camera, CameraArray, CameraSwitch, Light, Transform3D
  • threedim: add ScenePreprocessor (replaces SceneFlattener)
  • threedim: add SceneFilter + FlattenedSceneFilter + MergeGeometries
  • threedim: add SceneGraphFilter + SceneSwitch + SceneSelector + SceneGroup
  • threedim: add SceneDuplicator + SceneFromMeshes + SceneInspector + CreateCollection + SceneResourceRoute
  • threedim: add InjectBuffer + InjectTexture for live scene-resource injection
  • threedim: add PBRMesh + MaterialOverride + ConfigurePrimitive + Instancer + ShadowCascadeSetup
  • threedim: add EnvironmentLoader + improve CubemapLoader / CubemapComposer
  • threedim: add AnimationPlayer + Humanoid retargeting + InverseKinematics
  • threedim: add TextToMesh + TextToTexture
  • threedim: add ExtractBuffer2 + ExtractSceneBuffer + ExtractTexture
  • threedim: refresh ModelDisplay + RenderPipeline + Splat for scene pipeline
  • wip: many bugfixes across the board
  • build: more build fixes
  • deps: add zstd dep everywhere
  • deps: redirect find_package(zstd) to the vendored targets
  • deps: provide simdjson on nix & flatpak

@jcelerier jcelerier force-pushed the split/threedim branch 5 times, most recently from 11090c0 to a0bcfef Compare June 26, 2026 15:43
jcelerier added 25 commits July 6, 2026 21:02
…AUXILIARY, vertex inputs

Top-level descriptors (PIPELINE_STATE, MULTIVIEW, EXECUTION_MODEL, EXTENSIONS,
CLIP_DISTANCES / CULL_DISTANCES, DEPTH_LAYOUT). New input kinds: uniform_input
(UBO INPUTS), per-input sampler_config (WRAP / FILTER / COMPARE / MIPS),
audio_sampler_config. Storage / image / cubemap input extensions: PERSISTENT,
IS_ARRAY, GENERATE_MIPS, cube/3D textures. Geometry input AUXILIARY: storage +
sampled, persistent, is_uniform, depth companion, INDIRECT block. Per-pass:
LAYER, Z, FORMAT, PIPELINE_STATE override. OUTPUTS: LAYERS, DEPTH, FORMAT,
SAMPLES, CUBEMAP, GENERATE_MIPS, WIDTH/HEIGHT expressions. Vertex inputs:
REQUIRED, DEFAULT, SEMANTIC, INTERPOLATION (with auto-flat for int/bool
varyings).

Bug fixes: mat3/mat4 attribute slot count, TYPES struct emission in BOTH
stages, drop incorrect std430 padding heuristic that was breaking RawLight
stride.
jcelerier and others added 29 commits July 10, 2026 03:35
Avoids spz fetching a duplicate zstd (macOS) or finding broken SDK
configs (MSVC).
fastgltf downloads it at configure time otherwise, impossible in
sandboxed builds.
Run fastgltf::validate() after parsing so accessor/mesh/skin indices
from the file can't index out of bounds, and reject index values past
the vertex streams before mikktspace tangent generation writes through
them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
- CameraArray: allocate one RawCamera/RawTransform slot per face — the
  fixed-stride arenas reject 6-wide blocks, so every allocation failed
  and the camera array produced no GPU data at all; reset the published
  scene_state in release() like Light/Transform3D do.
- ShadowCascadeSetup: probe split depths through the projection alone
  (the probe point is view-space, not world-space); pad the light
  frustum toward the light instead of past the slice; bake the
  reverse-Z flip into the light projection to match the depth-pass
  convention (Greater, clear 0).
- ModelDisplay: rebuild passes when an edge connected after init has no
  pass yet; keep the filter-UBO binding counter in lockstep with the
  shader generator when a filter's renderer isn't resolved; pack the
  normal matrix as vec4-aligned std140 columns.
- TextToMesh: derive mesh stable_id from a monotonic counter — the
  recycled heap address could alias a previous mesh and serve stale
  slabs.
- Transform wrapper: pass through shadow_cascades / inject_buffers /
  inject_textures / time_seconds / variants; they were silently zeroed
  downstream of any Transform3D.
- CubemapLoader: clamp the source image to the device texture-size
  limit, fail gracefully when the equirect texture can't be created,
  reject face grids smaller than one pixel.
- Parsers: contain glTF external URIs inside the asset directory, cap
  scene-node recursion depth, bound ufbx allocations, free the ufbx
  scene on every path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
- TextToMesh: mint stable_id via ossia::mint_stable_id() — the
  per-instance counter collided across producers (registry keys slabs
  on the bare id process-wide), serving the wrong slab as 'unchanged'.
- glTF: bounds-check node indices + add a visited set in emit_node
  (validate() checks neither range nor cycles; a diamond/cyclic child
  graph re-expanded subtrees exponentially); harden URI containment
  with weakly_canonical (resolves symlinks) and reject an empty base.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
…ash)

ObjFromString dereferenced attrib.vertices/texcoords/normals using
idx.vertex_index/texcoord_index/normal_index straight from tinyobjloader
with no range check. tinyobjloader does not bounds-check: fixIndex()
returns idx-1 for any positive value (unbounded) and leaves an omitted
component at its -1 default, so a malformed .obj (e.g. `f 100000000 1 2`,
or `vt` present with position-only faces) indexes gigabytes past the
attrib buffers -> OOB read / segfault, reachable via AssetLoader and
GeometryLoader.

Validate every corner index in all four attribute branches: bail
(return {}) on an out-of-range or negative vertex index (position is
mandatory), and treat an out-of-range or -1 texcoord/normal index as
"attribute absent for this corner" by writing a default 0 instead of
indexing. Mirrors the glTF-path guard in TangentUtils.hpp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
…U OOB)

Instancer published inst->instance_count straight from the user Count
spinbox (1..1000000) with no relationship to the size of the wired
Transforms buffer. Downstream ScenePreprocessor issues a strided GPU
copy of instance_count regions out of the source QRhiBuffer with no
capacity guard, so an over-large count reads far past the source buffer
-> Vulkan/RHI out-of-bounds copy / device-lost (e.g. 100 mat4 instances
wired, Count=1000000).

Clamp effective_count in rebuild() to the maximum the source buffers can
back: max = (byte_size - byte_offset) / stride, using the same per-format
stride as the preprocessor (translation=16, trs=40, mat4=64 bytes) for
the Transforms buffer and 16 bytes/instance for the Colors buffer, taking
the tighter of the two. Clamping at the source keeps the published count
always valid downstream.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
…U UAF)

Instancer::operator() only cached the primary Points buffer (buffers[0])
handle, but rebuild() routes instance_transforms / instance_colors from
arbitrary attribute buffers (buffers[1], buffers[2], ...) and stores those
raw QRhiBuffer* inside the persistent m_wrapped_state. A producer that
reallocated a secondary attribute buffer (new QRhiBuffer for
transform_matrix / color0) while keeping buffers[0] and the vertex count
identical, without raising dirty_mesh, left upstream_changed false and
republished a dangling handle -> GPU use-after-free / device crash.

Fold every Points buffer handle into an FNV-1a fingerprint and compare it
in operator(); any change to a consumed buffer now forces a rebuild.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
createCubemapTexture() took faceSize straight from the Resolution
spinbox (up to 8192) with no clamp to the device TextureSizeMax and
ignored m_cubemapTex->create(); setupEquirectPipeline() likewise
ignored the 6 face QRhiTextureRenderTarget::create() returns and the
pipeline create(). On a GPU whose TextureSizeMax < 8192 (or low VRAM)
create() fails and rendering proceeds into a dead cube texture / dead
render targets -> validation abort or driver crash.

Clamp faceSize to rhi.resourceLimit(QRhi::TextureSizeMax) (in
runInitialPasses before the re-create guard so it stays stable, and
defensively inside createCubemapTexture), check the cube texture,
each face render-target, and the pipeline create() returns, and bail
out cleanly (release + no render) on any failure. Mirrors the existing
equirect-source clamp/check path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
renderEquirectangular() wrote the face index into a single Dynamic UBO
at offset 0 for every one of the 6 face passes, then recorded all 6
passes binding that buffer at offset 0. Since all 6 passes execute
together at endFrame, on Vulkan/Metal/D3D offset 0 holds the last
face's index (5, -Z) by the time any draw runs -> all 6 cube faces
sample the -Z direction and come out identical. GL happened to mask it
by serializing the host writes between draws.

Size the UBO as 6 * rhi.ubufAligned(sizeof FaceInfo), write face i into
slot i once (before the passes are committed), and bind FaceInfo
(binding 2) per pass via QRhiShaderResourceBinding::
uniformBufferWithDynamicOffset + QRhiCommandBuffer::DynamicOffset in
setShaderResources. Each face now samples with its own direction. No
shader change needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
The file port advertised *.hdr *.exr, but decode goes through QImage
(no stock Radiance-HDR/OpenEXR handler) and both the equirect source
and cube textures are RGBA8, so an HDR input either failed to decode
-> silent early return with no cube, or was force-converted to 8-bit,
destroying the range IBL needs.

No float decoder is readily available in the gfx/threedim include path
(the only stbi_loadf-capable stb_image in the tree is buried in a VST3
nanovg sample; no tinyexr), and vendoring one is out of scope. So take
the honest path: drop .hdr/.exr from the advertised extensions and emit
a qWarning whenever decode yields a null image, so the failure is never
silent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
…g X)

model_display_fragment_shader_triplanar combined the three planar samples
as `xaxis * blending.x + xaxis * blending.y + zaxis * blending.z`: the
middle term used `xaxis` instead of `yaxis`, so the Y-projected sample was
dead code and the X projection was applied with weight blending.x+blending.y.
Surfaces whose normal points along world Y (floors/ceilings) were textured
with the wrong planar projection. Use yaxis in the middle term.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
model_display_fragment_shader_spherical scaled the UV by
`vec2(1. / 2. * PI, 1. / PI)`. GLSL evaluates `1. / 2. * PI` left-to-right
as (1/2)*PI = 1.57, not the intended 1/(2*PI) = 0.159, so the longitude
term atan(v_n.z, v_n.x) in [-PI,PI] was scaled to ~[-4.9,4.9] and the
environment texture wrapped ~10x horizontally instead of once across 360.
Parenthesize the denominator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
ComputeExtractionStrategy::update() and IndexedExtractionStrategy::update()
rebuilt m_srb every frame with a binding set that omitted the Params uniform
buffer and shifted every binding index down by one, while createPipeline()
built the pipeline against uniform@0 / src@1 / [index@2] / out@last. The
rebound SRB was thus layout-incompatible with the pipeline (validation error
/ device-lost) and the shader read its Params block from the source storage
buffer, producing garbage dispatch parameters. This path is now reachable via
ExtractBuffer2 on interleaved / padded attributes. Rebind using the exact
binding indices createPipeline() used (Compute: uniform@0, src@1, out@2;
Indexed: uniform@0, src@1, index@2, out@3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
ExtractBuffer2::resolveBuffer()'s name=="index" branch computed the index
buffer byte_size from mesh.vertices, but the index buffer length is
mesh.indices (a distinct field on dynamic_gpu_geometry). This under-reported
the size for typical indexed meshes (index_count > vertex_count), silently
dropping triangles for SSBO consumers, and could over-report (OOB) when
index_count < vertex_count. Use mesh.indices * elemsize, and guard
mesh.indices <= 0 (non-indexed / unpopulated) by clearing the outlet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
ExtractBuffer2::initStrategy() discarded a failed strategy via
`m_strategy = std::monostate{}`, but the strategy classes have no destructor
and free their QRhiBuffer / QRhiShaderResourceBindings / QRhiComputePipeline
only in release(). Any resources init() allocated before failing were leaked.
Call release(renderer) (the existing std::visit helper) on both failure paths
before discarding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
…ot aliasing

Camera::release() freed both arena slots and cleared m_camera_ref /
m_xform_ref but never reset m_state, unlike every sibling transform
producer (Light/Transform3D/CameraArray). Since operator()() only
rebuilds when m_state is null, an in-place release+init cycle
republished a cached scene_transform whose raw_slot embedded the old
(freed) RawTransform index. The preprocessor flatten gates emission on
raw_slot.size != 0 (not isLive()), so it accepted the stale index and
aliased another producer's world-transform slot. Add m_state.reset().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
…joints

AnimationPlayer computed joint_matrix = world x inverse_bind into
skeleton_component::joint_matrices_buffer, but the sole skinning consumer
(SceneGPUState) always derives joint matrices from joints[].TRS via its
own forward kinematics and never reads joint_matrices_buffer, so skinned
meshes stayed frozen at bind pose.

Write the sampled overrides (keyed by scene_node id) into the cloned
skeleton's joints[] local TRS, mapped joint->node via joint_node_ids,
and bump the skeleton dirty_index so the renderer re-runs FK against the
animated pose. Drop the dead joint_matrices_buffer path and its unused
world-matrix helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
…vancing

The Speed integration gated on t == m_prev_time then overwrote m_prev_time
with the freshly-advanced t. With Time left at 0 and Speed set, the gate
matched only every other frame, so playback ping-ponged 0 -> speed/60 -> 0
forever and never accumulated.

Decouple the change-detection value from the playback position: m_prev_time
now only ever holds the last Time-inlet value (to detect user driving),
while a dedicated m_playback_time accumulator is advanced by speed*dt each
frame when Time is held constant, and resynced to Time when the user moves
it. Playback now advances monotonically.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants