[Flutter GPU] Add instanced draw support (#187359) Progress on [flutter/flutter#186307](https://github.com/flutter/flutter/issues/186307). Related to [flutter/flutter#156738](https://github.com/flutter/flutter/issues/156738). Related to [flutter/flutter#154145](https://github.com/flutter/flutter/issues/154145). Adds Flutter GPU support for instanced draws on top of Impeller's instance-rate vertex input support. This adds `VertexStepMode` and `VertexBuffer.stepMode`, allowing a vertex buffer slot to advance once per instance. `RenderPass.draw` and `RenderPass.drawIndexed` now take an optional `instanceCount` named parameter, defaulting to 1. Example usage: ```dart final gpu.RenderPipeline pipeline = gpu.gpuContext.createRenderPipeline( vertexShader, fragmentShader, vertexLayout: const gpu.VertexLayout( buffers: <gpu.VertexBuffer>[ gpu.VertexBuffer( strideInBytes: 8, attributes: <gpu.VertexAttribute>[ gpu.VertexAttribute( name: 'position', format: gpu.VertexFormat.float32x2, ), ], ), gpu.VertexBuffer( strideInBytes: 8, stepMode: gpu.VertexStepMode.instance, attributes: <gpu.VertexAttribute>[ gpu.VertexAttribute( name: 'instance_offset', format: gpu.VertexFormat.float32x2, ), ], ), ], ), ); renderPass ..bindPipeline(pipeline) ..bindVertexBuffer(vertexBuffer) ..bindVertexBuffer(instanceOffsets, slot: 1) ..draw(3, instanceCount: 10); ``` The implementation plumbs step mode through the custom `VertexLayout` packing into Impeller's `VertexInputRate`, forwards instance counts to the native render pass, and adds a Flutter GPU golden test that renders four instances from one geometry buffer plus one instance-rate offset/color buffer. ## 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. <!-- 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
diff --git a/engine/src/flutter/impeller/fixtures/BUILD.gn b/engine/src/flutter/impeller/fixtures/BUILD.gn index 821a5cf..efa6f00 100644 --- a/engine/src/flutter/impeller/fixtures/BUILD.gn +++ b/engine/src/flutter/impeller/fixtures/BUILD.gn
@@ -151,6 +151,8 @@ "blue_noise.png", "boston.jpg", "embarcadero.jpg", + "flutter_gpu_instanced.frag", + "flutter_gpu_instanced.vert", "flutter_gpu_texture.frag", "flutter_gpu_texture.vert", "flutter_gpu_unlit.frag", @@ -204,6 +206,8 @@ impellerc("flutter_gpu_shaders") { shaders = [ # Temporarily build Flutter GPU test shaders as runtime stages. + "flutter_gpu_instanced.frag", + "flutter_gpu_instanced.vert", "flutter_gpu_unlit.frag", "flutter_gpu_unlit.vert", "flutter_gpu_texture.frag", @@ -211,7 +215,7 @@ ] fixtures = rebase_path("//flutter/impeller/fixtures") - shader_bundle = "{\"UnlitFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_unlit.frag\"}, \"UnlitVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_unlit.vert\"}, \"TextureFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_texture.frag\"}, \"TextureVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_texture.vert\"}}" + shader_bundle = "{\"InstancedFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_instanced.frag\"}, \"InstancedVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_instanced.vert\"}, \"UnlitFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_unlit.frag\"}, \"UnlitVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_unlit.vert\"}, \"TextureFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_texture.frag\"}, \"TextureVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_texture.vert\"}}" shader_bundle_output = "playground.shaderbundle" }
diff --git a/engine/src/flutter/impeller/fixtures/flutter_gpu_instanced.frag b/engine/src/flutter/impeller/fixtures/flutter_gpu_instanced.frag new file mode 100644 index 0000000..fc61864 --- /dev/null +++ b/engine/src/flutter/impeller/fixtures/flutter_gpu_instanced.frag
@@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +in vec4 v_color; +out vec4 frag_color; + +void main() { + frag_color = v_color; +}
diff --git a/engine/src/flutter/impeller/fixtures/flutter_gpu_instanced.vert b/engine/src/flutter/impeller/fixtures/flutter_gpu_instanced.vert new file mode 100644 index 0000000..c2de308 --- /dev/null +++ b/engine/src/flutter/impeller/fixtures/flutter_gpu_instanced.vert
@@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +uniform VertInfo { + mat4 mvp; +} +vert_info; + +in vec2 position; +in vec2 instance_offset; +in vec4 instance_color; +out vec4 v_color; + +void main() { + v_color = instance_color; + gl_Position = vert_info.mvp * vec4(position + instance_offset, 0.0, 1.0); +}
diff --git a/engine/src/flutter/lib/gpu/lib/src/render_pass.dart b/engine/src/flutter/lib/gpu/lib/src/render_pass.dart index a4c7ad2..b9d6db4 100644 --- a/engine/src/flutter/lib/gpu/lib/src/render_pass.dart +++ b/engine/src/flutter/lib/gpu/lib/src/render_pass.dart
@@ -311,12 +311,9 @@ /// otherwise they throw a [StateError] naming the unbound slots. Slots /// can be bound in any order. /// - /// [slot] must be in `[0, 16)` (Impeller's HAL caps vertex buffer - /// bindings at 16). On the OpenGL ES backend, the per-pipeline limit on - /// the *total attribute count* across all bound buffers is whatever the - /// device reports for `GL_MAX_VERTEX_ATTRIBS` (minimum 8 on GL ES 2.0, - /// minimum 16 on GL ES 3.0+), and is enforced by the driver rather than - /// by this method. + /// [slot] must be in `[0, 16)` (the current Flutter GPU limit for vertex + /// buffer bindings). Some devices may also impose a lower limit on the + /// total number of vertex attributes used by a single pipeline. void bindVertexBuffer(BufferView bufferView, {int slot = 0}) { if (slot < 0 || slot >= _kMaxVertexBufferSlots) { throw RangeError.range( @@ -499,22 +496,61 @@ _setWindingOrder(windingOrder.index); } - /// Appends a non-indexed draw of [vertexCount] vertices, read from the - /// bound vertex buffers. - void draw(int vertexCount) { + /// Appends a non-indexed draw of [vertexCount] vertices. + /// + /// The vertices are read from the vertex buffers previously bound with + /// [bindVertexBuffer]. Buffers whose [VertexBuffer.stepMode] is + /// [VertexStepMode.vertex] advance once per vertex. Buffers whose + /// [VertexBuffer.stepMode] is [VertexStepMode.instance] advance once per + /// instance. + /// + /// Set [instanceCount] greater than 1 to repeat the same vertex range + /// multiple times while reading a new element from each instance-rate vertex + /// buffer for each instance. A [vertexCount] or [instanceCount] of 0 is + /// valid and records no drawing work. + /// + /// If the bound pipeline does not use any instance-rate vertex buffers, each + /// instance receives the same vertex buffer data. This is valid, but every + /// instance will produce the same geometry unless the shader varies its output + /// another way. + void draw(int vertexCount, {int instanceCount = 1}) { RangeError.checkNotNegative(vertexCount, 'vertexCount'); + RangeError.checkNotNegative(instanceCount, 'instanceCount'); + if (vertexCount == 0 || instanceCount == 0) { + return; + } _validateVertexBindings(); - if (!_draw(vertexCount)) { + if (!_draw(vertexCount, instanceCount)) { throw Exception("Failed to append draw"); } } - /// Appends an indexed draw of [indexCount] indices, read from the index - /// buffer bound with [bindIndexBuffer]. - void drawIndexed(int indexCount) { + /// Appends an indexed draw of [indexCount] indices. + /// + /// Indices are read from the index buffer previously bound with + /// [bindIndexBuffer]. Vertex data is read from the vertex buffers previously + /// bound with [bindVertexBuffer]. Buffers whose [VertexBuffer.stepMode] is + /// [VertexStepMode.vertex] advance once per vertex selected by the index + /// buffer. Buffers whose [VertexBuffer.stepMode] is [VertexStepMode.instance] + /// advance once per instance. + /// + /// Set [instanceCount] greater than 1 to repeat the same indexed geometry + /// multiple times while reading a new element from each instance-rate vertex + /// buffer for each instance. An [indexCount] or [instanceCount] of 0 is valid + /// and records no drawing work. + /// + /// If the bound pipeline does not use any instance-rate vertex buffers, each + /// instance receives the same vertex and index buffer data. This is valid, but + /// every instance will produce the same geometry unless the shader varies its + /// output another way. + void drawIndexed(int indexCount, {int instanceCount = 1}) { RangeError.checkNotNegative(indexCount, 'indexCount'); + RangeError.checkNotNegative(instanceCount, 'instanceCount'); + if (indexCount == 0 || instanceCount == 0) { + return; + } _validateVertexBindings(); - if (!_drawIndexed(indexCount)) { + if (!_drawIndexed(indexCount, instanceCount)) { throw Exception("Failed to append drawIndexed"); } } @@ -747,13 +783,13 @@ ) external void _setPolygonMode(int polygonMode); - @Native<Bool Function(Pointer<Void>, Int)>( + @Native<Bool Function(Pointer<Void>, Int, Int)>( symbol: 'InternalFlutterGpu_RenderPass_Draw', ) - external bool _draw(int vertexCount); + external bool _draw(int vertexCount, int instanceCount); - @Native<Bool Function(Pointer<Void>, Int)>( + @Native<Bool Function(Pointer<Void>, Int, Int)>( symbol: 'InternalFlutterGpu_RenderPass_DrawIndexed', ) - external bool _drawIndexed(int indexCount); + external bool _drawIndexed(int indexCount, int instanceCount); }
diff --git a/engine/src/flutter/lib/gpu/lib/src/render_pipeline.dart b/engine/src/flutter/lib/gpu/lib/src/render_pipeline.dart index 95a2086..4ab266f 100644 --- a/engine/src/flutter/lib/gpu/lib/src/render_pipeline.dart +++ b/engine/src/flutter/lib/gpu/lib/src/render_pipeline.dart
@@ -42,8 +42,9 @@ /// Packs a [VertexLayout] into the three ByteData buffers expected by the /// C++ side: /// - /// * `bufferLayouts` (`Int32List`): `[strideInBytes, attributeCount]` per - /// buffer entry. The buffer's binding slot is implicit in its position. + /// * `bufferLayouts` (`Int32List`): `[strideInBytes, attributeCount, + /// stepMode]` per buffer entry. The buffer's binding slot is implicit in + /// its position. /// * `attributes` (`Int32List`): `[offsetInBytes, formatIndex, /// nameByteLength]` per attribute entry, flattened across buffers in /// buffer-list order. Each buffer's `attributeCount` row tells the C++ @@ -60,11 +61,12 @@ totalAttributeCount += layout.buffers[i].attributes.length; } - final Int32List buffersData = Int32List(2 * layout.buffers.length); + final Int32List buffersData = Int32List(3 * layout.buffers.length); for (int i = 0; i < layout.buffers.length; i++) { final VertexBuffer buf = layout.buffers[i]; - buffersData[i * 2 + 0] = buf.strideInBytes; - buffersData[i * 2 + 1] = buf.attributes.length; + buffersData[i * 3 + 0] = buf.strideInBytes; + buffersData[i * 3 + 1] = buf.attributes.length; + buffersData[i * 3 + 2] = buf.stepMode.index; } // First pass: encode each name to bytes and compute the total length,
diff --git a/engine/src/flutter/lib/gpu/lib/src/vertex_layout.dart b/engine/src/flutter/lib/gpu/lib/src/vertex_layout.dart index 03004bf..21ccb78 100644 --- a/engine/src/flutter/lib/gpu/lib/src/vertex_layout.dart +++ b/engine/src/flutter/lib/gpu/lib/src/vertex_layout.dart
@@ -63,6 +63,28 @@ final int componentCount; } +/// Controls how a [VertexBuffer] advances through its elements while drawing. +/// +/// The step mode is a property of the buffer slot, not of an individual +/// attribute. All attributes in the same [VertexBuffer] advance together. +enum VertexStepMode { + /// Advance to the next buffer element for each vertex. + /// + /// This is the default mode and is appropriate for ordinary mesh data such + /// as positions, normals, texture coordinates, and vertex colors. During an + /// instanced draw, a vertex-rate buffer supplies the same sequence of vertex + /// elements to every instance. + vertex, + + /// Advance to the next buffer element for each instance. + /// + /// Use this for per-instance data such as an object offset, transform, or + /// tint color. During a draw with `instanceCount: n`, the first instance + /// reads element 0 from this buffer, the second instance reads element 1, + /// and so on. + instance, +} + /// Describes a single vertex attribute: which shader input it feeds (by name), /// its byte offset within the owning vertex buffer's element, and its format. /// @@ -100,27 +122,33 @@ final int offsetInBytes; } -/// Describes one vertex buffer slot: its per-element stride and the -/// attributes that are read from it. +/// Describes one vertex buffer slot: its per-element stride, step mode, and +/// the attributes that are read from it. /// /// A buffer's position in [VertexLayout.buffers] determines the binding /// slot it is bound to via [RenderPass.bindVertexBuffer]; the first buffer /// is slot 0, the second is slot 1, and so on. Sparse binding slots are /// not currently supported. /// -/// Step mode (vertex vs instance) and instance step rate are not yet -/// configurable; every buffer currently advances per vertex. Both options -/// will be added later as named parameters with sensible defaults, and the -/// addition will be a non-breaking change for existing call sites. -// TODO(https://github.com/flutter/flutter/issues/186307): Allow specifying -// vertex step mode and instance step rate. +/// The [stepMode] determines whether the buffer advances once per vertex or +/// once per instance. Keep the default [VertexStepMode.vertex] for mesh data +/// that describes the geometry being repeated. Use [VertexStepMode.instance] +/// for data that changes from one instance to the next when calling +/// [RenderPass.draw] or [RenderPass.drawIndexed] with an `instanceCount` +/// greater than 1. +// TODO(https://github.com/flutter/flutter/issues/186307): Add support for +// instance step rates greater than one. // TODO(https://github.com/flutter/flutter/issues/186308): Allow sparse // vertex buffer binding slots. final class VertexBuffer { /// Creates a vertex buffer slot description with the given per-element /// [strideInBytes] and the list of [attributes] that the vertex shader /// reads from this buffer. - const VertexBuffer({required this.strideInBytes, required this.attributes}); + const VertexBuffer({ + required this.strideInBytes, + required this.attributes, + this.stepMode = VertexStepMode.vertex, + }); /// Byte distance from the start of one element to the start of the next /// element in this vertex buffer (not the gap between an element's end and @@ -132,6 +160,13 @@ /// Attributes read from this vertex buffer by the vertex shader. final List<VertexAttribute> attributes; + + /// How this vertex buffer advances while drawing. + /// + /// Defaults to [VertexStepMode.vertex]. Set this to + /// [VertexStepMode.instance] when the buffer contains one element per + /// instance rather than one element per vertex. + final VertexStepMode stepMode; } /// A complete vertex input layout: zero or more vertex buffer slots and the
diff --git a/engine/src/flutter/lib/gpu/render_pass.cc b/engine/src/flutter/lib/gpu/render_pass.cc index f7b76c5..643e219 100644 --- a/engine/src/flutter/lib/gpu/render_pass.cc +++ b/engine/src/flutter/lib/gpu/render_pass.cc
@@ -152,10 +152,11 @@ if (context.GetBackendType() == impeller::Context::BackendType::kOpenGLES && !context.GetPipelineLibrary()->HasPipeline(pipeline_desc)) { - // For GLES, new pipeline creation must be done on the reactor (raster) - // thread. We're about the draw, so we need to synchronize with a raster - // task in order to get the new pipeline. Depending on how busy the raster - // thread is, this could hang the UI thread long enough to miss a frame. + // New pipeline creation for this backend must be done on the reactor + // (raster) thread. We're about the draw, so we need to synchronize with a + // raster task in order to get the new pipeline. Depending on how busy the + // raster thread is, this could hang the UI thread long enough to miss a + // frame. // Note that this branch is only called if a new pipeline actually needs to // be built. @@ -181,7 +182,13 @@ return pipeline; } -bool RenderPass::Draw(size_t element_count, bool indexed) { +bool RenderPass::Draw(size_t element_count, + size_t instance_count, + bool indexed) { + if (element_count == 0u || instance_count == 0u) { + return true; + } + if (indexed && index_buffer_type == impeller::IndexType::kNone) { // drawIndexed was called without an index buffer bound. return false; @@ -228,6 +235,7 @@ impeller::IndexType::kNone); } render_pass_->SetElementCount(element_count); + render_pass_->SetInstanceCount(instance_count); render_pass_->SetStencilReference(stencil_reference); @@ -649,14 +657,18 @@ } bool InternalFlutterGpu_RenderPass_Draw(flutter::gpu::RenderPass* wrapper, - int vertex_count) { - // Guard the cast to the size_t element count; a negative value would wrap. - return vertex_count >= 0 && wrapper->Draw(vertex_count, /*indexed=*/false); + int vertex_count, + int instance_count) { + // Guard the casts to size_t; a negative value would wrap. + return vertex_count >= 0 && instance_count >= 0 && + wrapper->Draw(vertex_count, instance_count, /*indexed=*/false); } bool InternalFlutterGpu_RenderPass_DrawIndexed( flutter::gpu::RenderPass* wrapper, - int index_count) { - // Guard the cast to the size_t element count; a negative value would wrap. - return index_count >= 0 && wrapper->Draw(index_count, /*indexed=*/true); + int index_count, + int instance_count) { + // Guard the casts to size_t; a negative value would wrap. + return index_count >= 0 && instance_count >= 0 && + wrapper->Draw(index_count, instance_count, /*indexed=*/true); }
diff --git a/engine/src/flutter/lib/gpu/render_pass.h b/engine/src/flutter/lib/gpu/render_pass.h index 23682163..a0b4c55 100644 --- a/engine/src/flutter/lib/gpu/render_pass.h +++ b/engine/src/flutter/lib/gpu/render_pass.h
@@ -58,8 +58,8 @@ /// Append a draw to the underlying render pass. [element_count] is the /// vertex count for a non-indexed draw, or the index count when - /// [indexed] is true. - bool Draw(size_t element_count, bool indexed); + /// [indexed] is true. [instance_count] is the number of instances to draw. + bool Draw(size_t element_count, size_t instance_count, bool indexed); struct BufferAndUniformSlot { impeller::ShaderUniformSlot slot; @@ -81,10 +81,9 @@ // Vertex buffers indexed by binding slot. Mirrors // `impeller::kMaxVertexBuffers`; Impeller's HAL caps vertex buffer // bindings at 16 per draw, and index `i` here corresponds to the binding - // declared at slot `i` in the active VertexLayout. On the OpenGL ES - // backend the per-pipeline limit on the *total attribute count* across - // all bound buffers is `GL_MAX_VERTEX_ATTRIBS` (spec minimum 8 on GLES - // 2.0, 16 on GLES 3.0+), enforced by the driver. + // declared at slot `i` in the active VertexLayout. Devices may impose a + // lower per-pipeline limit on the total attribute count across all bound + // buffers. static constexpr size_t kMaxVertexBufferSlots = 16; std::array<impeller::BufferView, kMaxVertexBufferSlots> vertex_buffers; // Highest slot index that has been bound on this pass (plus one). @@ -291,12 +290,14 @@ FLUTTER_GPU_EXPORT extern bool InternalFlutterGpu_RenderPass_Draw( flutter::gpu::RenderPass* wrapper, - int vertex_count); + int vertex_count, + int instance_count); FLUTTER_GPU_EXPORT extern bool InternalFlutterGpu_RenderPass_DrawIndexed( flutter::gpu::RenderPass* wrapper, - int index_count); + int index_count, + int instance_count); } // extern "C"
diff --git a/engine/src/flutter/lib/gpu/render_pipeline.cc b/engine/src/flutter/lib/gpu/render_pipeline.cc index 21cfcd2..8402ea8 100644 --- a/engine/src/flutter/lib/gpu/render_pipeline.cc +++ b/engine/src/flutter/lib/gpu/render_pipeline.cc
@@ -83,9 +83,10 @@ }}; // Width of each "row" in the packed `bufferLayouts` ByteData passed from -// Dart: `[strideInBytes, attributeCount]`. Each buffer's binding slot is -// implicit in its position in the array (the first buffer is slot 0, etc.). -constexpr size_t kBufferLayoutInts = 2; +// Dart: `[strideInBytes, attributeCount, stepMode]`. Each buffer's binding +// slot is implicit in its position in the array (the first buffer is slot 0, +// etc.). +constexpr size_t kBufferLayoutInts = 3; // Width of each "row" in the packed `attributes` ByteData passed from Dart: // `[offsetInBytes, formatIndex, nameByteLength]`. Attribute rows are @@ -128,11 +129,18 @@ const int32_t stride = buffer_layouts[buffer_index * kBufferLayoutInts + 0]; const int32_t attr_count_in_buffer = buffer_layouts[buffer_index * kBufferLayoutInts + 1]; + const int32_t step_mode = + buffer_layouts[buffer_index * kBufferLayoutInts + 2]; if (stride <= 0) { return absl::InvalidArgumentError( absl::StrCat("VertexBuffer.strideInBytes must be positive (got ", stride, ") on buffer at index ", buffer_index, ".")); } + if (step_mode < 0 || step_mode > 1) { + return absl::InvalidArgumentError( + absl::StrCat("VertexBuffer.stepMode is out of range (got ", step_mode, + ") on buffer at index ", buffer_index, ".")); + } if (attr_count_in_buffer < 0 || attr_cursor + static_cast<size_t>(attr_count_in_buffer) > attribute_count) { @@ -140,7 +148,10 @@ "Internal error: attribute count overruns the packed attributes " "blob."); } - stage_layouts.push_back({static_cast<size_t>(stride), buffer_index}); + stage_layouts.push_back({static_cast<size_t>(stride), buffer_index, + step_mode == 0 + ? impeller::VertexInputRate::kVertex + : impeller::VertexInputRate::kInstance}); // Track each attribute's byte range within this buffer so we can // detect overlaps after building them all. @@ -229,13 +240,12 @@ "shader.")); } // Match the shader's scalar type class (float vs signed int vs - // unsigned int). Mirroring WebGPU, Vulkan, and Metal, component-count - // mismatches are NOT errors: the shader receives default substitution - // (missing components default to (0, 0, 0, 1)) when the buffer - // supplies fewer components than declared, and reads only the leading - // components when the buffer supplies more. The shipped enum only - // contains 32-bit formats, so checking `type` alone is sufficient - // until 8/16-bit formats are added. + // unsigned int). Component-count mismatches are NOT errors: the shader + // receives default substitution (missing components default to + // (0, 0, 0, 1)) when the buffer supplies fewer components than + // declared, and reads only the leading components when the buffer + // supplies more. The shipped enum only contains 32-bit formats, so + // checking `type` alone is sufficient until 8/16-bit formats are added. // TODO(https://github.com/flutter/flutter/issues/186309): Add // normalized, packed, half-float, BGRA-swizzled, and 64-bit vertex // attribute formats; the format check will need to also verify
diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn b/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn index d6faf1f..e6f6071 100644 --- a/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn +++ b/engine/src/flutter/lib/ui/fixtures/shaders/BUILD.gn
@@ -57,6 +57,8 @@ impellerc("flutter_gpu_shaders") { shaders = [ + "//flutter/impeller/fixtures/flutter_gpu_instanced.frag", + "//flutter/impeller/fixtures/flutter_gpu_instanced.vert", "//flutter/impeller/fixtures/flutter_gpu_unlit.frag", "//flutter/impeller/fixtures/flutter_gpu_unlit.vert", "//flutter/impeller/fixtures/flutter_gpu_unlit_alt_instance.frag", @@ -65,7 +67,7 @@ ] fixtures = rebase_path("//flutter/impeller/fixtures") - shader_bundle = "{\"UnlitFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_unlit.frag\"}, \"UnlitVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_unlit.vert\"}, \"UnlitFragmentAltInstance\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_unlit_alt_instance.frag\"}, \"TextureFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_texture.frag\"}, \"TextureVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_texture.vert\"}}" + shader_bundle = "{\"InstancedFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_instanced.frag\"}, \"InstancedVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_instanced.vert\"}, \"UnlitFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_unlit.frag\"}, \"UnlitVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_unlit.vert\"}, \"UnlitFragmentAltInstance\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_unlit_alt_instance.frag\"}, \"TextureFragment\": {\"type\": \"fragment\", \"file\": \"${fixtures}/flutter_gpu_texture.frag\"}, \"TextureVertex\": {\"type\": \"vertex\", \"file\": \"${fixtures}/flutter_gpu_texture.vert\"}}" shader_bundle_output = "test.shaderbundle" }
diff --git a/engine/src/flutter/testing/dart/gpu_test.dart b/engine/src/flutter/testing/dart/gpu_test.dart index 850dfb5..94053c9 100644 --- a/engine/src/flutter/testing/dart/gpu_test.dart +++ b/engine/src/flutter/testing/dart/gpu_test.dart
@@ -32,6 +32,15 @@ ]); } +ByteData mvpUBO(Matrix4 mvp) { + return float32(<double>[ + mvp[0], mvp[1], mvp[2], mvp[3], // + mvp[4], mvp[5], mvp[6], mvp[7], // + mvp[8], mvp[9], mvp[10], mvp[11], // + mvp[12], mvp[13], mvp[14], mvp[15], // + ]); +} + gpu.RenderPipeline createUnlitRenderPipeline() { final gpu.ShaderLibrary? library = gpu.ShaderLibrary.fromAsset('test.shaderbundle'); assert(library != null); @@ -1048,6 +1057,67 @@ await comparer.addGoldenImage(image, 'flutter_gpu_test_triangle.png'); }, skip: !(impellerEnabled && flutterGpuEnabled)); + test('Can render instanced triangles with instance-rate vertex buffer', () async { + const triangleVertexCount = 3; + const triangleInstanceCount = 4; + final RenderPassState state = createSimpleRenderPass(); + + final gpu.ShaderLibrary library = gpu.ShaderLibrary.fromAsset('test.shaderbundle')!; + final gpu.RenderPipeline pipeline = gpu.gpuContext.createRenderPipeline( + library['InstancedVertex']!, + library['InstancedFragment']!, + vertexLayout: const gpu.VertexLayout( + buffers: <gpu.VertexBuffer>[ + gpu.VertexBuffer( + strideInBytes: 8, + attributes: <gpu.VertexAttribute>[ + gpu.VertexAttribute(name: 'position', format: gpu.VertexFormat.float32x2), + ], + ), + gpu.VertexBuffer( + strideInBytes: 24, + stepMode: gpu.VertexStepMode.instance, + attributes: <gpu.VertexAttribute>[ + gpu.VertexAttribute(name: 'instance_offset', format: gpu.VertexFormat.float32x2), + gpu.VertexAttribute( + name: 'instance_color', + offsetInBytes: 8, + format: gpu.VertexFormat.float32x4, + ), + ], + ), + ], + ), + ); + state.renderPass.bindPipeline(pipeline); + + final gpu.HostBuffer transients = gpu.gpuContext.createHostBuffer(); + final gpu.BufferView vertices = transients.emplace( + float32(<double>[ + -0.18, 0.18, // + 0.0, -0.18, // + 0.18, 0.18, // + ]), + ); + final gpu.BufferView instances = transients.emplace( + float32(<double>[ + -0.45, 0.45, 1, 0, 0, 1, // + 0.45, 0.45, 0, 1, 0, 1, // + -0.45, -0.45, 0, 0, 1, 1, // + 0.45, -0.45, 1, 1, 0, 1, // + ]), + ); + final gpu.BufferView vertInfo = transients.emplace(mvpUBO(Matrix4.identity())); + state.renderPass.bindVertexBuffer(vertices); + state.renderPass.bindVertexBuffer(instances, slot: 1); + state.renderPass.bindUniform(pipeline.vertexShader.getUniformSlot('VertInfo'), vertInfo); + state.renderPass.draw(triangleVertexCount, instanceCount: triangleInstanceCount); + state.commandBuffer.submit(); + + final ui.Image image = state.renderTexture.asImage(); + await comparer.addGoldenImage(image, 'flutter_gpu_test_instanced_triangles.png'); + }, skip: !(impellerEnabled && flutterGpuEnabled)); + test('createRenderPipeline rejects VertexLayout with wrong attribute format', () async { final gpu.ShaderLibrary library = gpu.ShaderLibrary.fromAsset('test.shaderbundle')!; try { @@ -1178,6 +1248,32 @@ ); }, skip: !(impellerEnabled && flutterGpuEnabled)); + test('draw calls reject negative instanceCount', () async { + final RenderPassState state = createSimpleRenderPass(); + final gpu.RenderPipeline pipeline = createUnlitRenderPipeline(); + state.renderPass.bindPipeline(pipeline); + + final gpu.HostBuffer transients = gpu.gpuContext.createHostBuffer(); + final gpu.BufferView vertices = transients.emplace( + float32(<double>[-0.5, 0.5, 0.0, -0.5, 0.5, 0.5]), + ); + state.renderPass.bindVertexBuffer(vertices); + + expect(() => state.renderPass.draw(3, instanceCount: -1), throwsRangeError); + expect(() => state.renderPass.drawIndexed(3, instanceCount: -1), throwsRangeError); + }, skip: !(impellerEnabled && flutterGpuEnabled)); + + test('draw calls with zero count or instanceCount are no-ops', () async { + final RenderPassState state = createSimpleRenderPass(); + final gpu.RenderPipeline pipeline = createUnlitRenderPipeline(); + state.renderPass.bindPipeline(pipeline); + + state.renderPass.draw(0); + state.renderPass.draw(3, instanceCount: 0); + state.renderPass.drawIndexed(0); + state.renderPass.drawIndexed(3, instanceCount: 0); + }, skip: !(impellerEnabled && flutterGpuEnabled)); + test('createRenderPipeline rejects VertexAttribute with unknown name', () async { final gpu.ShaderLibrary library = gpu.ShaderLibrary.fromAsset('test.shaderbundle')!; try {