draw method
Draws what is bound. Always indexed — there is no non-indexed path in this engine, which is why the debug overlay keeps an identity index sequence.
instanceCount draws the bound geometry that many times, with the vertex
stage seeing an instance index and reading any attribute whose
BufferLayout.stepMode is VertexStepMode.instance from a per-instance
buffer. One means an ordinary draw and is the only value anything asked
for before mesh particles.
The one count that belongs to the draw rather than to a binding. Every other count here travels with the thing it counts — vertices with the vertex buffer, indices with the index buffer — because that is where it is known. An instance count is not a property of any buffer: the same mesh and the same instance buffer can be drawn a hundred times or ten.
Zero draws nothing, which matters because the number usually comes from a live population: a particle system with nothing alive should not be a special case at every call site.
firstIndex and indexCount draw a window of the bound indices rather
than all of them — P7. Null indexCount reads to the end of the
binding, so leaving both out is the whole buffer, as it always was.
On the draw, not on the binding, for the reason instanceCount is.
One buffer holding a model's every part is bound once and drawn a part at
a time, each part with its own material; a window on the binding would
mean binding the same buffer again for every part to say something only
the draw knows. WebGPU and Metal take a first index on the draw for the
same reason, and WebGL2 takes a byte offset there.
A window past the end of the binding is refused with a RangeError,
on every backend, before anything reaches the driver — see
indexWindow, which all four call. Left to the drivers it would be four
different answers: WebGL2 reports INVALID_OPERATION and draws nothing,
WebGPU invalidates the whole pass, Metal leaves it undefined, and the
software rasteriser would read past its bytes and throw from somewhere
that names none of them. Held by the conformance check
a window of the index buffer draws that window.
A draw whose pipeline would not build is skipped, never thrown —
every backend, and one from outside this repository owes the same. A
driver that links lazily refuses here, at the first draw; the backend
reports it to GraphicsDevice.pipelineRefusals and returns, and the
draws that follow with a pipeline left refused do nothing. A throw here
fails the frame, and a frame that fails for a driver's reason fails
again at the next one.
Implementation
@override
void draw({int instanceCount = 1, int firstIndex = 0, int? indexCount}) {
// A pipeline the device's driver would not link is skipped, not
// recorded: the draw never happened, as on a real backend.
final pipelineHandle = _pipeline;
if (pipelineHandle != null && !(_links?.call(pipelineHandle) ?? true)) {
return;
}
final window = switch (_boundIndices) {
final bound? => indexWindow(
bound,
firstIndex: firstIndex,
indexCount: indexCount,
),
null => (first: firstIndex, count: indexCount),
};
commands.add(
RecordedDraw(
instanceCount: instanceCount,
firstIndex: window.first,
indexCount: indexCount == null ? null : window.count,
),
);
final bindings = stageBindings;
final pipeline = _pipelineName;
if (bindings == null || pipeline == null) return;
// The fake names its pipelines 'Vertex+Fragment'; see FakeBackend.
for (final stage in pipeline.split('+')) {
final declared = bindings[stage];
if (declared == null) continue;
for (final block in declared.blocks) {
if (_boundBlocks[stage]?.contains(block) ?? false) continue;
violations.add('$pipeline: $stage declares block "$block", unbound');
}
for (final sampler in declared.samplers) {
if (_boundSamplers[stage]?.contains(sampler) ?? false) continue;
violations.add(
'$pipeline: $stage declares sampler "$sampler", unbound',
);
}
}
}