From 97f19b4f8219b2258bc906055683eaf6ab57f24b Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Wed, 26 Aug 2026 10:23:16 +0800 Subject: [PATCH 01/14] fix: make editable glTF vertex buffers writable --- CHANGELOG.md | 3 + .../native/src/scene/GltfSceneAsset.cpp | 135 ++++++++++-------- thermion_dart/test/morph_animation_tests.dart | 27 +++- 3 files changed, 104 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d27432ef..a65799eb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,9 @@ - Apply overlapping custom morph animations oldest-first so the most recently added animation has final priority for shared targets. Active animations continue to overwrite manual weights on their next update. +- Fix `VertexBufferMode.editable` glTF assets so their vertex streams can be + updated through `VertexBuffer.setBufferAt`; editable buffers no longer use + the `BufferObject` backing reserved for unwelded smooth/flat shading swaps. ### Breaking changes - Replace the `rebuildVertices` in `ThermionViewer.loadGltf`, diff --git a/thermion_dart/native/src/scene/GltfSceneAsset.cpp b/thermion_dart/native/src/scene/GltfSceneAsset.cpp index f4d993a8c..e30dc0ba4 100644 --- a/thermion_dart/native/src/scene/GltfSceneAsset.cpp +++ b/thermion_dart/native/src/scene/GltfSceneAsset.cpp @@ -663,14 +663,25 @@ namespace thermion auto vbBuilder = VertexBuffer::Builder() .vertexCount(newVertexCount) - .bufferCount(bufferCount) - .enableBufferObjects() - .attribute(VertexAttribute::POSITION, 0, VertexBuffer::AttributeType::FLOAT3) - .attribute(VertexAttribute::TANGENTS, 1, VertexBuffer::AttributeType::SHORT4) - .normalized(VertexAttribute::TANGENTS) - .attribute(VertexAttribute::UV0, 2, VertexBuffer::AttributeType::FLOAT2) - .attribute(VertexAttribute::CUSTOM0, 3, VertexBuffer::AttributeType::FLOAT4) - .attribute(VertexAttribute::COLOR, 4, VertexBuffer::AttributeType::FLOAT4); + .bufferCount(bufferCount); + + // Unwelded geometry swaps BufferObjects at runtime to toggle + // between smooth and flat tangent frames. Editable geometry, + // on the other hand, must remain writable through the public + // VertexBuffer::setBufferAt API, which is incompatible with + // BufferObject-backed streams. + if (!editableTopology) + { + vbBuilder.enableBufferObjects(); + } + + vbBuilder + .attribute(VertexAttribute::POSITION, 0, VertexBuffer::AttributeType::FLOAT3) + .attribute(VertexAttribute::TANGENTS, 1, VertexBuffer::AttributeType::SHORT4) + .normalized(VertexAttribute::TANGENTS) + .attribute(VertexAttribute::UV0, 2, VertexBuffer::AttributeType::FLOAT2) + .attribute(VertexAttribute::CUSTOM0, 3, VertexBuffer::AttributeType::FLOAT4) + .attribute(VertexAttribute::COLOR, 4, VertexBuffer::AttributeType::FLOAT4); if (hasSkinning) { @@ -681,85 +692,87 @@ namespace thermion VertexBuffer *vb = vbBuilder.build(*_engine); + auto uploadDirect = [&](uint8_t bufferIndex, const void *source, size_t size) + { + auto *data = new uint8_t[size]; + memcpy(data, source, size); + vb->setBufferAt(*_engine, bufferIndex, + VertexBuffer::BufferDescriptor(data, size, FREE_CB)); + }; + + auto uploadStream = [&](uint8_t bufferIndex, const void *source, size_t size) + { + if (editableTopology) + { + uploadDirect(bufferIndex, source, size); + return; + } + + auto *data = new uint8_t[size]; + memcpy(data, source, size); + BufferObject *bo = BufferObject::Builder().size(size).build(*_engine); + bo->setBuffer(*_engine, BufferObject::BufferDescriptor(data, size, FREE_CB)); + vb->setBufferObjectAt(*_engine, bufferIndex, bo); + _preservedBufferObjects.push_back(bo); + }; + // Buffer 0: POSITION size_t posDataSize = newVertexCount * 3 * sizeof(float); - auto *posData = new uint8_t[posDataSize]; - memcpy(posData, newPositions.data(), posDataSize); - BufferObject *posBO = BufferObject::Builder().size(posDataSize).build(*_engine); - posBO->setBuffer(*_engine, BufferObject::BufferDescriptor(posData, posDataSize, FREE_CB)); - vb->setBufferObjectAt(*_engine, 0, posBO); + uploadStream(0, newPositions.data(), posDataSize); // Buffer 1: TANGENTS (SHORT4 quantized quaternions, matching gltfio's format) // Create both smooth and flat tangent BOs for runtime toggling. size_t tangDataSize = newVertexCount * sizeof(filament::math::short4); - auto *smoothTangData = new uint8_t[tangDataSize]; - memcpy(smoothTangData, smoothTangentQuats.data(), tangDataSize); - BufferObject *smoothTangBO = BufferObject::Builder().size(tangDataSize).build(*_engine); - smoothTangBO->setBuffer(*_engine, BufferObject::BufferDescriptor(smoothTangData, tangDataSize, FREE_CB)); - - auto *flatTangData = new uint8_t[tangDataSize]; - memcpy(flatTangData, flatTangentQuats.data(), tangDataSize); - BufferObject *flatTangBO = BufferObject::Builder().size(tangDataSize).build(*_engine); - flatTangBO->setBuffer(*_engine, BufferObject::BufferDescriptor(flatTangData, tangDataSize, FREE_CB)); - - // Bind smooth by default - vb->setBufferObjectAt(*_engine, 1, smoothTangBO); - _smoothTangentBOs.push_back(smoothTangBO); - _flatTangentBOs.push_back(flatTangBO); + if (editableTopology) + { + uploadDirect(1, smoothTangentQuats.data(), tangDataSize); + _smoothTangentBOs.push_back(nullptr); + _flatTangentBOs.push_back(nullptr); + } + else + { + auto *smoothTangData = new uint8_t[tangDataSize]; + memcpy(smoothTangData, smoothTangentQuats.data(), tangDataSize); + BufferObject *smoothTangBO = BufferObject::Builder().size(tangDataSize).build(*_engine); + smoothTangBO->setBuffer(*_engine, BufferObject::BufferDescriptor(smoothTangData, tangDataSize, FREE_CB)); + + auto *flatTangData = new uint8_t[tangDataSize]; + memcpy(flatTangData, flatTangentQuats.data(), tangDataSize); + BufferObject *flatTangBO = BufferObject::Builder().size(tangDataSize).build(*_engine); + flatTangBO->setBuffer(*_engine, BufferObject::BufferDescriptor(flatTangData, tangDataSize, FREE_CB)); + + // Bind smooth by default. + vb->setBufferObjectAt(*_engine, 1, smoothTangBO); + _smoothTangentBOs.push_back(smoothTangBO); + _flatTangentBOs.push_back(flatTangBO); + } // Buffer 2: UV0 size_t uvDataSize = newVertexCount * 2 * sizeof(float); - auto *uvData = new uint8_t[uvDataSize]; - memcpy(uvData, newUVs.data(), uvDataSize); - BufferObject *uvBO = BufferObject::Builder().size(uvDataSize).build(*_engine); - uvBO->setBuffer(*_engine, BufferObject::BufferDescriptor(uvData, uvDataSize, FREE_CB)); - vb->setBufferObjectAt(*_engine, 2, uvBO); + uploadStream(2, newUVs.data(), uvDataSize); // Buffer 3: CUSTOM0 (barycentrics) size_t baryDataSize = newVertexCount * 4 * sizeof(float); - auto *baryData = new uint8_t[baryDataSize]; - memcpy(baryData, newBarycentrics.data(), baryDataSize); - BufferObject *baryBO = BufferObject::Builder().size(baryDataSize).build(*_engine); - baryBO->setBuffer(*_engine, BufferObject::BufferDescriptor(baryData, baryDataSize, FREE_CB)); - vb->setBufferObjectAt(*_engine, 3, baryBO); + uploadStream(3, newBarycentrics.data(), baryDataSize); // Buffer 4: COLOR (dummy, all white = 1.0) size_t colorDataSize = newVertexCount * 4 * sizeof(float); - auto *colorData = new uint8_t[colorDataSize]; - auto *colorFloats = reinterpret_cast(colorData); + std::vector colorFloats(newVertexCount * 4); for (uint32_t i = 0; i < newVertexCount * 4; i++) { colorFloats[i] = 1.0f; } - BufferObject *colorBO = BufferObject::Builder().size(colorDataSize).build(*_engine); - colorBO->setBuffer(*_engine, BufferObject::BufferDescriptor(colorData, colorDataSize, FREE_CB)); - vb->setBufferObjectAt(*_engine, 4, colorBO); - - _preservedBufferObjects.push_back(posBO); - _preservedBufferObjects.push_back(uvBO); - _preservedBufferObjects.push_back(baryBO); - _preservedBufferObjects.push_back(colorBO); + uploadStream(4, colorFloats.data(), colorDataSize); if (hasSkinning) { // Buffer 5: BONE_INDICES size_t jointDataSize = newVertexCount * 4 * sizeof(uint8_t); - auto *jointData = new uint8_t[jointDataSize]; - memcpy(jointData, newJoints.data(), jointDataSize); - BufferObject *jointBO = BufferObject::Builder().size(jointDataSize).build(*_engine); - jointBO->setBuffer(*_engine, BufferObject::BufferDescriptor(jointData, jointDataSize, FREE_CB)); - vb->setBufferObjectAt(*_engine, 5, jointBO); + uploadStream(5, newJoints.data(), jointDataSize); // Buffer 6: BONE_WEIGHTS size_t weightDataSize = newVertexCount * 4 * sizeof(float); - auto *weightData = new uint8_t[weightDataSize]; - memcpy(weightData, newWeights.data(), weightDataSize); - BufferObject *weightBO = BufferObject::Builder().size(weightDataSize).build(*_engine); - weightBO->setBuffer(*_engine, BufferObject::BufferDescriptor(weightData, weightDataSize, FREE_CB)); - vb->setBufferObjectAt(*_engine, 6, weightBO); - - _preservedBufferObjects.push_back(jointBO); - _preservedBufferObjects.push_back(weightBO); + uploadStream(6, newWeights.data(), weightDataSize); } // Editable geometry retains source indices. Unwelded geometry @@ -863,6 +876,8 @@ namespace thermion if (!_preservedVertexBuffers[i]) continue; auto *bo = flatShading ? _flatTangentBOs[i] : _smoothTangentBOs[i]; + if (!bo) + continue; _preservedVertexBuffers[i]->setBufferObjectAt(*_engine, 1, bo); } } diff --git a/thermion_dart/test/morph_animation_tests.dart b/thermion_dart/test/morph_animation_tests.dart index 4230e8150..7ac50bcb8 100644 --- a/thermion_dart/test/morph_animation_tests.dart +++ b/thermion_dart/test/morph_animation_tests.dart @@ -53,7 +53,7 @@ void main() async { ); }); - test('editable vertices preserve source topology and morph animation', () async { + test('editable vertices support mutation and morph animation', () async { await testHelper.withViewer( (viewer) async { final path = '${testHelper.assetsDir}/cube_with_morph_targets.glb'; @@ -84,6 +84,31 @@ void main() async { expect(_meanAbsoluteDifference(originalPose.rest, editablePose.rest), lessThan(0.02)); expect(_meanAbsoluteDifference(originalPose.morphed, editablePose.morphed), lessThan(0.02)); + final targets = (await editable.getMorphTargetSets()).single; + await targets.setAllWeights([0.0]); + + final editedPositions = Float32List.fromList(source.vertices); + for (var i = 0; i < editedPositions.length; i += 3) { + editedPositions[i] += 0.5; + } + + await editable.getVertexBuffer()!.setBufferAt(0, editedPositions).timeout(const Duration(seconds: 5)); + final editedRest = (await testHelper.capture(viewer.view, null)).values.single; + + expect( + _meanAbsoluteDifference(editablePose.rest, editedRest), + greaterThan(0.005), + reason: 'updating the editable position stream must change the rendered geometry', + ); + + await targets.setAllWeights([1.0]); + final editedMorphed = (await testHelper.capture(viewer.view, null)).values.single; + expect( + _meanAbsoluteDifference(editedRest, editedMorphed), + greaterThan(0.005), + reason: 'morph weights must still deform geometry after editing its base positions', + ); + await viewer.destroyAsset(editable); }, bg: kRed, From 73be184b3e5fceeffba1f434c92b5bb8e2e19a19 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Wed, 26 Aug 2026 10:48:09 +0800 Subject: [PATCH 02/14] fix: reject incompatible vertex buffer operations --- CHANGELOG.md | 2 + .../src/implementation/ffi_asset.dart | 28 +++++++++---- .../src/implementation/ffi_filament_app.dart | 2 +- .../src/implementation/ffi_vertex_buffer.dart | 12 +++++- .../filament/src/implementation/ffi_view.dart | 20 ++++----- .../lib/src/filament/src/interface/asset.dart | 6 +-- .../filament/src/interface/vertex_buffer.dart | 8 ++++ thermion_dart/test/morph_animation_tests.dart | 6 ++- thermion_dart/test/overlay_tests.dart | 42 +++++++++---------- .../test/wireframe_renderable_test.dart | 9 ++++ 10 files changed, 89 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a65799eb8..4bc05ea0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ - Fix `VertexBufferMode.editable` glTF assets so their vertex streams can be updated through `VertexBuffer.setBufferAt`; editable buffers no longer use the `BufferObject` backing reserved for unwelded smooth/flat shading swaps. + Buffer updates, flat shading, and stencil highlighting now throw actionable + errors when used with an incompatible vertex-buffer mode. ### Breaking changes - Replace the `rebuildVertices` in `ThermionViewer.loadGltf`, diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart index 25a023484..ae0404f5f 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart @@ -20,6 +20,12 @@ class FFIAsset extends ThermionAsset> { final FFIAsset? instanceOwner; + final VertexBufferMode? _vertexBufferMode; + + /// The mode used to load this glTF asset, inherited by asset instances. + /// Null for non-glTF assets. + VertexBufferMode? get vertexBufferMode => instanceOwner?.vertexBufferMode ?? _vertexBufferMode; + late final ThermionEntity entity; // Mutable only on the owning asset. Instance wrappers read the owner's value @@ -33,7 +39,9 @@ class FFIAsset extends ThermionAsset> { final FFIFilamentApp _app; - FFIAsset(this.asset, {this.instanceOwner = null, required FFIFilamentApp app}) : _app = app { + FFIAsset(this.asset, {this.instanceOwner = null, VertexBufferMode? vertexBufferMode, required FFIFilamentApp app}) + : _vertexBufferMode = vertexBufferMode, + _app = app { entity = SceneAsset_getEntity(asset); } @@ -279,11 +287,13 @@ class FFIAsset extends ThermionAsset> { @override Future setFlatShading(bool flatShading) async { - // Flat shading swaps TANGENTS on the preserved (rebuilt) vertex buffers; - // without them it would silently do nothing — throw instead. - if (getVertexBuffer() == null) { - throw Exception( - "setFlatShading: asset has no preserved geometry. " + // Flat shading swaps between the BufferObjects created specifically for + // unwelded geometry. Editable geometry also has preserved buffers, but it + // deliberately uses ordinary writable streams and cannot perform this + // swap. + if (vertexBufferMode != VertexBufferMode.unwelded) { + throw StateError( + "setFlatShading requires unwelded geometry. " "Load it with loadGltf(..., vertexBufferMode: VertexBufferMode.unwelded).", ); } @@ -905,7 +915,11 @@ class FFIAsset extends ThermionAsset> { if (vbPtr == nullptr) { return null; } - return FFIVertexBuffer(vbPtr, _app.engine); + return FFIVertexBuffer( + vbPtr, + _app.engine, + supportsSetBufferAt: type == SceneAssetType.geometry || vertexBufferMode == VertexBufferMode.editable, + ); } } diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart index aaaf181dd..d25f26cff 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart @@ -1240,7 +1240,7 @@ class FFIFilamentApp extends FilamentApp { (requestId, cb) => GltfResourceLoader_destroyRenderThread(engine, gltfResourceLoader, requestId, cb), ); - final ffiAsset = FFIAsset(asset, app: this); + final ffiAsset = FFIAsset(asset, app: this, vertexBufferMode: vertexBufferMode); if (releaseSourceData) { await ffiAsset.releaseSourceData(); } diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart index 63584c427..80b77f670 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart @@ -6,7 +6,10 @@ class FFIVertexBuffer extends VertexBuffer { final bindings.Pointer _ptr; final bindings.Pointer _engine; - FFIVertexBuffer(this._ptr, this._engine); + @override + final bool supportsSetBufferAt; + + FFIVertexBuffer(this._ptr, this._engine, {this.supportsSetBufferAt = true}); /// Returns the native handle for FFI calls. bindings.Pointer getNativeHandle() => _ptr; @@ -18,6 +21,13 @@ class FFIVertexBuffer extends VertexBuffer { @override Future setBufferAt(int bufferIndex, TypedData data, {int byteOffset = 0}) async { + if (!supportsSetBufferAt) { + throw StateError( + 'VertexBuffer.setBufferAt cannot update a BufferObject-backed buffer. ' + 'Load glTF assets with vertexBufferMode: VertexBufferMode.editable ' + 'when mutable vertex streams are required.', + ); + } final byteData = data.asUint8List(); await withVoidCallback((requestId, cb) { bindings.VertexBuffer_setBufferAtRenderThread( diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart index 2961472d2..70046930e 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart @@ -567,10 +567,6 @@ class FFIView extends View> { }) async { // primitiveIndex parameter is deprecated and ignored // The offset is now computed automatically from the entity - if (_highlightOverlayManager == null) { - await setHighlightOverlayEnabled(true); - } - entity ??= asset.entity; // Use geometrySource for vertex/index buffers when provided (e.g. for @@ -578,16 +574,20 @@ class FFIView extends View> { final geoAsset = geometrySource ?? asset; final ffiGeoAsset = geoAsset as FFIAsset; - // Misuse check: highlighting needs the preserved (rebuilt) vertex - // buffers, which only exist when the glTF was loaded with - // vertexBufferMode: VertexBufferMode.unwelded. Throw rather than silently doing nothing. - if (ffiGeoAsset.getVertexBuffer() == null) { - throw Exception( - "setStencilHighlight: asset has no preserved geometry. " + // Stencil highlighting needs the barycentric coordinates generated only + // for unwelded geometry. Editable geometry also has preserved buffers but + // its CUSTOM0 stream does not contain those coordinates. + if (ffiGeoAsset.vertexBufferMode != VertexBufferMode.unwelded) { + throw StateError( + "setStencilHighlight requires unwelded geometry. " "Load it with loadGltf(..., vertexBufferMode: VertexBufferMode.unwelded).", ); } + if (_highlightOverlayManager == null) { + await setHighlightOverlayEnabled(true); + } + // Get the starting primitive offset for this entity final offset = await ffiGeoAsset.getPrimitiveOffsetForEntity(entity); if (offset < 0) { diff --git a/thermion_dart/lib/src/filament/src/interface/asset.dart b/thermion_dart/lib/src/filament/src/interface/asset.dart index c0c9b3281..ad08bbee4 100644 --- a/thermion_dart/lib/src/filament/src/interface/asset.dart +++ b/thermion_dart/lib/src/filament/src/interface/asset.dart @@ -423,12 +423,12 @@ abstract class ThermionAsset extends NativeHandle { throw UnimplementedError(); } - // Unweld all mesh primitives so each triangle has unique vertices, - // then assign barycentric coordinates to CUSTOM0 for wireframe rendering. // Returns the underlying [VertexBuffer] for this asset, if available. // // For geometry assets this exposes the backing Filament vertex buffer so you - // can update data via [VertexBuffer.setBufferAt]. + // can update data via [VertexBuffer.setBufferAt]. For glTF assets, editable + // buffers support updates while unwelded buffers are read-only because their + // streams use Filament BufferObjects. // // [primitiveIndex] is reserved for future use. Geometry assets currently // only support a single primitive, so it is ignored. diff --git a/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart b/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart index 52ccee304..2eda7854e 100644 --- a/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart +++ b/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart @@ -150,11 +150,19 @@ abstract class VertexBuffer { /// Returns the number of vertices in this buffer. int getVertexCount(); + /// Whether [setBufferAt] can update this buffer's streams. + /// + /// Buffers created with Filament's BufferObject storage mode are not + /// writable through [setBufferAt]. + bool get supportsSetBufferAt; + /// Asynchronously copy-initializes the specified buffer from the given data. /// /// [bufferIndex] Index of the buffer to initialize (0 to bufferCount-1) /// [data] Raw vertex data to copy into the buffer /// [byteOffset] Offset in bytes into the buffer (default 0) + /// + /// Throws [StateError] when [supportsSetBufferAt] is false. Future setBufferAt(int bufferIndex, TypedData data, {int byteOffset = 0}); /// Destroys this vertex buffer and releases GPU resources. diff --git a/thermion_dart/test/morph_animation_tests.dart b/thermion_dart/test/morph_animation_tests.dart index 7ac50bcb8..19a6bb23a 100644 --- a/thermion_dart/test/morph_animation_tests.dart +++ b/thermion_dart/test/morph_animation_tests.dart @@ -73,7 +73,9 @@ void main() async { await viewer.destroyAsset(original); final editable = await viewer.loadGltf(path, vertexBufferMode: VertexBufferMode.editable); - expect(editable.getVertexBuffer()!.getVertexCount(), source.vertices.length ~/ 3); + final editableVertexBuffer = editable.getVertexBuffer()!; + expect(editableVertexBuffer.supportsSetBufferAt, isTrue); + expect(editableVertexBuffer.getVertexCount(), source.vertices.length ~/ 3); final editablePose = await capture(editable); expect( @@ -92,7 +94,7 @@ void main() async { editedPositions[i] += 0.5; } - await editable.getVertexBuffer()!.setBufferAt(0, editedPositions).timeout(const Duration(seconds: 5)); + await editableVertexBuffer.setBufferAt(0, editedPositions).timeout(const Duration(seconds: 5)); final editedRest = (await testHelper.capture(viewer.view, null)).values.single; expect( diff --git a/thermion_dart/test/overlay_tests.dart b/thermion_dart/test/overlay_tests.dart index ed6beeab6..7155e4901 100644 --- a/thermion_dart/test/overlay_tests.dart +++ b/thermion_dart/test/overlay_tests.dart @@ -1,6 +1,5 @@ import 'package:test/test.dart'; import 'package:thermion_dart/thermion_dart.dart'; -import 'package:vector_math/vector_math_64.dart'; import 'helpers.dart'; void main() async { @@ -224,32 +223,31 @@ void main() async { test('setStencilHighlight and setFlatShading throw without unwelded vertex buffers', () async { await testHelper.withViewer((viewer) async { - final cube = await viewer.loadGltf("file://${testHelper.assetsDir}/cube.glb", addToScene: true); - - // Outlining and flat shading both need the preserved (rebuilt) vertex - // buffers — without unwelded mode these used to silently do nothing; - // they must now throw with an actionable message. - await viewer.view.setHighlightOverlayEnabled(true); - expect( - () => viewer.view.setStencilHighlight(cube), - throwsA( - isA().having( + Future expectUnweldedOperationsToThrow(ThermionAsset cube) async { + final matcher = throwsA( + isA().having( (e) => e.toString(), 'message', contains('vertexBufferMode: VertexBufferMode.unwelded'), ), - ), - ); - expect( - () => cube.setFlatShading(true), - throwsA( - isA().having( - (e) => e.toString(), - 'message', - contains('vertexBufferMode: VertexBufferMode.unwelded'), - ), - ), + ); + await expectLater(viewer.view.setStencilHighlight(cube), matcher); + await expectLater(cube.setFlatShading(true), matcher); + } + + final original = await viewer.loadGltf("file://${testHelper.assetsDir}/cube.glb", addToScene: true); + await expectUnweldedOperationsToThrow(original); + + // Editable assets have preserved geometry too, but no barycentric data + // or swappable tangent BufferObjects. They must not pass a mere + // getVertexBuffer() != null check. + final editable = await viewer.loadGltf( + "file://${testHelper.assetsDir}/cube.glb", + vertexBufferMode: VertexBufferMode.editable, + addToScene: true, ); + expect(editable.getVertexBuffer(), isNotNull); + await expectUnweldedOperationsToThrow(editable); }); }); } diff --git a/thermion_dart/test/wireframe_renderable_test.dart b/thermion_dart/test/wireframe_renderable_test.dart index 161e46711..7f9f218da 100644 --- a/thermion_dart/test/wireframe_renderable_test.dart +++ b/thermion_dart/test/wireframe_renderable_test.dart @@ -22,6 +22,15 @@ void main() async { addToScene: true, ); + final unweldedVertexBuffer = rebuilt.getVertexBuffer()!; + expect(unweldedVertexBuffer.supportsSetBufferAt, isFalse); + await expectLater( + unweldedVertexBuffer.setBufferAt(0, Float32List(0)), + throwsA( + isA().having((error) => error.toString(), 'message', contains('VertexBufferMode.editable')), + ), + ); + await testHelper.capture(result.viewer.view, "vertex_buffer_unwelded"); // Use typed wireframe wrapper From edaa7492225e4fad1725b86f4afd505cf06a72dd Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Wed, 26 Aug 2026 11:40:48 +0800 Subject: [PATCH 03/14] refactor: model vertex buffer storage natively --- CHANGELOG.md | 4 + .../src/bindings/src/thermion_dart_ffi.g.dart | 134 ++++++++++ .../src/thermion_dart_js_interop.g.dart | 244 ++++++++++++++++++ thermion_dart/lib/src/filament/filament.dart | 1 + .../src/implementation/ffi_asset.dart | 31 +-- .../src/implementation/ffi_buffer_object.dart | 76 ++++++ .../src/implementation/ffi_filament_app.dart | 2 +- .../ffi_renderable_manager.dart | 6 + .../src/implementation/ffi_vertex_buffer.dart | 56 +++- .../filament/src/implementation/ffi_view.dart | 2 +- .../lib/src/filament/src/interface/asset.dart | 6 + .../filament/src/interface/buffer_object.dart | 15 ++ .../src/interface/renderable_manager.dart | 2 + .../filament/src/interface/vertex_buffer.dart | 28 +- .../native/include/c_api/APIBoundaryTypes.h | 17 ++ .../native/include/c_api/TBufferObject.h | 26 ++ .../native/include/c_api/TSceneAsset.h | 1 + .../native/include/c_api/TVertexBuffer.h | 8 + .../c_api/ThermionDartRenderThreadApi.h | 30 +++ .../native/include/scene/GltfSceneAsset.hpp | 14 + .../include/scene/GltfSceneAssetInstance.hpp | 4 +- .../native/include/scene/SceneAsset.hpp | 7 +- .../include/scene/VertexBufferMetadata.hpp | 17 ++ .../native/src/c_api/TBufferObject.cpp | 67 +++++ .../native/src/c_api/TSceneAsset.cpp | 13 +- .../native/src/c_api/TVertexBuffer.cpp | 96 ++++++- .../src/c_api/ThermionDartRenderThreadApi.cpp | 76 ++++++ .../native/src/scene/GeometrySceneAsset.cpp | 7 +- .../native/src/scene/GltfSceneAsset.cpp | 10 +- .../src/scene/GltfSceneAssetInstance.cpp | 7 +- thermion_dart/test/geometry_tests.dart | 5 +- thermion_dart/test/morph_animation_tests.dart | 6 + .../test/vertex_index_buffer_tests.dart | 34 +++ 33 files changed, 1000 insertions(+), 52 deletions(-) create mode 100644 thermion_dart/lib/src/filament/src/implementation/ffi_buffer_object.dart create mode 100644 thermion_dart/lib/src/filament/src/interface/buffer_object.dart create mode 100644 thermion_dart/native/include/c_api/TBufferObject.h create mode 100644 thermion_dart/native/include/scene/VertexBufferMetadata.hpp create mode 100644 thermion_dart/native/src/c_api/TBufferObject.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bc05ea0b..e2963f80c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,10 @@ the `BufferObject` backing reserved for unwelded smooth/flat shading swaps. Buffer updates, flat shading, and stencil highlighting now throw actionable errors when used with an incompatible vertex-buffer mode. +- Expose native `VertexBuffer.storageMode` metadata and first-class + `BufferObject` creation, upload, and attachment APIs. `supportsSetBufferAt` + is now derived from native buffer storage instead of duplicated glTF load + state in Dart, and asset-owned vertex buffers are explicitly borrowed. ### Breaking changes - Replace the `rebuildVertices` in `ThermionViewer.loadGltf`, diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart index 33e6665dc..a006394c6 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart @@ -698,6 +698,9 @@ external ffi.Pointer SceneAsset_createInstance( @ffi.Native)>(isLeaf: true) external Aabb3 SceneAsset_getBoundingBox(ffi.Pointer asset); +@ffi.Native)>(isLeaf: true) +external int SceneAsset_getGeometryCapabilities(ffi.Pointer asset); + @ffi.Native Function(ffi.Pointer, ffi.Int)>(isLeaf: true) external ffi.Pointer SceneAsset_getVertexBuffer( ffi.Pointer tSceneAsset, @@ -1396,6 +1399,9 @@ external void VertexBufferBuilder_bufferCount(ffi.Pointer @ffi.Native, ffi.Uint32)>(isLeaf: true) external void VertexBufferBuilder_vertexCount(ffi.Pointer builder, int count); +@ffi.Native, ffi.Bool)>(isLeaf: true) +external void VertexBufferBuilder_enableBufferObjects(ffi.Pointer builder, bool enabled); + @ffi.Native< ffi.Void Function( ffi.Pointer, @@ -1430,6 +1436,9 @@ external void VertexBufferBuilder_destroy(ffi.Pointer buil @ffi.Native)>(isLeaf: true) external int VertexBuffer_getVertexCount(ffi.Pointer buffer); +@ffi.Native)>(isLeaf: true) +external int VertexBuffer_getStorageMode(ffi.Pointer buffer); + @ffi.Native< ffi.Void Function( ffi.Pointer, @@ -1449,9 +1458,54 @@ external void VertexBuffer_setBufferAt( int byteOffset, ); +@ffi.Native, ffi.Pointer, ffi.Uint8, ffi.Pointer)>( + isLeaf: true, +) +external void VertexBuffer_setBufferObjectAt( + ffi.Pointer engine, + ffi.Pointer buffer, + int bufferIndex, + ffi.Pointer bufferObject, +); + @ffi.Native, ffi.Pointer)>(isLeaf: true) external void VertexBuffer_destroy(ffi.Pointer engine, ffi.Pointer buffer); +@ffi.Native Function()>(isLeaf: true) +external ffi.Pointer BufferObjectBuilder_create(); + +@ffi.Native, ffi.Uint32)>(isLeaf: true) +external void BufferObjectBuilder_size(ffi.Pointer builder, int sizeInBytes); + +@ffi.Native Function(ffi.Pointer, ffi.Pointer)>(isLeaf: true) +external ffi.Pointer BufferObjectBuilder_build( + ffi.Pointer builder, + ffi.Pointer engine, +); + +@ffi.Native)>(isLeaf: true) +external void BufferObjectBuilder_destroy(ffi.Pointer builder); + +@ffi.Native< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Size, + ffi.Uint32, + ) +>(isLeaf: true) +external void BufferObject_setBuffer( + ffi.Pointer engine, + ffi.Pointer buffer, + ffi.Pointer data, + int sizeInBytes, + int byteOffset, +); + +@ffi.Native, ffi.Pointer)>(isLeaf: true) +external void BufferObject_destroy(ffi.Pointer engine, ffi.Pointer buffer); + @ffi.Native Function()>(isLeaf: true) external ffi.Pointer IndexBufferBuilder_create(); @@ -3429,6 +3483,69 @@ external void VertexBuffer_setBufferAtRenderThread( VoidCallback onComplete, ); +@ffi.Native< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Uint8, + ffi.Pointer, + ffi.Uint32, + VoidCallback, + ) +>(isLeaf: true) +external void VertexBuffer_setBufferObjectAtRenderThread( + ffi.Pointer tEngine, + ffi.Pointer tBuffer, + int bufferIndex, + ffi.Pointer tBufferObject, + int requestId, + VoidCallback onComplete, +); + +@ffi.Native< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>, + ) +>(isLeaf: true) +external void BufferObjectBuilder_buildRenderThread( + ffi.Pointer tBuilder, + ffi.Pointer tEngine, + ffi.Pointer)>> onComplete, +); + +@ffi.Native< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Size, + ffi.Uint32, + ffi.Uint32, + VoidCallback, + ) +>(isLeaf: true) +external void BufferObject_setBufferRenderThread( + ffi.Pointer tEngine, + ffi.Pointer tBuffer, + ffi.Pointer data, + int sizeInBytes, + int byteOffset, + int requestId, + VoidCallback onComplete, +); + +@ffi.Native, ffi.Pointer, ffi.Uint32, VoidCallback)>( + isLeaf: true, +) +external void BufferObject_destroyRenderThread( + ffi.Pointer tEngine, + ffi.Pointer tBuffer, + int requestId, + VoidCallback onComplete, +); + @ffi.Native< ffi.Void Function( ffi.Pointer, @@ -4801,6 +4918,23 @@ final class TVertexBufferBuilder extends ffi.Opaque {} final class TIndexBufferBuilder extends ffi.Opaque {} +final class TBufferObject extends ffi.Opaque {} + +final class TBufferObjectBuilder extends ffi.Opaque {} + +sealed class TVertexBufferStorageMode { + static const VERTEX_BUFFER_STORAGE_MODE_UNKNOWN = 0; + static const VERTEX_BUFFER_STORAGE_MODE_DIRECT = 1; + static const VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS = 2; +} + +sealed class TSceneAssetGeometryCapability { + static const SCENE_ASSET_GEOMETRY_CAPABILITY_NONE = 0; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING = 1; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 2; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY = 4; +} + final class TSurfaceOrientation extends ffi.Opaque {} final class TSurfaceOrientationBuilder extends ffi.Opaque {} diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart index a96ce9dcf..f6f674714 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart @@ -323,6 +323,7 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { int materialInstanceCount, ); external void _SceneAsset_getBoundingBox(Pointer Aabb3_out, Pointer asset); + external int _SceneAsset_getGeometryCapabilities(Pointer asset); external Pointer _SceneAsset_getVertexBuffer(Pointer tSceneAsset, int primitiveIndex); external Pointer _SceneAsset_getIndexBuffer(Pointer tSceneAsset, int primitiveIndex); external int _SceneAsset_getPrimitiveOffsetForEntity(Pointer tSceneAsset, EntityId entity); @@ -623,6 +624,7 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { external Pointer _VertexBufferBuilder_create(); external void _VertexBufferBuilder_bufferCount(Pointer builder, int count); external void _VertexBufferBuilder_vertexCount(Pointer builder, int count); + external void _VertexBufferBuilder_enableBufferObjects(Pointer builder, bool enabled); external void _VertexBufferBuilder_attribute( Pointer builder, int attribute, @@ -638,6 +640,7 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { ); external void _VertexBufferBuilder_destroy(Pointer builder); external size_t _VertexBuffer_getVertexCount(Pointer buffer); + external int _VertexBuffer_getStorageMode(Pointer buffer); external void _VertexBuffer_setBufferAt( Pointer engine, Pointer buffer, @@ -646,7 +649,28 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { size_t sizeInBytes, int byteOffset, ); + external void _VertexBuffer_setBufferObjectAt( + Pointer engine, + Pointer buffer, + int bufferIndex, + Pointer bufferObject, + ); external void _VertexBuffer_destroy(Pointer engine, Pointer buffer); + external Pointer _BufferObjectBuilder_create(); + external void _BufferObjectBuilder_size(Pointer builder, int sizeInBytes); + external Pointer _BufferObjectBuilder_build( + Pointer builder, + Pointer engine, + ); + external void _BufferObjectBuilder_destroy(Pointer builder); + external void _BufferObject_setBuffer( + Pointer engine, + Pointer buffer, + Pointer data, + size_t sizeInBytes, + int byteOffset, + ); + external void _BufferObject_destroy(Pointer engine, Pointer buffer); external Pointer _IndexBufferBuilder_create(); external void _IndexBufferBuilder_indexCount(Pointer builder, int count); external void _IndexBufferBuilder_bufferType(Pointer builder, int indexType); @@ -1730,6 +1754,34 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { int requestId, VoidCallback onComplete, ); + external void _VertexBuffer_setBufferObjectAtRenderThread( + Pointer tEngine, + Pointer tBuffer, + int bufferIndex, + Pointer tBufferObject, + int requestId, + VoidCallback onComplete, + ); + external void _BufferObjectBuilder_buildRenderThread( + Pointer tBuilder, + Pointer tEngine, + Pointer)>> onComplete, + ); + external void _BufferObject_setBufferRenderThread( + Pointer tEngine, + Pointer tBuffer, + Pointer data, + size_t sizeInBytes, + int byteOffset, + int requestId, + VoidCallback onComplete, + ); + external void _BufferObject_destroyRenderThread( + Pointer tEngine, + Pointer tBuffer, + int requestId, + VoidCallback onComplete, + ); external void _IndexBufferBuilder_buildRenderThread( Pointer tBuilder, Pointer tEngine, @@ -3443,6 +3495,11 @@ Aabb3 SceneAsset_getBoundingBox(Pointer asset) { return Aabb3_out.toDart(); } +int SceneAsset_getGeometryCapabilities(Pointer asset) { + final result = GeneratedBindings.instance._SceneAsset_getGeometryCapabilities(asset.cast()); + return result; +} + Pointer SceneAsset_getVertexBuffer(Pointer tSceneAsset, int primitiveIndex) { final result = GeneratedBindings.instance._SceneAsset_getVertexBuffer(tSceneAsset.cast(), primitiveIndex); return Pointer(result); @@ -4346,6 +4403,11 @@ void VertexBufferBuilder_vertexCount(Pointer builder, int return result; } +void VertexBufferBuilder_enableBufferObjects(Pointer builder, bool enabled) { + final result = GeneratedBindings.instance._VertexBufferBuilder_enableBufferObjects(builder.cast(), enabled); + return result; +} + void VertexBufferBuilder_attribute( Pointer builder, int attribute, @@ -4385,6 +4447,11 @@ Dartsize_t VertexBuffer_getVertexCount(Pointer buffer) { return result; } +int VertexBuffer_getStorageMode(Pointer buffer) { + final result = GeneratedBindings.instance._VertexBuffer_getStorageMode(buffer.cast()); + return result; +} + void VertexBuffer_setBufferAt( Pointer engine, Pointer buffer, @@ -4404,11 +4471,71 @@ void VertexBuffer_setBufferAt( return result; } +void VertexBuffer_setBufferObjectAt( + Pointer engine, + Pointer buffer, + int bufferIndex, + Pointer bufferObject, +) { + final result = GeneratedBindings.instance._VertexBuffer_setBufferObjectAt( + engine.cast(), + buffer.cast(), + bufferIndex, + bufferObject.cast(), + ); + return result; +} + void VertexBuffer_destroy(Pointer engine, Pointer buffer) { final result = GeneratedBindings.instance._VertexBuffer_destroy(engine.cast(), buffer.cast()); return result; } +Pointer BufferObjectBuilder_create() { + final result = GeneratedBindings.instance._BufferObjectBuilder_create(); + return Pointer(result); +} + +void BufferObjectBuilder_size(Pointer builder, int sizeInBytes) { + final result = GeneratedBindings.instance._BufferObjectBuilder_size(builder.cast(), sizeInBytes); + return result; +} + +Pointer BufferObjectBuilder_build( + Pointer builder, + Pointer engine, +) { + final result = GeneratedBindings.instance._BufferObjectBuilder_build(builder.cast(), engine.cast()); + return Pointer(result); +} + +void BufferObjectBuilder_destroy(Pointer builder) { + final result = GeneratedBindings.instance._BufferObjectBuilder_destroy(builder.cast()); + return result; +} + +void BufferObject_setBuffer( + Pointer engine, + Pointer buffer, + Pointer data, + Dartsize_t sizeInBytes, + int byteOffset, +) { + final result = GeneratedBindings.instance._BufferObject_setBuffer( + engine.cast(), + buffer.cast(), + data, + sizeInBytes, + byteOffset, + ); + return result; +} + +void BufferObject_destroy(Pointer engine, Pointer buffer) { + final result = GeneratedBindings.instance._BufferObject_destroy(engine.cast(), buffer.cast()); + return result; +} + Pointer IndexBufferBuilder_create() { final result = GeneratedBindings.instance._IndexBufferBuilder_create(); return Pointer(result); @@ -7056,6 +7183,74 @@ void VertexBuffer_setBufferAtRenderThread( return result; } +void VertexBuffer_setBufferObjectAtRenderThread( + Pointer tEngine, + Pointer tBuffer, + int bufferIndex, + Pointer tBufferObject, + int requestId, + DartVoidCallback onComplete, +) { + final result = GeneratedBindings.instance._VertexBuffer_setBufferObjectAtRenderThread( + tEngine.cast(), + tBuffer.cast(), + bufferIndex, + tBufferObject.cast(), + requestId, + onComplete as Pointer>, + ); + return result; +} + +void BufferObjectBuilder_buildRenderThread( + Pointer tBuilder, + Pointer tEngine, + Pointer)>> onComplete, +) { + final result = GeneratedBindings.instance._BufferObjectBuilder_buildRenderThread( + tBuilder.cast(), + tEngine.cast(), + onComplete.cast(), + ); + return result; +} + +void BufferObject_setBufferRenderThread( + Pointer tEngine, + Pointer tBuffer, + Pointer data, + Dartsize_t sizeInBytes, + int byteOffset, + int requestId, + DartVoidCallback onComplete, +) { + final result = GeneratedBindings.instance._BufferObject_setBufferRenderThread( + tEngine.cast(), + tBuffer.cast(), + data, + sizeInBytes, + byteOffset, + requestId, + onComplete as Pointer>, + ); + return result; +} + +void BufferObject_destroyRenderThread( + Pointer tEngine, + Pointer tBuffer, + int requestId, + DartVoidCallback onComplete, +) { + final result = GeneratedBindings.instance._BufferObject_destroyRenderThread( + tEngine.cast(), + tBuffer.cast(), + requestId, + onComplete as Pointer>, + ); + return result; +} + void IndexBufferBuilder_buildRenderThread( Pointer tBuilder, Pointer tEngine, @@ -10121,6 +10316,21 @@ final class TVertexBuffer extends Struct { } } +extension TBufferObjectExt on Pointer { + TBufferObject toDart() { + return TBufferObject(this); + } +} + +final class TBufferObject extends Struct { + Pointer get address => super.address.cast(); + TBufferObject(super.address); + + static Pointer stackAlloc() { + return Pointer(NativeLibrary.instance.stackAlloc(0)); + } +} + extension TIndexBufferExt on Pointer { TIndexBuffer toDart() { return TIndexBuffer(this); @@ -10870,6 +11080,34 @@ final class TVertexBufferBuilder extends Struct { } } +extension TBufferObjectBuilderExt on Pointer { + TBufferObjectBuilder toDart() { + return TBufferObjectBuilder(this); + } +} + +final class TBufferObjectBuilder extends Struct { + Pointer get address => super.address.cast(); + TBufferObjectBuilder(super.address); + + static Pointer stackAlloc() { + return Pointer(NativeLibrary.instance.stackAlloc(0)); + } +} + +sealed class TVertexBufferStorageMode { + static const VERTEX_BUFFER_STORAGE_MODE_UNKNOWN = 0; + static const VERTEX_BUFFER_STORAGE_MODE_DIRECT = 1; + static const VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS = 2; +} + +sealed class TSceneAssetGeometryCapability { + static const SCENE_ASSET_GEOMETRY_CAPABILITY_NONE = 0; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING = 1; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 2; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY = 4; +} + sealed class TVertexAttribute { static const TVERTEX_ATTRIBUTE_POSITION = 0; static const TVERTEX_ATTRIBUTE_TANGENTS = 1; @@ -11420,6 +11658,9 @@ extension StructAllocator on Struct { case TVertexBuffer: final ptr = TVertexBuffer.stackAlloc(); return ptr.toDart() as T; + case TBufferObject: + final ptr = TBufferObject.stackAlloc(); + return ptr.toDart() as T; case TIndexBuffer: final ptr = TIndexBuffer.stackAlloc(); return ptr.toDart() as T; @@ -11480,6 +11721,9 @@ extension StructAllocator on Struct { case TVertexBufferBuilder: final ptr = TVertexBufferBuilder.stackAlloc(); return ptr.toDart() as T; + case TBufferObjectBuilder: + final ptr = TBufferObjectBuilder.stackAlloc(); + return ptr.toDart() as T; case TIndexBufferBuilder: final ptr = TIndexBufferBuilder.stackAlloc(); return ptr.toDart() as T; diff --git a/thermion_dart/lib/src/filament/filament.dart b/thermion_dart/lib/src/filament/filament.dart index 01c1178f4..29e273e04 100644 --- a/thermion_dart/lib/src/filament/filament.dart +++ b/thermion_dart/lib/src/filament/filament.dart @@ -13,6 +13,7 @@ export 'src/interface/tone_mapper.dart'; export 'src/interface/gltf_mesh_data.dart'; export 'src/interface/vertex_buffer.dart'; export 'src/interface/index_buffer.dart'; +export 'src/interface/buffer_object.dart'; export 'src/interface/translation_axis_material.dart'; export 'src/interface/ubershader_material.dart'; export 'src/interface/wireframe_material.dart'; diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart index ae0404f5f..704aeb107 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart @@ -20,12 +20,6 @@ class FFIAsset extends ThermionAsset> { final FFIAsset? instanceOwner; - final VertexBufferMode? _vertexBufferMode; - - /// The mode used to load this glTF asset, inherited by asset instances. - /// Null for non-glTF assets. - VertexBufferMode? get vertexBufferMode => instanceOwner?.vertexBufferMode ?? _vertexBufferMode; - late final ThermionEntity entity; // Mutable only on the owning asset. Instance wrappers read the owner's value @@ -39,12 +33,23 @@ class FFIAsset extends ThermionAsset> { final FFIFilamentApp _app; - FFIAsset(this.asset, {this.instanceOwner = null, VertexBufferMode? vertexBufferMode, required FFIFilamentApp app}) - : _vertexBufferMode = vertexBufferMode, - _app = app { + FFIAsset(this.asset, {this.instanceOwner = null, required FFIFilamentApp app}) : _app = app { entity = SceneAsset_getEntity(asset); } + @override + Set get geometryCapabilities { + final bits = SceneAsset_getGeometryCapabilities(asset); + return { + if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING != 0) + SceneAssetGeometryCapability.flatShading, + if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS != 0) + SceneAssetGeometryCapability.barycentrics, + if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY != 0) + SceneAssetGeometryCapability.editableTopology, + }; + } + @override SceneAssetType get type { final t = SceneAsset_getType(asset); @@ -291,7 +296,7 @@ class FFIAsset extends ThermionAsset> { // unwelded geometry. Editable geometry also has preserved buffers, but it // deliberately uses ordinary writable streams and cannot perform this // swap. - if (vertexBufferMode != VertexBufferMode.unwelded) { + if (!geometryCapabilities.contains(SceneAssetGeometryCapability.flatShading)) { throw StateError( "setFlatShading requires unwelded geometry. " "Load it with loadGltf(..., vertexBufferMode: VertexBufferMode.unwelded).", @@ -915,11 +920,7 @@ class FFIAsset extends ThermionAsset> { if (vbPtr == nullptr) { return null; } - return FFIVertexBuffer( - vbPtr, - _app.engine, - supportsSetBufferAt: type == SceneAssetType.geometry || vertexBufferMode == VertexBufferMode.editable, - ); + return FFIVertexBuffer(vbPtr, _app.engine, ownsResource: false); } } diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_buffer_object.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_buffer_object.dart new file mode 100644 index 000000000..7d74e33bc --- /dev/null +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_buffer_object.dart @@ -0,0 +1,76 @@ +import '../../../bindings/bindings.dart' as bindings; +import 'package:thermion_dart/thermion_dart.dart'; + +class FFIBufferObject extends BufferObject { + final bindings.Pointer _ptr; + final bindings.Pointer _engine; + + FFIBufferObject(this._ptr, this._engine); + + bindings.Pointer getNativeHandle() => _ptr; + + bool isOwnedBy(bindings.Pointer engine) => _engine == engine; + + @override + Future setBuffer(TypedData data, {int byteOffset = 0}) async { + final bytes = data.asUint8List(); + await withVoidCallback((requestId, cb) { + bindings.BufferObject_setBufferRenderThread( + _engine, + _ptr, + bytes.address.cast(), + bytes.lengthInBytes, + byteOffset, + requestId, + cb, + ); + }); + } + + @override + Future destroy() async { + await withVoidCallback((requestId, cb) { + bindings.BufferObject_destroyRenderThread(_engine, _ptr, requestId, cb); + }); + } +} + +class FFIBufferObjectBuilder implements BufferObjectBuilder { + bindings.Pointer? _builder; + final bindings.Pointer _engine; + bool _built = false; + + FFIBufferObjectBuilder(this._engine) { + _builder = bindings.BufferObjectBuilder_create(); + } + + void _checkNotBuilt() { + if (_built || _builder == null || _builder == bindings.nullptr) { + throw StateError('BufferObjectBuilder has already been built'); + } + } + + @override + void size(int sizeInBytes) { + _checkNotBuilt(); + if (sizeInBytes <= 0) { + throw ArgumentError.value(sizeInBytes, 'sizeInBytes', 'must be positive'); + } + bindings.BufferObjectBuilder_size(_builder!, sizeInBytes); + } + + @override + Future build() async { + _checkNotBuilt(); + final pointer = await withPointerCallback( + (cb) => bindings.BufferObjectBuilder_buildRenderThread(_builder!, _engine, cb), + ); + bindings.BufferObjectBuilder_destroy(_builder!); + _builder = null; + _built = true; + if (pointer == bindings.nullptr) { + throw StateError('Failed to build BufferObject'); + } + return FFIBufferObject(pointer, _engine); + } +} diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart index d25f26cff..aaaf181dd 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart @@ -1240,7 +1240,7 @@ class FFIFilamentApp extends FilamentApp { (requestId, cb) => GltfResourceLoader_destroyRenderThread(engine, gltfResourceLoader, requestId, cb), ); - final ffiAsset = FFIAsset(asset, app: this, vertexBufferMode: vertexBufferMode); + final ffiAsset = FFIAsset(asset, app: this); if (releaseSourceData) { await ffiAsset.releaseSourceData(); } diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_renderable_manager.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_renderable_manager.dart index 2d0a075d3..42bc2b262 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_renderable_manager.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_renderable_manager.dart @@ -2,6 +2,7 @@ import 'package:thermion_dart/src/filament/src/implementation/ffi_filament_app.d import '../../../bindings/bindings.dart' as bindings; import 'package:thermion_dart/src/filament/src/implementation/ffi_material.dart'; import 'package:thermion_dart/src/filament/src/implementation/ffi_vertex_buffer.dart'; +import 'package:thermion_dart/src/filament/src/implementation/ffi_buffer_object.dart'; import 'package:thermion_dart/src/filament/src/implementation/ffi_index_buffer.dart'; import 'package:thermion_dart/thermion_dart.dart'; @@ -415,6 +416,11 @@ class FFIRenderableManager extends RenderableManager return FFIVertexBufferBuilder(app.engine); } + @override + BufferObjectBuilder createBufferObjectBuilder() { + return FFIBufferObjectBuilder(app.engine); + } + @override IndexBufferBuilder createIndexBufferBuilder() { return FFIIndexBufferBuilder(app.engine); diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart index 80b77f670..0333f24b1 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart @@ -1,5 +1,6 @@ import '../../../bindings/bindings.dart' as bindings; import 'package:thermion_dart/thermion_dart.dart'; +import 'ffi_buffer_object.dart'; /// FFI implementation of VertexBuffer for native platforms. class FFIVertexBuffer extends VertexBuffer { @@ -7,9 +8,9 @@ class FFIVertexBuffer extends VertexBuffer { final bindings.Pointer _engine; @override - final bool supportsSetBufferAt; + final bool ownsResource; - FFIVertexBuffer(this._ptr, this._engine, {this.supportsSetBufferAt = true}); + FFIVertexBuffer(this._ptr, this._engine, {this.ownsResource = true}); /// Returns the native handle for FFI calls. bindings.Pointer getNativeHandle() => _ptr; @@ -19,13 +20,21 @@ class FFIVertexBuffer extends VertexBuffer { return bindings.VertexBuffer_getVertexCount(_ptr); } + @override + VertexBufferStorageMode get storageMode => switch (bindings.VertexBuffer_getStorageMode(_ptr)) { + bindings.TVertexBufferStorageMode.VERTEX_BUFFER_STORAGE_MODE_DIRECT => VertexBufferStorageMode.direct, + bindings.TVertexBufferStorageMode.VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS => + VertexBufferStorageMode.bufferObjects, + _ => VertexBufferStorageMode.unknown, + }; + @override Future setBufferAt(int bufferIndex, TypedData data, {int byteOffset = 0}) async { - if (!supportsSetBufferAt) { + if (storageMode != VertexBufferStorageMode.direct) { throw StateError( - 'VertexBuffer.setBufferAt cannot update a BufferObject-backed buffer. ' - 'Load glTF assets with vertexBufferMode: VertexBufferMode.editable ' - 'when mutable vertex streams are required.', + 'VertexBuffer.setBufferAt requires direct storage. Build the buffer ' + 'without enableBufferObjects(), or load glTF assets with ' + 'vertexBufferMode: VertexBufferMode.editable.', ); } final byteData = data.asUint8List(); @@ -43,8 +52,37 @@ class FFIVertexBuffer extends VertexBuffer { }); } + @override + Future setBufferObjectAt(int bufferIndex, BufferObject bufferObject) async { + if (storageMode != VertexBufferStorageMode.bufferObjects) { + throw StateError( + 'VertexBuffer.setBufferObjectAt requires BufferObject-backed storage. ' + 'Call VertexBufferBuilder.enableBufferObjects() before build().', + ); + } + if (bufferObject is! FFIBufferObject) { + throw ArgumentError.value(bufferObject, 'bufferObject', 'must be created by this Filament backend'); + } + if (!bufferObject.isOwnedBy(_engine)) { + throw ArgumentError.value(bufferObject, 'bufferObject', 'must belong to the same Filament engine'); + } + await withVoidCallback((requestId, cb) { + bindings.VertexBuffer_setBufferObjectAtRenderThread( + _engine, + _ptr, + bufferIndex, + bufferObject.getNativeHandle(), + requestId, + cb, + ); + }); + } + @override Future destroy() async { + if (!ownsResource) { + throw StateError('Cannot destroy a VertexBuffer borrowed from a ThermionAsset'); + } await withVoidCallback((requestId, cb) { bindings.VertexBuffer_destroyRenderThread(_engine, _ptr, requestId, cb); }); @@ -82,6 +120,12 @@ class FFIVertexBufferBuilder implements VertexBufferBuilder { bindings.VertexBufferBuilder_vertexCount(_builderPtr!, count); } + @override + void enableBufferObjects({bool enabled = true}) { + _checkNotBuilt(); + bindings.VertexBufferBuilder_enableBufferObjects(_builderPtr!, enabled); + } + @override void attribute( VertexAttribute attribute, diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart index 70046930e..247edfd22 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart @@ -577,7 +577,7 @@ class FFIView extends View> { // Stencil highlighting needs the barycentric coordinates generated only // for unwelded geometry. Editable geometry also has preserved buffers but // its CUSTOM0 stream does not contain those coordinates. - if (ffiGeoAsset.vertexBufferMode != VertexBufferMode.unwelded) { + if (!ffiGeoAsset.geometryCapabilities.contains(SceneAssetGeometryCapability.barycentrics)) { throw StateError( "setStencilHighlight requires unwelded geometry. " "Load it with loadGltf(..., vertexBufferMode: VertexBufferMode.unwelded).", diff --git a/thermion_dart/lib/src/filament/src/interface/asset.dart b/thermion_dart/lib/src/filament/src/interface/asset.dart index ad08bbee4..0f07b0664 100644 --- a/thermion_dart/lib/src/filament/src/interface/asset.dart +++ b/thermion_dart/lib/src/filament/src/interface/asset.dart @@ -5,6 +5,8 @@ import 'package:thermion_dart/thermion_dart.dart'; export 'geometry.dart'; +enum SceneAssetGeometryCapability { flatShading, barycentrics, editableTopology } + enum SceneAssetType { gltf, geometry, light, skybox, ibl, image, gizmo } /// Describes one morph target on a renderable entity. @@ -69,6 +71,10 @@ abstract interface class MorphTargetSet { // entities. // abstract class ThermionAsset extends NativeHandle { + Set get geometryCapabilities { + return const {}; + } + // The top-most entity in the hierarchy (if this is a glTF asset, this // entity will have a transform that sits at the top of the transform // hierarchy but is not itself renderable. diff --git a/thermion_dart/lib/src/filament/src/interface/buffer_object.dart b/thermion_dart/lib/src/filament/src/interface/buffer_object.dart new file mode 100644 index 000000000..f3663c000 --- /dev/null +++ b/thermion_dart/lib/src/filament/src/interface/buffer_object.dart @@ -0,0 +1,15 @@ +import 'dart:typed_data'; + +/// GPU storage that can be shared or swapped between compatible vertex +/// buffers. +abstract class BufferObject { + Future setBuffer(TypedData data, {int byteOffset = 0}); + + Future destroy(); +} + +abstract class BufferObjectBuilder { + void size(int sizeInBytes); + + Future build(); +} diff --git a/thermion_dart/lib/src/filament/src/interface/renderable_manager.dart b/thermion_dart/lib/src/filament/src/interface/renderable_manager.dart index d62f57d72..275d68b2e 100644 --- a/thermion_dart/lib/src/filament/src/interface/renderable_manager.dart +++ b/thermion_dart/lib/src/filament/src/interface/renderable_manager.dart @@ -296,6 +296,8 @@ abstract class RenderableManager extends NativeHandle { /// Creates a builder for constructing vertex buffers. VertexBufferBuilder createVertexBufferBuilder(); + BufferObjectBuilder createBufferObjectBuilder(); + /// Creates a builder for constructing index buffers. IndexBufferBuilder createIndexBufferBuilder(); } diff --git a/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart b/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart index 2eda7854e..672007388 100644 --- a/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart +++ b/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart @@ -1,4 +1,3 @@ -import 'dart:typed_data'; import 'package:thermion_dart/thermion_dart.dart'; /// Vertex attribute types that can be stored in a VertexBuffer. @@ -136,6 +135,9 @@ enum VertexAttributeType { HALF4, } +/// Storage used by a [VertexBuffer]'s attribute streams. +enum VertexBufferStorageMode { unknown, direct, bufferObjects } + /// Holds a set of buffers that define the geometry of a Renderable. /// /// The geometry is defined by vertex attributes such as position, color, @@ -150,11 +152,12 @@ abstract class VertexBuffer { /// Returns the number of vertices in this buffer. int getVertexCount(); - /// Whether [setBufferAt] can update this buffer's streams. - /// - /// Buffers created with Filament's BufferObject storage mode are not - /// writable through [setBufferAt]. - bool get supportsSetBufferAt; + VertexBufferStorageMode get storageMode; + + bool get supportsSetBufferAt => storageMode == VertexBufferStorageMode.direct; + + /// Whether this wrapper owns the native resource and may destroy it. + bool get ownsResource; /// Asynchronously copy-initializes the specified buffer from the given data. /// @@ -165,9 +168,17 @@ abstract class VertexBuffer { /// Throws [StateError] when [supportsSetBufferAt] is false. Future setBufferAt(int bufferIndex, TypedData data, {int byteOffset = 0}); + /// Attaches a [BufferObject] to a stream. + /// + /// Throws [StateError] unless [storageMode] is + /// [VertexBufferStorageMode.bufferObjects]. + Future setBufferObjectAt(int bufferIndex, BufferObject bufferObject); + /// Destroys this vertex buffer and releases GPU resources. /// - /// The buffer must not be used after calling this method. + /// The buffer must not be used after calling this method. Throws + /// [StateError] when this is a borrowed buffer returned by a + /// [ThermionAsset]. Destroy the owning asset instead. Future destroy(); } @@ -199,6 +210,9 @@ abstract class VertexBufferBuilder { /// [count] Number of vertices in each buffer in this set void vertexCount(int count); + /// Enables BufferObject-backed streams. Direct storage is used by default. + void enableBufferObjects({bool enabled = true}); + /// Sets up an attribute for this vertex buffer. /// /// Attributes can be interleaved in the same buffer using byteOffset and byteStride. diff --git a/thermion_dart/native/include/c_api/APIBoundaryTypes.h b/thermion_dart/native/include/c_api/APIBoundaryTypes.h index 6c68b4bb6..1bb55cb68 100644 --- a/thermion_dart/native/include/c_api/APIBoundaryTypes.h +++ b/thermion_dart/native/include/c_api/APIBoundaryTypes.h @@ -52,6 +52,8 @@ extern "C" typedef struct TIndexBuffer TIndexBuffer; typedef struct TVertexBufferBuilder TVertexBufferBuilder; typedef struct TIndexBufferBuilder TIndexBufferBuilder; + typedef struct TBufferObject TBufferObject; + typedef struct TBufferObjectBuilder TBufferObjectBuilder; typedef struct TSurfaceOrientation TSurfaceOrientation; typedef struct TSurfaceOrientationBuilder TSurfaceOrientationBuilder; @@ -125,6 +127,21 @@ extern "C" }; typedef enum TVertexBufferMode TVertexBufferMode; + enum TVertexBufferStorageMode { + VERTEX_BUFFER_STORAGE_MODE_UNKNOWN = 0, + VERTEX_BUFFER_STORAGE_MODE_DIRECT = 1, + VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS = 2 + }; + typedef enum TVertexBufferStorageMode TVertexBufferStorageMode; + + enum TSceneAssetGeometryCapability { + SCENE_ASSET_GEOMETRY_CAPABILITY_NONE = 0, + SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING = 1 << 0, + SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 1 << 1, + SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY = 1 << 2 + }; + typedef enum TSceneAssetGeometryCapability TSceneAssetGeometryCapability; + enum TFeatureLevel { FEATURE_LEVEL_0 = 0, FEATURE_LEVEL_1 = 1, diff --git a/thermion_dart/native/include/c_api/TBufferObject.h b/thermion_dart/native/include/c_api/TBufferObject.h new file mode 100644 index 000000000..64ad9ecca --- /dev/null +++ b/thermion_dart/native/include/c_api/TBufferObject.h @@ -0,0 +1,26 @@ +#pragma once + +#include "APIExport.h" +#include "APIBoundaryTypes.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + EMSCRIPTEN_KEEPALIVE TBufferObjectBuilder* BufferObjectBuilder_create(); + EMSCRIPTEN_KEEPALIVE void BufferObjectBuilder_size(TBufferObjectBuilder* builder, uint32_t sizeInBytes); + EMSCRIPTEN_KEEPALIVE TBufferObject* BufferObjectBuilder_build(TBufferObjectBuilder* builder, TEngine* engine); + EMSCRIPTEN_KEEPALIVE void BufferObjectBuilder_destroy(TBufferObjectBuilder* builder); + + EMSCRIPTEN_KEEPALIVE void BufferObject_setBuffer( + TEngine* engine, + TBufferObject* buffer, + void* data, + size_t sizeInBytes, + uint32_t byteOffset); + EMSCRIPTEN_KEEPALIVE void BufferObject_destroy(TEngine* engine, TBufferObject* buffer); + +#ifdef __cplusplus +} +#endif diff --git a/thermion_dart/native/include/c_api/TSceneAsset.h b/thermion_dart/native/include/c_api/TSceneAsset.h index 920d84311..087c750b1 100644 --- a/thermion_dart/native/include/c_api/TSceneAsset.h +++ b/thermion_dart/native/include/c_api/TSceneAsset.h @@ -41,6 +41,7 @@ extern "C" EMSCRIPTEN_KEEPALIVE size_t SceneAsset_getInstanceCount(TSceneAsset *tSceneAsset); EMSCRIPTEN_KEEPALIVE TSceneAsset * SceneAsset_createInstance(TSceneAsset *asset, TMaterialInstance **materialInstances, int materialInstanceCount); EMSCRIPTEN_KEEPALIVE Aabb3 SceneAsset_getBoundingBox(TSceneAsset *asset); + EMSCRIPTEN_KEEPALIVE uint32_t SceneAsset_getGeometryCapabilities(TSceneAsset *asset); EMSCRIPTEN_KEEPALIVE TVertexBuffer *SceneAsset_getVertexBuffer(TSceneAsset *tSceneAsset, int primitiveIndex); EMSCRIPTEN_KEEPALIVE TIndexBuffer *SceneAsset_getIndexBuffer(TSceneAsset *tSceneAsset, int primitiveIndex); EMSCRIPTEN_KEEPALIVE int SceneAsset_getPrimitiveOffsetForEntity(TSceneAsset *tSceneAsset, EntityId entity); diff --git a/thermion_dart/native/include/c_api/TVertexBuffer.h b/thermion_dart/native/include/c_api/TVertexBuffer.h index 618c26d07..d656453d4 100644 --- a/thermion_dart/native/include/c_api/TVertexBuffer.h +++ b/thermion_dart/native/include/c_api/TVertexBuffer.h @@ -18,6 +18,7 @@ extern "C" // Configure the builder EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_bufferCount(TVertexBufferBuilder* builder, uint8_t count); EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_vertexCount(TVertexBufferBuilder* builder, uint32_t count); + EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_enableBufferObjects(TVertexBufferBuilder* builder, bool enabled); EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_attribute( TVertexBufferBuilder* builder, TVertexAttribute attribute, @@ -38,6 +39,7 @@ extern "C" // Get vertex count EMSCRIPTEN_KEEPALIVE size_t VertexBuffer_getVertexCount(TVertexBuffer* buffer); + EMSCRIPTEN_KEEPALIVE TVertexBufferStorageMode VertexBuffer_getStorageMode(TVertexBuffer* buffer); // Set buffer data EMSCRIPTEN_KEEPALIVE void VertexBuffer_setBufferAt( @@ -48,6 +50,12 @@ extern "C" size_t sizeInBytes, uint32_t byteOffset ); + EMSCRIPTEN_KEEPALIVE void VertexBuffer_setBufferObjectAt( + TEngine* engine, + TVertexBuffer* buffer, + uint8_t bufferIndex, + TBufferObject* bufferObject + ); // Destroy EMSCRIPTEN_KEEPALIVE void VertexBuffer_destroy(TEngine* engine, TVertexBuffer* buffer); diff --git a/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h b/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h index 4c56df86f..24a4c9477 100644 --- a/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h +++ b/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h @@ -8,6 +8,7 @@ #include "TMaterialProvider.h" #include "TVertexBuffer.h" #include "TIndexBuffer.h" +#include "TBufferObject.h" #include "TTransformManager.h" #include "TLightManager.h" @@ -408,6 +409,35 @@ namespace thermion uint32_t requestId, VoidCallback onComplete ); + EMSCRIPTEN_KEEPALIVE void VertexBuffer_setBufferObjectAtRenderThread( + TEngine* tEngine, + TVertexBuffer* tBuffer, + uint8_t bufferIndex, + TBufferObject* tBufferObject, + uint32_t requestId, + VoidCallback onComplete + ); + + EMSCRIPTEN_KEEPALIVE void BufferObjectBuilder_buildRenderThread( + TBufferObjectBuilder* tBuilder, + TEngine* tEngine, + void (*onComplete)(TBufferObject*) + ); + EMSCRIPTEN_KEEPALIVE void BufferObject_setBufferRenderThread( + TEngine* tEngine, + TBufferObject* tBuffer, + void* data, + size_t sizeInBytes, + uint32_t byteOffset, + uint32_t requestId, + VoidCallback onComplete + ); + EMSCRIPTEN_KEEPALIVE void BufferObject_destroyRenderThread( + TEngine* tEngine, + TBufferObject* tBuffer, + uint32_t requestId, + VoidCallback onComplete + ); // IndexBuffer render thread methods EMSCRIPTEN_KEEPALIVE void IndexBufferBuilder_buildRenderThread( diff --git a/thermion_dart/native/include/scene/GltfSceneAsset.hpp b/thermion_dart/native/include/scene/GltfSceneAsset.hpp index 32caa5596..b8b000c71 100644 --- a/thermion_dart/native/include/scene/GltfSceneAsset.hpp +++ b/thermion_dart/native/include/scene/GltfSceneAsset.hpp @@ -141,6 +141,19 @@ namespace thermion return _asset->getBoundingBox(); } + uint32_t getGeometryCapabilities() const override { + switch (_vertexBufferMode) { + case VERTEX_BUFFER_MODE_EDITABLE: + return SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY; + case VERTEX_BUFFER_MODE_UNWELDED: + return SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING | + SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS; + case VERTEX_BUFFER_MODE_ORIGINAL: + default: + return SCENE_ASSET_GEOMETRY_CAPABILITY_NONE; + } + } + /// Rebuild all mesh primitives with a superset vertex buffer layout /// (POSITION + TANGENTS + UV0 + CUSTOM0 + optional BONE_INDICES/WEIGHTS). /// [VERTEX_BUFFER_MODE_UNWELDED] gives each triangle unique vertices @@ -202,6 +215,7 @@ namespace thermion bool _sourceDataReleased = false; bool _geometryPreserved = false; bool _flatShading = false; + TVertexBufferMode _vertexBufferMode = VERTEX_BUFFER_MODE_ORIGINAL; // Buffers created by rebuildVertexBuffers, owned by this asset. std::vector _preservedVertexBuffers; diff --git a/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp b/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp index e8abdeac1..b863b079e 100644 --- a/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp +++ b/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp @@ -140,6 +140,8 @@ namespace thermion return _instance->getBoundingBox(); } + uint32_t getGeometryCapabilities() const override; + size_t getBoneCount(size_t skinIndex) const override; const utils::Entity *getBones(size_t skinIndex) const override; const char *getBoneName(size_t skinIndex, size_t boneIndex) const override; @@ -155,4 +157,4 @@ namespace thermion GltfSceneAsset *_instanceOwner = std::nullptr_t(); }; -} // namespace thermion \ No newline at end of file +} // namespace thermion diff --git a/thermion_dart/native/include/scene/SceneAsset.hpp b/thermion_dart/native/include/scene/SceneAsset.hpp index 83dc97f5f..fac6586a6 100644 --- a/thermion_dart/native/include/scene/SceneAsset.hpp +++ b/thermion_dart/native/include/scene/SceneAsset.hpp @@ -9,6 +9,7 @@ #include "CustomGeometry.hpp" #include "Log.hpp" +#include "c_api/APIBoundaryTypes.h" namespace thermion { @@ -56,6 +57,10 @@ class SceneAsset { virtual const filament::Aabb getBoundingBox() const = 0; + virtual uint32_t getGeometryCapabilities() const { + return SCENE_ASSET_GEOMETRY_CAPABILITY_NONE; + } + virtual size_t getBoneCount(size_t skinIndex) const { return 0; } @@ -69,4 +74,4 @@ class SceneAsset { } }; -} \ No newline at end of file +} diff --git a/thermion_dart/native/include/scene/VertexBufferMetadata.hpp b/thermion_dart/native/include/scene/VertexBufferMetadata.hpp new file mode 100644 index 000000000..e23a9b16f --- /dev/null +++ b/thermion_dart/native/include/scene/VertexBufferMetadata.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "c_api/APIBoundaryTypes.h" + +namespace thermion +{ + void registerVertexBufferStorageMode( + filament::VertexBuffer *buffer, + TVertexBufferStorageMode storageMode); + + void unregisterVertexBufferStorageMode(filament::VertexBuffer *buffer); + + TVertexBufferStorageMode getVertexBufferStorageMode( + const filament::VertexBuffer *buffer); +} diff --git a/thermion_dart/native/src/c_api/TBufferObject.cpp b/thermion_dart/native/src/c_api/TBufferObject.cpp new file mode 100644 index 000000000..61e1fd38a --- /dev/null +++ b/thermion_dart/native/src/c_api/TBufferObject.cpp @@ -0,0 +1,67 @@ +#include +#include + +#include +#include + +#include "c_api/TBufferObject.h" + +namespace thermion +{ + extern "C" + { + using namespace filament; + + EMSCRIPTEN_KEEPALIVE TBufferObjectBuilder* BufferObjectBuilder_create() + { + return reinterpret_cast(new BufferObject::Builder()); + } + + EMSCRIPTEN_KEEPALIVE void BufferObjectBuilder_size( + TBufferObjectBuilder* tBuilder, + uint32_t sizeInBytes) + { + reinterpret_cast(tBuilder)->size(sizeInBytes); + } + + EMSCRIPTEN_KEEPALIVE TBufferObject* BufferObjectBuilder_build( + TBufferObjectBuilder* tBuilder, + TEngine* tEngine) + { + auto* builder = reinterpret_cast(tBuilder); + auto* engine = reinterpret_cast(tEngine); + return reinterpret_cast(builder->build(*engine)); + } + + EMSCRIPTEN_KEEPALIVE void BufferObjectBuilder_destroy(TBufferObjectBuilder* tBuilder) + { + delete reinterpret_cast(tBuilder); + } + + EMSCRIPTEN_KEEPALIVE void BufferObject_setBuffer( + TEngine* tEngine, + TBufferObject* tBuffer, + void* data, + size_t sizeInBytes, + uint32_t byteOffset) + { + auto* copy = new uint8_t[sizeInBytes]; + std::memcpy(copy, data, sizeInBytes); + auto* engine = reinterpret_cast(tEngine); + auto* buffer = reinterpret_cast(tBuffer); + buffer->setBuffer( + *engine, + BufferObject::BufferDescriptor( + copy, + sizeInBytes, + [](void* data, size_t, void*) { delete[] static_cast(data); }), + byteOffset); + } + + EMSCRIPTEN_KEEPALIVE void BufferObject_destroy(TEngine* tEngine, TBufferObject* tBuffer) + { + reinterpret_cast(tEngine)->destroy( + reinterpret_cast(tBuffer)); + } + } +} diff --git a/thermion_dart/native/src/c_api/TSceneAsset.cpp b/thermion_dart/native/src/c_api/TSceneAsset.cpp index 278aadd9e..ddd57090f 100644 --- a/thermion_dart/native/src/c_api/TSceneAsset.cpp +++ b/thermion_dart/native/src/c_api/TSceneAsset.cpp @@ -227,6 +227,10 @@ extern "C" return Aabb3{box.center().x, box.center().y, box.center().z, box.extent().x, box.extent().y, box.extent().z}; } + EMSCRIPTEN_KEEPALIVE uint32_t SceneAsset_getGeometryCapabilities(TSceneAsset *tSceneAsset) { + return reinterpret_cast(tSceneAsset)->getGeometryCapabilities(); + } + EMSCRIPTEN_KEEPALIVE TVertexBuffer *SceneAsset_getVertexBuffer(TSceneAsset *tSceneAsset, int primitiveIndex) { auto *asset = reinterpret_cast(tSceneAsset); if (asset->getType() == SceneAsset::SceneAssetType::Geometry) { @@ -235,7 +239,8 @@ extern "C" return reinterpret_cast(vertexBuffer); } if (asset->getType() == SceneAsset::SceneAssetType::Gltf) { - auto gltfSceneAsset = reinterpret_cast(asset); + auto gltfSceneAsset = reinterpret_cast( + asset->isInstance() ? asset->getInstanceOwner() : asset); auto *vertexBuffer = gltfSceneAsset->getPreservedVertexBuffer(primitiveIndex); return reinterpret_cast(vertexBuffer); } @@ -250,7 +255,8 @@ extern "C" return reinterpret_cast(indexBuffer); } if (asset->getType() == SceneAsset::SceneAssetType::Gltf) { - auto gltfSceneAsset = reinterpret_cast(asset); + auto gltfSceneAsset = reinterpret_cast( + asset->isInstance() ? asset->getInstanceOwner() : asset); auto *indexBuffer = gltfSceneAsset->getPreservedIndexBuffer(primitiveIndex); return reinterpret_cast(indexBuffer); } @@ -262,7 +268,8 @@ extern "C" if (asset->getType() != SceneAsset::SceneAssetType::Gltf) { return -1; } - auto gltfSceneAsset = reinterpret_cast(asset); + auto gltfSceneAsset = reinterpret_cast( + asset->isInstance() ? asset->getInstanceOwner() : asset); // Convert EntityId to utils::Entity for the internal method return gltfSceneAsset->getPrimitiveOffsetForEntity(utils::Entity::import(entity)); } diff --git a/thermion_dart/native/src/c_api/TVertexBuffer.cpp b/thermion_dart/native/src/c_api/TVertexBuffer.cpp index cdda3dca9..75644f1fc 100644 --- a/thermion_dart/native/src/c_api/TVertexBuffer.cpp +++ b/thermion_dart/native/src/c_api/TVertexBuffer.cpp @@ -1,11 +1,56 @@ #include +#include #include +#include +#include + #include "Log.hpp" #include "c_api/TVertexBuffer.h" +#include "scene/VertexBufferMetadata.hpp" + +namespace +{ + struct VertexBufferBuilderState + { + filament::VertexBuffer::Builder builder; + TVertexBufferStorageMode storageMode = VERTEX_BUFFER_STORAGE_MODE_DIRECT; + }; + + std::mutex gVertexBufferMetadataMutex; + std::unordered_map gVertexBufferStorageModes; +} namespace thermion { + void registerVertexBufferStorageMode( + filament::VertexBuffer *buffer, + TVertexBufferStorageMode storageMode) + { + if (!buffer) + return; + std::lock_guard lock(gVertexBufferMetadataMutex); + gVertexBufferStorageModes[buffer] = storageMode; + } + + void unregisterVertexBufferStorageMode(filament::VertexBuffer *buffer) + { + if (!buffer) + return; + std::lock_guard lock(gVertexBufferMetadataMutex); + gVertexBufferStorageModes.erase(buffer); + } + + TVertexBufferStorageMode getVertexBufferStorageMode( + const filament::VertexBuffer *buffer) + { + std::lock_guard lock(gVertexBufferMetadataMutex); + const auto entry = gVertexBufferStorageModes.find(buffer); + return entry == gVertexBufferStorageModes.end() + ? VERTEX_BUFFER_STORAGE_MODE_UNKNOWN + : entry->second; + } + extern "C" { using namespace filament; @@ -15,18 +60,26 @@ namespace thermion // ============================================================================ EMSCRIPTEN_KEEPALIVE TVertexBufferBuilder* VertexBufferBuilder_create() { - auto* builder = new filament::VertexBuffer::Builder(); + auto* builder = new VertexBufferBuilderState(); return reinterpret_cast(builder); } EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_bufferCount(TVertexBufferBuilder* tBuilder, uint8_t count) { - auto* builder = reinterpret_cast(tBuilder); - builder->bufferCount(count); + auto* builder = reinterpret_cast(tBuilder); + builder->builder.bufferCount(count); } EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_vertexCount(TVertexBufferBuilder* tBuilder, uint32_t count) { - auto* builder = reinterpret_cast(tBuilder); - builder->vertexCount(count); + auto* builder = reinterpret_cast(tBuilder); + builder->builder.vertexCount(count); + } + + EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_enableBufferObjects(TVertexBufferBuilder* tBuilder, bool enabled) { + auto* builder = reinterpret_cast(tBuilder); + builder->builder.enableBufferObjects(enabled); + builder->storageMode = enabled + ? VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS + : VERTEX_BUFFER_STORAGE_MODE_DIRECT; } EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_attribute( @@ -37,7 +90,7 @@ namespace thermion uint32_t byteOffset, uint8_t byteStride ) { - auto* builder = reinterpret_cast(tBuilder); + auto* builder = reinterpret_cast(tBuilder); // Map TVertexAttribute to filament::VertexAttribute explicitly VertexAttribute vertexAttribute; @@ -97,11 +150,11 @@ namespace thermion return; } - builder->attribute(vertexAttribute, bufferIndex, elementType, byteOffset, byteStride); + builder->builder.attribute(vertexAttribute, bufferIndex, elementType, byteOffset, byteStride); } EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_normalized(TVertexBufferBuilder* tBuilder, TVertexAttribute attribute, bool normalize) { - auto* builder = reinterpret_cast(tBuilder); + auto* builder = reinterpret_cast(tBuilder); // Map TVertexAttribute to filament::VertexAttribute explicitly VertexAttribute vertexAttribute; @@ -126,18 +179,19 @@ namespace thermion return; } - builder->normalized(vertexAttribute, normalize); + builder->builder.normalized(vertexAttribute, normalize); } EMSCRIPTEN_KEEPALIVE TVertexBuffer* VertexBufferBuilder_build(TVertexBufferBuilder* tBuilder, TEngine* tEngine) { - auto* builder = reinterpret_cast(tBuilder); + auto* builder = reinterpret_cast(tBuilder); auto* engine = reinterpret_cast(tEngine); - auto* vertexBuffer = builder->build(*engine); + auto* vertexBuffer = builder->builder.build(*engine); + registerVertexBufferStorageMode(vertexBuffer, builder->storageMode); return reinterpret_cast(vertexBuffer); } EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_destroy(TVertexBufferBuilder* tBuilder) { - auto* builder = reinterpret_cast(tBuilder); + auto* builder = reinterpret_cast(tBuilder); delete builder; } @@ -150,6 +204,11 @@ namespace thermion return vertexBuffer->getVertexCount(); } + EMSCRIPTEN_KEEPALIVE TVertexBufferStorageMode VertexBuffer_getStorageMode(TVertexBuffer* tBuffer) { + auto* vertexBuffer = reinterpret_cast(tBuffer); + return getVertexBufferStorageMode(vertexBuffer); + } + EMSCRIPTEN_KEEPALIVE void VertexBuffer_setBufferAt( TEngine* tEngine, TVertexBuffer* tBuffer, @@ -181,9 +240,22 @@ namespace thermion vertexBuffer->setBufferAt(*engine, bufferIndex, std::move(bufferDescriptor), byteOffset); } + EMSCRIPTEN_KEEPALIVE void VertexBuffer_setBufferObjectAt( + TEngine* tEngine, + TVertexBuffer* tBuffer, + uint8_t bufferIndex, + TBufferObject* tBufferObject + ) { + auto* engine = reinterpret_cast(tEngine); + auto* vertexBuffer = reinterpret_cast(tBuffer); + auto* bufferObject = reinterpret_cast(tBufferObject); + vertexBuffer->setBufferObjectAt(*engine, bufferIndex, bufferObject); + } + EMSCRIPTEN_KEEPALIVE void VertexBuffer_destroy(TEngine* tEngine, TVertexBuffer* tBuffer) { auto* engine = reinterpret_cast(tEngine); auto* vertexBuffer = reinterpret_cast(tBuffer); + unregisterVertexBufferStorageMode(vertexBuffer); engine->destroy(vertexBuffer); } diff --git a/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp b/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp index 925e9dc9f..dbdac20b0 100644 --- a/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp +++ b/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp @@ -11,6 +11,7 @@ #endif #include +#include #include "c_api/APIBoundaryTypes.h" #include "c_api/TAnimationManager.h" @@ -30,6 +31,7 @@ #include "c_api/TView.h" #include "c_api/TVertexBuffer.h" #include "c_api/TIndexBuffer.h" +#include "c_api/TBufferObject.h" #include "c_api/ThermionDartRenderThreadApi.h" #include "rendering/RenderThread.hpp" @@ -2532,6 +2534,80 @@ extern "C" auto fut = rt->addTask(lambda); } + EMSCRIPTEN_KEEPALIVE void VertexBuffer_setBufferObjectAtRenderThread( + TEngine* tEngine, + TVertexBuffer* tBuffer, + uint8_t bufferIndex, + TBufferObject* tBufferObject, + uint32_t requestId, + VoidCallback onComplete) + { + auto* rt = RT(tEngine); + std::packaged_task lambda([=]() mutable { + VertexBuffer_setBufferObjectAt(tEngine, tBuffer, bufferIndex, tBufferObject); + PROXY(onComplete(requestId)); + }); + auto fut = rt->addTask(lambda); + } + + EMSCRIPTEN_KEEPALIVE void BufferObjectBuilder_buildRenderThread( + TBufferObjectBuilder* tBuilder, + TEngine* tEngine, + void (*onComplete)(TBufferObject*)) + { + auto* rt = RT(tEngine); + std::packaged_task lambda([=]() mutable { + auto* buffer = BufferObjectBuilder_build(tBuilder, tEngine); + setOwner(buffer, rt); + PROXY(onComplete(buffer)); + }); + auto fut = rt->addTask(lambda); + } + + EMSCRIPTEN_KEEPALIVE void BufferObject_setBufferRenderThread( + TEngine* tEngine, + TBufferObject* tBuffer, + void* data, + size_t sizeInBytes, + uint32_t byteOffset, + uint32_t requestId, + VoidCallback onComplete) + { + auto* rt = RT(tEngine); + auto* copy = new std::vector(sizeInBytes); + std::copy(static_cast(data), static_cast(data) + sizeInBytes, copy->begin()); + std::packaged_task lambda([=]() mutable { + auto* engine = reinterpret_cast(tEngine); + auto* buffer = reinterpret_cast(tBuffer); + buffer->setBuffer( + *engine, + filament::BufferObject::BufferDescriptor( + copy->data(), + copy->size(), + [](void*, size_t, void* user) { + delete reinterpret_cast*>(user); + }, + copy), + byteOffset); + PROXY(onComplete(requestId)); + }); + auto fut = rt->addTask(lambda); + } + + EMSCRIPTEN_KEEPALIVE void BufferObject_destroyRenderThread( + TEngine* tEngine, + TBufferObject* tBuffer, + uint32_t requestId, + VoidCallback onComplete) + { + auto* rt = RT(tEngine); + std::packaged_task lambda([=]() mutable { + BufferObject_destroy(tEngine, tBuffer); + PROXY(onComplete(requestId)); + }); + auto fut = rt->addTask(lambda); + } + EMSCRIPTEN_KEEPALIVE void IndexBufferBuilder_buildRenderThread( TIndexBufferBuilder *tBuilder, TEngine *tEngine, diff --git a/thermion_dart/native/src/scene/GeometrySceneAsset.cpp b/thermion_dart/native/src/scene/GeometrySceneAsset.cpp index ef40e561a..e66b94fab 100644 --- a/thermion_dart/native/src/scene/GeometrySceneAsset.cpp +++ b/thermion_dart/native/src/scene/GeometrySceneAsset.cpp @@ -11,6 +11,7 @@ #include "Log.hpp" #include "scene/GeometrySceneAsset.hpp" +#include "scene/VertexBufferMetadata.hpp" namespace thermion { @@ -71,8 +72,10 @@ namespace thermion utils::EntityManager::get().destroy(_entity); - if (_vertexBuffer && !isInstance()) + if (_vertexBuffer && !isInstance()) { + unregisterVertexBufferStorageMode(_vertexBuffer); _engine->destroy(_vertexBuffer); + } if (_indexBuffer && !isInstance()) _engine->destroy(_indexBuffer); @@ -113,4 +116,4 @@ namespace thermion _instances.erase(it, _instances.end()); } -} // namespace thermion \ No newline at end of file +} // namespace thermion diff --git a/thermion_dart/native/src/scene/GltfSceneAsset.cpp b/thermion_dart/native/src/scene/GltfSceneAsset.cpp index e30dc0ba4..a255b2652 100644 --- a/thermion_dart/native/src/scene/GltfSceneAsset.cpp +++ b/thermion_dart/native/src/scene/GltfSceneAsset.cpp @@ -1,5 +1,6 @@ #include "scene/GltfSceneAsset.hpp" +#include "scene/VertexBufferMetadata.hpp" #include "scene/GltfSceneAssetInstance.hpp" #include "gltfio/FilamentInstance.h" #include "Log.hpp" @@ -44,7 +45,8 @@ namespace thermion _engine(engine), _ncm(ncm), _materialInstances(materialInstances), - _materialInstanceCount(materialInstanceCount) + _materialInstanceCount(materialInstanceCount), + _vertexBufferMode(vertexBufferMode) { if (vertexBufferMode != VERTEX_BUFFER_MODE_ORIGINAL) { @@ -62,6 +64,7 @@ namespace thermion _instances.clear(); for (auto *vb : _preservedVertexBuffers) { + unregisterVertexBufferStorageMode(vb); _engine->destroy(vb); } for (auto *ib : _preservedIndexBuffers) @@ -691,6 +694,11 @@ namespace thermion } VertexBuffer *vb = vbBuilder.build(*_engine); + registerVertexBufferStorageMode( + vb, + editableTopology + ? VERTEX_BUFFER_STORAGE_MODE_DIRECT + : VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS); auto uploadDirect = [&](uint8_t bufferIndex, const void *source, size_t size) { diff --git a/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp b/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp index c0fc84767..27e2152a8 100644 --- a/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp +++ b/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp @@ -14,6 +14,11 @@ namespace thermion return static_cast(_instanceOwner); } + uint32_t GltfSceneAssetInstance::getGeometryCapabilities() const + { + return _instanceOwner->getGeometryCapabilities(); + } + size_t GltfSceneAssetInstance::getBoneCount(size_t skinIndex) const { return _instance->getJointCountAt(skinIndex); @@ -39,4 +44,4 @@ namespace thermion return _ncm->getName(ci); } -} \ No newline at end of file +} diff --git a/thermion_dart/test/geometry_tests.dart b/thermion_dart/test/geometry_tests.dart index a9cf59126..e03ac11a3 100644 --- a/thermion_dart/test/geometry_tests.dart +++ b/thermion_dart/test/geometry_tests.dart @@ -27,6 +27,9 @@ void main() async { await testHelper.capture(result.viewer.view, "update_vertex_buffer_1"); final vb = await asset.getVertexBuffer(); expect(vb, isNotNull); + expect(vb!.storageMode, VertexBufferStorageMode.direct); + expect(vb.ownsResource, isFalse); + await expectLater(vb.destroy(), throwsStateError); final vertices = Float32List.fromList([ // Front face -1, -1, 1, // 0 @@ -59,7 +62,7 @@ void main() async { -1, 1, 1, // 3 (22) -1, 1, -1, // 7 (23) ]); - await vb!.setBufferAt(0, vertices); + await vb.setBufferAt(0, vertices); await testHelper.capture(result.viewer.view, "update_vertex_buffer_2"); }); }); diff --git a/thermion_dart/test/morph_animation_tests.dart b/thermion_dart/test/morph_animation_tests.dart index 19a6bb23a..1c33e46e9 100644 --- a/thermion_dart/test/morph_animation_tests.dart +++ b/thermion_dart/test/morph_animation_tests.dart @@ -75,7 +75,13 @@ void main() async { final editable = await viewer.loadGltf(path, vertexBufferMode: VertexBufferMode.editable); final editableVertexBuffer = editable.getVertexBuffer()!; expect(editableVertexBuffer.supportsSetBufferAt, isTrue); + expect(editableVertexBuffer.storageMode, VertexBufferStorageMode.direct); + expect(editableVertexBuffer.ownsResource, isFalse); expect(editableVertexBuffer.getVertexCount(), source.vertices.length ~/ 3); + + final instanceVertexBuffer = (await editable.getInstance(0)).getVertexBuffer()!; + expect(instanceVertexBuffer.storageMode, VertexBufferStorageMode.direct); + expect(instanceVertexBuffer.ownsResource, isFalse); final editablePose = await capture(editable); expect( diff --git a/thermion_dart/test/vertex_index_buffer_tests.dart b/thermion_dart/test/vertex_index_buffer_tests.dart index 181ce41b0..a2403d809 100644 --- a/thermion_dart/test/vertex_index_buffer_tests.dart +++ b/thermion_dart/test/vertex_index_buffer_tests.dart @@ -8,6 +8,40 @@ void main() async { final testHelper = TestHelper("vertex_index_buffer"); await testHelper.setup(); group("VertexBufferBuilder tests", () { + test('direct and BufferObject storage enforce their update APIs', () async { + await ViewerBuilder(testHelper).execute((result) async { + final manager = FilamentApp.instance!.renderableManager; + + final directBuilder = manager.createVertexBufferBuilder() + ..bufferCount(1) + ..vertexCount(3) + ..attribute(VertexAttribute.POSITION, 0, VertexAttributeType.FLOAT3); + final direct = await directBuilder.build(); + expect(direct.storageMode, VertexBufferStorageMode.direct); + + final bufferObjectBuilder = manager.createBufferObjectBuilder()..size(3 * 3 * Float32List.bytesPerElement); + final bufferObject = await bufferObjectBuilder.build(); + await bufferObject.setBuffer(Float32List.fromList([-1, -1, 0, 1, -1, 0, 0, 1, 0])); + + await expectLater(direct.setBufferObjectAt(0, bufferObject), throwsStateError); + + final bufferObjectVertexBuilder = manager.createVertexBufferBuilder() + ..bufferCount(1) + ..vertexCount(3) + ..enableBufferObjects() + ..attribute(VertexAttribute.POSITION, 0, VertexAttributeType.FLOAT3); + final bufferObjectVertexBuffer = await bufferObjectVertexBuilder.build(); + expect(bufferObjectVertexBuffer.storageMode, VertexBufferStorageMode.bufferObjects); + expect(bufferObjectVertexBuffer.supportsSetBufferAt, isFalse); + await expectLater(bufferObjectVertexBuffer.setBufferAt(0, Float32List(9)), throwsStateError); + await bufferObjectVertexBuffer.setBufferObjectAt(0, bufferObject); + + await bufferObjectVertexBuffer.destroy(); + await bufferObject.destroy(); + await direct.destroy(); + }); + }); + test('create and build simple vertex buffer', () async { await ViewerBuilder(testHelper).setBackgroundColor(kBlue).execute((result) async { final app = FilamentApp.instance!; From ac601d1b5711a7653e3b4b68dc2636173e96883e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 26 Aug 2026 03:42:24 +0000 Subject: [PATCH 04/14] chore: update generated artifacts + format (CI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with GitHub Actions --- .../src/bindings/src/thermion_dart_ffi.g.dart | 94 ++++---- .../src/thermion_dart_js_interop.g.dart | 202 +++++++++--------- 2 files changed, 139 insertions(+), 157 deletions(-) diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart index a006394c6..fc909d5e6 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart @@ -1471,41 +1471,6 @@ external void VertexBuffer_setBufferObjectAt( @ffi.Native, ffi.Pointer)>(isLeaf: true) external void VertexBuffer_destroy(ffi.Pointer engine, ffi.Pointer buffer); -@ffi.Native Function()>(isLeaf: true) -external ffi.Pointer BufferObjectBuilder_create(); - -@ffi.Native, ffi.Uint32)>(isLeaf: true) -external void BufferObjectBuilder_size(ffi.Pointer builder, int sizeInBytes); - -@ffi.Native Function(ffi.Pointer, ffi.Pointer)>(isLeaf: true) -external ffi.Pointer BufferObjectBuilder_build( - ffi.Pointer builder, - ffi.Pointer engine, -); - -@ffi.Native)>(isLeaf: true) -external void BufferObjectBuilder_destroy(ffi.Pointer builder); - -@ffi.Native< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Size, - ffi.Uint32, - ) ->(isLeaf: true) -external void BufferObject_setBuffer( - ffi.Pointer engine, - ffi.Pointer buffer, - ffi.Pointer data, - int sizeInBytes, - int byteOffset, -); - -@ffi.Native, ffi.Pointer)>(isLeaf: true) -external void BufferObject_destroy(ffi.Pointer engine, ffi.Pointer buffer); - @ffi.Native Function()>(isLeaf: true) external ffi.Pointer IndexBufferBuilder_create(); @@ -1541,6 +1506,35 @@ external void IndexBuffer_setBuffer( @ffi.Native, ffi.Pointer)>(isLeaf: true) external void IndexBuffer_destroy(ffi.Pointer engine, ffi.Pointer buffer); +@ffi.Native Function()>(isLeaf: true) +external ffi.Pointer BufferObjectBuilder_create(); + +@ffi.Native, ffi.Uint32)>(isLeaf: true) +external void BufferObjectBuilder_size(ffi.Pointer builder, int sizeInBytes); + +@ffi.Native Function(ffi.Pointer, ffi.Pointer)>(isLeaf: true) +external ffi.Pointer BufferObjectBuilder_build( + ffi.Pointer builder, + ffi.Pointer engine, +); + +@ffi.Native)>(isLeaf: true) +external void BufferObjectBuilder_destroy(ffi.Pointer builder); + +@ffi.Native< + ffi.Void Function(ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.Size, ffi.Uint32) +>(isLeaf: true) +external void BufferObject_setBuffer( + ffi.Pointer engine, + ffi.Pointer buffer, + ffi.Pointer data, + int sizeInBytes, + int byteOffset, +); + +@ffi.Native, ffi.Pointer)>(isLeaf: true) +external void BufferObject_destroy(ffi.Pointer engine, ffi.Pointer buffer); + @ffi.Native, EntityId)>(isLeaf: true) external double4x4 TransformManager_getLocalTransform(ffi.Pointer tTransformManager, int entityId); @@ -3536,9 +3530,7 @@ external void BufferObject_setBufferRenderThread( VoidCallback onComplete, ); -@ffi.Native, ffi.Pointer, ffi.Uint32, VoidCallback)>( - isLeaf: true, -) +@ffi.Native, ffi.Pointer, ffi.Uint32, VoidCallback)>(isLeaf: true) external void BufferObject_destroyRenderThread( ffi.Pointer tEngine, ffi.Pointer tBuffer, @@ -4922,19 +4914,6 @@ final class TBufferObject extends ffi.Opaque {} final class TBufferObjectBuilder extends ffi.Opaque {} -sealed class TVertexBufferStorageMode { - static const VERTEX_BUFFER_STORAGE_MODE_UNKNOWN = 0; - static const VERTEX_BUFFER_STORAGE_MODE_DIRECT = 1; - static const VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS = 2; -} - -sealed class TSceneAssetGeometryCapability { - static const SCENE_ASSET_GEOMETRY_CAPABILITY_NONE = 0; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING = 1; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 2; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY = 4; -} - final class TSurfaceOrientation extends ffi.Opaque {} final class TSurfaceOrientationBuilder extends ffi.Opaque {} @@ -5041,6 +5020,19 @@ sealed class TVertexBufferMode { static const VERTEX_BUFFER_MODE_EDITABLE = 2; } +sealed class TVertexBufferStorageMode { + static const VERTEX_BUFFER_STORAGE_MODE_UNKNOWN = 0; + static const VERTEX_BUFFER_STORAGE_MODE_DIRECT = 1; + static const VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS = 2; +} + +sealed class TSceneAssetGeometryCapability { + static const SCENE_ASSET_GEOMETRY_CAPABILITY_NONE = 0; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING = 1; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 2; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY = 4; +} + sealed class TFeatureLevel { static const FEATURE_LEVEL_0 = 0; static const FEATURE_LEVEL_1 = 1; diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart index f6f674714..9ecb2c265 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart @@ -656,21 +656,6 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { Pointer bufferObject, ); external void _VertexBuffer_destroy(Pointer engine, Pointer buffer); - external Pointer _BufferObjectBuilder_create(); - external void _BufferObjectBuilder_size(Pointer builder, int sizeInBytes); - external Pointer _BufferObjectBuilder_build( - Pointer builder, - Pointer engine, - ); - external void _BufferObjectBuilder_destroy(Pointer builder); - external void _BufferObject_setBuffer( - Pointer engine, - Pointer buffer, - Pointer data, - size_t sizeInBytes, - int byteOffset, - ); - external void _BufferObject_destroy(Pointer engine, Pointer buffer); external Pointer _IndexBufferBuilder_create(); external void _IndexBufferBuilder_indexCount(Pointer builder, int count); external void _IndexBufferBuilder_bufferType(Pointer builder, int indexType); @@ -688,6 +673,21 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { int byteOffset, ); external void _IndexBuffer_destroy(Pointer engine, Pointer buffer); + external Pointer _BufferObjectBuilder_create(); + external void _BufferObjectBuilder_size(Pointer builder, int sizeInBytes); + external Pointer _BufferObjectBuilder_build( + Pointer builder, + Pointer engine, + ); + external void _BufferObjectBuilder_destroy(Pointer builder); + external void _BufferObject_setBuffer( + Pointer engine, + Pointer buffer, + Pointer data, + size_t sizeInBytes, + int byteOffset, + ); + external void _BufferObject_destroy(Pointer engine, Pointer buffer); external void _TransformManager_getLocalTransform( Pointer double4x4_out, Pointer tTransformManager, @@ -4491,37 +4491,44 @@ void VertexBuffer_destroy(Pointer engine, Pointer buffer return result; } -Pointer BufferObjectBuilder_create() { - final result = GeneratedBindings.instance._BufferObjectBuilder_create(); - return Pointer(result); +Pointer IndexBufferBuilder_create() { + final result = GeneratedBindings.instance._IndexBufferBuilder_create(); + return Pointer(result); } -void BufferObjectBuilder_size(Pointer builder, int sizeInBytes) { - final result = GeneratedBindings.instance._BufferObjectBuilder_size(builder.cast(), sizeInBytes); +void IndexBufferBuilder_indexCount(Pointer builder, int count) { + final result = GeneratedBindings.instance._IndexBufferBuilder_indexCount(builder.cast(), count); return result; } -Pointer BufferObjectBuilder_build( - Pointer builder, - Pointer engine, -) { - final result = GeneratedBindings.instance._BufferObjectBuilder_build(builder.cast(), engine.cast()); - return Pointer(result); +void IndexBufferBuilder_bufferType(Pointer builder, int indexType) { + final result = GeneratedBindings.instance._IndexBufferBuilder_bufferType(builder.cast(), indexType); + return result; } -void BufferObjectBuilder_destroy(Pointer builder) { - final result = GeneratedBindings.instance._BufferObjectBuilder_destroy(builder.cast()); +Pointer IndexBufferBuilder_build(Pointer builder, Pointer engine) { + final result = GeneratedBindings.instance._IndexBufferBuilder_build(builder.cast(), engine.cast()); + return Pointer(result); +} + +void IndexBufferBuilder_destroy(Pointer builder) { + final result = GeneratedBindings.instance._IndexBufferBuilder_destroy(builder.cast()); return result; } -void BufferObject_setBuffer( +Dartsize_t IndexBuffer_getIndexCount(Pointer buffer) { + final result = GeneratedBindings.instance._IndexBuffer_getIndexCount(buffer.cast()); + return result; +} + +void IndexBuffer_setBuffer( Pointer engine, - Pointer buffer, + Pointer buffer, Pointer data, Dartsize_t sizeInBytes, int byteOffset, ) { - final result = GeneratedBindings.instance._BufferObject_setBuffer( + final result = GeneratedBindings.instance._IndexBuffer_setBuffer( engine.cast(), buffer.cast(), data, @@ -4531,49 +4538,39 @@ void BufferObject_setBuffer( return result; } -void BufferObject_destroy(Pointer engine, Pointer buffer) { - final result = GeneratedBindings.instance._BufferObject_destroy(engine.cast(), buffer.cast()); +void IndexBuffer_destroy(Pointer engine, Pointer buffer) { + final result = GeneratedBindings.instance._IndexBuffer_destroy(engine.cast(), buffer.cast()); return result; } -Pointer IndexBufferBuilder_create() { - final result = GeneratedBindings.instance._IndexBufferBuilder_create(); - return Pointer(result); -} - -void IndexBufferBuilder_indexCount(Pointer builder, int count) { - final result = GeneratedBindings.instance._IndexBufferBuilder_indexCount(builder.cast(), count); - return result; +Pointer BufferObjectBuilder_create() { + final result = GeneratedBindings.instance._BufferObjectBuilder_create(); + return Pointer(result); } -void IndexBufferBuilder_bufferType(Pointer builder, int indexType) { - final result = GeneratedBindings.instance._IndexBufferBuilder_bufferType(builder.cast(), indexType); +void BufferObjectBuilder_size(Pointer builder, int sizeInBytes) { + final result = GeneratedBindings.instance._BufferObjectBuilder_size(builder.cast(), sizeInBytes); return result; } -Pointer IndexBufferBuilder_build(Pointer builder, Pointer engine) { - final result = GeneratedBindings.instance._IndexBufferBuilder_build(builder.cast(), engine.cast()); - return Pointer(result); -} - -void IndexBufferBuilder_destroy(Pointer builder) { - final result = GeneratedBindings.instance._IndexBufferBuilder_destroy(builder.cast()); - return result; +Pointer BufferObjectBuilder_build(Pointer builder, Pointer engine) { + final result = GeneratedBindings.instance._BufferObjectBuilder_build(builder.cast(), engine.cast()); + return Pointer(result); } -Dartsize_t IndexBuffer_getIndexCount(Pointer buffer) { - final result = GeneratedBindings.instance._IndexBuffer_getIndexCount(buffer.cast()); +void BufferObjectBuilder_destroy(Pointer builder) { + final result = GeneratedBindings.instance._BufferObjectBuilder_destroy(builder.cast()); return result; } -void IndexBuffer_setBuffer( +void BufferObject_setBuffer( Pointer engine, - Pointer buffer, + Pointer buffer, Pointer data, Dartsize_t sizeInBytes, int byteOffset, ) { - final result = GeneratedBindings.instance._IndexBuffer_setBuffer( + final result = GeneratedBindings.instance._BufferObject_setBuffer( engine.cast(), buffer.cast(), data, @@ -4583,8 +4580,8 @@ void IndexBuffer_setBuffer( return result; } -void IndexBuffer_destroy(Pointer engine, Pointer buffer) { - final result = GeneratedBindings.instance._IndexBuffer_destroy(engine.cast(), buffer.cast()); +void BufferObject_destroy(Pointer engine, Pointer buffer) { + final result = GeneratedBindings.instance._BufferObject_destroy(engine.cast(), buffer.cast()); return result; } @@ -10316,21 +10313,6 @@ final class TVertexBuffer extends Struct { } } -extension TBufferObjectExt on Pointer { - TBufferObject toDart() { - return TBufferObject(this); - } -} - -final class TBufferObject extends Struct { - Pointer get address => super.address.cast(); - TBufferObject(super.address); - - static Pointer stackAlloc() { - return Pointer(NativeLibrary.instance.stackAlloc(0)); - } -} - extension TIndexBufferExt on Pointer { TIndexBuffer toDart() { return TIndexBuffer(this); @@ -11080,34 +11062,6 @@ final class TVertexBufferBuilder extends Struct { } } -extension TBufferObjectBuilderExt on Pointer { - TBufferObjectBuilder toDart() { - return TBufferObjectBuilder(this); - } -} - -final class TBufferObjectBuilder extends Struct { - Pointer get address => super.address.cast(); - TBufferObjectBuilder(super.address); - - static Pointer stackAlloc() { - return Pointer(NativeLibrary.instance.stackAlloc(0)); - } -} - -sealed class TVertexBufferStorageMode { - static const VERTEX_BUFFER_STORAGE_MODE_UNKNOWN = 0; - static const VERTEX_BUFFER_STORAGE_MODE_DIRECT = 1; - static const VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS = 2; -} - -sealed class TSceneAssetGeometryCapability { - static const SCENE_ASSET_GEOMETRY_CAPABILITY_NONE = 0; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING = 1; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 2; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY = 4; -} - sealed class TVertexAttribute { static const TVERTEX_ATTRIBUTE_POSITION = 0; static const TVERTEX_ATTRIBUTE_TANGENTS = 1; @@ -11155,6 +11109,27 @@ sealed class TVertexAttributeType { static const TVERTEXATTRIBUTE_TYPE_HALF4 = 25; } +sealed class TVertexBufferStorageMode { + static const VERTEX_BUFFER_STORAGE_MODE_UNKNOWN = 0; + static const VERTEX_BUFFER_STORAGE_MODE_DIRECT = 1; + static const VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS = 2; +} + +extension TBufferObjectExt on Pointer { + TBufferObject toDart() { + return TBufferObject(this); + } +} + +final class TBufferObject extends Struct { + Pointer get address => super.address.cast(); + TBufferObject(super.address); + + static Pointer stackAlloc() { + return Pointer(NativeLibrary.instance.stackAlloc(0)); + } +} + extension TIndexBufferBuilderExt on Pointer { TIndexBufferBuilder toDart() { return TIndexBufferBuilder(this); @@ -11175,6 +11150,21 @@ sealed class TIndexType { static const TINDEX_TYPE_UINT = 1; } +extension TBufferObjectBuilderExt on Pointer { + TBufferObjectBuilder toDart() { + return TBufferObjectBuilder(this); + } +} + +final class TBufferObjectBuilder extends Struct { + Pointer get address => super.address.cast(); + TBufferObjectBuilder(super.address); + + static Pointer stackAlloc() { + return Pointer(NativeLibrary.instance.stackAlloc(0)); + } +} + sealed class TLightType { static const LIGHT_TYPE_SUN = 0; static const LIGHT_TYPE_DIRECTIONAL = 1; @@ -11658,9 +11648,6 @@ extension StructAllocator on Struct { case TVertexBuffer: final ptr = TVertexBuffer.stackAlloc(); return ptr.toDart() as T; - case TBufferObject: - final ptr = TBufferObject.stackAlloc(); - return ptr.toDart() as T; case TIndexBuffer: final ptr = TIndexBuffer.stackAlloc(); return ptr.toDart() as T; @@ -11721,12 +11708,15 @@ extension StructAllocator on Struct { case TVertexBufferBuilder: final ptr = TVertexBufferBuilder.stackAlloc(); return ptr.toDart() as T; - case TBufferObjectBuilder: - final ptr = TBufferObjectBuilder.stackAlloc(); + case TBufferObject: + final ptr = TBufferObject.stackAlloc(); return ptr.toDart() as T; case TIndexBufferBuilder: final ptr = TIndexBufferBuilder.stackAlloc(); return ptr.toDart() as T; + case TBufferObjectBuilder: + final ptr = TBufferObjectBuilder.stackAlloc(); + return ptr.toDart() as T; case TShadowOptions: final ptr = TShadowOptions.stackAlloc(); return ptr.toDart() as T; From e2d950df3cfd4026076b7ce1702270e93e711e13 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Wed, 26 Aug 2026 11:44:12 +0800 Subject: [PATCH 05/14] docs: clarify vertex buffer capability guards --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2963f80c..56c771e82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,7 @@ updated through `VertexBuffer.setBufferAt`; editable buffers no longer use the `BufferObject` backing reserved for unwelded smooth/flat shading swaps. Buffer updates, flat shading, and stencil highlighting now throw actionable - errors when used with an incompatible vertex-buffer mode. + errors when used with incompatible buffer storage or asset capabilities. - Expose native `VertexBuffer.storageMode` metadata and first-class `BufferObject` creation, upload, and attachment APIs. `supportsSetBufferAt` is now derived from native buffer storage instead of duplicated glTF load From 5c3b6c27a0e6f770f8dd398ee934df525a9dea09 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Wed, 26 Aug 2026 12:12:29 +0800 Subject: [PATCH 06/14] refactor: make vertex buffer metadata explicit --- CHANGELOG.md | 5 +- .../src/bindings/src/thermion_dart_ffi.g.dart | 16 ++++-- .../src/thermion_dart_js_interop.g.dart | 27 +++++++--- .../src/implementation/ffi_asset.dart | 3 +- .../src/implementation/ffi_filament_app.dart | 1 + .../src/implementation/ffi_vertex_buffer.dart | 54 ++++++++++++++----- .../filament/src/interface/vertex_buffer.dart | 3 -- .../native/include/c_api/TSceneAsset.h | 2 + .../native/include/c_api/TVertexBuffer.h | 3 +- .../c_api/ThermionDartRenderThreadApi.h | 1 + .../include/scene/GeometrySceneAsset.hpp | 9 +++- .../native/include/scene/GltfSceneAsset.hpp | 15 ++++++ .../include/scene/GltfSceneAssetInstance.hpp | 1 + .../native/include/scene/SceneAsset.hpp | 4 ++ .../include/scene/VertexBufferMetadata.hpp | 17 ------ .../native/src/c_api/TSceneAsset.cpp | 12 +++++ .../native/src/c_api/TVertexBuffer.cpp | 46 ++-------------- .../src/c_api/ThermionDartRenderThreadApi.cpp | 11 +++- .../native/src/scene/GeometrySceneAsset.cpp | 7 +-- .../native/src/scene/GltfSceneAsset.cpp | 8 --- .../src/scene/GltfSceneAssetInstance.cpp | 5 ++ thermion_dart/test/geometry_tests.dart | 1 - thermion_dart/test/morph_animation_tests.dart | 2 - 23 files changed, 149 insertions(+), 104 deletions(-) delete mode 100644 thermion_dart/native/include/scene/VertexBufferMetadata.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 56c771e82..22bcd86f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,8 +47,9 @@ errors when used with incompatible buffer storage or asset capabilities. - Expose native `VertexBuffer.storageMode` metadata and first-class `BufferObject` creation, upload, and attachment APIs. `supportsSetBufferAt` - is now derived from native buffer storage instead of duplicated glTF load - state in Dart, and asset-owned vertex buffers are explicitly borrowed. + is now derived from immutable metadata supplied by the native builder or + owning asset, without a global pointer registry or duplicated glTF load + state in Dart. Asset-owned vertex buffers are explicitly borrowed. ### Breaking changes - Replace the `rebuildVertices` in `ThermionViewer.loadGltf`, diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart index fc909d5e6..f59456682 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart @@ -614,6 +614,7 @@ external ffi.Pointer NameComponentManager_getName( ffi.Pointer>, ffi.Int, ffi.UnsignedInt, + ffi.UnsignedInt, Aabb3, ) >(isLeaf: true) @@ -624,6 +625,7 @@ external ffi.Pointer SceneAsset_createFromBuffers( ffi.Pointer> materialInstances, int materialInstanceCount, int tPrimitiveType, + int vertexBufferStorageMode, Aabb3 boundingBox, ); @@ -707,6 +709,12 @@ external ffi.Pointer SceneAsset_getVertexBuffer( int primitiveIndex, ); +@ffi.Native, ffi.Int)>(isLeaf: true) +external int SceneAsset_getVertexBufferStorageMode( + ffi.Pointer tSceneAsset, + int primitiveIndex, +); + @ffi.Native Function(ffi.Pointer, ffi.Int)>(isLeaf: true) external ffi.Pointer SceneAsset_getIndexBuffer(ffi.Pointer tSceneAsset, int primitiveIndex); @@ -1402,6 +1410,9 @@ external void VertexBufferBuilder_vertexCount(ffi.Pointer @ffi.Native, ffi.Bool)>(isLeaf: true) external void VertexBufferBuilder_enableBufferObjects(ffi.Pointer builder, bool enabled); +@ffi.Native)>(isLeaf: true) +external int VertexBufferBuilder_getStorageMode(ffi.Pointer builder); + @ffi.Native< ffi.Void Function( ffi.Pointer, @@ -1436,9 +1447,6 @@ external void VertexBufferBuilder_destroy(ffi.Pointer buil @ffi.Native)>(isLeaf: true) external int VertexBuffer_getVertexCount(ffi.Pointer buffer); -@ffi.Native)>(isLeaf: true) -external int VertexBuffer_getStorageMode(ffi.Pointer buffer); - @ffi.Native< ffi.Void Function( ffi.Pointer, @@ -2764,6 +2772,7 @@ external void SceneAsset_createFromFilamentAssetRenderThread( ffi.Pointer>, ffi.Int, ffi.UnsignedInt, + ffi.UnsignedInt, Aabb3, ffi.Pointer)>>, ) @@ -2775,6 +2784,7 @@ external void SceneAsset_createFromBuffersRenderThread( ffi.Pointer> materialInstances, int materialInstanceCount, int tPrimitiveType, + int vertexBufferStorageMode, Aabb3 boundingBox, ffi.Pointer)>> callback, ); diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart index 9ecb2c265..bd1dd3186 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart @@ -294,6 +294,7 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { Pointer> materialInstances, int materialInstanceCount, int tPrimitiveType, + int vertexBufferStorageMode, Pointer boundingBoxPtr, ); external Pointer _SceneAsset_createFromFilamentAsset( @@ -325,6 +326,7 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { external void _SceneAsset_getBoundingBox(Pointer Aabb3_out, Pointer asset); external int _SceneAsset_getGeometryCapabilities(Pointer asset); external Pointer _SceneAsset_getVertexBuffer(Pointer tSceneAsset, int primitiveIndex); + external int _SceneAsset_getVertexBufferStorageMode(Pointer tSceneAsset, int primitiveIndex); external Pointer _SceneAsset_getIndexBuffer(Pointer tSceneAsset, int primitiveIndex); external int _SceneAsset_getPrimitiveOffsetForEntity(Pointer tSceneAsset, EntityId entity); external void _SceneAsset_releaseSourceData(Pointer tSceneAsset); @@ -625,6 +627,7 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { external void _VertexBufferBuilder_bufferCount(Pointer builder, int count); external void _VertexBufferBuilder_vertexCount(Pointer builder, int count); external void _VertexBufferBuilder_enableBufferObjects(Pointer builder, bool enabled); + external int _VertexBufferBuilder_getStorageMode(Pointer builder); external void _VertexBufferBuilder_attribute( Pointer builder, int attribute, @@ -640,7 +643,6 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { ); external void _VertexBufferBuilder_destroy(Pointer builder); external size_t _VertexBuffer_getVertexCount(Pointer buffer); - external int _VertexBuffer_getStorageMode(Pointer buffer); external void _VertexBuffer_setBufferAt( Pointer engine, Pointer buffer, @@ -1396,6 +1398,7 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { Pointer> materialInstances, int materialInstanceCount, int tPrimitiveType, + int vertexBufferStorageMode, Pointer boundingBoxPtr, Pointer)>> callback, ); @@ -3374,6 +3377,7 @@ Pointer SceneAsset_createFromBuffers( Pointer> materialInstances, int materialInstanceCount, int tPrimitiveType, + int vertexBufferStorageMode, Aabb3 boundingBox, ) { final boundingBoxPtr = boundingBox.address; @@ -3384,6 +3388,7 @@ Pointer SceneAsset_createFromBuffers( materialInstances.cast(), materialInstanceCount, tPrimitiveType, + vertexBufferStorageMode, boundingBoxPtr.cast(), ); return Pointer(result); @@ -3505,6 +3510,14 @@ Pointer SceneAsset_getVertexBuffer(Pointer tSceneAss return Pointer(result); } +int SceneAsset_getVertexBufferStorageMode(Pointer tSceneAsset, int primitiveIndex) { + final result = GeneratedBindings.instance._SceneAsset_getVertexBufferStorageMode( + tSceneAsset.cast(), + primitiveIndex, + ); + return result; +} + Pointer SceneAsset_getIndexBuffer(Pointer tSceneAsset, int primitiveIndex) { final result = GeneratedBindings.instance._SceneAsset_getIndexBuffer(tSceneAsset.cast(), primitiveIndex); return Pointer(result); @@ -4408,6 +4421,11 @@ void VertexBufferBuilder_enableBufferObjects(Pointer build return result; } +int VertexBufferBuilder_getStorageMode(Pointer builder) { + final result = GeneratedBindings.instance._VertexBufferBuilder_getStorageMode(builder.cast()); + return result; +} + void VertexBufferBuilder_attribute( Pointer builder, int attribute, @@ -4447,11 +4465,6 @@ Dartsize_t VertexBuffer_getVertexCount(Pointer buffer) { return result; } -int VertexBuffer_getStorageMode(Pointer buffer) { - final result = GeneratedBindings.instance._VertexBuffer_getStorageMode(buffer.cast()); - return result; -} - void VertexBuffer_setBufferAt( Pointer engine, Pointer buffer, @@ -6321,6 +6334,7 @@ void SceneAsset_createFromBuffersRenderThread( Pointer> materialInstances, int materialInstanceCount, int tPrimitiveType, + int vertexBufferStorageMode, Aabb3 boundingBox, Pointer)>> callback, ) { @@ -6332,6 +6346,7 @@ void SceneAsset_createFromBuffersRenderThread( materialInstances.cast(), materialInstanceCount, tPrimitiveType, + vertexBufferStorageMode, boundingBoxPtr.cast(), callback.cast(), ); diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart index 704aeb107..f900236cd 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart @@ -920,7 +920,8 @@ class FFIAsset extends ThermionAsset> { if (vbPtr == nullptr) { return null; } - return FFIVertexBuffer(vbPtr, _app.engine, ownsResource: false); + final storageMode = vertexBufferStorageModeFromNative(SceneAsset_getVertexBufferStorageMode(asset, primitiveIndex)); + return FFIVertexBuffer.assetOwned(vbPtr, _app.engine, storageMode: storageMode); } } diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart index aaaf181dd..a91b51277 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart @@ -1461,6 +1461,7 @@ class FFIFilamentApp extends FilamentApp { ptrList.address.cast(), ptrList.length, geometry.primitiveType.index, + vertexBufferStorageModeToNative(vertexBuffer.storageMode), cAabb, callback, ); diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart index 0333f24b1..5ac518c86 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart @@ -6,11 +6,29 @@ import 'ffi_buffer_object.dart'; class FFIVertexBuffer extends VertexBuffer { final bindings.Pointer _ptr; final bindings.Pointer _engine; + final bool _ownedByCaller; @override - final bool ownsResource; + final VertexBufferStorageMode storageMode; - FFIVertexBuffer(this._ptr, this._engine, {this.ownsResource = true}); + FFIVertexBuffer._(this._ptr, this._engine, {required this.storageMode, required bool ownedByCaller}) + : _ownedByCaller = ownedByCaller; + + factory FFIVertexBuffer.callerOwned( + bindings.Pointer ptr, + bindings.Pointer engine, { + required VertexBufferStorageMode storageMode, + }) { + return FFIVertexBuffer._(ptr, engine, storageMode: storageMode, ownedByCaller: true); + } + + factory FFIVertexBuffer.assetOwned( + bindings.Pointer ptr, + bindings.Pointer engine, { + required VertexBufferStorageMode storageMode, + }) { + return FFIVertexBuffer._(ptr, engine, storageMode: storageMode, ownedByCaller: false); + } /// Returns the native handle for FFI calls. bindings.Pointer getNativeHandle() => _ptr; @@ -20,14 +38,6 @@ class FFIVertexBuffer extends VertexBuffer { return bindings.VertexBuffer_getVertexCount(_ptr); } - @override - VertexBufferStorageMode get storageMode => switch (bindings.VertexBuffer_getStorageMode(_ptr)) { - bindings.TVertexBufferStorageMode.VERTEX_BUFFER_STORAGE_MODE_DIRECT => VertexBufferStorageMode.direct, - bindings.TVertexBufferStorageMode.VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS => - VertexBufferStorageMode.bufferObjects, - _ => VertexBufferStorageMode.unknown, - }; - @override Future setBufferAt(int bufferIndex, TypedData data, {int byteOffset = 0}) async { if (storageMode != VertexBufferStorageMode.direct) { @@ -80,7 +90,7 @@ class FFIVertexBuffer extends VertexBuffer { @override Future destroy() async { - if (!ownsResource) { + if (!_ownedByCaller) { throw StateError('Cannot destroy a VertexBuffer borrowed from a ThermionAsset'); } await withVoidCallback((requestId, cb) { @@ -160,6 +170,8 @@ class FFIVertexBufferBuilder implements VertexBufferBuilder { Future build() async { _checkNotBuilt(); + final storageMode = vertexBufferStorageModeFromNative(bindings.VertexBufferBuilder_getStorageMode(_builderPtr!)); + final vertexBufferPtr = await withPointerCallback( (cb) => bindings.VertexBufferBuilder_buildRenderThread(_builderPtr!, _engine, cb), ); @@ -172,7 +184,7 @@ class FFIVertexBufferBuilder implements VertexBufferBuilder { throw Exception('Failed to build VertexBuffer'); } - return FFIVertexBuffer(vertexBufferPtr, _engine); + return FFIVertexBuffer.callerOwned(vertexBufferPtr, _engine, storageMode: storageMode); } int _vertexAttributeToInt(VertexAttribute attribute) { @@ -226,3 +238,21 @@ class FFIVertexBufferBuilder implements VertexBufferBuilder { }; } } + +VertexBufferStorageMode vertexBufferStorageModeFromNative(int storageMode) { + return switch (storageMode) { + bindings.TVertexBufferStorageMode.VERTEX_BUFFER_STORAGE_MODE_DIRECT => VertexBufferStorageMode.direct, + bindings.TVertexBufferStorageMode.VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS => + VertexBufferStorageMode.bufferObjects, + _ => VertexBufferStorageMode.unknown, + }; +} + +int vertexBufferStorageModeToNative(VertexBufferStorageMode storageMode) { + return switch (storageMode) { + VertexBufferStorageMode.direct => bindings.TVertexBufferStorageMode.VERTEX_BUFFER_STORAGE_MODE_DIRECT, + VertexBufferStorageMode.bufferObjects => + bindings.TVertexBufferStorageMode.VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS, + VertexBufferStorageMode.unknown => bindings.TVertexBufferStorageMode.VERTEX_BUFFER_STORAGE_MODE_UNKNOWN, + }; +} diff --git a/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart b/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart index 672007388..341ef1cc5 100644 --- a/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart +++ b/thermion_dart/lib/src/filament/src/interface/vertex_buffer.dart @@ -156,9 +156,6 @@ abstract class VertexBuffer { bool get supportsSetBufferAt => storageMode == VertexBufferStorageMode.direct; - /// Whether this wrapper owns the native resource and may destroy it. - bool get ownsResource; - /// Asynchronously copy-initializes the specified buffer from the given data. /// /// [bufferIndex] Index of the buffer to initialize (0 to bufferCount-1) diff --git a/thermion_dart/native/include/c_api/TSceneAsset.h b/thermion_dart/native/include/c_api/TSceneAsset.h index 087c750b1..025480427 100644 --- a/thermion_dart/native/include/c_api/TSceneAsset.h +++ b/thermion_dart/native/include/c_api/TSceneAsset.h @@ -16,6 +16,7 @@ extern "C" TMaterialInstance **materialInstances, int materialInstanceCount, enum TPrimitiveType tPrimitiveType, + enum TVertexBufferStorageMode vertexBufferStorageMode, Aabb3 boundingBox ); EMSCRIPTEN_KEEPALIVE TSceneAsset * SceneAsset_createFromFilamentAsset( @@ -43,6 +44,7 @@ extern "C" EMSCRIPTEN_KEEPALIVE Aabb3 SceneAsset_getBoundingBox(TSceneAsset *asset); EMSCRIPTEN_KEEPALIVE uint32_t SceneAsset_getGeometryCapabilities(TSceneAsset *asset); EMSCRIPTEN_KEEPALIVE TVertexBuffer *SceneAsset_getVertexBuffer(TSceneAsset *tSceneAsset, int primitiveIndex); + EMSCRIPTEN_KEEPALIVE TVertexBufferStorageMode SceneAsset_getVertexBufferStorageMode(TSceneAsset *tSceneAsset, int primitiveIndex); EMSCRIPTEN_KEEPALIVE TIndexBuffer *SceneAsset_getIndexBuffer(TSceneAsset *tSceneAsset, int primitiveIndex); EMSCRIPTEN_KEEPALIVE int SceneAsset_getPrimitiveOffsetForEntity(TSceneAsset *tSceneAsset, EntityId entity); EMSCRIPTEN_KEEPALIVE void SceneAsset_releaseSourceData(TSceneAsset *tSceneAsset); diff --git a/thermion_dart/native/include/c_api/TVertexBuffer.h b/thermion_dart/native/include/c_api/TVertexBuffer.h index d656453d4..3d1f7a2a3 100644 --- a/thermion_dart/native/include/c_api/TVertexBuffer.h +++ b/thermion_dart/native/include/c_api/TVertexBuffer.h @@ -19,6 +19,7 @@ extern "C" EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_bufferCount(TVertexBufferBuilder* builder, uint8_t count); EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_vertexCount(TVertexBufferBuilder* builder, uint32_t count); EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_enableBufferObjects(TVertexBufferBuilder* builder, bool enabled); + EMSCRIPTEN_KEEPALIVE TVertexBufferStorageMode VertexBufferBuilder_getStorageMode(TVertexBufferBuilder* builder); EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_attribute( TVertexBufferBuilder* builder, TVertexAttribute attribute, @@ -39,8 +40,6 @@ extern "C" // Get vertex count EMSCRIPTEN_KEEPALIVE size_t VertexBuffer_getVertexCount(TVertexBuffer* buffer); - EMSCRIPTEN_KEEPALIVE TVertexBufferStorageMode VertexBuffer_getStorageMode(TVertexBuffer* buffer); - // Set buffer data EMSCRIPTEN_KEEPALIVE void VertexBuffer_setBufferAt( TEngine* engine, diff --git a/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h b/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h index 24a4c9477..7f9d6050c 100644 --- a/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h +++ b/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h @@ -172,6 +172,7 @@ namespace thermion TMaterialInstance **materialInstances, int materialInstanceCount, TPrimitiveType tPrimitiveType, + TVertexBufferStorageMode vertexBufferStorageMode, Aabb3 boundingBox, void (*callback)(TSceneAsset *) ); diff --git a/thermion_dart/native/include/scene/GeometrySceneAsset.hpp b/thermion_dart/native/include/scene/GeometrySceneAsset.hpp index 43ba6c8f2..634ae9083 100644 --- a/thermion_dart/native/include/scene/GeometrySceneAsset.hpp +++ b/thermion_dart/native/include/scene/GeometrySceneAsset.hpp @@ -23,6 +23,7 @@ namespace thermion size_t materialInstanceCount, RenderableManager::PrimitiveType primitiveType, Box boundingBox, + TVertexBufferStorageMode vertexBufferStorageMode, GeometrySceneAsset *instanceParent = std::nullptr_t()); ~GeometrySceneAsset(); @@ -61,6 +62,11 @@ namespace thermion VertexBuffer *getVertexBuffer() const { return _vertexBuffer; } IndexBuffer *getIndexBuffer() const { return _indexBuffer; } + TVertexBufferStorageMode getVertexBufferStorageMode(size_t primitiveIndex) const override { + return primitiveIndex == 0 + ? _vertexBufferStorageMode + : VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; + } void addAllEntities(Scene *scene) override { @@ -132,7 +138,8 @@ namespace thermion GeometrySceneAsset *_instanceOwner = std::nullptr_t(); utils::Entity _entity; RenderableManager::PrimitiveType _primitiveType; + TVertexBufferStorageMode _vertexBufferStorageMode = VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; std::vector> _instances; }; -} // namespace thermion \ No newline at end of file +} // namespace thermion diff --git a/thermion_dart/native/include/scene/GltfSceneAsset.hpp b/thermion_dart/native/include/scene/GltfSceneAsset.hpp index b8b000c71..fc7d0d047 100644 --- a/thermion_dart/native/include/scene/GltfSceneAsset.hpp +++ b/thermion_dart/native/include/scene/GltfSceneAsset.hpp @@ -154,6 +154,21 @@ namespace thermion } } + TVertexBufferStorageMode getVertexBufferStorageMode(size_t primitiveIndex) const override { + if (primitiveIndex >= _preservedVertexBuffers.size()) { + return VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; + } + switch (_vertexBufferMode) { + case VERTEX_BUFFER_MODE_EDITABLE: + return VERTEX_BUFFER_STORAGE_MODE_DIRECT; + case VERTEX_BUFFER_MODE_UNWELDED: + return VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS; + case VERTEX_BUFFER_MODE_ORIGINAL: + default: + return VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; + } + } + /// Rebuild all mesh primitives with a superset vertex buffer layout /// (POSITION + TANGENTS + UV0 + CUSTOM0 + optional BONE_INDICES/WEIGHTS). /// [VERTEX_BUFFER_MODE_UNWELDED] gives each triangle unique vertices diff --git a/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp b/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp index b863b079e..2f66ca33c 100644 --- a/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp +++ b/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp @@ -141,6 +141,7 @@ namespace thermion } uint32_t getGeometryCapabilities() const override; + TVertexBufferStorageMode getVertexBufferStorageMode(size_t primitiveIndex) const override; size_t getBoneCount(size_t skinIndex) const override; const utils::Entity *getBones(size_t skinIndex) const override; diff --git a/thermion_dart/native/include/scene/SceneAsset.hpp b/thermion_dart/native/include/scene/SceneAsset.hpp index fac6586a6..85a2c10a8 100644 --- a/thermion_dart/native/include/scene/SceneAsset.hpp +++ b/thermion_dart/native/include/scene/SceneAsset.hpp @@ -61,6 +61,10 @@ class SceneAsset { return SCENE_ASSET_GEOMETRY_CAPABILITY_NONE; } + virtual TVertexBufferStorageMode getVertexBufferStorageMode(size_t primitiveIndex) const { + return VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; + } + virtual size_t getBoneCount(size_t skinIndex) const { return 0; } diff --git a/thermion_dart/native/include/scene/VertexBufferMetadata.hpp b/thermion_dart/native/include/scene/VertexBufferMetadata.hpp deleted file mode 100644 index e23a9b16f..000000000 --- a/thermion_dart/native/include/scene/VertexBufferMetadata.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - -#include "c_api/APIBoundaryTypes.h" - -namespace thermion -{ - void registerVertexBufferStorageMode( - filament::VertexBuffer *buffer, - TVertexBufferStorageMode storageMode); - - void unregisterVertexBufferStorageMode(filament::VertexBuffer *buffer); - - TVertexBufferStorageMode getVertexBufferStorageMode( - const filament::VertexBuffer *buffer); -} diff --git a/thermion_dart/native/src/c_api/TSceneAsset.cpp b/thermion_dart/native/src/c_api/TSceneAsset.cpp index ddd57090f..4e3cdf31f 100644 --- a/thermion_dart/native/src/c_api/TSceneAsset.cpp +++ b/thermion_dart/native/src/c_api/TSceneAsset.cpp @@ -27,6 +27,7 @@ extern "C" TMaterialInstance **materialInstances, int materialInstanceCount, TPrimitiveType tPrimitiveType, + TVertexBufferStorageMode vertexBufferStorageMode, Aabb3 boundingBox ) { auto *engine = reinterpret_cast(tEngine); @@ -59,6 +60,7 @@ extern "C" materialInstanceCount, primitiveType, box, + vertexBufferStorageMode, nullptr // instanceOwner - this is not an instance ); @@ -247,6 +249,16 @@ extern "C" return nullptr; } + EMSCRIPTEN_KEEPALIVE TVertexBufferStorageMode SceneAsset_getVertexBufferStorageMode( + TSceneAsset *tSceneAsset, + int primitiveIndex) { + if (primitiveIndex < 0) { + return VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; + } + return reinterpret_cast(tSceneAsset)->getVertexBufferStorageMode( + static_cast(primitiveIndex)); + } + EMSCRIPTEN_KEEPALIVE TIndexBuffer *SceneAsset_getIndexBuffer(TSceneAsset *tSceneAsset, int primitiveIndex) { auto *asset = reinterpret_cast(tSceneAsset); if (asset->getType() == SceneAsset::SceneAssetType::Geometry) { diff --git a/thermion_dart/native/src/c_api/TVertexBuffer.cpp b/thermion_dart/native/src/c_api/TVertexBuffer.cpp index 75644f1fc..f5cbf366d 100644 --- a/thermion_dart/native/src/c_api/TVertexBuffer.cpp +++ b/thermion_dart/native/src/c_api/TVertexBuffer.cpp @@ -2,12 +2,8 @@ #include #include -#include -#include - #include "Log.hpp" #include "c_api/TVertexBuffer.h" -#include "scene/VertexBufferMetadata.hpp" namespace { @@ -16,41 +12,10 @@ namespace filament::VertexBuffer::Builder builder; TVertexBufferStorageMode storageMode = VERTEX_BUFFER_STORAGE_MODE_DIRECT; }; - - std::mutex gVertexBufferMetadataMutex; - std::unordered_map gVertexBufferStorageModes; } namespace thermion { - void registerVertexBufferStorageMode( - filament::VertexBuffer *buffer, - TVertexBufferStorageMode storageMode) - { - if (!buffer) - return; - std::lock_guard lock(gVertexBufferMetadataMutex); - gVertexBufferStorageModes[buffer] = storageMode; - } - - void unregisterVertexBufferStorageMode(filament::VertexBuffer *buffer) - { - if (!buffer) - return; - std::lock_guard lock(gVertexBufferMetadataMutex); - gVertexBufferStorageModes.erase(buffer); - } - - TVertexBufferStorageMode getVertexBufferStorageMode( - const filament::VertexBuffer *buffer) - { - std::lock_guard lock(gVertexBufferMetadataMutex); - const auto entry = gVertexBufferStorageModes.find(buffer); - return entry == gVertexBufferStorageModes.end() - ? VERTEX_BUFFER_STORAGE_MODE_UNKNOWN - : entry->second; - } - extern "C" { using namespace filament; @@ -82,6 +47,10 @@ namespace thermion : VERTEX_BUFFER_STORAGE_MODE_DIRECT; } + EMSCRIPTEN_KEEPALIVE TVertexBufferStorageMode VertexBufferBuilder_getStorageMode(TVertexBufferBuilder* tBuilder) { + return reinterpret_cast(tBuilder)->storageMode; + } + EMSCRIPTEN_KEEPALIVE void VertexBufferBuilder_attribute( TVertexBufferBuilder* tBuilder, TVertexAttribute attribute, @@ -186,7 +155,6 @@ namespace thermion auto* builder = reinterpret_cast(tBuilder); auto* engine = reinterpret_cast(tEngine); auto* vertexBuffer = builder->builder.build(*engine); - registerVertexBufferStorageMode(vertexBuffer, builder->storageMode); return reinterpret_cast(vertexBuffer); } @@ -204,11 +172,6 @@ namespace thermion return vertexBuffer->getVertexCount(); } - EMSCRIPTEN_KEEPALIVE TVertexBufferStorageMode VertexBuffer_getStorageMode(TVertexBuffer* tBuffer) { - auto* vertexBuffer = reinterpret_cast(tBuffer); - return getVertexBufferStorageMode(vertexBuffer); - } - EMSCRIPTEN_KEEPALIVE void VertexBuffer_setBufferAt( TEngine* tEngine, TVertexBuffer* tBuffer, @@ -255,7 +218,6 @@ namespace thermion EMSCRIPTEN_KEEPALIVE void VertexBuffer_destroy(TEngine* tEngine, TVertexBuffer* tBuffer) { auto* engine = reinterpret_cast(tEngine); auto* vertexBuffer = reinterpret_cast(tBuffer); - unregisterVertexBufferStorageMode(vertexBuffer); engine->destroy(vertexBuffer); } diff --git a/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp b/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp index dbdac20b0..57f3d4070 100644 --- a/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp +++ b/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp @@ -1010,6 +1010,7 @@ extern "C" TMaterialInstance **materialInstances, int materialInstanceCount, TPrimitiveType tPrimitiveType, + TVertexBufferStorageMode vertexBufferStorageMode, Aabb3 boundingBox, void (*callback)(TSceneAsset *)) { @@ -1017,7 +1018,15 @@ extern "C" std::packaged_task lambda( [=] { - auto sceneAsset = SceneAsset_createFromBuffers(tEngine, tVertexBuffer, tIndexBuffer, materialInstances, materialInstanceCount, tPrimitiveType, boundingBox); + auto sceneAsset = SceneAsset_createFromBuffers( + tEngine, + tVertexBuffer, + tIndexBuffer, + materialInstances, + materialInstanceCount, + tPrimitiveType, + vertexBufferStorageMode, + boundingBox); setOwner(sceneAsset, rt); PROXY(callback(sceneAsset)); }); diff --git a/thermion_dart/native/src/scene/GeometrySceneAsset.cpp b/thermion_dart/native/src/scene/GeometrySceneAsset.cpp index e66b94fab..0b6b01a0c 100644 --- a/thermion_dart/native/src/scene/GeometrySceneAsset.cpp +++ b/thermion_dart/native/src/scene/GeometrySceneAsset.cpp @@ -11,7 +11,6 @@ #include "Log.hpp" #include "scene/GeometrySceneAsset.hpp" -#include "scene/VertexBufferMetadata.hpp" namespace thermion { @@ -26,12 +25,14 @@ namespace thermion size_t materialInstanceCount, RenderableManager::PrimitiveType primitiveType, Box boundingBox, + TVertexBufferStorageMode vertexBufferStorageMode, GeometrySceneAsset *instanceOwner) : _engine(engine), _vertexBuffer(vertexBuffer), _indexBuffer(indexBuffer), + _instanceOwner(instanceOwner), _primitiveType(primitiveType), - _instanceOwner(instanceOwner) + _vertexBufferStorageMode(vertexBufferStorageMode) { _materialInstances.insert(_materialInstances.begin(), materialInstances, materialInstances + materialInstanceCount); @@ -73,7 +74,6 @@ namespace thermion utils::EntityManager::get().destroy(_entity); if (_vertexBuffer && !isInstance()) { - unregisterVertexBufferStorageMode(_vertexBuffer); _engine->destroy(_vertexBuffer); } if (_indexBuffer && !isInstance()) @@ -103,6 +103,7 @@ namespace thermion materialInstanceCount, _primitiveType, filament::Box().set(_boundingBox.min, _boundingBox.max), + _vertexBufferStorageMode, this); auto *raw = instance.get(); _instances.push_back(std::move(instance)); diff --git a/thermion_dart/native/src/scene/GltfSceneAsset.cpp b/thermion_dart/native/src/scene/GltfSceneAsset.cpp index a255b2652..898824c40 100644 --- a/thermion_dart/native/src/scene/GltfSceneAsset.cpp +++ b/thermion_dart/native/src/scene/GltfSceneAsset.cpp @@ -1,6 +1,5 @@ #include "scene/GltfSceneAsset.hpp" -#include "scene/VertexBufferMetadata.hpp" #include "scene/GltfSceneAssetInstance.hpp" #include "gltfio/FilamentInstance.h" #include "Log.hpp" @@ -64,7 +63,6 @@ namespace thermion _instances.clear(); for (auto *vb : _preservedVertexBuffers) { - unregisterVertexBufferStorageMode(vb); _engine->destroy(vb); } for (auto *ib : _preservedIndexBuffers) @@ -694,12 +692,6 @@ namespace thermion } VertexBuffer *vb = vbBuilder.build(*_engine); - registerVertexBufferStorageMode( - vb, - editableTopology - ? VERTEX_BUFFER_STORAGE_MODE_DIRECT - : VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS); - auto uploadDirect = [&](uint8_t bufferIndex, const void *source, size_t size) { auto *data = new uint8_t[size]; diff --git a/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp b/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp index 27e2152a8..4127573ec 100644 --- a/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp +++ b/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp @@ -19,6 +19,11 @@ namespace thermion return _instanceOwner->getGeometryCapabilities(); } + TVertexBufferStorageMode GltfSceneAssetInstance::getVertexBufferStorageMode(size_t primitiveIndex) const + { + return _instanceOwner->getVertexBufferStorageMode(primitiveIndex); + } + size_t GltfSceneAssetInstance::getBoneCount(size_t skinIndex) const { return _instance->getJointCountAt(skinIndex); diff --git a/thermion_dart/test/geometry_tests.dart b/thermion_dart/test/geometry_tests.dart index e03ac11a3..d4b5d796a 100644 --- a/thermion_dart/test/geometry_tests.dart +++ b/thermion_dart/test/geometry_tests.dart @@ -28,7 +28,6 @@ void main() async { final vb = await asset.getVertexBuffer(); expect(vb, isNotNull); expect(vb!.storageMode, VertexBufferStorageMode.direct); - expect(vb.ownsResource, isFalse); await expectLater(vb.destroy(), throwsStateError); final vertices = Float32List.fromList([ // Front face diff --git a/thermion_dart/test/morph_animation_tests.dart b/thermion_dart/test/morph_animation_tests.dart index 1c33e46e9..11ebce5c0 100644 --- a/thermion_dart/test/morph_animation_tests.dart +++ b/thermion_dart/test/morph_animation_tests.dart @@ -76,12 +76,10 @@ void main() async { final editableVertexBuffer = editable.getVertexBuffer()!; expect(editableVertexBuffer.supportsSetBufferAt, isTrue); expect(editableVertexBuffer.storageMode, VertexBufferStorageMode.direct); - expect(editableVertexBuffer.ownsResource, isFalse); expect(editableVertexBuffer.getVertexCount(), source.vertices.length ~/ 3); final instanceVertexBuffer = (await editable.getInstance(0)).getVertexBuffer()!; expect(instanceVertexBuffer.storageMode, VertexBufferStorageMode.direct); - expect(instanceVertexBuffer.ownsResource, isFalse); final editablePose = await capture(editable); expect( From 904aa18e363a4660bf7bf212a051788c458cfb51 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 26 Aug 2026 04:14:03 +0000 Subject: [PATCH 07/14] chore: update generated artifacts + format (CI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with GitHub Actions --- .../src/bindings/src/thermion_dart_ffi.g.dart | 5 +---- .../src/thermion_dart_js_interop.g.dart | 17 +++++++---------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart index f59456682..b0317992f 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart @@ -710,10 +710,7 @@ external ffi.Pointer SceneAsset_getVertexBuffer( ); @ffi.Native, ffi.Int)>(isLeaf: true) -external int SceneAsset_getVertexBufferStorageMode( - ffi.Pointer tSceneAsset, - int primitiveIndex, -); +external int SceneAsset_getVertexBufferStorageMode(ffi.Pointer tSceneAsset, int primitiveIndex); @ffi.Native Function(ffi.Pointer, ffi.Int)>(isLeaf: true) external ffi.Pointer SceneAsset_getIndexBuffer(ffi.Pointer tSceneAsset, int primitiveIndex); diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart index bd1dd3186..1d131d555 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart @@ -3511,10 +3511,7 @@ Pointer SceneAsset_getVertexBuffer(Pointer tSceneAss } int SceneAsset_getVertexBufferStorageMode(Pointer tSceneAsset, int primitiveIndex) { - final result = GeneratedBindings.instance._SceneAsset_getVertexBufferStorageMode( - tSceneAsset.cast(), - primitiveIndex, - ); + final result = GeneratedBindings.instance._SceneAsset_getVertexBufferStorageMode(tSceneAsset.cast(), primitiveIndex); return result; } @@ -10360,6 +10357,12 @@ sealed class TPrimitiveType { static const PRIMITIVETYPE_TRIANGLE_STRIP = 5; } +sealed class TVertexBufferStorageMode { + static const VERTEX_BUFFER_STORAGE_MODE_UNKNOWN = 0; + static const VERTEX_BUFFER_STORAGE_MODE_DIRECT = 1; + static const VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS = 2; +} + extension Aabb3Ext on Pointer { Aabb3 toDart() { return Aabb3(this); @@ -11124,12 +11127,6 @@ sealed class TVertexAttributeType { static const TVERTEXATTRIBUTE_TYPE_HALF4 = 25; } -sealed class TVertexBufferStorageMode { - static const VERTEX_BUFFER_STORAGE_MODE_UNKNOWN = 0; - static const VERTEX_BUFFER_STORAGE_MODE_DIRECT = 1; - static const VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS = 2; -} - extension TBufferObjectExt on Pointer { TBufferObject toDart() { return TBufferObject(this); From f82644a13a85e57bc24207cdc835ce836b4b499b Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Wed, 26 Aug 2026 13:39:40 +0800 Subject: [PATCH 08/14] fix: allow highlights with preserved geometry --- CHANGELOG.md | 3 ++ .../src/bindings/src/thermion_dart_ffi.g.dart | 1 + .../src/implementation/ffi_asset.dart | 2 + .../filament/src/implementation/ffi_view.dart | 13 ++++--- .../lib/src/filament/src/interface/asset.dart | 2 +- .../native/include/c_api/APIBoundaryTypes.h | 3 +- .../include/scene/GeometrySceneAsset.hpp | 3 ++ .../native/include/scene/GltfSceneAsset.hpp | 6 ++- thermion_dart/test/overlay_tests.dart | 37 ++++++++++--------- 9 files changed, 43 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22bcd86f3..7fd641bb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,9 @@ the `BufferObject` backing reserved for unwelded smooth/flat shading swaps. Buffer updates, flat shading, and stencil highlighting now throw actionable errors when used with incompatible buffer storage or asset capabilities. + Stencil highlighting accepts both editable and unwelded glTF geometry, plus + procedural geometry, because its silhouette pass only requires preserved + vertex/index buffers; flat shading remains unwelded-only. - Expose native `VertexBuffer.storageMode` metadata and first-class `BufferObject` creation, upload, and attachment APIs. `supportsSetBufferAt` is now derived from immutable metadata supplied by the native builder or diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart index b0317992f..4473bc3cc 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart @@ -5038,6 +5038,7 @@ sealed class TSceneAssetGeometryCapability { static const SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING = 1; static const SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 2; static const SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY = 4; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY = 8; } sealed class TFeatureLevel { diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart index f900236cd..b81dc50fd 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart @@ -47,6 +47,8 @@ class FFIAsset extends ThermionAsset> { SceneAssetGeometryCapability.barycentrics, if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY != 0) SceneAssetGeometryCapability.editableTopology, + if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY != 0) + SceneAssetGeometryCapability.preservedGeometry, }; } diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart index 247edfd22..219fccf35 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart @@ -574,13 +574,14 @@ class FFIView extends View> { final geoAsset = geometrySource ?? asset; final ffiGeoAsset = geoAsset as FFIAsset; - // Stencil highlighting needs the barycentric coordinates generated only - // for unwelded geometry. Editable geometry also has preserved buffers but - // its CUSTOM0 stream does not contain those coordinates. - if (!ffiGeoAsset.geometryCapabilities.contains(SceneAssetGeometryCapability.barycentrics)) { + // The silhouette pass reuses the asset's vertex and index buffers but only + // consumes POSITION. It therefore needs preserved geometry, not the + // barycentric coordinates used by wireframe and flat-shading features. + if (!ffiGeoAsset.geometryCapabilities.contains(SceneAssetGeometryCapability.preservedGeometry)) { throw StateError( - "setStencilHighlight requires unwelded geometry. " - "Load it with loadGltf(..., vertexBufferMode: VertexBufferMode.unwelded).", + "setStencilHighlight requires preserved geometry. " + "Load glTF assets with vertexBufferMode: VertexBufferMode.unwelded " + "or VertexBufferMode.editable.", ); } diff --git a/thermion_dart/lib/src/filament/src/interface/asset.dart b/thermion_dart/lib/src/filament/src/interface/asset.dart index 0f07b0664..2b5f1b06b 100644 --- a/thermion_dart/lib/src/filament/src/interface/asset.dart +++ b/thermion_dart/lib/src/filament/src/interface/asset.dart @@ -5,7 +5,7 @@ import 'package:thermion_dart/thermion_dart.dart'; export 'geometry.dart'; -enum SceneAssetGeometryCapability { flatShading, barycentrics, editableTopology } +enum SceneAssetGeometryCapability { flatShading, barycentrics, editableTopology, preservedGeometry } enum SceneAssetType { gltf, geometry, light, skybox, ibl, image, gizmo } diff --git a/thermion_dart/native/include/c_api/APIBoundaryTypes.h b/thermion_dart/native/include/c_api/APIBoundaryTypes.h index 1bb55cb68..45368b423 100644 --- a/thermion_dart/native/include/c_api/APIBoundaryTypes.h +++ b/thermion_dart/native/include/c_api/APIBoundaryTypes.h @@ -138,7 +138,8 @@ extern "C" SCENE_ASSET_GEOMETRY_CAPABILITY_NONE = 0, SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING = 1 << 0, SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 1 << 1, - SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY = 1 << 2 + SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY = 1 << 2, + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY = 1 << 3 }; typedef enum TSceneAssetGeometryCapability TSceneAssetGeometryCapability; diff --git a/thermion_dart/native/include/scene/GeometrySceneAsset.hpp b/thermion_dart/native/include/scene/GeometrySceneAsset.hpp index 634ae9083..860b6d75c 100644 --- a/thermion_dart/native/include/scene/GeometrySceneAsset.hpp +++ b/thermion_dart/native/include/scene/GeometrySceneAsset.hpp @@ -62,6 +62,9 @@ namespace thermion VertexBuffer *getVertexBuffer() const { return _vertexBuffer; } IndexBuffer *getIndexBuffer() const { return _indexBuffer; } + uint32_t getGeometryCapabilities() const override { + return SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY; + } TVertexBufferStorageMode getVertexBufferStorageMode(size_t primitiveIndex) const override { return primitiveIndex == 0 ? _vertexBufferStorageMode diff --git a/thermion_dart/native/include/scene/GltfSceneAsset.hpp b/thermion_dart/native/include/scene/GltfSceneAsset.hpp index fc7d0d047..eceb67881 100644 --- a/thermion_dart/native/include/scene/GltfSceneAsset.hpp +++ b/thermion_dart/native/include/scene/GltfSceneAsset.hpp @@ -144,10 +144,12 @@ namespace thermion uint32_t getGeometryCapabilities() const override { switch (_vertexBufferMode) { case VERTEX_BUFFER_MODE_EDITABLE: - return SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY; + return SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY | + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY; case VERTEX_BUFFER_MODE_UNWELDED: return SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING | - SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS; + SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY; case VERTEX_BUFFER_MODE_ORIGINAL: default: return SCENE_ASSET_GEOMETRY_CAPABILITY_NONE; diff --git a/thermion_dart/test/overlay_tests.dart b/thermion_dart/test/overlay_tests.dart index 7155e4901..b5546399e 100644 --- a/thermion_dart/test/overlay_tests.dart +++ b/thermion_dart/test/overlay_tests.dart @@ -221,33 +221,36 @@ void main() async { }, postProcessing: true); }); - test('setStencilHighlight and setFlatShading throw without unwelded vertex buffers', () async { + test('highlighting requires preserved geometry while flat shading requires unwelded geometry', () async { await testHelper.withViewer((viewer) async { - Future expectUnweldedOperationsToThrow(ThermionAsset cube) async { - final matcher = throwsA( - isA().having( - (e) => e.toString(), - 'message', - contains('vertexBufferMode: VertexBufferMode.unwelded'), - ), - ); - await expectLater(viewer.view.setStencilHighlight(cube), matcher); - await expectLater(cube.setFlatShading(true), matcher); - } + final preservedGeometryMatcher = throwsA( + isA().having((e) => e.toString(), 'message', contains('requires preserved geometry')), + ); + final unweldedMatcher = throwsA( + isA().having( + (e) => e.toString(), + 'message', + contains('vertexBufferMode: VertexBufferMode.unwelded'), + ), + ); final original = await viewer.loadGltf("file://${testHelper.assetsDir}/cube.glb", addToScene: true); - await expectUnweldedOperationsToThrow(original); + await expectLater(viewer.view.setStencilHighlight(original), preservedGeometryMatcher); + await expectLater(original.setFlatShading(true), unweldedMatcher); - // Editable assets have preserved geometry too, but no barycentric data - // or swappable tangent BufferObjects. They must not pass a mere - // getVertexBuffer() != null check. + // Editable assets preserve reusable vertex/index buffers, so the + // POSITION-only silhouette pass works without barycentrics. They still + // cannot swap the tangent BufferObjects required by flat shading. final editable = await viewer.loadGltf( "file://${testHelper.assetsDir}/cube.glb", vertexBufferMode: VertexBufferMode.editable, addToScene: true, ); expect(editable.getVertexBuffer(), isNotNull); - await expectUnweldedOperationsToThrow(editable); + expect(editable.geometryCapabilities, contains(SceneAssetGeometryCapability.preservedGeometry)); + await viewer.view.setStencilHighlight(editable); + await viewer.view.removeStencilHighlight(editable); + await expectLater(editable.setFlatShading(true), unweldedMatcher); }); }); } From 891bfab1e719cd4b988e76eef093cd83d07914d8 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Wed, 26 Aug 2026 14:00:11 +0800 Subject: [PATCH 09/14] test: preserve wireframe golden artifact names --- thermion_dart/test/wireframe_renderable_test.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/thermion_dart/test/wireframe_renderable_test.dart b/thermion_dart/test/wireframe_renderable_test.dart index 7f9f218da..3577e7858 100644 --- a/thermion_dart/test/wireframe_renderable_test.dart +++ b/thermion_dart/test/wireframe_renderable_test.dart @@ -13,7 +13,9 @@ void main() async { vertexBufferMode: VertexBufferMode.original, addToScene: true, ); - await testHelper.capture(result.viewer.view, "vertex_buffer_original"); + // Golden artifact names are stable IDs; keep the legacy names even when + // the public API terminology changes. + await testHelper.capture(result.viewer.view, "rebuildVertices_false"); await result.viewer.removeFromScene(original); final rebuilt = await result.viewer.loadGltf( @@ -31,7 +33,7 @@ void main() async { ), ); - await testHelper.capture(result.viewer.view, "vertex_buffer_unwelded"); + await testHelper.capture(result.viewer.view, "rebuildVertices_true"); // Use typed wireframe wrapper final wireframe = await FilamentApp.instance!.createWireframeMaterialInstance(); @@ -41,7 +43,7 @@ void main() async { await wireframe.setDoubleSided(true); await rebuilt.setMaterialInstanceForAll(wireframe.materialInstance); - await testHelper.capture(result.viewer.view, "vertex_buffer_unwelded_wireframe"); + await testHelper.capture(result.viewer.view, "rebuildVertices_true_wireframe"); final ubershader = await FilamentApp.instance!.createUbershaderMaterial(doubleSided: true); @@ -50,7 +52,7 @@ void main() async { await ubershader.setRoughnessFactor(1.0); await rebuilt.setMaterialInstanceForAll(ubershader.materialInstance); - await testHelper.capture(result.viewer.view, "vertex_buffer_unwelded_ubershader"); + await testHelper.capture(result.viewer.view, "rebuildVertices_true_ubershader"); await result.viewer.removeFromScene(rebuilt); From 6d6a79c303d4e05e13d8342e3e33160b3a61a452 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Wed, 26 Aug 2026 14:12:48 +0800 Subject: [PATCH 10/14] test: refresh reviewed golden baseline --- .github/workflows/run-dart-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-dart-tests.yml b/.github/workflows/run-dart-tests.yml index 60ba6cb14..e0603f846 100644 --- a/.github/workflows/run-dart-tests.yml +++ b/.github/workflows/run-dart-tests.yml @@ -389,7 +389,7 @@ jobs: github-token: ${{ github.token }} # To refresh every baseline, first push a capture-only commit, # inspect its dart-*-output-* artifacts, then update this run ID. - run-id: 32575493711 + run-id: 32812764648 name: ${{ matrix.artifact }} path: thermion_dart/test/golden-baseline From 3e8536fe791e6f7af9addd9794b0ff709602705a Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Wed, 26 Aug 2026 17:51:51 +0800 Subject: [PATCH 11/14] refactor!: request asset geometry capabilities --- CHANGELOG.md | 17 ++-- .../src/bindings/src/thermion_dart_ffi.g.dart | 14 ++- .../src/thermion_dart_js_interop.g.dart | 18 ++-- .../src/implementation/ffi_asset.dart | 10 ++- .../src/implementation/ffi_filament_app.dart | 52 +++++++++++- .../src/implementation/ffi_vertex_buffer.dart | 2 +- .../filament/src/implementation/ffi_view.dart | 4 +- .../lib/src/filament/src/interface/asset.dart | 36 ++++++-- .../filament/src/interface/filament_app.dart | 24 ++---- .../lib/src/filament/src/interface/view.dart | 4 +- .../src/ffi/src/thermion_viewer_ffi.dart | 8 +- .../src/viewer/src/thermion_viewer_base.dart | 23 +++-- .../native/include/c_api/APIBoundaryTypes.h | 13 +-- .../native/include/c_api/TSceneAsset.h | 2 +- .../c_api/ThermionDartRenderThreadApi.h | 2 +- .../native/include/scene/GltfSceneAsset.hpp | 42 ++++----- thermion_dart/native/src/c_api/TGizmo.cpp | 2 +- .../native/src/c_api/TSceneAsset.cpp | 10 ++- .../src/c_api/ThermionDartRenderThreadApi.cpp | 4 +- .../native/src/scene/GltfSceneAsset.cpp | 85 +++++++++++++++---- .../test/all_materials_smoke_test.dart | 2 +- thermion_dart/test/morph_animation_tests.dart | 8 +- thermion_dart/test/overlay_tests.dart | 18 +++- thermion_dart/test/view_tests.dart | 2 +- .../test/wireframe_renderable_test.dart | 22 +++-- 25 files changed, 278 insertions(+), 146 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fd641bb0..52fa8dfac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,8 +40,8 @@ - Apply overlapping custom morph animations oldest-first so the most recently added animation has final priority for shared targets. Active animations continue to overwrite manual weights on their next update. -- Fix `VertexBufferMode.editable` glTF assets so their vertex streams can be - updated through `VertexBuffer.setBufferAt`; editable buffers no longer use +- Fix topology-preserving glTF assets so their vertex streams can be updated + through `VertexBuffer.setBufferAt`; editable buffers no longer use the `BufferObject` backing reserved for unwelded smooth/flat shading swaps. Buffer updates, flat shading, and stencil highlighting now throw actionable errors when used with incompatible buffer storage or asset capabilities. @@ -57,12 +57,13 @@ ### Breaking changes - Replace the `rebuildVertices` in `ThermionViewer.loadGltf`, `ThermionViewer.loadGltfFromBuffer`, and `FilamentApp.loadGltfFromBuffer` with - `vertexBufferMode: VertexBufferMode.unwelded`. Use - `VertexBufferMode.editable` when mutable indexed topology is required. - `original` leaves gltfio geometry untouched, `unwelded` creates per-triangle - vertices with barycentric coordinates, and `editable` exposes mutable vertex - buffers while preserving source vertex order, indices, and morph-target - compatibility. + `requiredGeometryCapabilities`. An empty set leaves gltfio geometry untouched; + requesting `barycentrics` or `flatShading` creates per-triangle vertices, while + requesting `writableVertices`, `preservedTopology`, or `preservedGeometry` + preserves source vertex order, indices, and morph-target compatibility in + directly writable buffers. + Assets report the complete capability set actually provided through + `ThermionAsset.geometryCapabilities`. - remove the unused `FilamentApp.createColorGrading` — it returned a raw pointer nobody could destroy; use `View.createColorGradingBuilder().build()` instead. diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart index 4473bc3cc..c77cf7f5e 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart @@ -643,7 +643,7 @@ external ffi.Pointer SceneAsset_createFromFilamentAsset( ffi.Pointer tAssetLoader, ffi.Pointer tNameComponentManager, ffi.Pointer tFilamentAsset, - int vertexBufferMode, + int requiredGeometryCapabilities, ); @ffi.Native Function(ffi.Pointer)>(isLeaf: true) @@ -2757,7 +2757,7 @@ external void SceneAsset_createFromFilamentAssetRenderThread( ffi.Pointer tAssetLoader, ffi.Pointer tNameComponentManager, ffi.Pointer tFilamentAsset, - int vertexBufferMode, + int requiredGeometryCapabilities, ffi.Pointer)>> onComplete, ); @@ -5021,12 +5021,6 @@ sealed class TSceneAssetType { static const SCENE_ASSET_TYPE_GIZMO = 6; } -sealed class TVertexBufferMode { - static const VERTEX_BUFFER_MODE_ORIGINAL = 0; - static const VERTEX_BUFFER_MODE_UNWELDED = 1; - static const VERTEX_BUFFER_MODE_EDITABLE = 2; -} - sealed class TVertexBufferStorageMode { static const VERTEX_BUFFER_STORAGE_MODE_UNKNOWN = 0; static const VERTEX_BUFFER_STORAGE_MODE_DIRECT = 1; @@ -5037,8 +5031,10 @@ sealed class TSceneAssetGeometryCapability { static const SCENE_ASSET_GEOMETRY_CAPABILITY_NONE = 0; static const SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING = 1; static const SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 2; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY = 4; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES = 4; static const SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY = 8; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY = 16; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS = 32; } sealed class TFeatureLevel { diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart index 1d131d555..2af46eed5 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart @@ -302,7 +302,7 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { Pointer tAssetLoader, Pointer tNameComponentManager, Pointer tFilamentAsset, - int vertexBufferMode, + int requiredGeometryCapabilities, ); external Pointer _SceneAsset_getFilamentAsset(Pointer tSceneAsset); external int _SceneAsset_getType(Pointer tSceneAsset); @@ -1388,7 +1388,7 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { Pointer tAssetLoader, Pointer tNameComponentManager, Pointer tFilamentAsset, - int vertexBufferMode, + int requiredGeometryCapabilities, Pointer)>> onComplete, ); external void _SceneAsset_createFromBuffersRenderThread( @@ -3399,14 +3399,14 @@ Pointer SceneAsset_createFromFilamentAsset( Pointer tAssetLoader, Pointer tNameComponentManager, Pointer tFilamentAsset, - int vertexBufferMode, + int requiredGeometryCapabilities, ) { final result = GeneratedBindings.instance._SceneAsset_createFromFilamentAsset( tEngine.cast(), tAssetLoader.cast(), tNameComponentManager.cast(), tFilamentAsset.cast(), - vertexBufferMode, + requiredGeometryCapabilities, ); return Pointer(result); } @@ -6310,7 +6310,7 @@ void SceneAsset_createFromFilamentAssetRenderThread( Pointer tAssetLoader, Pointer tNameComponentManager, Pointer tFilamentAsset, - int vertexBufferMode, + int requiredGeometryCapabilities, Pointer)>> onComplete, ) { final result = GeneratedBindings.instance._SceneAsset_createFromFilamentAssetRenderThread( @@ -6318,7 +6318,7 @@ void SceneAsset_createFromFilamentAssetRenderThread( tAssetLoader.cast(), tNameComponentManager.cast(), tFilamentAsset.cast(), - vertexBufferMode, + requiredGeometryCapabilities, onComplete.cast(), ); return result; @@ -10453,12 +10453,6 @@ final class TFilamentAsset extends Struct { } } -sealed class TVertexBufferMode { - static const VERTEX_BUFFER_MODE_ORIGINAL = 0; - static const VERTEX_BUFFER_MODE_UNWELDED = 1; - static const VERTEX_BUFFER_MODE_EDITABLE = 2; -} - sealed class TSceneAssetType { static const SCENE_ASSET_TYPE_GLTF = 0; static const SCENE_ASSET_TYPE_GEOMETRY = 1; diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart index b81dc50fd..9a7fb9998 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart @@ -45,10 +45,14 @@ class FFIAsset extends ThermionAsset> { SceneAssetGeometryCapability.flatShading, if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS != 0) SceneAssetGeometryCapability.barycentrics, - if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY != 0) - SceneAssetGeometryCapability.editableTopology, + if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES != 0) + SceneAssetGeometryCapability.writableVertices, if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY != 0) SceneAssetGeometryCapability.preservedGeometry, + if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY != 0) + SceneAssetGeometryCapability.preservedTopology, + if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS != 0) + SceneAssetGeometryCapability.uniqueTriangleCorners, }; } @@ -301,7 +305,7 @@ class FFIAsset extends ThermionAsset> { if (!geometryCapabilities.contains(SceneAssetGeometryCapability.flatShading)) { throw StateError( "setFlatShading requires unwelded geometry. " - "Load it with loadGltf(..., vertexBufferMode: VertexBufferMode.unwelded).", + "Load it with requiredGeometryCapabilities containing flatShading.", ); } await withVoidCallback((requestId, cb) => SceneAsset_setFlatShadingRenderThread(asset, flatShading, requestId, cb)); diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart index a91b51277..58b1de82c 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart @@ -32,6 +32,45 @@ import 'package:logging/logging.dart'; import 'ffi_gltf_mesh_data.dart'; import 'resource_loader.dart'; +int _geometryCapabilitiesToNative(Set capabilities) { + var bits = TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_NONE; + for (final capability in capabilities) { + bits |= switch (capability) { + SceneAssetGeometryCapability.flatShading => + TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING, + SceneAssetGeometryCapability.barycentrics => + TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS, + SceneAssetGeometryCapability.writableVertices => + TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES, + SceneAssetGeometryCapability.preservedGeometry => + TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY, + SceneAssetGeometryCapability.preservedTopology => + TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY, + SceneAssetGeometryCapability.uniqueTriangleCorners => + TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS, + }; + } + return bits; +} + +void _validateRequiredGeometryCapabilities(Set capabilities) { + final requiresUnwelded = + capabilities.contains(SceneAssetGeometryCapability.flatShading) || + capabilities.contains(SceneAssetGeometryCapability.barycentrics) || + capabilities.contains(SceneAssetGeometryCapability.uniqueTriangleCorners); + final requiresPreservedTopology = + capabilities.contains(SceneAssetGeometryCapability.writableVertices) || + capabilities.contains(SceneAssetGeometryCapability.preservedTopology); + if (requiresUnwelded && requiresPreservedTopology) { + throw ArgumentError.value( + capabilities, + 'requiredGeometryCapabilities', + 'writableVertices or preservedTopology cannot be combined with ' + 'flatShading, barycentrics, or uniqueTriangleCorners', + ); + } +} + class FFIFilamentConfig extends FilamentConfig { FFIFilamentConfig({ super.loadResource = null, @@ -1123,12 +1162,13 @@ class FFIFilamentApp extends FilamentApp { int initialInstances = 1, bool releaseSourceData = false, bool loadResourcesAsync = false, - VertexBufferMode vertexBufferMode = VertexBufferMode.original, + Set requiredGeometryCapabilities = const {}, String? resourceUri, }) async { if (initialInstances <= 0) { throw Exception("initialInstances must be at least 1"); } + _validateRequiredGeometryCapabilities(requiredGeometryCapabilities); _logger.info( "Loading glTF from buffer (${data.lengthInBytes} bytes)" " with resourceUri ${resourceUri}", @@ -1227,7 +1267,7 @@ class FFIFilamentApp extends FilamentApp { gltfAssetLoader, nameComponentManager, filamentAsset, - vertexBufferMode.index, + _geometryCapabilitiesToNative(requiredGeometryCapabilities), cb, ), ); @@ -1241,6 +1281,14 @@ class FFIFilamentApp extends FilamentApp { ); final ffiAsset = FFIAsset(asset, app: this); + if (!ffiAsset.geometryCapabilities.containsAll(requiredGeometryCapabilities)) { + await withVoidCallback((requestId, cb) => SceneAsset_destroyRenderThread(asset, requestId, cb)); + throw StateError( + 'The loaded asset does not provide all required geometry ' + 'capabilities. Required: $requiredGeometryCapabilities; provided: ' + '${ffiAsset.geometryCapabilities}.', + ); + } if (releaseSourceData) { await ffiAsset.releaseSourceData(); } diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart index 5ac518c86..0f38067f4 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_vertex_buffer.dart @@ -44,7 +44,7 @@ class FFIVertexBuffer extends VertexBuffer { throw StateError( 'VertexBuffer.setBufferAt requires direct storage. Build the buffer ' 'without enableBufferObjects(), or load glTF assets with ' - 'vertexBufferMode: VertexBufferMode.editable.', + 'requiredGeometryCapabilities containing writableVertices.', ); } final byteData = data.asUint8List(); diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart index 219fccf35..3997190b4 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart @@ -580,8 +580,8 @@ class FFIView extends View> { if (!ffiGeoAsset.geometryCapabilities.contains(SceneAssetGeometryCapability.preservedGeometry)) { throw StateError( "setStencilHighlight requires preserved geometry. " - "Load glTF assets with vertexBufferMode: VertexBufferMode.unwelded " - "or VertexBufferMode.editable.", + "Load the asset with requiredGeometryCapabilities containing " + "preservedGeometry.", ); } diff --git a/thermion_dart/lib/src/filament/src/interface/asset.dart b/thermion_dart/lib/src/filament/src/interface/asset.dart index 2b5f1b06b..d39278760 100644 --- a/thermion_dart/lib/src/filament/src/interface/asset.dart +++ b/thermion_dart/lib/src/filament/src/interface/asset.dart @@ -5,7 +5,31 @@ import 'package:thermion_dart/thermion_dart.dart'; export 'geometry.dart'; -enum SceneAssetGeometryCapability { flatShading, barycentrics, editableTopology, preservedGeometry } +/// A geometry operation guaranteed to be supported by a scene asset. +/// +/// The same values can be supplied to asset loaders as requirements. Loaders +/// may provide additional capabilities when they share the same geometry +/// representation. +enum SceneAssetGeometryCapability { + /// Smooth and per-face tangent frames can be selected at runtime. + flatShading, + + /// Triangle-corner barycentric coordinates are available to materials. + barycentrics, + + /// Vertex attributes can be updated through [VertexBuffer.setBufferAt]. + writableVertices, + + /// Thermion retains reusable vertex and index buffers for operations such as + /// stencil highlighting. + preservedGeometry, + + /// Source vertex order and triangle indices are preserved. + preservedTopology, + + /// Every triangle corner has its own vertex. + uniqueTriangleCorners, +} enum SceneAssetType { gltf, geometry, light, skybox, ibl, image, gizmo } @@ -128,8 +152,8 @@ abstract class ThermionAsset extends NativeHandle { } // Toggle between flat (per-face) and smooth (per-vertex) shading. - // Throws unless the asset was loaded with vertexBufferMode: VertexBufferMode.unwelded (flat - // shading swaps TANGENTS on the rebuilt vertex buffers). + // Throws unless [geometryCapabilities] contains + // [SceneAssetGeometryCapability.flatShading]. Future setFlatShading(bool flatShading) { throw UnimplementedError(); } @@ -432,9 +456,9 @@ abstract class ThermionAsset extends NativeHandle { // Returns the underlying [VertexBuffer] for this asset, if available. // // For geometry assets this exposes the backing Filament vertex buffer so you - // can update data via [VertexBuffer.setBufferAt]. For glTF assets, editable - // buffers support updates while unwelded buffers are read-only because their - // streams use Filament BufferObjects. + // can update data via [VertexBuffer.setBufferAt]. Assets with + // [SceneAssetGeometryCapability.writableVertices] expose directly writable + // buffers; barycentric/flat-shading geometry uses Filament BufferObjects. // // [primitiveIndex] is reserved for future use. Geometry assets currently // only support a single primitive, so it is ignored. diff --git a/thermion_dart/lib/src/filament/src/interface/filament_app.dart b/thermion_dart/lib/src/filament/src/interface/filament_app.dart index b9344b6e0..6ee820365 100644 --- a/thermion_dart/lib/src/filament/src/interface/filament_app.dart +++ b/thermion_dart/lib/src/filament/src/interface/filament_app.dart @@ -3,19 +3,6 @@ import 'package:thermion_dart/src/filament/src/interface/render_manager.dart'; import 'package:thermion_dart/src/filament/src/interface/scene.dart'; import 'package:thermion_dart/thermion_dart.dart'; -/// Controls whether glTF vertex buffers are left untouched or rebuilt for a -/// specific editing workflow. -enum VertexBufferMode { - /// Keep gltfio's original vertex and index buffers. - original, - - /// Duplicate vertices per triangle corner and add barycentric coordinates. - unwelded, - - /// Preserve source vertex order and indices in mutable vertex buffers. - editable, -} - class FilamentConfig { final Backend backend; Future Function(String)? loadResource; @@ -271,7 +258,7 @@ abstract class FilamentApp { Future createUnlitMaterialInstance(); /// Creates a wireframe material instance for use with assets loaded - /// with `vertexBufferMode: VertexBufferMode.unwelded`. Set parameters (edgeColor, faceColor, + /// with the required barycentric geometry capability. Set parameters (edgeColor, faceColor, /// edgeWidth) on the returned [WireframeMaterialInstance], then apply with /// [ThermionAsset.setMaterialInstanceForAll]. Future createWireframeMaterialInstance(); @@ -355,12 +342,17 @@ abstract class FilamentApp { bool clear = true, }); - // Loads a glTF asset from a raw memory buffer. + /// Loads a glTF asset from a raw memory buffer. + /// + /// [requiredGeometryCapabilities] describes the operations that the loaded + /// asset must support. The loader may provide a compatible superset, which + /// is reported by [ThermionAsset.geometryCapabilities]. Incompatible + /// requirements throw [ArgumentError]. Future loadGltfFromBuffer( Uint8List data, { int initialInstances = 1, bool releaseSourceData = false, - VertexBufferMode vertexBufferMode = VertexBufferMode.original, + Set requiredGeometryCapabilities = const {}, bool loadResourcesAsync = false, String? resourceUri, }); diff --git a/thermion_dart/lib/src/filament/src/interface/view.dart b/thermion_dart/lib/src/filament/src/interface/view.dart index 9911c21f9..f83bd53ed 100644 --- a/thermion_dart/lib/src/filament/src/interface/view.dart +++ b/thermion_dart/lib/src/filament/src/interface/view.dart @@ -482,8 +482,8 @@ abstract class View extends NativeHandle { /// Uses a stencil-based two-pass rendering approach for clean, flicker-free /// outlines. /// - /// Throws if the asset (or [geometrySource]) has no preserved geometry — - /// glTF assets must be loaded with `vertexBufferMode: VertexBufferMode.unwelded` for outlining. + /// Throws if the asset (or [geometrySource]) does not provide + /// [SceneAssetGeometryCapability.preservedGeometry]. /// /// The [scale] parameter is deprecated and ignored; use [outlineWidth] instead. Future setStencilHighlight( diff --git a/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart b/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart index 323b4bf33..38329618b 100644 --- a/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart +++ b/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart @@ -516,7 +516,7 @@ class ThermionViewerFFI extends ThermionViewer { bool addToScene = true, int initialInstances = 1, bool releaseSourceData = false, - VertexBufferMode vertexBufferMode = VertexBufferMode.original, + Set requiredGeometryCapabilities = const {}, String? resourceUri, bool loadAsync = false, }) async { @@ -536,7 +536,7 @@ class ThermionViewerFFI extends ThermionViewer { addToScene: addToScene, initialInstances: initialInstances, releaseSourceData: releaseSourceData, - vertexBufferMode: vertexBufferMode, + requiredGeometryCapabilities: requiredGeometryCapabilities, resourceUri: resourceUri, loadResourcesAsync: loadAsync, ); @@ -549,7 +549,7 @@ class ThermionViewerFFI extends ThermionViewer { bool addToScene = true, int initialInstances = 1, bool releaseSourceData = false, - VertexBufferMode vertexBufferMode = VertexBufferMode.original, + Set requiredGeometryCapabilities = const {}, bool loadResourcesAsync = false, String? resourceUri, }) async { @@ -557,7 +557,7 @@ class ThermionViewerFFI extends ThermionViewer { data, initialInstances: initialInstances, releaseSourceData: releaseSourceData, - vertexBufferMode: vertexBufferMode, + requiredGeometryCapabilities: requiredGeometryCapabilities, loadResourcesAsync: loadResourcesAsync, resourceUri: resourceUri, ); diff --git a/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart b/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart index d53051d3a..44b8e5464 100644 --- a/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart +++ b/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart @@ -166,8 +166,9 @@ abstract class ThermionViewer { // Creating instances by specifying [initialInstances] at asset load time is // generally more efficient than dynamically instantating at a later time. // - // If [vertexBufferMode] is [VertexBufferMode.unwelded], vertex buffers are - // rebuilt after loading with a superset of attributes (POSITION, TANGENTS, + // If [requiredGeometryCapabilities] contains [SceneAssetGeometryCapability.barycentrics] + // or [SceneAssetGeometryCapability.flatShading], vertex buffers are rebuilt + // after loading with a superset of attributes (POSITION, TANGENTS, // UV0, CUSTOM0, and // optionally BONE_INDICES/BONE_WEIGHTS). Vertices are unwelded so each // triangle has unique vertices with barycentric coordinates in CUSTOM0. @@ -176,11 +177,17 @@ abstract class ThermionViewer { // Increases vertex memory usage (~3x vertex count) but preserves the full // glTF feature set (animations, skeleton, instancing). // - // If [vertexBufferMode] is [VertexBufferMode.editable], vertex buffers are + // If [requiredGeometryCapabilities] contains + // [SceneAssetGeometryCapability.writableVertices] or + // [SceneAssetGeometryCapability.preservedTopology], vertex buffers are // rebuilt without unwelding: source vertex order and triangle indices are - // preserved. This - // exposes mutable buffers while retaining compatibility with glTF morph - // targets. + // preserved in mutable buffers compatible with glTF morph targets. + // + // [SceneAssetGeometryCapability.writableVertices] and + // [SceneAssetGeometryCapability.preservedTopology] cannot be combined with + // [SceneAssetGeometryCapability.barycentrics] or + // [SceneAssetGeometryCapability.flatShading]. The returned asset reports the + // complete set actually provided through [ThermionAsset.geometryCapabilities]. // // If [loadResourcesAsync] is true, resources (textures, materials, etc) will // be loaded asynchronously. Some material/texture pop-in is expected. @@ -190,7 +197,7 @@ abstract class ThermionViewer { bool addToScene = true, int initialInstances = 1, bool releaseSourceData = false, - VertexBufferMode vertexBufferMode = VertexBufferMode.original, + Set requiredGeometryCapabilities = const {}, String? resourceUri, bool loadAsync = false, }); @@ -204,7 +211,7 @@ abstract class ThermionViewer { String? resourceUri, int initialInstances = 1, bool releaseSourceData = false, - VertexBufferMode vertexBufferMode = VertexBufferMode.original, + Set requiredGeometryCapabilities = const {}, bool loadResourcesAsync = false, bool addToScene = true, }); diff --git a/thermion_dart/native/include/c_api/APIBoundaryTypes.h b/thermion_dart/native/include/c_api/APIBoundaryTypes.h index 45368b423..3a0ecc988 100644 --- a/thermion_dart/native/include/c_api/APIBoundaryTypes.h +++ b/thermion_dart/native/include/c_api/APIBoundaryTypes.h @@ -120,13 +120,6 @@ extern "C" }; typedef enum TSceneAssetType TSceneAssetType; - enum TVertexBufferMode { - VERTEX_BUFFER_MODE_ORIGINAL = 0, - VERTEX_BUFFER_MODE_UNWELDED = 1, - VERTEX_BUFFER_MODE_EDITABLE = 2 - }; - typedef enum TVertexBufferMode TVertexBufferMode; - enum TVertexBufferStorageMode { VERTEX_BUFFER_STORAGE_MODE_UNKNOWN = 0, VERTEX_BUFFER_STORAGE_MODE_DIRECT = 1, @@ -138,8 +131,10 @@ extern "C" SCENE_ASSET_GEOMETRY_CAPABILITY_NONE = 0, SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING = 1 << 0, SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 1 << 1, - SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY = 1 << 2, - SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY = 1 << 3 + SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES = 1 << 2, + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY = 1 << 3, + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY = 1 << 4, + SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS = 1 << 5 }; typedef enum TSceneAssetGeometryCapability TSceneAssetGeometryCapability; diff --git a/thermion_dart/native/include/c_api/TSceneAsset.h b/thermion_dart/native/include/c_api/TSceneAsset.h index 025480427..a750e97ee 100644 --- a/thermion_dart/native/include/c_api/TSceneAsset.h +++ b/thermion_dart/native/include/c_api/TSceneAsset.h @@ -24,7 +24,7 @@ extern "C" TGltfAssetLoader *tAssetLoader, TNameComponentManager *tNameComponentManager, TFilamentAsset *tFilamentAsset, - enum TVertexBufferMode vertexBufferMode + uint32_t requiredGeometryCapabilities ); EMSCRIPTEN_KEEPALIVE TFilamentAsset *SceneAsset_getFilamentAsset(TSceneAsset *tSceneAsset); EMSCRIPTEN_KEEPALIVE enum TSceneAssetType SceneAsset_getType(TSceneAsset *tSceneAsset); diff --git a/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h b/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h index 7f9d6050c..9e479db3c 100644 --- a/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h +++ b/thermion_dart/native/include/c_api/ThermionDartRenderThreadApi.h @@ -162,7 +162,7 @@ namespace thermion TGltfAssetLoader *tAssetLoader, TNameComponentManager *tNameComponentManager, TFilamentAsset *tFilamentAsset, - enum TVertexBufferMode vertexBufferMode, + uint32_t requiredGeometryCapabilities, void (*onComplete)(TSceneAsset *) ); EMSCRIPTEN_KEEPALIVE void SceneAsset_createFromBuffersRenderThread( diff --git a/thermion_dart/native/include/scene/GltfSceneAsset.hpp b/thermion_dart/native/include/scene/GltfSceneAsset.hpp index eceb67881..c8ff7737f 100644 --- a/thermion_dart/native/include/scene/GltfSceneAsset.hpp +++ b/thermion_dart/native/include/scene/GltfSceneAsset.hpp @@ -35,7 +35,7 @@ namespace thermion gltfio::AssetLoader *assetLoader, Engine *engine, utils::NameComponentManager* ncm, - TVertexBufferMode vertexBufferMode = VERTEX_BUFFER_MODE_ORIGINAL, + uint32_t requiredGeometryCapabilities = SCENE_ASSET_GEOMETRY_CAPABILITY_NONE, MaterialInstance **materialInstances = nullptr, size_t materialInstanceCount = 0); @@ -141,43 +141,31 @@ namespace thermion return _asset->getBoundingBox(); } - uint32_t getGeometryCapabilities() const override { - switch (_vertexBufferMode) { - case VERTEX_BUFFER_MODE_EDITABLE: - return SCENE_ASSET_GEOMETRY_CAPABILITY_EDITABLE_TOPOLOGY | - SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY; - case VERTEX_BUFFER_MODE_UNWELDED: - return SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING | - SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | - SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY; - case VERTEX_BUFFER_MODE_ORIGINAL: - default: - return SCENE_ASSET_GEOMETRY_CAPABILITY_NONE; - } - } + uint32_t getGeometryCapabilities() const override { return _geometryCapabilities; } TVertexBufferStorageMode getVertexBufferStorageMode(size_t primitiveIndex) const override { if (primitiveIndex >= _preservedVertexBuffers.size()) { return VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; } - switch (_vertexBufferMode) { - case VERTEX_BUFFER_MODE_EDITABLE: + if ((_geometryCapabilities & SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES) != 0) { return VERTEX_BUFFER_STORAGE_MODE_DIRECT; - case VERTEX_BUFFER_MODE_UNWELDED: + } + if ((_geometryCapabilities & SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS) != 0) { return VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS; - case VERTEX_BUFFER_MODE_ORIGINAL: - default: - return VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; } + return VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; } /// Rebuild all mesh primitives with a superset vertex buffer layout /// (POSITION + TANGENTS + UV0 + CUSTOM0 + optional BONE_INDICES/WEIGHTS). - /// [VERTEX_BUFFER_MODE_UNWELDED] gives each triangle unique vertices - /// for barycentric wireframe rendering. [VERTEX_BUFFER_MODE_EDITABLE] - /// retains source vertex order and indices so glTF morph target buffers - /// remain compatible with the rebuilt geometry. - void rebuildVertexBuffers(TVertexBufferMode vertexBufferMode); + /// When [preserveTopology] is false, each triangle receives unique + /// vertices for barycentric wireframe rendering. When true, source + /// vertex order and indices remain compatible with glTF morph targets. + void rebuildVertexBuffers(bool preserveTopology); + + /// Returns false when the requested capability combination cannot be + /// provided by a single rebuilt geometry representation. + static bool supportsRequiredGeometryCapabilities(uint32_t requiredGeometryCapabilities); /// Toggle between flat (per-face) and smooth (per-vertex) shading. /// Only valid after rebuildVertexBuffers() has been called. @@ -232,7 +220,7 @@ namespace thermion bool _sourceDataReleased = false; bool _geometryPreserved = false; bool _flatShading = false; - TVertexBufferMode _vertexBufferMode = VERTEX_BUFFER_MODE_ORIGINAL; + uint32_t _geometryCapabilities = SCENE_ASSET_GEOMETRY_CAPABILITY_NONE; // Buffers created by rebuildVertexBuffers, owned by this asset. std::vector _preservedVertexBuffers; diff --git a/thermion_dart/native/src/c_api/TGizmo.cpp b/thermion_dart/native/src/c_api/TGizmo.cpp index 0fcc82d05..56f8573cc 100644 --- a/thermion_dart/native/src/c_api/TGizmo.cpp +++ b/thermion_dart/native/src/c_api/TGizmo.cpp @@ -67,7 +67,7 @@ namespace thermion tAssetLoader, tNameComponentManager, tFilamentAsset, - VERTEX_BUFFER_MODE_ORIGINAL); + SCENE_ASSET_GEOMETRY_CAPABILITY_NONE); auto *gltfSceneAsset = reinterpret_cast(sceneAsset); diff --git a/thermion_dart/native/src/c_api/TSceneAsset.cpp b/thermion_dart/native/src/c_api/TSceneAsset.cpp index 4e3cdf31f..9a7010b91 100644 --- a/thermion_dart/native/src/c_api/TSceneAsset.cpp +++ b/thermion_dart/native/src/c_api/TSceneAsset.cpp @@ -7,6 +7,7 @@ #include "c_api/TGltfAssetLoader.h" #include "c_api/TSceneAsset.h" +#include "Log.hpp" #include "scene/GeometrySceneAsset.hpp" #include "scene/GltfSceneAsset.hpp" @@ -72,19 +73,24 @@ extern "C" TGltfAssetLoader *tAssetLoader, TNameComponentManager *tNameComponentManager, TFilamentAsset *tFilamentAsset, - TVertexBufferMode vertexBufferMode + uint32_t requiredGeometryCapabilities ) { auto *engine = reinterpret_cast(tEngine); auto *nameComponentManager = reinterpret_cast(tNameComponentManager); auto *filamentAsset = reinterpret_cast(tFilamentAsset); auto *assetLoader = reinterpret_cast(tAssetLoader); + if (!GltfSceneAsset::supportsRequiredGeometryCapabilities(requiredGeometryCapabilities)) { + Log("Unsupported or incompatible required geometry capabilities: 0x%x", + requiredGeometryCapabilities); + return nullptr; + } auto *sceneAsset = new GltfSceneAsset( filamentAsset, assetLoader, engine, nameComponentManager, - vertexBufferMode + requiredGeometryCapabilities ); return reinterpret_cast(sceneAsset); diff --git a/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp b/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp index 57f3d4070..4d71a86d5 100644 --- a/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp +++ b/thermion_dart/native/src/c_api/ThermionDartRenderThreadApi.cpp @@ -987,7 +987,7 @@ extern "C" TGltfAssetLoader *tAssetLoader, TNameComponentManager *tNameComponentManager, TFilamentAsset *tFilamentAsset, - TVertexBufferMode vertexBufferMode, + uint32_t requiredGeometryCapabilities, void (*onComplete)(TSceneAsset *)) { auto *rt = RT(tEngine); @@ -996,7 +996,7 @@ extern "C" { auto sceneAsset = SceneAsset_createFromFilamentAsset( tEngine, tAssetLoader, tNameComponentManager, tFilamentAsset, - vertexBufferMode); + requiredGeometryCapabilities); setOwner(sceneAsset, rt); PROXY(onComplete(sceneAsset)); }); diff --git a/thermion_dart/native/src/scene/GltfSceneAsset.cpp b/thermion_dart/native/src/scene/GltfSceneAsset.cpp index 898824c40..9f777625a 100644 --- a/thermion_dart/native/src/scene/GltfSceneAsset.cpp +++ b/thermion_dart/native/src/scene/GltfSceneAsset.cpp @@ -37,19 +37,42 @@ namespace thermion gltfio::AssetLoader *assetLoader, Engine *engine, utils::NameComponentManager *ncm, - TVertexBufferMode vertexBufferMode, + uint32_t requiredGeometryCapabilities, MaterialInstance **materialInstances, size_t materialInstanceCount) : _asset(asset), _assetLoader(assetLoader), _engine(engine), _ncm(ncm), _materialInstances(materialInstances), - _materialInstanceCount(materialInstanceCount), - _vertexBufferMode(vertexBufferMode) + _materialInstanceCount(materialInstanceCount) { - if (vertexBufferMode != VERTEX_BUFFER_MODE_ORIGINAL) + const bool requiresUnwelded = + (requiredGeometryCapabilities & + (SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING | + SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | + SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS)) != 0; + const bool requiresPreserved = + (requiredGeometryCapabilities & + (SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES | + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY | + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY)) != 0; + + if (requiresUnwelded) { - rebuildVertexBuffers(vertexBufferMode); + _geometryCapabilities = + SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING | + SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY | + SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS; + rebuildVertexBuffers(false); + } + else if (requiresPreserved) + { + _geometryCapabilities = + SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES | + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY | + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY; + rebuildVertexBuffers(true); } for (int i = 0; i < asset->getAssetInstanceCount(); i++) { @@ -58,6 +81,32 @@ namespace thermion TRACE("Created GltfSceneAsset from FilamentAsset %d with %d reserved instances", asset, asset->getAssetInstanceCount()); } + bool GltfSceneAsset::supportsRequiredGeometryCapabilities(uint32_t requiredGeometryCapabilities) + { + constexpr uint32_t supported = + SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING | + SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | + SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES | + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY | + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY | + SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS; + if ((requiredGeometryCapabilities & ~supported) != 0) + { + return false; + } + + const bool requiresUnwelded = + (requiredGeometryCapabilities & + (SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING | + SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | + SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS)) != 0; + const bool requiresPreservedTopology = + (requiredGeometryCapabilities & + (SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES | + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY)) != 0; + return !(requiresUnwelded && requiresPreservedTopology); + } + GltfSceneAsset::~GltfSceneAsset() { _instances.clear(); @@ -269,9 +318,9 @@ namespace thermion return nullptr; } - void GltfSceneAsset::rebuildVertexBuffers(TVertexBufferMode vertexBufferMode) + void GltfSceneAsset::rebuildVertexBuffers(bool preserveTopology) { - const bool editableTopology = vertexBufferMode == VERTEX_BUFFER_MODE_EDITABLE; + const bool preserveSourceTopology = preserveTopology; auto *sourceData = (const cgltf_data *)_asset->getSourceAsset(); if (!sourceData) { @@ -430,7 +479,7 @@ namespace thermion continue; uint32_t triangleCount = (uint32_t)(indices.size() / 3); - uint32_t newVertexCount = editableTopology + uint32_t newVertexCount = preserveSourceTopology ? (uint32_t)posAccessor->count : triangleCount * 3; @@ -506,7 +555,7 @@ namespace thermion for (uint32_t dstIdx = 0; dstIdx < newVertexCount; dstIdx++) { - uint32_t srcIdx = editableTopology ? dstIdx : indices[dstIdx]; + uint32_t srcIdx = preserveSourceTopology ? dstIdx : indices[dstIdx]; // Position newPositions[dstIdx * 3 + 0] = srcPositions[srcIdx * posComponents + 0]; @@ -551,7 +600,7 @@ namespace thermion } // Barycentric - if (!editableTopology) + if (!preserveSourceTopology) { const int corner = dstIdx % 3; newBarycentrics[dstIdx * 4 + 0] = bary[corner][0]; @@ -595,7 +644,7 @@ namespace thermion tris.resize(triangleCount); for (uint32_t i = 0; i < triangleCount; i++) { - tris[i] = editableTopology + tris[i] = preserveSourceTopology ? filament::math::uint3{indices[i * 3], indices[i * 3 + 1], indices[i * 3 + 2]} : filament::math::uint3{i * 3, i * 3 + 1, i * 3 + 2}; } @@ -614,7 +663,7 @@ namespace thermion // preserves shared vertices, so its flat buffer intentionally // matches the smooth buffer. std::vector flatTangentQuats; - if (editableTopology) + if (preserveSourceTopology) { flatTangentQuats = smoothTangentQuats; } @@ -671,7 +720,7 @@ namespace thermion // on the other hand, must remain writable through the public // VertexBuffer::setBufferAt API, which is incompatible with // BufferObject-backed streams. - if (!editableTopology) + if (!preserveSourceTopology) { vbBuilder.enableBufferObjects(); } @@ -702,7 +751,7 @@ namespace thermion auto uploadStream = [&](uint8_t bufferIndex, const void *source, size_t size) { - if (editableTopology) + if (preserveSourceTopology) { uploadDirect(bufferIndex, source, size); return; @@ -724,7 +773,7 @@ namespace thermion // Create both smooth and flat tangent BOs for runtime toggling. size_t tangDataSize = newVertexCount * sizeof(filament::math::short4); - if (editableTopology) + if (preserveSourceTopology) { uploadDirect(1, smoothTangentQuats.data(), tangDataSize); _smoothTangentBOs.push_back(nullptr); @@ -777,13 +826,13 @@ namespace thermion // Editable geometry retains source indices. Unwelded geometry // uses a sequential index buffer. - const size_t newIndexCount = editableTopology ? indices.size() : newVertexCount; + const size_t newIndexCount = preserveSourceTopology ? indices.size() : newVertexCount; size_t indexDataSize = newIndexCount * sizeof(uint32_t); auto *newIndices = new uint8_t[indexDataSize]; auto *indexPtr = reinterpret_cast(newIndices); for (uint32_t i = 0; i < newIndexCount; i++) { - indexPtr[i] = editableTopology ? indices[i] : i; + indexPtr[i] = preserveSourceTopology ? indices[i] : i; } IndexBuffer *ib = IndexBuffer::Builder() @@ -803,7 +852,7 @@ namespace thermion _preservedIndexCounts.push_back(newIndexCount); TRACE("rebuildVertexBuffers: primitive %zu %s with %u vertices and %zu indices (skinned=%d)", - pi, editableTopology ? "editable topology" : "unwelded", + pi, preserveSourceTopology ? "preserved topology" : "unwelded", newVertexCount, newIndexCount, hasSkinning); } } diff --git a/thermion_dart/test/all_materials_smoke_test.dart b/thermion_dart/test/all_materials_smoke_test.dart index 3641faef2..d5fa43eaf 100644 --- a/thermion_dart/test/all_materials_smoke_test.dart +++ b/thermion_dart/test/all_materials_smoke_test.dart @@ -30,7 +30,7 @@ void main() async { // stencil highlight path below needs. final cube = await viewer.loadGltf( "file://${testHelper.assetsDir}/cube.glb", - vertexBufferMode: VertexBufferMode.unwelded, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.barycentrics}, addToScene: true, ); final wireframe = await app.createWireframeMaterialInstance(); diff --git a/thermion_dart/test/morph_animation_tests.dart b/thermion_dart/test/morph_animation_tests.dart index 11ebce5c0..f5b902bf6 100644 --- a/thermion_dart/test/morph_animation_tests.dart +++ b/thermion_dart/test/morph_animation_tests.dart @@ -72,7 +72,13 @@ void main() async { final originalPose = await capture(original); await viewer.destroyAsset(original); - final editable = await viewer.loadGltf(path, vertexBufferMode: VertexBufferMode.editable); + final editable = await viewer.loadGltf( + path, + requiredGeometryCapabilities: const { + SceneAssetGeometryCapability.writableVertices, + SceneAssetGeometryCapability.preservedTopology, + }, + ); final editableVertexBuffer = editable.getVertexBuffer()!; expect(editableVertexBuffer.supportsSetBufferAt, isTrue); expect(editableVertexBuffer.storageMode, VertexBufferStorageMode.direct); diff --git a/thermion_dart/test/overlay_tests.dart b/thermion_dart/test/overlay_tests.dart index b5546399e..ab6df6537 100644 --- a/thermion_dart/test/overlay_tests.dart +++ b/thermion_dart/test/overlay_tests.dart @@ -230,7 +230,7 @@ void main() async { isA().having( (e) => e.toString(), 'message', - contains('vertexBufferMode: VertexBufferMode.unwelded'), + contains('requiredGeometryCapabilities containing flatShading'), ), ); @@ -238,16 +238,30 @@ void main() async { await expectLater(viewer.view.setStencilHighlight(original), preservedGeometryMatcher); await expectLater(original.setFlatShading(true), unweldedMatcher); + await expectLater( + viewer.loadGltf( + "file://${testHelper.assetsDir}/cube.glb", + requiredGeometryCapabilities: const { + SceneAssetGeometryCapability.preservedTopology, + SceneAssetGeometryCapability.flatShading, + }, + ), + throwsArgumentError, + ); + // Editable assets preserve reusable vertex/index buffers, so the // POSITION-only silhouette pass works without barycentrics. They still // cannot swap the tangent BufferObjects required by flat shading. final editable = await viewer.loadGltf( "file://${testHelper.assetsDir}/cube.glb", - vertexBufferMode: VertexBufferMode.editable, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.preservedGeometry}, addToScene: true, ); expect(editable.getVertexBuffer(), isNotNull); + expect(editable.getVertexBuffer()!.supportsSetBufferAt, isTrue); expect(editable.geometryCapabilities, contains(SceneAssetGeometryCapability.preservedGeometry)); + expect(editable.geometryCapabilities, contains(SceneAssetGeometryCapability.writableVertices)); + expect(editable.geometryCapabilities, contains(SceneAssetGeometryCapability.preservedTopology)); await viewer.view.setStencilHighlight(editable); await viewer.view.removeStencilHighlight(editable); await expectLater(editable.setFlatShading(true), unweldedMatcher); diff --git a/thermion_dart/test/view_tests.dart b/thermion_dart/test/view_tests.dart index a47e868ad..e9f7dfb20 100644 --- a/thermion_dart/test/view_tests.dart +++ b/thermion_dart/test/view_tests.dart @@ -652,7 +652,7 @@ void main() async { // Load FlightHelmet, a multi-mesh glTF asset final asset = await result.viewer.loadGltf( p.join(testHelper.assetsDir, "FlightHelmet", "FlightHelmet.gltf"), - vertexBufferMode: VertexBufferMode.unwelded, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.preservedGeometry}, ); expect(asset, isNotNull); diff --git a/thermion_dart/test/wireframe_renderable_test.dart b/thermion_dart/test/wireframe_renderable_test.dart index 3577e7858..e778dc414 100644 --- a/thermion_dart/test/wireframe_renderable_test.dart +++ b/thermion_dart/test/wireframe_renderable_test.dart @@ -10,27 +10,35 @@ void main() async { await ViewerBuilder(testHelper).addSun().setCameraPosition(Vector3(0, 1, 1.5)).execute((result) async { final original = await result.viewer.loadGltf( "file://${testHelper.assetsDir}/FlightHelmet/FlightHelmet.gltf", - vertexBufferMode: VertexBufferMode.original, addToScene: true, ); // Golden artifact names are stable IDs; keep the legacy names even when // the public API terminology changes. await testHelper.capture(result.viewer.view, "rebuildVertices_false"); + expect(original.geometryCapabilities, isEmpty); await result.viewer.removeFromScene(original); final rebuilt = await result.viewer.loadGltf( "file://${testHelper.assetsDir}/FlightHelmet/FlightHelmet.gltf", - vertexBufferMode: VertexBufferMode.unwelded, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.barycentrics}, addToScene: true, ); + expect( + rebuilt.geometryCapabilities, + containsAll(const { + SceneAssetGeometryCapability.flatShading, + SceneAssetGeometryCapability.barycentrics, + SceneAssetGeometryCapability.preservedGeometry, + }), + ); + expect(rebuilt.geometryCapabilities, isNot(contains(SceneAssetGeometryCapability.writableVertices))); + expect(rebuilt.geometryCapabilities, contains(SceneAssetGeometryCapability.uniqueTriangleCorners)); final unweldedVertexBuffer = rebuilt.getVertexBuffer()!; expect(unweldedVertexBuffer.supportsSetBufferAt, isFalse); await expectLater( unweldedVertexBuffer.setBufferAt(0, Float32List(0)), - throwsA( - isA().having((error) => error.toString(), 'message', contains('VertexBufferMode.editable')), - ), + throwsA(isA().having((error) => error.toString(), 'message', contains('writableVertices'))), ); await testHelper.capture(result.viewer.view, "rebuildVertices_true"); @@ -58,7 +66,7 @@ void main() async { final flatAsset = await result.viewer.loadGltf( "file://${testHelper.assetsDir}/FlightHelmet/FlightHelmet.gltf", - vertexBufferMode: VertexBufferMode.unwelded, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.flatShading}, addToScene: true, ); @@ -81,7 +89,7 @@ void main() async { "file://${testHelper.assetsDir}/cube.glb", addToScene: true, initialInstances: 2, - vertexBufferMode: VertexBufferMode.unwelded, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.barycentrics}, ); final instance2 = await asset.createInstance(); From 78523939243f1bb1a05da058d695d85705814fa3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 26 Aug 2026 09:53:34 +0000 Subject: [PATCH 12/14] chore: update generated artifacts + format (CI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with GitHub Actions --- thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart index c77cf7f5e..4405d7405 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart @@ -635,7 +635,7 @@ external ffi.Pointer SceneAsset_createFromBuffers( ffi.Pointer, ffi.Pointer, ffi.Pointer, - ffi.UnsignedInt, + ffi.Uint32, ) >(isLeaf: true) external ffi.Pointer SceneAsset_createFromFilamentAsset( @@ -2748,7 +2748,7 @@ external void SceneAsset_destroyRenderThread( ffi.Pointer, ffi.Pointer, ffi.Pointer, - ffi.UnsignedInt, + ffi.Uint32, ffi.Pointer)>>, ) >(isLeaf: true) From a5ca6141abc65bd8e918dae4d15e6b15ab396ae7 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Wed, 26 Aug 2026 22:19:48 +0800 Subject: [PATCH 13/14] fix: make geometry capabilities truthful --- CHANGELOG.md | 4 +- .../src/bindings/src/thermion_dart_ffi.g.dart | 14 +- .../src/thermion_dart_js_interop.g.dart | 15 ++ .../src/implementation/ffi_asset.dart | 14 +- .../src/implementation/ffi_filament_app.dart | 29 +-- .../filament/src/implementation/ffi_view.dart | 10 +- .../lib/src/filament/src/interface/asset.dart | 16 +- .../filament/src/interface/filament_app.dart | 2 +- .../lib/src/filament/src/interface/view.dart | 2 +- .../src/ffi/src/thermion_viewer_ffi.dart | 3 +- .../src/viewer/src/thermion_viewer_base.dart | 4 +- .../native/include/c_api/APIBoundaryTypes.h | 11 +- .../native/include/c_api/TSceneAsset.h | 1 + .../include/scene/GeometrySceneAsset.hpp | 6 +- .../native/include/scene/GltfSceneAsset.hpp | 16 +- .../include/scene/GltfSceneAssetInstance.hpp | 1 + .../native/include/scene/SceneAsset.hpp | 4 + .../native/src/c_api/TSceneAsset.cpp | 15 +- .../native/src/scene/GltfSceneAsset.cpp | 207 +++++++++++++----- .../src/scene/GltfSceneAssetInstance.cpp | 5 + thermion_dart/test/geometry_tests.dart | 7 + thermion_dart/test/overlay_tests.dart | 16 +- thermion_dart/test/view_tests.dart | 2 +- .../test/wireframe_renderable_test.dart | 103 ++++++++- 24 files changed, 380 insertions(+), 127 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52fa8dfac..c9e1cf594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,8 +58,8 @@ - Replace the `rebuildVertices` in `ThermionViewer.loadGltf`, `ThermionViewer.loadGltfFromBuffer`, and `FilamentApp.loadGltfFromBuffer` with `requiredGeometryCapabilities`. An empty set leaves gltfio geometry untouched; - requesting `barycentrics` or `flatShading` creates per-triangle vertices, while - requesting `writableVertices`, `preservedTopology`, or `preservedGeometry` + requesting `barycentrics` or `uniqueTriangleCorners` creates per-triangle vertices, while + requesting `writableVertices`, `preservedTopology`, or `accessibleGeometryBuffers` preserves source vertex order, indices, and morph-target compatibility in directly writable buffers. Assets report the complete capability set actually provided through diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart index 4405d7405..05c7cd3b8 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_ffi.g.dart @@ -703,6 +703,9 @@ external Aabb3 SceneAsset_getBoundingBox(ffi.Pointer asset); @ffi.Native)>(isLeaf: true) external int SceneAsset_getGeometryCapabilities(ffi.Pointer asset); +@ffi.Native)>(isLeaf: true) +external bool SceneAsset_supportsFlatShading(ffi.Pointer asset); + @ffi.Native Function(ffi.Pointer, ffi.Int)>(isLeaf: true) external ffi.Pointer SceneAsset_getVertexBuffer( ffi.Pointer tSceneAsset, @@ -5029,12 +5032,11 @@ sealed class TVertexBufferStorageMode { sealed class TSceneAssetGeometryCapability { static const SCENE_ASSET_GEOMETRY_CAPABILITY_NONE = 0; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING = 1; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 2; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES = 4; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY = 8; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY = 16; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS = 32; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 1; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES = 2; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS = 4; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY = 8; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS = 16; } sealed class TFeatureLevel { diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart index 2af46eed5..c3ab65071 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart @@ -325,6 +325,7 @@ extension type GeneratedBindings(NativeLibrary _) implements JSObject { ); external void _SceneAsset_getBoundingBox(Pointer Aabb3_out, Pointer asset); external int _SceneAsset_getGeometryCapabilities(Pointer asset); + external int _SceneAsset_supportsFlatShading(Pointer asset); external Pointer _SceneAsset_getVertexBuffer(Pointer tSceneAsset, int primitiveIndex); external int _SceneAsset_getVertexBufferStorageMode(Pointer tSceneAsset, int primitiveIndex); external Pointer _SceneAsset_getIndexBuffer(Pointer tSceneAsset, int primitiveIndex); @@ -3505,6 +3506,11 @@ int SceneAsset_getGeometryCapabilities(Pointer asset) { return result; } +bool SceneAsset_supportsFlatShading(Pointer asset) { + final result = GeneratedBindings.instance._SceneAsset_supportsFlatShading(asset.cast()); + return result != 0; +} + Pointer SceneAsset_getVertexBuffer(Pointer tSceneAsset, int primitiveIndex) { final result = GeneratedBindings.instance._SceneAsset_getVertexBuffer(tSceneAsset.cast(), primitiveIndex); return Pointer(result); @@ -10363,6 +10369,15 @@ sealed class TVertexBufferStorageMode { static const VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS = 2; } +sealed class TSceneAssetGeometryCapability { + static const SCENE_ASSET_GEOMETRY_CAPABILITY_NONE = 0; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 1; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES = 2; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS = 4; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY = 8; + static const SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS = 16; +} + extension Aabb3Ext on Pointer { Aabb3 toDart() { return Aabb3(this); diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart index 9a7fb9998..8f99fe361 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_asset.dart @@ -41,14 +41,12 @@ class FFIAsset extends ThermionAsset> { Set get geometryCapabilities { final bits = SceneAsset_getGeometryCapabilities(asset); return { - if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING != 0) - SceneAssetGeometryCapability.flatShading, if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS != 0) SceneAssetGeometryCapability.barycentrics, if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES != 0) SceneAssetGeometryCapability.writableVertices, - if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY != 0) - SceneAssetGeometryCapability.preservedGeometry, + if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS != 0) + SceneAssetGeometryCapability.accessibleGeometryBuffers, if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY != 0) SceneAssetGeometryCapability.preservedTopology, if (bits & TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS != 0) @@ -56,6 +54,9 @@ class FFIAsset extends ThermionAsset> { }; } + @override + bool get supportsFlatShading => SceneAsset_supportsFlatShading(asset); + @override SceneAssetType get type { final t = SceneAsset_getType(asset); @@ -302,10 +303,11 @@ class FFIAsset extends ThermionAsset> { // unwelded geometry. Editable geometry also has preserved buffers, but it // deliberately uses ordinary writable streams and cannot perform this // swap. - if (!geometryCapabilities.contains(SceneAssetGeometryCapability.flatShading)) { + if (!supportsFlatShading) { throw StateError( "setFlatShading requires unwelded geometry. " - "Load it with requiredGeometryCapabilities containing flatShading.", + "Load it with requiredGeometryCapabilities containing " + "uniqueTriangleCorners.", ); } await withVoidCallback((requestId, cb) => SceneAsset_setFlatShadingRenderThread(asset, flatShading, requestId, cb)); diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart index 58b1de82c..7a31cc966 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_filament_app.dart @@ -36,14 +36,12 @@ int _geometryCapabilitiesToNative(Set capabilities var bits = TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_NONE; for (final capability in capabilities) { bits |= switch (capability) { - SceneAssetGeometryCapability.flatShading => - TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING, SceneAssetGeometryCapability.barycentrics => TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS, SceneAssetGeometryCapability.writableVertices => TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES, - SceneAssetGeometryCapability.preservedGeometry => - TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY, + SceneAssetGeometryCapability.accessibleGeometryBuffers => + TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS, SceneAssetGeometryCapability.preservedTopology => TSceneAssetGeometryCapability.SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY, SceneAssetGeometryCapability.uniqueTriangleCorners => @@ -55,7 +53,6 @@ int _geometryCapabilitiesToNative(Set capabilities void _validateRequiredGeometryCapabilities(Set capabilities) { final requiresUnwelded = - capabilities.contains(SceneAssetGeometryCapability.flatShading) || capabilities.contains(SceneAssetGeometryCapability.barycentrics) || capabilities.contains(SceneAssetGeometryCapability.uniqueTriangleCorners); final requiresPreservedTopology = @@ -66,7 +63,7 @@ void _validateRequiredGeometryCapabilities(Set cap capabilities, 'requiredGeometryCapabilities', 'writableVertices or preservedTopology cannot be combined with ' - 'flatShading, barycentrics, or uniqueTriangleCorners', + 'barycentrics or uniqueTriangleCorners', ); } } @@ -1165,10 +1162,11 @@ class FFIFilamentApp extends FilamentApp { Set requiredGeometryCapabilities = const {}, String? resourceUri, }) async { + final geometryRequirements = Set.unmodifiable(requiredGeometryCapabilities); if (initialInstances <= 0) { throw Exception("initialInstances must be at least 1"); } - _validateRequiredGeometryCapabilities(requiredGeometryCapabilities); + _validateRequiredGeometryCapabilities(geometryRequirements); _logger.info( "Loading glTF from buffer (${data.lengthInBytes} bytes)" " with resourceUri ${resourceUri}", @@ -1267,25 +1265,28 @@ class FFIFilamentApp extends FilamentApp { gltfAssetLoader, nameComponentManager, filamentAsset, - _geometryCapabilitiesToNative(requiredGeometryCapabilities), + _geometryCapabilitiesToNative(geometryRequirements), cb, ), ); - if (asset == nullptr) { - throw Exception("Unknown error loading glTF asset. See logs for details."); - } - await withVoidCallback( (requestId, cb) => GltfResourceLoader_destroyRenderThread(engine, gltfResourceLoader, requestId, cb), ); + if (asset == nullptr) { + throw StateError( + 'Failed to load a glTF asset satisfying the required geometry ' + 'capabilities: $geometryRequirements. See native logs for details.', + ); + } + final ffiAsset = FFIAsset(asset, app: this); - if (!ffiAsset.geometryCapabilities.containsAll(requiredGeometryCapabilities)) { + if (!ffiAsset.geometryCapabilities.containsAll(geometryRequirements)) { await withVoidCallback((requestId, cb) => SceneAsset_destroyRenderThread(asset, requestId, cb)); throw StateError( 'The loaded asset does not provide all required geometry ' - 'capabilities. Required: $requiredGeometryCapabilities; provided: ' + 'capabilities. Required: $geometryRequirements; provided: ' '${ffiAsset.geometryCapabilities}.', ); } diff --git a/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart b/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart index 3997190b4..ec893497a 100644 --- a/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart +++ b/thermion_dart/lib/src/filament/src/implementation/ffi_view.dart @@ -575,13 +575,13 @@ class FFIView extends View> { final ffiGeoAsset = geoAsset as FFIAsset; // The silhouette pass reuses the asset's vertex and index buffers but only - // consumes POSITION. It therefore needs preserved geometry, not the + // consumes POSITION. It therefore needs accessible geometry buffers, not the // barycentric coordinates used by wireframe and flat-shading features. - if (!ffiGeoAsset.geometryCapabilities.contains(SceneAssetGeometryCapability.preservedGeometry)) { + if (!ffiGeoAsset.geometryCapabilities.contains(SceneAssetGeometryCapability.accessibleGeometryBuffers)) { throw StateError( - "setStencilHighlight requires preserved geometry. " + "setStencilHighlight requires accessible geometry buffers. " "Load the asset with requiredGeometryCapabilities containing " - "preservedGeometry.", + "accessibleGeometryBuffers.", ); } @@ -595,7 +595,7 @@ class FFIView extends View> { // The asset has preserved geometry, but this particular entity has no // rebuilt buffers (e.g. its primitives are all lines/points). _logger.warning( - "Stencil highlight: no preserved geometry for entity $entity " + "Stencil highlight: no accessible geometry buffers for entity $entity " "(its primitives are all non-triangles and have no rebuilt buffers).", ); return; diff --git a/thermion_dart/lib/src/filament/src/interface/asset.dart b/thermion_dart/lib/src/filament/src/interface/asset.dart index d39278760..94198d66b 100644 --- a/thermion_dart/lib/src/filament/src/interface/asset.dart +++ b/thermion_dart/lib/src/filament/src/interface/asset.dart @@ -5,24 +5,21 @@ import 'package:thermion_dart/thermion_dart.dart'; export 'geometry.dart'; -/// A geometry operation guaranteed to be supported by a scene asset. +/// A geometry property guaranteed to be available on a scene asset. /// /// The same values can be supplied to asset loaders as requirements. Loaders /// may provide additional capabilities when they share the same geometry /// representation. enum SceneAssetGeometryCapability { - /// Smooth and per-face tangent frames can be selected at runtime. - flatShading, - /// Triangle-corner barycentric coordinates are available to materials. barycentrics, /// Vertex attributes can be updated through [VertexBuffer.setBufferAt]. writableVertices, - /// Thermion retains reusable vertex and index buffers for operations such as + /// Thermion exposes reusable vertex and index buffers for operations such as /// stencil highlighting. - preservedGeometry, + accessibleGeometryBuffers, /// Source vertex order and triangle indices are preserved. preservedTopology, @@ -99,6 +96,10 @@ abstract class ThermionAsset extends NativeHandle { return const {}; } + /// Whether [setFlatShading] can switch this asset between smooth and + /// per-face tangent frames at runtime. + bool get supportsFlatShading => false; + // The top-most entity in the hierarchy (if this is a glTF asset, this // entity will have a transform that sits at the top of the transform // hierarchy but is not itself renderable. @@ -152,8 +153,7 @@ abstract class ThermionAsset extends NativeHandle { } // Toggle between flat (per-face) and smooth (per-vertex) shading. - // Throws unless [geometryCapabilities] contains - // [SceneAssetGeometryCapability.flatShading]. + // Throws unless [supportsFlatShading] is true. Future setFlatShading(bool flatShading) { throw UnimplementedError(); } diff --git a/thermion_dart/lib/src/filament/src/interface/filament_app.dart b/thermion_dart/lib/src/filament/src/interface/filament_app.dart index 6ee820365..949a3db0b 100644 --- a/thermion_dart/lib/src/filament/src/interface/filament_app.dart +++ b/thermion_dart/lib/src/filament/src/interface/filament_app.dart @@ -344,7 +344,7 @@ abstract class FilamentApp { /// Loads a glTF asset from a raw memory buffer. /// - /// [requiredGeometryCapabilities] describes the operations that the loaded + /// [requiredGeometryCapabilities] describes the geometry properties that the loaded /// asset must support. The loader may provide a compatible superset, which /// is reported by [ThermionAsset.geometryCapabilities]. Incompatible /// requirements throw [ArgumentError]. diff --git a/thermion_dart/lib/src/filament/src/interface/view.dart b/thermion_dart/lib/src/filament/src/interface/view.dart index f83bd53ed..6f12d0aa3 100644 --- a/thermion_dart/lib/src/filament/src/interface/view.dart +++ b/thermion_dart/lib/src/filament/src/interface/view.dart @@ -483,7 +483,7 @@ abstract class View extends NativeHandle { /// outlines. /// /// Throws if the asset (or [geometrySource]) does not provide - /// [SceneAssetGeometryCapability.preservedGeometry]. + /// [SceneAssetGeometryCapability.accessibleGeometryBuffers]. /// /// The [scale] parameter is deprecated and ignored; use [outlineWidth] instead. Future setStencilHighlight( diff --git a/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart b/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart index 38329618b..0eed2b4bb 100644 --- a/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart +++ b/thermion_dart/lib/src/viewer/src/ffi/src/thermion_viewer_ffi.dart @@ -520,6 +520,7 @@ class ThermionViewerFFI extends ThermionViewer { String? resourceUri, bool loadAsync = false, }) async { + final geometryRequirements = Set.unmodifiable(requiredGeometryCapabilities); final data = await _app.loadResource(path); if (resourceUri == null) { var normalised = path.replaceAll("\\", "/"); @@ -536,7 +537,7 @@ class ThermionViewerFFI extends ThermionViewer { addToScene: addToScene, initialInstances: initialInstances, releaseSourceData: releaseSourceData, - requiredGeometryCapabilities: requiredGeometryCapabilities, + requiredGeometryCapabilities: geometryRequirements, resourceUri: resourceUri, loadResourcesAsync: loadAsync, ); diff --git a/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart b/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart index 44b8e5464..80236f20b 100644 --- a/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart +++ b/thermion_dart/lib/src/viewer/src/thermion_viewer_base.dart @@ -167,7 +167,7 @@ abstract class ThermionViewer { // generally more efficient than dynamically instantating at a later time. // // If [requiredGeometryCapabilities] contains [SceneAssetGeometryCapability.barycentrics] - // or [SceneAssetGeometryCapability.flatShading], vertex buffers are rebuilt + // or [SceneAssetGeometryCapability.uniqueTriangleCorners], vertex buffers are rebuilt // after loading with a superset of attributes (POSITION, TANGENTS, // UV0, CUSTOM0, and // optionally BONE_INDICES/BONE_WEIGHTS). Vertices are unwelded so each @@ -186,7 +186,7 @@ abstract class ThermionViewer { // [SceneAssetGeometryCapability.writableVertices] and // [SceneAssetGeometryCapability.preservedTopology] cannot be combined with // [SceneAssetGeometryCapability.barycentrics] or - // [SceneAssetGeometryCapability.flatShading]. The returned asset reports the + // [SceneAssetGeometryCapability.uniqueTriangleCorners]. The returned asset reports the // complete set actually provided through [ThermionAsset.geometryCapabilities]. // // If [loadResourcesAsync] is true, resources (textures, materials, etc) will diff --git a/thermion_dart/native/include/c_api/APIBoundaryTypes.h b/thermion_dart/native/include/c_api/APIBoundaryTypes.h index 3a0ecc988..38c3b061a 100644 --- a/thermion_dart/native/include/c_api/APIBoundaryTypes.h +++ b/thermion_dart/native/include/c_api/APIBoundaryTypes.h @@ -129,12 +129,11 @@ extern "C" enum TSceneAssetGeometryCapability { SCENE_ASSET_GEOMETRY_CAPABILITY_NONE = 0, - SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING = 1 << 0, - SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 1 << 1, - SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES = 1 << 2, - SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY = 1 << 3, - SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY = 1 << 4, - SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS = 1 << 5 + SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 1 << 0, + SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES = 1 << 1, + SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS = 1 << 2, + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY = 1 << 3, + SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS = 1 << 4 }; typedef enum TSceneAssetGeometryCapability TSceneAssetGeometryCapability; diff --git a/thermion_dart/native/include/c_api/TSceneAsset.h b/thermion_dart/native/include/c_api/TSceneAsset.h index a750e97ee..5e16d14d2 100644 --- a/thermion_dart/native/include/c_api/TSceneAsset.h +++ b/thermion_dart/native/include/c_api/TSceneAsset.h @@ -43,6 +43,7 @@ extern "C" EMSCRIPTEN_KEEPALIVE TSceneAsset * SceneAsset_createInstance(TSceneAsset *asset, TMaterialInstance **materialInstances, int materialInstanceCount); EMSCRIPTEN_KEEPALIVE Aabb3 SceneAsset_getBoundingBox(TSceneAsset *asset); EMSCRIPTEN_KEEPALIVE uint32_t SceneAsset_getGeometryCapabilities(TSceneAsset *asset); + EMSCRIPTEN_KEEPALIVE bool SceneAsset_supportsFlatShading(TSceneAsset *asset); EMSCRIPTEN_KEEPALIVE TVertexBuffer *SceneAsset_getVertexBuffer(TSceneAsset *tSceneAsset, int primitiveIndex); EMSCRIPTEN_KEEPALIVE TVertexBufferStorageMode SceneAsset_getVertexBufferStorageMode(TSceneAsset *tSceneAsset, int primitiveIndex); EMSCRIPTEN_KEEPALIVE TIndexBuffer *SceneAsset_getIndexBuffer(TSceneAsset *tSceneAsset, int primitiveIndex); diff --git a/thermion_dart/native/include/scene/GeometrySceneAsset.hpp b/thermion_dart/native/include/scene/GeometrySceneAsset.hpp index 860b6d75c..67fc18ded 100644 --- a/thermion_dart/native/include/scene/GeometrySceneAsset.hpp +++ b/thermion_dart/native/include/scene/GeometrySceneAsset.hpp @@ -63,7 +63,11 @@ namespace thermion VertexBuffer *getVertexBuffer() const { return _vertexBuffer; } IndexBuffer *getIndexBuffer() const { return _indexBuffer; } uint32_t getGeometryCapabilities() const override { - return SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY; + uint32_t capabilities = SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS; + if (_vertexBufferStorageMode == VERTEX_BUFFER_STORAGE_MODE_DIRECT) { + capabilities |= SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES; + } + return capabilities; } TVertexBufferStorageMode getVertexBufferStorageMode(size_t primitiveIndex) const override { return primitiveIndex == 0 diff --git a/thermion_dart/native/include/scene/GltfSceneAsset.hpp b/thermion_dart/native/include/scene/GltfSceneAsset.hpp index c8ff7737f..2eb056898 100644 --- a/thermion_dart/native/include/scene/GltfSceneAsset.hpp +++ b/thermion_dart/native/include/scene/GltfSceneAsset.hpp @@ -143,17 +143,13 @@ namespace thermion uint32_t getGeometryCapabilities() const override { return _geometryCapabilities; } + bool supportsFlatShading() const override { return _supportsFlatShading; } + TVertexBufferStorageMode getVertexBufferStorageMode(size_t primitiveIndex) const override { - if (primitiveIndex >= _preservedVertexBuffers.size()) { + if (primitiveIndex >= _preservedVertexBufferStorageModes.size()) { return VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; } - if ((_geometryCapabilities & SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES) != 0) { - return VERTEX_BUFFER_STORAGE_MODE_DIRECT; - } - if ((_geometryCapabilities & SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS) != 0) { - return VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS; - } - return VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; + return _preservedVertexBufferStorageModes[primitiveIndex]; } /// Rebuild all mesh primitives with a superset vertex buffer layout @@ -161,7 +157,7 @@ namespace thermion /// When [preserveTopology] is false, each triangle receives unique /// vertices for barycentric wireframe rendering. When true, source /// vertex order and indices remain compatible with glTF morph targets. - void rebuildVertexBuffers(bool preserveTopology); + bool rebuildVertexBuffers(bool preserveTopology); /// Returns false when the requested capability combination cannot be /// provided by a single rebuilt geometry representation. @@ -220,10 +216,12 @@ namespace thermion bool _sourceDataReleased = false; bool _geometryPreserved = false; bool _flatShading = false; + bool _supportsFlatShading = false; uint32_t _geometryCapabilities = SCENE_ASSET_GEOMETRY_CAPABILITY_NONE; // Buffers created by rebuildVertexBuffers, owned by this asset. std::vector _preservedVertexBuffers; + std::vector _preservedVertexBufferStorageModes; std::vector _preservedIndexBuffers; std::vector _preservedIndexCounts; std::vector _preservedBufferObjects; diff --git a/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp b/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp index 2f66ca33c..8793d6df3 100644 --- a/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp +++ b/thermion_dart/native/include/scene/GltfSceneAssetInstance.hpp @@ -141,6 +141,7 @@ namespace thermion } uint32_t getGeometryCapabilities() const override; + bool supportsFlatShading() const override; TVertexBufferStorageMode getVertexBufferStorageMode(size_t primitiveIndex) const override; size_t getBoneCount(size_t skinIndex) const override; diff --git a/thermion_dart/native/include/scene/SceneAsset.hpp b/thermion_dart/native/include/scene/SceneAsset.hpp index 85a2c10a8..d70dc911a 100644 --- a/thermion_dart/native/include/scene/SceneAsset.hpp +++ b/thermion_dart/native/include/scene/SceneAsset.hpp @@ -61,6 +61,10 @@ class SceneAsset { return SCENE_ASSET_GEOMETRY_CAPABILITY_NONE; } + virtual bool supportsFlatShading() const { + return false; + } + virtual TVertexBufferStorageMode getVertexBufferStorageMode(size_t primitiveIndex) const { return VERTEX_BUFFER_STORAGE_MODE_UNKNOWN; } diff --git a/thermion_dart/native/src/c_api/TSceneAsset.cpp b/thermion_dart/native/src/c_api/TSceneAsset.cpp index 9a7010b91..cb83d1c62 100644 --- a/thermion_dart/native/src/c_api/TSceneAsset.cpp +++ b/thermion_dart/native/src/c_api/TSceneAsset.cpp @@ -93,6 +93,14 @@ extern "C" requiredGeometryCapabilities ); + if ((sceneAsset->getGeometryCapabilities() & requiredGeometryCapabilities) != + requiredGeometryCapabilities) { + Log("Failed to provide required geometry capabilities: requested 0x%x, provided 0x%x", + requiredGeometryCapabilities, sceneAsset->getGeometryCapabilities()); + delete sceneAsset; + return nullptr; + } + return reinterpret_cast(sceneAsset); } @@ -239,6 +247,10 @@ extern "C" return reinterpret_cast(tSceneAsset)->getGeometryCapabilities(); } + EMSCRIPTEN_KEEPALIVE bool SceneAsset_supportsFlatShading(TSceneAsset *tSceneAsset) { + return reinterpret_cast(tSceneAsset)->supportsFlatShading(); + } + EMSCRIPTEN_KEEPALIVE TVertexBuffer *SceneAsset_getVertexBuffer(TSceneAsset *tSceneAsset, int primitiveIndex) { auto *asset = reinterpret_cast(tSceneAsset); if (asset->getType() == SceneAsset::SceneAssetType::Geometry) { @@ -313,7 +325,8 @@ extern "C" Log("setFlatShading only supported on glTF assets"); return; } - auto *gltfAsset = reinterpret_cast(tSceneAsset); + auto *gltfAsset = reinterpret_cast( + asset->isInstance() ? asset->getInstanceOwner() : asset); gltfAsset->setFlatShading(flatShading); } diff --git a/thermion_dart/native/src/scene/GltfSceneAsset.cpp b/thermion_dart/native/src/scene/GltfSceneAsset.cpp index 9f777625a..21e3f3d85 100644 --- a/thermion_dart/native/src/scene/GltfSceneAsset.cpp +++ b/thermion_dart/native/src/scene/GltfSceneAsset.cpp @@ -48,31 +48,32 @@ namespace thermion { const bool requiresUnwelded = (requiredGeometryCapabilities & - (SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING | - SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | + (SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS)) != 0; const bool requiresPreserved = (requiredGeometryCapabilities & (SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES | SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY | - SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY)) != 0; + SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS)) != 0; if (requiresUnwelded) { - _geometryCapabilities = - SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING | - SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | - SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY | - SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS; - rebuildVertexBuffers(false); + if (rebuildVertexBuffers(false)) { + _geometryCapabilities = + SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | + SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS | + SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS; + _supportsFlatShading = true; + } } else if (requiresPreserved) { - _geometryCapabilities = - SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES | - SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY | - SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY; - rebuildVertexBuffers(true); + if (rebuildVertexBuffers(true)) { + _geometryCapabilities = + SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES | + SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS | + SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY; + } } for (int i = 0; i < asset->getAssetInstanceCount(); i++) { @@ -84,10 +85,9 @@ namespace thermion bool GltfSceneAsset::supportsRequiredGeometryCapabilities(uint32_t requiredGeometryCapabilities) { constexpr uint32_t supported = - SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING | SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES | - SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_GEOMETRY | + SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS | SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY | SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS; if ((requiredGeometryCapabilities & ~supported) != 0) @@ -97,8 +97,7 @@ namespace thermion const bool requiresUnwelded = (requiredGeometryCapabilities & - (SCENE_ASSET_GEOMETRY_CAPABILITY_FLAT_SHADING | - SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | + (SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS | SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS)) != 0; const bool requiresPreservedTopology = (requiredGeometryCapabilities & @@ -112,23 +111,23 @@ namespace thermion _instances.clear(); for (auto *vb : _preservedVertexBuffers) { - _engine->destroy(vb); + if (vb) _engine->destroy(vb); } for (auto *ib : _preservedIndexBuffers) { - _engine->destroy(ib); + if (ib) _engine->destroy(ib); } for (auto *bo : _preservedBufferObjects) { - _engine->destroy(bo); + if (bo) _engine->destroy(bo); } for (auto *bo : _smoothTangentBOs) { - _engine->destroy(bo); + if (bo) _engine->destroy(bo); } for (auto *bo : _flatTangentBOs) { - _engine->destroy(bo); + if (bo) _engine->destroy(bo); } releaseSourceData(); _assetLoader->destroyAsset(_asset); @@ -318,20 +317,20 @@ namespace thermion return nullptr; } - void GltfSceneAsset::rebuildVertexBuffers(bool preserveTopology) + bool GltfSceneAsset::rebuildVertexBuffers(bool preserveTopology) { const bool preserveSourceTopology = preserveTopology; auto *sourceData = (const cgltf_data *)_asset->getSourceAsset(); if (!sourceData) { Log("rebuildVertexBuffers: source data already released"); - return; + return false; } if (_geometryPreserved) { Log("rebuildVertexBuffers: already called"); - return; + return false; } std::vector meshEntries; @@ -343,6 +342,18 @@ namespace thermion auto *allEntities = _asset->getEntities(); size_t allEntityCount = _asset->getEntityCount(); auto &rm = _engine->getRenderableManager(); + bool allPrimitivesRebuilt = true; + size_t rebuiltPrimitiveCount = 0; + + auto appendPlaceholder = [&]() { + _preservedVertexBuffers.push_back(nullptr); + _preservedVertexBufferStorageModes.push_back(VERTEX_BUFFER_STORAGE_MODE_UNKNOWN); + _preservedIndexBuffers.push_back(nullptr); + _preservedIndexCounts.push_back(0); + _smoothTangentBOs.push_back(nullptr); + _flatTangentBOs.push_back(nullptr); + allPrimitivesRebuilt = false; + }; TRACE("rebuildVertexBuffers: meshEntries=%zu entityCount=%zu nodes=%zu", meshEntries.size(), allEntityCount, sourceData->nodes_count); @@ -384,18 +395,26 @@ namespace thermion TRACE("rebuildVertexBuffers: no mesh (named or fallback) for entity %zu — padding %zu placeholder slots", ei, skippedPrimCount); for (size_t pi = 0; pi < skippedPrimCount; pi++) { - _preservedVertexBuffers.push_back(nullptr); - _preservedIndexBuffers.push_back(nullptr); - _preservedIndexCounts.push_back(0); - _smoothTangentBOs.push_back(nullptr); - _flatTangentBOs.push_back(nullptr); + appendPlaceholder(); } continue; } } - for (cgltf_size pi = 0; pi < mesh->primitives_count; pi++) + const size_t renderablePrimitiveCount = rm.getPrimitiveCount(ri); + if (mesh->primitives_count != renderablePrimitiveCount) { + allPrimitivesRebuilt = false; + TRACE("rebuildVertexBuffers: mesh/renderable primitive count mismatch at entity %zu (%zu vs %zu)", + ei, static_cast(mesh->primitives_count), renderablePrimitiveCount); + } + for (size_t pi = 0; pi < renderablePrimitiveCount; pi++) + { + if (pi >= mesh->primitives_count) + { + appendPlaceholder(); + continue; + } const cgltf_primitive &prim = mesh->primitives[pi]; if (prim.type != cgltf_primitive_type_triangles) @@ -407,11 +426,7 @@ namespace thermion // this primitive. Callers (e.g. setStencilHighlight) // null-check getPreservedVertexBuffer. TRACE("rebuildVertexBuffers: placeholder for non-triangle primitive at entity %zu prim %zu", ei, pi); - _preservedVertexBuffers.push_back(nullptr); - _preservedIndexBuffers.push_back(nullptr); - _preservedIndexCounts.push_back(0); - _smoothTangentBOs.push_back(nullptr); - _flatTangentBOs.push_back(nullptr); + appendPlaceholder(); continue; } @@ -420,6 +435,8 @@ namespace thermion const cgltf_accessor *nrmAccessor = nullptr; const cgltf_accessor *tanAccessor = nullptr; const cgltf_accessor *uvAccessor = nullptr; + const cgltf_accessor *uv1Accessor = nullptr; + const cgltf_accessor *colorAccessor = nullptr; const cgltf_accessor *jointsAccessor = nullptr; const cgltf_accessor *weightsAccessor = nullptr; @@ -437,8 +454,14 @@ namespace thermion tanAccessor = prim.attributes[ai].data; break; case cgltf_attribute_type_texcoord: - if (!uvAccessor) + if (prim.attributes[ai].index == 0) uvAccessor = prim.attributes[ai].data; + else if (prim.attributes[ai].index == 1) + uv1Accessor = prim.attributes[ai].data; + break; + case cgltf_attribute_type_color: + if (prim.attributes[ai].index == 0) + colorAccessor = prim.attributes[ai].data; break; case cgltf_attribute_type_joints: if (!jointsAccessor) @@ -452,8 +475,10 @@ namespace thermion break; } } - if (!posAccessor) + if (!posAccessor) { + appendPlaceholder(); continue; + } // --- Read indices --- std::vector indices; @@ -475,8 +500,21 @@ namespace thermion } } - if (indices.size() < 3 || indices.size() % 3 != 0) + if (indices.size() < 3 || indices.size() % 3 != 0) { + appendPlaceholder(); + continue; + } + bool indicesInRange = true; + for (const auto index : indices) { + if (index >= posAccessor->count) { + indicesInRange = false; + break; + } + } + if (!indicesInRange) { + appendPlaceholder(); continue; + } uint32_t triangleCount = (uint32_t)(indices.size() / 3); uint32_t newVertexCount = preserveSourceTopology @@ -506,6 +544,24 @@ namespace thermion cgltf_accessor_unpack_floats(uvAccessor, srcUVs.data(), srcUVs.size()); } + std::vector srcUV1s; + size_t uv1Components = 0; + if (uv1Accessor) + { + uv1Components = cgltf_num_components(uv1Accessor->type); + srcUV1s.resize(uv1Accessor->count * uv1Components); + cgltf_accessor_unpack_floats(uv1Accessor, srcUV1s.data(), srcUV1s.size()); + } + + std::vector srcColors; + size_t colorComponents = 0; + if (colorAccessor) + { + colorComponents = cgltf_num_components(colorAccessor->type); + srcColors.resize(colorAccessor->count * colorComponents); + cgltf_accessor_unpack_floats(colorAccessor, srcColors.data(), srcColors.size()); + } + std::vector srcTangents; size_t tanComponents = 0; if (tanAccessor) @@ -539,6 +595,12 @@ namespace thermion newTangents.resize(newVertexCount * 4); } std::vector newUVs(newVertexCount * 2); + std::vector newUV1s; + if (uv1Accessor) + { + newUV1s.resize(newVertexCount * 2); + } + std::vector newColors(newVertexCount * 4, 1.0f); std::vector newBarycentrics(newVertexCount * 4); std::vector newJoints; std::vector newWeights; @@ -576,6 +638,24 @@ namespace thermion newNormals[dstIdx * 3 + 2] = 0.0f; } + // Secondary UV + if (uv1Accessor && srcIdx < uv1Accessor->count) + { + newUV1s[dstIdx * 2 + 0] = srcUV1s[srcIdx * uv1Components + 0]; + newUV1s[dstIdx * 2 + 1] = srcUV1s[srcIdx * uv1Components + 1]; + } + + // Vertex color + if (colorAccessor && srcIdx < colorAccessor->count) + { + const size_t sourceOffset = srcIdx * colorComponents; + newColors[dstIdx * 4 + 0] = srcColors[sourceOffset + 0]; + newColors[dstIdx * 4 + 1] = srcColors[sourceOffset + 1]; + newColors[dstIdx * 4 + 2] = srcColors[sourceOffset + 2]; + newColors[dstIdx * 4 + 3] = colorComponents >= 4 + ? srcColors[sourceOffset + 3] + : 1.0f; + } // UV if (uvAccessor && srcIdx < uvAccessor->count) { @@ -708,8 +788,12 @@ namespace thermion } // --- Build VertexBuffer --- - // Buffer layout: POSITION(0), TANGENTS(1), UV0(2), CUSTOM0(3), COLOR(4), [BONE_INDICES(5), BONE_WEIGHTS(6)] - uint8_t bufferCount = hasSkinning ? 7 : 5; + // Buffer layout: POSITION(0), TANGENTS(1), UV0(2), CUSTOM0(3), + // COLOR(4), [UV1], [BONE_INDICES, BONE_WEIGHTS]. + const uint8_t uv1BufferIndex = 5; + const uint8_t boneIndicesBufferIndex = uv1Accessor ? 6 : 5; + const uint8_t boneWeightsBufferIndex = boneIndicesBufferIndex + 1; + uint8_t bufferCount = 5 + (uv1Accessor ? 1 : 0) + (hasSkinning ? 2 : 0); auto vbBuilder = VertexBuffer::Builder() .vertexCount(newVertexCount) @@ -733,11 +817,17 @@ namespace thermion .attribute(VertexAttribute::CUSTOM0, 3, VertexBuffer::AttributeType::FLOAT4) .attribute(VertexAttribute::COLOR, 4, VertexBuffer::AttributeType::FLOAT4); + if (uv1Accessor) + { + vbBuilder.attribute(VertexAttribute::UV1, uv1BufferIndex, + VertexBuffer::AttributeType::FLOAT2); + } + if (hasSkinning) { vbBuilder - .attribute(VertexAttribute::BONE_INDICES, 5, VertexBuffer::AttributeType::UBYTE4) - .attribute(VertexAttribute::BONE_WEIGHTS, 6, VertexBuffer::AttributeType::FLOAT4); + .attribute(VertexAttribute::BONE_INDICES, boneIndicesBufferIndex, VertexBuffer::AttributeType::UBYTE4) + .attribute(VertexAttribute::BONE_WEIGHTS, boneWeightsBufferIndex, VertexBuffer::AttributeType::FLOAT4); } VertexBuffer *vb = vbBuilder.build(*_engine); @@ -805,23 +895,25 @@ namespace thermion size_t baryDataSize = newVertexCount * 4 * sizeof(float); uploadStream(3, newBarycentrics.data(), baryDataSize); - // Buffer 4: COLOR (dummy, all white = 1.0) + // Buffer 4: COLOR (source COLOR_0 or white when absent) size_t colorDataSize = newVertexCount * 4 * sizeof(float); - std::vector colorFloats(newVertexCount * 4); - for (uint32_t i = 0; i < newVertexCount * 4; i++) { - colorFloats[i] = 1.0f; + uploadStream(4, newColors.data(), colorDataSize); + + if (uv1Accessor) + { + const size_t uv1DataSize = newVertexCount * 2 * sizeof(float); + uploadStream(uv1BufferIndex, newUV1s.data(), uv1DataSize); } - uploadStream(4, colorFloats.data(), colorDataSize); if (hasSkinning) { // Buffer 5: BONE_INDICES size_t jointDataSize = newVertexCount * 4 * sizeof(uint8_t); - uploadStream(5, newJoints.data(), jointDataSize); + uploadStream(boneIndicesBufferIndex, newJoints.data(), jointDataSize); // Buffer 6: BONE_WEIGHTS size_t weightDataSize = newVertexCount * 4 * sizeof(float); - uploadStream(6, newWeights.data(), weightDataSize); + uploadStream(boneWeightsBufferIndex, newWeights.data(), weightDataSize); } // Editable geometry retains source indices. Unwelded geometry @@ -848,8 +940,13 @@ namespace thermion vb, ib, 0, newIndexCount); _preservedVertexBuffers.push_back(vb); + _preservedVertexBufferStorageModes.push_back( + preserveSourceTopology + ? VERTEX_BUFFER_STORAGE_MODE_DIRECT + : VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS); _preservedIndexBuffers.push_back(ib); _preservedIndexCounts.push_back(newIndexCount); + rebuiltPrimitiveCount++; TRACE("rebuildVertexBuffers: primitive %zu %s with %u vertices and %zu indices (skinned=%d)", pi, preserveSourceTopology ? "preserved topology" : "unwelded", @@ -896,7 +993,8 @@ namespace thermion } } - _geometryPreserved = true; + _geometryPreserved = rebuiltPrimitiveCount > 0; + return allPrimitivesRebuilt && rebuiltPrimitiveCount > 0; } int GltfSceneAsset::getPrimitiveOffsetForEntity(utils::Entity entity) const @@ -911,6 +1009,11 @@ namespace thermion void GltfSceneAsset::setFlatShading(bool flatShading) { + if (!_supportsFlatShading) + { + Log("setFlatShading called on an asset without flat-shading support"); + return; + } if (flatShading == _flatShading) return; _flatShading = flatShading; diff --git a/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp b/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp index 4127573ec..b9b443333 100644 --- a/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp +++ b/thermion_dart/native/src/scene/GltfSceneAssetInstance.cpp @@ -19,6 +19,11 @@ namespace thermion return _instanceOwner->getGeometryCapabilities(); } + bool GltfSceneAssetInstance::supportsFlatShading() const + { + return _instanceOwner->supportsFlatShading(); + } + TVertexBufferStorageMode GltfSceneAssetInstance::getVertexBufferStorageMode(size_t primitiveIndex) const { return _instanceOwner->getVertexBufferStorageMode(primitiveIndex); diff --git a/thermion_dart/test/geometry_tests.dart b/thermion_dart/test/geometry_tests.dart index d4b5d796a..34ff10c16 100644 --- a/thermion_dart/test/geometry_tests.dart +++ b/thermion_dart/test/geometry_tests.dart @@ -28,6 +28,13 @@ void main() async { final vb = await asset.getVertexBuffer(); expect(vb, isNotNull); expect(vb!.storageMode, VertexBufferStorageMode.direct); + expect( + asset.geometryCapabilities, + containsAll(const { + SceneAssetGeometryCapability.accessibleGeometryBuffers, + SceneAssetGeometryCapability.writableVertices, + }), + ); await expectLater(vb.destroy(), throwsStateError); final vertices = Float32List.fromList([ // Front face diff --git a/thermion_dart/test/overlay_tests.dart b/thermion_dart/test/overlay_tests.dart index ab6df6537..de04cf393 100644 --- a/thermion_dart/test/overlay_tests.dart +++ b/thermion_dart/test/overlay_tests.dart @@ -221,21 +221,21 @@ void main() async { }, postProcessing: true); }); - test('highlighting requires preserved geometry while flat shading requires unwelded geometry', () async { + test('highlighting requires accessible buffers while flat shading requires unwelded geometry', () async { await testHelper.withViewer((viewer) async { - final preservedGeometryMatcher = throwsA( - isA().having((e) => e.toString(), 'message', contains('requires preserved geometry')), + final accessibleGeometryMatcher = throwsA( + isA().having((e) => e.toString(), 'message', contains('requires accessible geometry buffers')), ); final unweldedMatcher = throwsA( isA().having( (e) => e.toString(), 'message', - contains('requiredGeometryCapabilities containing flatShading'), + contains('requiredGeometryCapabilities containing uniqueTriangleCorners'), ), ); final original = await viewer.loadGltf("file://${testHelper.assetsDir}/cube.glb", addToScene: true); - await expectLater(viewer.view.setStencilHighlight(original), preservedGeometryMatcher); + await expectLater(viewer.view.setStencilHighlight(original), accessibleGeometryMatcher); await expectLater(original.setFlatShading(true), unweldedMatcher); await expectLater( @@ -243,7 +243,7 @@ void main() async { "file://${testHelper.assetsDir}/cube.glb", requiredGeometryCapabilities: const { SceneAssetGeometryCapability.preservedTopology, - SceneAssetGeometryCapability.flatShading, + SceneAssetGeometryCapability.uniqueTriangleCorners, }, ), throwsArgumentError, @@ -254,12 +254,12 @@ void main() async { // cannot swap the tangent BufferObjects required by flat shading. final editable = await viewer.loadGltf( "file://${testHelper.assetsDir}/cube.glb", - requiredGeometryCapabilities: const {SceneAssetGeometryCapability.preservedGeometry}, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.accessibleGeometryBuffers}, addToScene: true, ); expect(editable.getVertexBuffer(), isNotNull); expect(editable.getVertexBuffer()!.supportsSetBufferAt, isTrue); - expect(editable.geometryCapabilities, contains(SceneAssetGeometryCapability.preservedGeometry)); + expect(editable.geometryCapabilities, contains(SceneAssetGeometryCapability.accessibleGeometryBuffers)); expect(editable.geometryCapabilities, contains(SceneAssetGeometryCapability.writableVertices)); expect(editable.geometryCapabilities, contains(SceneAssetGeometryCapability.preservedTopology)); await viewer.view.setStencilHighlight(editable); diff --git a/thermion_dart/test/view_tests.dart b/thermion_dart/test/view_tests.dart index e9f7dfb20..d252b8464 100644 --- a/thermion_dart/test/view_tests.dart +++ b/thermion_dart/test/view_tests.dart @@ -652,7 +652,7 @@ void main() async { // Load FlightHelmet, a multi-mesh glTF asset final asset = await result.viewer.loadGltf( p.join(testHelper.assetsDir, "FlightHelmet", "FlightHelmet.gltf"), - requiredGeometryCapabilities: const {SceneAssetGeometryCapability.preservedGeometry}, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.accessibleGeometryBuffers}, ); expect(asset, isNotNull); diff --git a/thermion_dart/test/wireframe_renderable_test.dart b/thermion_dart/test/wireframe_renderable_test.dart index e778dc414..655c89cbd 100644 --- a/thermion_dart/test/wireframe_renderable_test.dart +++ b/thermion_dart/test/wireframe_renderable_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:thermion_dart/thermion_dart.dart'; import 'package:test/test.dart'; import 'helpers.dart'; @@ -6,6 +8,98 @@ void main() async { final testHelper = TestHelper("wireframe_renderable"); await testHelper.setup(); + test('required geometry capabilities reject unsupported primitives', () async { + await testHelper.withViewer((viewer) async { + final binary = ByteData(28) + ..setFloat32(12, 1.0, Endian.little) + ..setUint16(26, 1, Endian.little); + final jsonBytes = utf8.encode( + jsonEncode({ + 'asset': {'version': '2.0'}, + 'buffers': [ + {'byteLength': 28}, + ], + 'bufferViews': [ + {'buffer': 0, 'byteOffset': 0, 'byteLength': 24, 'target': 34962}, + {'buffer': 0, 'byteOffset': 24, 'byteLength': 4, 'target': 34963}, + ], + 'accessors': [ + { + 'bufferView': 0, + 'componentType': 5126, + 'count': 2, + 'type': 'VEC3', + 'min': [0, 0, 0], + 'max': [1, 0, 0], + }, + {'bufferView': 1, 'componentType': 5123, 'count': 2, 'type': 'SCALAR'}, + ], + 'meshes': [ + { + 'primitives': [ + { + 'attributes': {'POSITION': 0}, + 'indices': 1, + 'mode': 1, + }, + ], + }, + ], + 'nodes': [ + {'mesh': 0}, + ], + 'scenes': [ + { + 'nodes': [0], + }, + ], + 'scene': 0, + }), + ); + final paddedJsonLength = (jsonBytes.length + 3) & ~3; + final totalLength = 12 + 8 + paddedJsonLength + 8 + 28; + final glbData = ByteData(totalLength) + ..setUint32(0, 0x46546c67, Endian.little) + ..setUint32(4, 2, Endian.little) + ..setUint32(8, totalLength, Endian.little) + ..setUint32(12, paddedJsonLength, Endian.little) + ..setUint32(16, 0x4e4f534a, Endian.little); + final glb = glbData.buffer.asUint8List(); + glb.setRange(20, 20 + jsonBytes.length, jsonBytes); + glb.fillRange(20 + jsonBytes.length, 20 + paddedJsonLength, 0x20); + final binaryHeaderOffset = 20 + paddedJsonLength; + glbData + ..setUint32(binaryHeaderOffset, 28, Endian.little) + ..setUint32(binaryHeaderOffset + 4, 0x004e4942, Endian.little); + glb.setRange(binaryHeaderOffset + 8, totalLength, binary.buffer.asUint8List()); + + await expectLater( + viewer.loadGltfFromBuffer( + glb, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.writableVertices}, + ), + throwsA(isA()), + ); + }); + }); + + test('geometry requirements are snapshotted when loading begins', () async { + await testHelper.withViewer((viewer) async { + final requirements = {SceneAssetGeometryCapability.writableVertices}; + final load = viewer.loadGltf( + "file://${testHelper.assetsDir}/cube.glb", + requiredGeometryCapabilities: requirements, + ); + requirements + ..clear() + ..add(SceneAssetGeometryCapability.barycentrics); + + final asset = await load; + expect(asset.geometryCapabilities, contains(SceneAssetGeometryCapability.writableVertices)); + expect(asset.geometryCapabilities, isNot(contains(SceneAssetGeometryCapability.barycentrics))); + }); + }); + test('load glTF with unwelded vertex buffers and apply wireframe material', () async { await ViewerBuilder(testHelper).addSun().setCameraPosition(Vector3(0, 1, 1.5)).execute((result) async { final original = await result.viewer.loadGltf( @@ -26,11 +120,11 @@ void main() async { expect( rebuilt.geometryCapabilities, containsAll(const { - SceneAssetGeometryCapability.flatShading, SceneAssetGeometryCapability.barycentrics, - SceneAssetGeometryCapability.preservedGeometry, + SceneAssetGeometryCapability.accessibleGeometryBuffers, }), ); + expect(rebuilt.supportsFlatShading, isTrue); expect(rebuilt.geometryCapabilities, isNot(contains(SceneAssetGeometryCapability.writableVertices))); expect(rebuilt.geometryCapabilities, contains(SceneAssetGeometryCapability.uniqueTriangleCorners)); @@ -66,7 +160,7 @@ void main() async { final flatAsset = await result.viewer.loadGltf( "file://${testHelper.assetsDir}/FlightHelmet/FlightHelmet.gltf", - requiredGeometryCapabilities: const {SceneAssetGeometryCapability.flatShading}, + requiredGeometryCapabilities: const {SceneAssetGeometryCapability.uniqueTriangleCorners}, addToScene: true, ); @@ -93,6 +187,9 @@ void main() async { ); final instance2 = await asset.createInstance(); + expect(instance2.supportsFlatShading, isTrue); + await instance2.setFlatShading(true); + await instance2.setFlatShading(false); await instance2.setTransform(Matrix4.translation(Vector3(2, 0, 0))); await result.viewer.addToScene(instance2); await testHelper.capture(result.viewer.view, "instanced_preserved_before"); From 78f0a852a25813e22dc6f4ea46943c70d58338d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 26 Aug 2026 14:22:03 +0000 Subject: [PATCH 14/14] chore: update generated artifacts + format (CI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with GitHub Actions --- .../src/bindings/src/thermion_dart_js_interop.g.dart | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart index c3ab65071..24494b9db 100644 --- a/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart +++ b/thermion_dart/lib/src/bindings/src/thermion_dart_js_interop.g.dart @@ -3508,7 +3508,7 @@ int SceneAsset_getGeometryCapabilities(Pointer asset) { bool SceneAsset_supportsFlatShading(Pointer asset) { final result = GeneratedBindings.instance._SceneAsset_supportsFlatShading(asset.cast()); - return result != 0; + return result == 1; } Pointer SceneAsset_getVertexBuffer(Pointer tSceneAsset, int primitiveIndex) { @@ -10369,15 +10369,6 @@ sealed class TVertexBufferStorageMode { static const VERTEX_BUFFER_STORAGE_MODE_BUFFER_OBJECTS = 2; } -sealed class TSceneAssetGeometryCapability { - static const SCENE_ASSET_GEOMETRY_CAPABILITY_NONE = 0; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_BARYCENTRICS = 1; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_WRITABLE_VERTICES = 2; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_ACCESSIBLE_GEOMETRY_BUFFERS = 4; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_PRESERVED_TOPOLOGY = 8; - static const SCENE_ASSET_GEOMETRY_CAPABILITY_UNIQUE_TRIANGLE_CORNERS = 16; -} - extension Aabb3Ext on Pointer { Aabb3 toDart() { return Aabb3(this);