split/threedim#2079
Open
jcelerier wants to merge 115 commits into
Open
Conversation
jcelerier
commented
Jun 14, 2026
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
11090c0 to
a0bcfef
Compare
…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.
…remental scene rebuild
Avoids spz fetching a duplicate zstd (macOS) or finding broken SDK configs (MSVC).
fastgltf downloads it at configure time otherwise, impossible in sandboxed builds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A8dWKnm8kgkuDaMMjapgFT
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
54e4fef to
a93205e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.