PaintEngine & Performance
Simple Painter is built around a single-pass-per-frame command architecture rather than
scattered per-draw calls. PaintEngine is the GPU command dispatcher: it collects
commands during the frame and executes them in one batch.
One command buffer per frame
Every frame, the engine runs a five-step cycle:
- Get a CommandBuffer from the pool.
- Iterate phases in order — Setup → Process → Draw → Commit → Composition.
- Record commands for each phase into the buffer (each phase wrapped in a profiler marker).
- Execute with a single
Graphics.ExecuteCommandBuffercall. - Cleanup — release the buffer to the pool and dispose all commands back to their object pools.
Every GPU operation is a small, object-pooled command bucketed into one of five ordered phases, recorded once and submitted with a single execute call. If nothing was painted this frame, no command buffer is submitted at all.
Enqueuing commands
Any system can enqueue a GPU command during the frame. Commands are typically rented from an object pool, so this is zero-allocation:
// Rent a command from its pool instead of allocating a new one.
var cmd = StampDepositCommand.Get(targetTexture, ink, in brush, samples, context, in surface);
PaintEngine.EnqueueCommand(cmd);
// After execution the command is disposed back to its pool automatically.
Pooling throughout
- Commands, command buffers, materials, shared meshes and solid-colour textures are all rented and reused instead of allocated per frame.
- Render-texture pooling — persistent simulation buffers are only reallocated when their format/size actually changes; short-lived scratch buffers route through Unity's native temporary render-texture pool; ping-pong buffer pairs standardise multi-pass simulation steps.
- Guaranteed VRAM teardown — every pooled render texture and cached solid texture is explicitly released when the paint engine shuts down.
Upfront shader warm-up
A SimulationCanvas pays every one of its shaders' first-compile cost once, right after
Awake, instead of leaving it to land on the player's first stroke. It draws Flow, the
active solver's own passes, and Bake/Commit for every active channel against small
throwaway targets — matching exactly what the canvas's normal update path will draw, so
nothing gets warmed that won't actually be used.
Batch raycasting & async readback
- Job System batch raycasting — dense per-frame stroke sampling (e.g. a fast Bezier stroke) automatically switches from a plain loop to parallel, job-scheduled raycasts once the batch is large enough to benefit.
- Async GPU readback — the Progress Tracker uses non-blocking
AsyncGPUReadbackexclusively, so measuring coverage never stalls the main thread. - Cross-platform format fallback — render-target format selection automatically degrades from floating-point to normalized formats on platforms without float-blend support (e.g. WebGL without the relevant extension).
Profiler markers
Each phase is wrapped in a Unity Profiler marker, making GPU bottlenecks easy to spot in the Profiler and Frame Debugger:
SimplePainter.Setup
SimplePainter.Process
SimplePainter.Draw
SimplePainter.Commit
SimplePainter.Composition
PaintEngine.EnqueueCommand() must be called from the main thread. Never reuse a command
after enqueuing it — the engine takes ownership and disposes pooled commands after execution.
Next: Paintable & Seam Fixing