Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 12d86bc

Browse files
authored
[Flutter GPU] Allow customizing the vertex layout on a RenderPipeline (#186310)
Adds an explicit `VertexLayout` value type that the caller can pass to `GpuContext.createRenderPipeline` to override the default interleaved layout declared by the bound vertex shader's shader bundle, plus a `slot:` parameter on `RenderPass.bindVertexBuffer` so multiple vertex buffers can be bound to a single draw. This unblocks structure-of-arrays mesh loading (positions in one buffer, normals + UVs in another, etc.) and lets a renderer reorder attributes from the impellerc-generated default. The shipped scope intentionally pins only what's expressible against today's HAL without backing the API into a corner. The deferred capabilities (instancing, sparse bindings, normalized / packed / half-float / BGRA / 64-bit formats) are tracked at #186307, #186308, and #186309, with TODO comments at the relevant call sites pointing at each tracking issue. ### Dart surface ```dart enum VertexFormat { float32, float32x2, float32x3, float32x4, uint32, uint32x2, uint32x3, uint32x4, sint32, sint32x2, sint32x3, sint32x4, } class VertexAttribute { String name; VertexFormat format; int offsetInBytes; // defaults to 0 } class VertexBuffer { int strideInBytes; List<VertexAttribute> attributes; } class VertexLayout { List<VertexBuffer> buffers; } GpuContext.createRenderPipeline(vertex, fragment, {VertexLayout? vertexLayout}); RenderPass.bindVertexBuffer(BufferView, int vertexCount, {int slot = 0}); ``` If `vertexLayout` is `null`, the default for the bound vertex shader is used (today's behavior). The new `slot:` parameter defaults to `0`, so all existing single-buffer call sites compile unchanged. Attributes nest under the `VertexBuffer` they read from; each buffer's position in `VertexLayout.buffers` determines the binding slot it must be bound to via `RenderPass.bindVertexBuffer` (the first buffer is slot 0, the second is slot 1, and so on). `offsetInBytes` defaults to 0 so the common structure-of-arrays case (one attribute per buffer at the start of each element) doesn't need to spell it out. Attributes are keyed by the shader-side input `name` rather than a raw integer location, mirroring how uniform bindings are resolved via `Shader.getUniformSlot('VertInfo')`. This keeps the Dart layout robust to shader edits that reorder `in` declarations (the underlying location, which is what every backend ultimately consumes, is read from the shader's reflection at pipeline build time). ### Example: structure-of-arrays glTF mesh Most glTF mesh primitives store each vertex attribute (POSITION, NORMAL, TEXCOORD_0, ...) in its own accessor, often inside its own buffer view. Without a configurable vertex layout, callers were forced to interleave those attributes on the CPU before upload. With this change, each attribute can keep its own buffer and bind at its own slot. Given a vertex shader that declares three named inputs: ```glsl in vec3 position; in vec3 normal; in vec2 texcoord; ``` A renderer can describe the SoA layout once at pipeline creation and then bind one buffer per slot per draw: ```dart import 'package:flutter_gpu/gpu.dart' as gpu; final pipeline = gpu.gpuContext.createRenderPipeline( vertexShader, fragmentShader, vertexLayout: const gpu.VertexLayout( buffers: <gpu.VertexBuffer>[ gpu.VertexBuffer( strideInBytes: 12, attributes: <gpu.VertexAttribute>[ gpu.VertexAttribute(name: 'position', format: gpu.VertexFormat.float32x3), ], ), gpu.VertexBuffer( strideInBytes: 12, attributes: <gpu.VertexAttribute>[ gpu.VertexAttribute(name: 'normal', format: gpu.VertexFormat.float32x3), ], ), gpu.VertexBuffer( strideInBytes: 8, attributes: <gpu.VertexAttribute>[ gpu.VertexAttribute(name: 'texcoord', format: gpu.VertexFormat.float32x2), ], ), ], ), ); // Per draw call: bind one buffer per slot. renderPass.bindPipeline(pipeline); renderPass.bindVertexBuffer(positionsView, vertexCount, slot: 0); renderPass.bindVertexBuffer(normalsView, vertexCount, slot: 1); renderPass.bindVertexBuffer(texcoordsView, vertexCount, slot: 2); renderPass.draw(); ``` Interleaved layouts work too: declare one `VertexBuffer` whose `strideInBytes` covers the whole vertex, list every attribute under it, and give each attribute past the first an explicit `offsetInBytes` into the element: ```dart vertexLayout: const gpu.VertexLayout( buffers: <gpu.VertexBuffer>[ gpu.VertexBuffer( strideInBytes: 32, attributes: <gpu.VertexAttribute>[ gpu.VertexAttribute(name: 'position', format: gpu.VertexFormat.float32x3), gpu.VertexAttribute( name: 'normal', format: gpu.VertexFormat.float32x3, offsetInBytes: 12, ), gpu.VertexAttribute( name: 'texcoord', format: gpu.VertexFormat.float32x2, offsetInBytes: 24, ), ], ), ], ), ``` This is also how a caller would override the impellerc-generated default to skip an unused attribute or reorder the components. ### Validation `createRenderPipeline` throws a Dart exception when: - A `VertexAttribute.format` doesn't match the bound vertex shader's declared scalar type class (float vs signed int vs unsigned int). Component-count mismatches are NOT errors, mirroring the default-substitution rules every modern HAL uses ((0, 0, 0, 1) fill). - An attribute's `offsetInBytes + format.bytesPerElement` overruns the owning `VertexBuffer`'s stride. - Two attributes within the same `VertexBuffer` occupy overlapping byte ranges (i.e. `[offsetInBytes, offsetInBytes + format.bytesPerElement)` ranges that intersect). - An attribute's `name` doesn't match any vertex shader input declaration. `RenderPass.bindVertexBuffer` throws `RangeError` if `slot` is outside `[0, 16)`. ### C++ plumbing - `Shader::GetStageInputs()` exposes the impellerc-reflected attribute metadata so the pipeline initializer can resolve user attribute names to `(location, set, columns, relaxed_precision)` and validate user formats against the shader's declared scalar type. - `RenderPipeline` stores its own `impeller::VertexDescriptor`, built from the user layout when supplied or fetched from the shader's reflection otherwise. - `RenderPass` upgrades `vertex_buffer` to a `std::array<BufferView, 16>` indexed by binding slot, tracks the highest bound slot, and forwards the whole array to `impeller::RenderPass::SetVertexBuffer(BufferView*, count)`. The packed `(buffer layouts, attributes, attribute names)` data is passed via FFI as three `ByteData` handles and copied out of the typed-data handles before any callback into the Dart VM (else `Dart_TypedDataAcquireData` would forbid the callback). With nested attributes, `bufferLayouts` rows shrink to `[strideInBytes, attributeCount]` and `attributes` rows shrink to `[offsetInBytes, formatIndex, nameByteLength]`; binding slots are implicit in each buffer's position, and the C++ side walks attribute rows by consuming each buffer's `attributeCount` in order. Attribute names are encoded as concatenated UTF-8 bytes walked in parallel with the attributes integer table using each entry's `nameByteLength`. ### Tests Adds six `gpu_test.dart` tests covering an explicit-layout-matching-default render, a slot-range check, and four `createRenderPipeline` validation paths (wrong format, overrun stride, overlapping attributes within a buffer, unknown attribute name). All pass on `flutter_tester_opengles` (SwANGLE) and `flutter_tester` (Metal) locally. Fixes #145013. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent 095d8cf commit 12d86bc

14 files changed

Lines changed: 968 additions & 39 deletions

engine/src/flutter/lib/gpu/BUILD.gn

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ source_set("gpu") {
6161
"//flutter/impeller/shader_bundle:shader_bundle_flatbuffers",
6262
"//flutter/lib/ui",
6363
"//flutter/third_party/tonic",
64+
"//third_party/abseil-cpp/absl/status",
65+
"//third_party/abseil-cpp/absl/status:statusor",
66+
"//third_party/abseil-cpp/absl/strings",
6467
]
6568

6669
if (impeller_supports_rendering) {

engine/src/flutter/lib/gpu/lib/gpu.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ part 'src/render_pass.dart';
3737
part 'src/render_pipeline.dart';
3838
part 'src/shader.dart';
3939
part 'src/shader_library.dart';
40+
part 'src/vertex_layout.dart';

engine/src/flutter/lib/gpu/lib/src/buffer.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,14 @@ base class DeviceBuffer extends NativeFieldWrapperClass1 {
5959
int offsetInBytes,
6060
int lengthInBytes,
6161
int vertexCount,
62+
int slot,
6263
) {
6364
renderPass._bindVertexBufferDevice(
6465
this,
6566
offsetInBytes,
6667
lengthInBytes,
6768
vertexCount,
69+
slot,
6870
);
6971
}
7072

engine/src/flutter/lib/gpu/lib/src/context.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,15 @@ base class GpuContext extends NativeFieldWrapperClass1 {
157157

158158
RenderPipeline createRenderPipeline(
159159
Shader vertexShader,
160-
Shader fragmentShader,
161-
) {
162-
return RenderPipeline._(this, vertexShader, fragmentShader);
160+
Shader fragmentShader, {
161+
VertexLayout? vertexLayout,
162+
}) {
163+
return RenderPipeline._(
164+
this,
165+
vertexShader,
166+
fragmentShader,
167+
vertexLayout: vertexLayout,
168+
);
163169
}
164170

165171
/// Associates the default Impeller context with this Context.

engine/src/flutter/lib/gpu/lib/src/render_pass.dart

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,22 @@ base class RenderTarget {
224224
}
225225

226226
base class RenderPass extends NativeFieldWrapperClass1 {
227+
/// The maximum number of vertex buffer slots that can be bound to a single
228+
/// draw. Matches `flutter::gpu::RenderPass::kMaxVertexBufferSlots` on the
229+
/// native side, which in turn matches `impeller::kMaxVertexBuffers`; keep
230+
/// them in sync.
231+
static const int _kMaxVertexBufferSlots = 16;
232+
233+
/// Bitmask of slots that have been bound via [bindVertexBuffer] since the
234+
/// most recent [clearBindings] (or since this RenderPass was created).
235+
/// Bit `i` is set when slot `i` has been bound.
236+
int _boundVertexSlotsMask = 0;
237+
238+
/// Highest slot index that has been bound, or -1 if no slot has been
239+
/// bound. Tracked so [draw] can detect sparse bindings without scanning
240+
/// the entire bitmask.
241+
int _maxBoundVertexSlot = -1;
242+
227243
/// Creates a new RenderPass.
228244
RenderPass._(
229245
GpuContext gpuContext,
@@ -279,12 +295,52 @@ base class RenderPass extends NativeFieldWrapperClass1 {
279295
_bindPipeline(pipeline);
280296
}
281297

282-
void bindVertexBuffer(BufferView bufferView, int vertexCount) {
298+
/// Binds [bufferView] as the vertex buffer at the given [slot].
299+
///
300+
/// [slot] selects which vertex-buffer binding the data appears at,
301+
/// matching the corresponding `VertexBuffer` declared on the active
302+
/// pipeline's `VertexLayout`. The default of 0 matches the default
303+
/// layout declared by a shader bundle, which is what every single-buffer
304+
/// call site expects. To bind multiple structure-of-arrays vertex
305+
/// buffers, call this method once per slot. The [vertexCount] takes
306+
/// effect only when no index buffer is bound, and is read from the
307+
/// binding at slot 0, so values supplied at higher slots are ignored.
308+
///
309+
/// Sparse bindings are not supported. Every slot in `[0, highestBound]`
310+
/// must have been bound before [draw] is called, otherwise [draw] throws
311+
/// a [StateError] naming the unbound slots. Slots can be bound in any
312+
/// order.
313+
///
314+
/// [slot] must be in `[0, 16)` (Impeller's HAL caps vertex buffer
315+
/// bindings at 16). On the OpenGL ES backend, the per-pipeline limit on
316+
/// the *total attribute count* across all bound buffers is whatever the
317+
/// device reports for `GL_MAX_VERTEX_ATTRIBS` (minimum 8 on GL ES 2.0,
318+
/// minimum 16 on GL ES 3.0+), and is enforced by the driver rather than
319+
/// by this method.
320+
void bindVertexBuffer(
321+
BufferView bufferView,
322+
int vertexCount, {
323+
int slot = 0,
324+
}) {
325+
if (slot < 0 || slot >= _kMaxVertexBufferSlots) {
326+
throw RangeError.range(
327+
slot,
328+
0,
329+
_kMaxVertexBufferSlots - 1,
330+
'slot',
331+
'bindVertexBuffer slot must be in [0, $_kMaxVertexBufferSlots)',
332+
);
333+
}
334+
_boundVertexSlotsMask |= 1 << slot;
335+
if (slot > _maxBoundVertexSlot) {
336+
_maxBoundVertexSlot = slot;
337+
}
283338
bufferView.buffer._bindAsVertexBuffer(
284339
this,
285340
bufferView.offsetInBytes,
286341
bufferView.lengthInBytes,
287342
vertexCount,
343+
slot,
288344
);
289345
}
290346

@@ -349,6 +405,8 @@ base class RenderPass extends NativeFieldWrapperClass1 {
349405

350406
void clearBindings() {
351407
_clearBindings();
408+
_boundVertexSlotsMask = 0;
409+
_maxBoundVertexSlot = -1;
352410
}
353411

354412
void setColorBlendEnable(bool enable, {int colorAttachmentIndex = 0}) {
@@ -448,6 +506,20 @@ base class RenderPass extends NativeFieldWrapperClass1 {
448506
}
449507

450508
void draw() {
509+
if (_maxBoundVertexSlot >= 0) {
510+
final int expectedMask = (1 << (_maxBoundVertexSlot + 1)) - 1;
511+
if (_boundVertexSlotsMask != expectedMask) {
512+
final List<int> missing = <int>[
513+
for (int i = 0; i <= _maxBoundVertexSlot; i++)
514+
if ((_boundVertexSlotsMask & (1 << i)) == 0) i,
515+
];
516+
throw StateError(
517+
'draw() called with sparse vertex buffer bindings: slot(s) '
518+
'${missing.join(', ')} were not bound but slot $_maxBoundVertexSlot '
519+
'was. Bind every slot in [0, $_maxBoundVertexSlot] before drawing.',
520+
);
521+
}
522+
}
451523
if (!_draw()) {
452524
throw Exception("Failed to append draw");
453525
}
@@ -519,14 +591,15 @@ base class RenderPass extends NativeFieldWrapperClass1 {
519591
)
520592
external void _bindPipeline(RenderPipeline pipeline);
521593

522-
@Native<Void Function(Pointer<Void>, Pointer<Void>, Int, Int, Int)>(
594+
@Native<Void Function(Pointer<Void>, Pointer<Void>, Int, Int, Int, Int)>(
523595
symbol: 'InternalFlutterGpu_RenderPass_BindVertexBufferDevice',
524596
)
525597
external void _bindVertexBufferDevice(
526598
DeviceBuffer buffer,
527599
int offsetInBytes,
528600
int lengthInBytes,
529601
int vertexCount,
602+
int slot,
530603
);
531604

532605
@Native<Void Function(Pointer<Void>, Pointer<Void>, Int, Int, Int, Int)>(

engine/src/flutter/lib/gpu/lib/src/render_pipeline.dart

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,29 @@ part of flutter_gpu;
88

99
base class RenderPipeline extends NativeFieldWrapperClass1 {
1010
/// Creates a new RenderPipeline.
11+
///
12+
/// If [vertexLayout] is null, the default interleaved layout declared by
13+
/// the bound vertex shader's shader bundle is used. Supply a layout to
14+
/// override the default, e.g. to bind multiple structure-of-arrays vertex
15+
/// buffers or to reorder attributes.
1116
RenderPipeline._(
1217
GpuContext gpuContext,
1318
Shader vertexShader,
14-
Shader fragmentShader,
15-
) : vertexShader = vertexShader,
16-
fragmentShader = fragmentShader {
17-
String? error = _initialize(gpuContext, vertexShader, fragmentShader);
19+
Shader fragmentShader, {
20+
VertexLayout? vertexLayout,
21+
}) : vertexShader = vertexShader,
22+
fragmentShader = fragmentShader {
23+
final (ByteData?, ByteData?, ByteData?) packed = vertexLayout == null
24+
? (null, null, null)
25+
: _packVertexLayout(vertexLayout);
26+
String? error = _initialize(
27+
gpuContext,
28+
vertexShader,
29+
fragmentShader,
30+
packed.$1,
31+
packed.$2,
32+
packed.$3,
33+
);
1834
if (error != null) {
1935
throw Exception(error);
2036
}
@@ -23,13 +39,91 @@ base class RenderPipeline extends NativeFieldWrapperClass1 {
2339
final Shader vertexShader;
2440
final Shader fragmentShader;
2541

42+
/// Packs a [VertexLayout] into the three ByteData buffers expected by the
43+
/// C++ side:
44+
///
45+
/// * `bufferLayouts` (`Int32List`): `[strideInBytes, attributeCount]` per
46+
/// buffer entry. The buffer's binding slot is implicit in its position.
47+
/// * `attributes` (`Int32List`): `[offsetInBytes, formatIndex,
48+
/// nameByteLength]` per attribute entry, flattened across buffers in
49+
/// buffer-list order. Each buffer's `attributeCount` row tells the C++
50+
/// side how many attribute rows belong to it.
51+
/// * `attributeNames`: concatenated UTF-8 bytes of every attribute name,
52+
/// walked in parallel with `attributes` using the per-entry name length.
53+
///
54+
/// Attribute names are ASCII GLSL identifiers, so encoding them as UTF-8
55+
/// byte sequences via `codeUnits` is sufficient without a separate
56+
/// dependency on `dart:convert`.
57+
static (ByteData, ByteData, ByteData) _packVertexLayout(VertexLayout layout) {
58+
int totalAttributeCount = 0;
59+
for (int i = 0; i < layout.buffers.length; i++) {
60+
totalAttributeCount += layout.buffers[i].attributes.length;
61+
}
62+
63+
final Int32List buffersData = Int32List(2 * layout.buffers.length);
64+
for (int i = 0; i < layout.buffers.length; i++) {
65+
final VertexBuffer buf = layout.buffers[i];
66+
buffersData[i * 2 + 0] = buf.strideInBytes;
67+
buffersData[i * 2 + 1] = buf.attributes.length;
68+
}
69+
70+
// First pass: encode each name to bytes and compute the total length,
71+
// walking attributes in buffer-list order.
72+
final List<Uint8List> nameBytes = <Uint8List>[];
73+
int totalNameBytes = 0;
74+
for (int b = 0; b < layout.buffers.length; b++) {
75+
final List<VertexAttribute> attrs = layout.buffers[b].attributes;
76+
for (int a = 0; a < attrs.length; a++) {
77+
final Uint8List bytes = Uint8List.fromList(attrs[a].name.codeUnits);
78+
nameBytes.add(bytes);
79+
totalNameBytes += bytes.length;
80+
}
81+
}
82+
83+
// Second pass: pack the attribute integer table and the names blob.
84+
final Int32List attributesData = Int32List(3 * totalAttributeCount);
85+
final Uint8List namesData = Uint8List(totalNameBytes);
86+
int attrIndex = 0;
87+
int nameCursor = 0;
88+
for (int b = 0; b < layout.buffers.length; b++) {
89+
final List<VertexAttribute> attrs = layout.buffers[b].attributes;
90+
for (int a = 0; a < attrs.length; a++) {
91+
final VertexAttribute attr = attrs[a];
92+
final Uint8List bytes = nameBytes[attrIndex];
93+
attributesData[attrIndex * 3 + 0] = attr.offsetInBytes;
94+
attributesData[attrIndex * 3 + 1] = attr.format.index;
95+
attributesData[attrIndex * 3 + 2] = bytes.length;
96+
namesData.setRange(nameCursor, nameCursor + bytes.length, bytes);
97+
nameCursor += bytes.length;
98+
attrIndex++;
99+
}
100+
}
101+
102+
return (
103+
buffersData.buffer.asByteData(),
104+
attributesData.buffer.asByteData(),
105+
namesData.buffer.asByteData(),
106+
);
107+
}
108+
26109
/// Wrap with native counterpart.
27-
@Native<Handle Function(Handle, Pointer<Void>, Pointer<Void>, Pointer<Void>)>(
28-
symbol: 'InternalFlutterGpu_RenderPipeline_Initialize',
29-
)
110+
@Native<
111+
Handle Function(
112+
Handle,
113+
Pointer<Void>,
114+
Pointer<Void>,
115+
Pointer<Void>,
116+
Handle,
117+
Handle,
118+
Handle,
119+
)
120+
>(symbol: 'InternalFlutterGpu_RenderPipeline_Initialize')
30121
external String? _initialize(
31122
GpuContext gpuContext,
32123
Shader vertexShader,
33124
Shader fragmentShader,
125+
ByteData? bufferLayoutsData,
126+
ByteData? attributesData,
127+
ByteData? attributeNamesData,
34128
);
35129
}

0 commit comments

Comments
 (0)