bevy-canvas-2d
A Bevy plugin that provides a very fast chunked 2D pixel canvas for pixel-art rendering, simulations, and procedural graphics, backed by CPU buffers and GPU Images.
Overview
The canvas is split into a grid of chunks. Each chunk has:
- a CPU
Vec<u32>storing packed RGBA8 pixels (row-major), and - a GPU
Imageupdated via partial texture uploads using dirty rectangles.
Updates are submitted to the render world each frame and uploaded with RenderQueue::write_texture, keeping writes efficient even when you only change small parts of the canvas.
Features
- Message-based draw API:
ClearCanvasDrawPixelDrawPixelsDrawRect(row-major)DrawSpan(row-major stream)
- Chunked textures (helps keep uploads small and predictable)
- Dirty-rect tracking per chunk (uploads only changed regions)
- Toroidal wrap for draw operations that exceed canvas bounds
- Bottom-left origin canvas coordinates
Use cases
- Cellular automata and grid-based simulations
- Pixel-art tools and editors
- Procedural texture generation
- Visual debugging buffers
- High-frequency per-pixel updates in Bevy
Compatibility
| Bevy | Canvas 2D |
|---|---|
| 0.17 | 0.1 |
Installation
[]
= "0.1"
Quick Start
use *;
use *;
Canvas Configuration
Configure the canvas via the CanvasConfig struct passed to the CanvasPlugin.
use ;
use *;
new
.add_plugins
.add_plugins
.run;
| Parameter | Description |
|---|---|
canvas_z_index |
Z index of the canvas images |
clear_colour |
Default clear colour (packed RGBA8 u32) |
canvas_size |
Size of the canvas in pixels |
num_chunks |
Number of chunks in X and Y. Note that canvas_size must be exactly divisible by num_chunks |
Drawing API
All drawing is done by sending messages that are consumed each update. Note that all writes overwrite existing pixels; there is no blending.
Packing Colours
Colours are packed as little-endian RGBA8 u32 for efficiency.
use *;
let white = pack_rgba8;
let red = pack_rgba8;
Clear Canvas
use *;
use *;
Draw Individual Pixels
use *;
use *;
Draw Multiple Independent Pixels
use *;
use *;
Draw Rectangles (Row-Major)
DrawRect draws a rectangle of size starting at start. The source pixels are row-major: $\text{index} = y \times \text{width} + x$.
If the rectangle exceeds the canvas bounds, it will wrap around toroidally.
use *;
use *;
Draw Spans (Row-Major Stream)
DrawSpan writes a contiguous stream of pixels starting at start.
It advances across X, then moves up a row, wrapping at edges.
If the wrap exceeds the horizontal bound, then the pixels will wrap to the next row up. If the wrap exceeds the vertical bound, then the pixels will wrap back to the bottom of the canvas.
use *;
use *;
Examples
See the examples/ folder for example Bevy apps using the canvas.
| Example | Description | Run Command |
|---|---|---|
| simple | Basic canvas setup. (No interaction) | cargo run --example simple |
| clear_colour | Clears the canvas to random colours each frame. | cargo run --example clear_colour |
| draw_pixel | Draws random individual pixels each frame. | cargo run --example draw_pixel |
| draw_pixels | Draws random multiple independent pixels each frame. | cargo run --example draw_pixels |
| draw_rect | Draws rectangles of random positions, sizes and colours each frame. | cargo run --example draw_rect |
| draw_span | Fills the canvas with random spans each frame. | cargo run --example draw_span |
Details
Coordinate system
Canvas coordinates are bottom-left origin. Internally this is achieved by flipping the chunk sprites on Y (scale.y = -1.0).
All draw operations wrap toroidally within the canvas bounds.
Performance notes
Dirty tracking is per chunk and unions all writes into a single axis-aligned dirty rectangle. Smaller chunks reduce upload size but increase sprite count; larger chunks reduce sprite count but increase upload cost.
GPU uploads are done via RenderQueue::write_texture.
Upload rows are padded in X to satisfy the WGPU constraint that bytes_per_row is 256-byte aligned. For RGBA8, that’s 64 pixels alignment.