render

package
v1.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 12, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package render provides a GPU-driven render submission layer.

Package render provides the GPU submission layer for the Aqwabor engine.

The high-level entry point is GPU, which wraps all pipeline and buffer management behind a small set of methods:

gfx := render.New(dp)
gfx.Begin(dc, render.Clear{R: 0.05, G: 0.05, B: 0.1, A: 1})
gfx.SetCamera(viewProj, vpW, vpH)
batch := gfx.Sprites(cap)
batch.SetAll(sprites)
gfx.DrawSprites(batch)
gfx.DrawSpritesCulled(batch, viewBounds)
gfx.DrawStrokes(segments)
gfx.End()

Game code never touches wgpu.Buffer, bind groups, or WGSL directly.

Package render provides a GPU-driven render submission layer.

Index

Constants

View Source
const (
	WidthModePixels = 0 // width in screen pixels (constant under zoom)
	WidthModeWorld  = 1 // width in world units (scales with zoom)
)
View Source
const CameraUniformSize = 80

CameraUniformSize is the byte size of the CameraUniform struct (80 bytes).

View Source
const RampEntries = 1024

RampEntries is how many colours the ramp table holds.

A material with several ramp rows takes one entry per row, so the table is materials times rows: 49 materials at 16 rows is 784. The table is a uniform buffer at 16 bytes an entry, so this is 16 KB, well inside the 64 KB a uniform binding is guaranteed.

Variables

View Source
var InstanceBufferLayout = gputypes.VertexBufferLayout{
	ArrayStride: instanceDataSize,
	StepMode:    gputypes.VertexStepModeInstance,
	Attributes: []gputypes.VertexAttribute{
		{Format: gputypes.VertexFormatFloat32x2, Offset: 0, ShaderLocation: 2},
		{Format: gputypes.VertexFormatFloat32x2, Offset: 8, ShaderLocation: 3},
		{Format: gputypes.VertexFormatFloat32, Offset: 16, ShaderLocation: 4},
		{Format: gputypes.VertexFormatFloat32x4, Offset: 32, ShaderLocation: 5},
		{Format: gputypes.VertexFormatFloat32x2, Offset: 48, ShaderLocation: 6},
		{Format: gputypes.VertexFormatFloat32, Offset: 56, ShaderLocation: 7},
	},
}

InstanceBufferLayout describes the vertex buffer layout for instanced attributes. Offsets must match the Go struct layout (which matches WGSL alignment).

View Source
var MeshVertexLayout = gputypes.VertexBufferLayout{
	ArrayStride: 24,
	StepMode:    gputypes.VertexStepModeVertex,
	Attributes: []gputypes.VertexAttribute{
		{Format: gputypes.VertexFormatFloat32x2, Offset: 0, ShaderLocation: 0},
		{Format: gputypes.VertexFormatFloat32x4, Offset: 8, ShaderLocation: 1},
	},
}

MeshVertexLayout describes the vertex buffer layout for the mesh (locations 0-1).

View Source
var StrokeSegmentLayout = gputypes.VertexBufferLayout{
	ArrayStride: strokeSegmentSize,
	StepMode:    gputypes.VertexStepModeInstance,
	Attributes: []gputypes.VertexAttribute{
		{Format: gputypes.VertexFormatFloat32x2, Offset: 0, ShaderLocation: 0},
		{Format: gputypes.VertexFormatFloat32x2, Offset: 8, ShaderLocation: 1},
		{Format: gputypes.VertexFormatFloat32x2, Offset: 16, ShaderLocation: 2},
		{Format: gputypes.VertexFormatFloat32x2, Offset: 24, ShaderLocation: 3},
		{Format: gputypes.VertexFormatFloat32x4, Offset: 32, ShaderLocation: 4},
		{Format: gputypes.VertexFormatFloat32x4, Offset: 48, ShaderLocation: 5},
		{Format: gputypes.VertexFormatFloat32, Offset: 64, ShaderLocation: 6},
		{Format: gputypes.VertexFormatUint8x2, Offset: 68, ShaderLocation: 7},
	},
}

StrokeSegmentLayout describes the vertex buffer layout for instanced stroke segments.

View Source
var SubcellBufferLayout = gputypes.VertexBufferLayout{
	ArrayStride: subcellInstanceSize,
	StepMode:    gputypes.VertexStepModeInstance,
	Attributes: []gputypes.VertexAttribute{
		{Format: gputypes.VertexFormatFloat32x2, Offset: 0, ShaderLocation: 2},
		{Format: gputypes.VertexFormatUint32, Offset: 8, ShaderLocation: 3},
	},
}

SubcellBufferLayout is the vertex layout for the compact path. The mesh is a unit quad and the cell size comes from the pipeline uniform, so nothing here is per-instance except position and palette.

Functions

func BlendAlpha

func BlendAlpha() *gputypes.BlendState

BlendAlpha returns the standard alpha-blend state used by most pipelines.

func Clamp255

func Clamp255(v float32) uint32

Clamp255 clamps a float32 in [0,1] to a uint32 in [0,255].

func InstanceSlice

func InstanceSlice[T any](ib *instanceBufferOf[T], n int) []T

InstanceSlice returns a mutable slice of the CPU-side backing array. Write into it, then call WriteAll or WriteAt to mark the range dirty. This avoids per-slot Write calls when packing active instances into a contiguous block (see particle.Emitter.WriteInstances).

func MergeRects

func MergeRects(rects []image.Rectangle, max int) []image.Rectangle

MergeRects reduces damage to at most max rectangles by tiling the area they cover and unioning what falls in each tile.

A compositor wants a handful of rectangles, not one per moving thing, and unioning the nearest is cheaper than reporting the whole surface: the pixels in a tile's gaps are recomposited, the ones outside it are not. Tiling rather than pairwise merging keeps it linear, which matters when the count is thousands.

func PaletteOf

func PaletteOf(material int) uint32

PaletteOf is the value a cell carries to draw with a material's colour. Zero is not a material: a cell carrying it draws nothing.

func ScreenRect

func ScreenRect(r ViewBounds, camX, camY, zoom, vpW, vpH, scale float32) image.Rectangle

ScreenRect turns a world rectangle into the pixels it covers, which is what a compositor is told about when only part of a frame changed.

The result is in physical pixels with Y running down, the way a surface is addressed, and it is grown outward to whole pixels: a rectangle that covers half a pixel still dirties it.

func SimplifyPolyline

func SimplifyPolyline(coords []int32, closed bool, minSegPx float32) [][2]float32

SimplifyPolyline converts int32 coords to float32 points and skips segments shorter than minSegPx.

Types

type CameraUniform

type CameraUniform struct {
	ViewProj [16]float32 // 64 bytes
	Viewport [2]float32  // 8 bytes
	// contains filtered or unexported fields
}

CameraUniform is the GPU-side camera data. Must be 80 bytes to match WGSL struct alignment (mat4x4 + vec2 = 72 bytes, rounded up to next multiple of 16).

type CellStats

type CellStats struct {
	// Written is cells rewritten by the last Sync. A still grid drives it to
	// zero.
	Written int
	// Submitted is cells the last Draw covered.
	Submitted int
	// Draws is draw calls the last Draw issued.
	Draws int
}

CellStats is what the last Sync and Draw did.

type Cells

type Cells struct {
	// contains filtered or unexported fields
}

Cells draws a fixed grid whose colour per cell is a palette index: the compact 16-byte instance, against 64 for a sprite.

It is the layer for the thing a game has most of and moves least. The grid does not live in the ECS - a cell is not an entity, it is a row in a dense array the game already owns - so this takes that array and a set of what changed in it, and turns them into writes.

A cell's slot is a pure function of its coordinates, because cells never move between chunks. So there is no arena here, no holes and no relayout: the buffer is built once in chunk order and only the palette index of a cell ever changes afterwards.

cells := render.NewCells(gfx, render.CellsConfig{W: 512, H: 512, CellSize: 1})
cells.TouchAll()
cells.Sync(materials)          // the game's dense array, read not kept
cells.Draw(view)

func NewCells

func NewCells(gfx *GPU, cfg CellsConfig) *Cells

NewCells builds the grid's buffer with every cell in its place and every palette index zero, which draws nothing. Touch what should be visible, or TouchAll, and Sync.

func (*Cells) Damaged

func (c *Cells) Damaged() []ViewBounds

Damaged is the world rectangles the last Sync wrote into, one per chunk.

func (*Cells) Draw

func (c *Cells) Draw(view ViewBounds)

Draw submits the chunks a view covers. Call it between Begin and End, with the camera already set.

There is no per-cell cull: a cell is 16 bytes and the chunk test is what keeps the count down. The sprite path culls per instance because an instance there carries a size and can be anywhere; a cell cannot.

func (*Cells) Release

func (c *Cells) Release()

Release frees the grid's buffer.

func (*Cells) Stats

func (c *Cells) Stats() CellStats

Stats is what the last Sync and Draw did.

func (*Cells) Sync

func (c *Cells) Sync(materials []uint32)

Sync writes the cells that changed. materials is the game's dense array of palette values, one per cell in row-major order: it is read and neither kept nor written.

A value of zero draws nothing, so a cleared cell needs no special case. Use PaletteOf to turn a material into the value a cell carries.

func (*Cells) Touch

func (c *Cells) Touch(x, y int)

Touch records that a cell's material changed and its instance has to be written again. Out-of-range coordinates are ignored.

func (*Cells) TouchAll

func (c *Cells) TouchAll()

TouchAll records the whole grid, which is what a load or a teleport needs.

type CellsConfig

type CellsConfig struct {
	// W and H are the grid in cells.
	W, H int

	// CellSize is the world size of one cell. A grid shares it, which is what
	// keeps the instance at 16 bytes: the size is a uniform, not a field.
	// Defaults to 1.
	CellSize float32

	// OriginX and OriginY place cell 0,0 in the world.
	OriginX, OriginY float32

	// Chunk is how many cells a chunk covers on a side. A view draws whole
	// chunks, so smaller means a tighter fit and more draws. Defaults to 32.
	Chunk int
}

CellsConfig is what a grid needs to know that it cannot work out.

type Clear

type Clear struct {
	R, G, B, A float32
}

Clear describes the clear colour for Begin.

type Color

type Color struct {
	R, G, B, A float32
}

Color is an RGBA colour component. Values are typically in [0, 1].

type Components

type Components struct {
	Transform ecs.Comp[Transform]
	Color     ecs.Comp[Color]
	Sprite    ecs.Comp[Sprite]
}

Components holds the handles for the render component types. Registration returns it and a caller keeps it, because a handle is the only way to reach a component value and it is what makes access an array index rather than a lookup.

func MustRegisterECS

func MustRegisterECS(w *ecs.World) Components

MustRegisterECS is RegisterECS, panicking on error.

func RegisterECS

func RegisterECS(w *ecs.World) (Components, error)

RegisterECS registers the render component types with the world. Call once during startup, before spawning anything renderable.

type CullParams

type CullParams struct {
	InstanceCount uint32
	// OutputBase is the first index of this slot's region in the output buffer.
	// The shader compacts survivors from there rather than from zero, which is
	// what lets several culls share one buffer.
	OutputBase uint32
	// InputBase is the first instance the cull reads, so a cull can cover one
	// range of a layer's buffer rather than the whole of it.
	InputBase uint32

	MinBounds [2]float32
	MaxBounds [2]float32
	// contains filtered or unexported fields
}

CullParams is the compute shader uniform for culling parameters.

type CullPipeline

type CullPipeline struct {
	// contains filtered or unexported fields
}

CullPipeline manages GPU compute culling and indirect draw support.

Ordering contract: the compute pass that writes outputBuf/indirectBuf must be encoded (and submitted) before the render pass that reads them. WebGPU inserts the required memory barriers between the compute and render passes as long as EncodeDispatch happens-before the DrawInstancedIndirect call on the same queue submission order.

func NewCullPipeline

func NewCullPipeline(dev *wgpu.Device, queue *wgpu.Queue, maxInstances int) *CullPipeline

NewCullPipeline creates the compute cull pipeline and GPU buffers. queue is retained for params uploads; if nil, dev.Queue() is used.

func (*CullPipeline) BeginFrame

func (cp *CullPipeline) BeginFrame()

BeginFrame returns the pipeline to its first slot. Call once a frame, before any cull.

func (*CullPipeline) Claim

func (cp *CullPipeline) Claim(n int) (int, bool)

Claim reserves the next slot, reporting false when the frame is out of them. Claim takes a slot and the room in the output buffer for n survivors. The room is taken in order, so what the buffer has to hold is the frame's total rather than its largest cull times the slot count.

func (*CullPipeline) EncodeDispatch

func (cp *CullPipeline) EncodeDispatch(
	enc *wgpu.CommandEncoder,
	slot int,
	inputBuf *wgpu.Buffer,
	cameraBuf *wgpu.Buffer,
	firstInstance, instanceCount int,
	minBounds, maxBounds [2]float32,
)

EncodeDispatch records the compute cull pass into the command encoder. inputBuf: the InstanceBuffer's GPU buffer with all instances. cameraBuf: the camera uniform buffer (binding 3 in the shader).

func (*CullPipeline) Fits

func (cp *CullPipeline) Fits(n int) bool

Fits reports whether n more instances can be culled this frame.

func (*CullPipeline) IndirectBuffer

func (cp *CullPipeline) IndirectBuffer() *wgpu.Buffer

IndirectBuffer returns the indirect command buffer for DrawIndexedIndirect.

func (*CullPipeline) IndirectOffset

func (cp *CullPipeline) IndirectOffset(slot int) uint64

IndirectOffset is the byte offset of a slot's draw command.

func (*CullPipeline) OutputBuffer

func (cp *CullPipeline) OutputBuffer() *wgpu.Buffer

OutputBuffer returns the compacted instance output buffer.

func (*CullPipeline) OutputOffset

func (cp *CullPipeline) OutputOffset(slot int) uint64

OutputOffset is the byte offset of a slot's region in the output buffer, which is what the draw binds the instance stream at.

func (*CullPipeline) Release

func (cp *CullPipeline) Release()

Release releases GPU resources.

func (*CullPipeline) ResetIndirect

func (cp *CullPipeline) ResetIndirect(slot int, indexCount uint32)

ResetIndirect clears one slot's instance count. The compute shader adds to it atomically, so it starts each cull at zero. FirstInstance stays zero because the draw binds this slot's region of the output buffer directly.

func (*CullPipeline) SlotsLeft

func (cp *CullPipeline) SlotsLeft() int

SlotsLeft is how many culled draws remain available this frame.

type Culled

type Culled struct {
	// contains filtered or unexported fields
}

Culled names a cull dispatch that has been encoded and is waiting to be drawn. The zero value draws nothing.

It holds the pipeline it was encoded against, not only the slot: a later cull in the same frame can need a larger pipeline, and the draw has to read the buffers its own dispatch wrote rather than the ones the replacement holds.

type DeviceProvider

type DeviceProvider interface {
	Device() *wgpu.Device
	Queue() *wgpu.Queue
	SurfaceFormat() gputypes.TextureFormat
}

DeviceProvider gives the renderer access to GPU resources.

type DrawCmd

type DrawCmd struct {
	Mesh           *Mesh
	InstanceBuffer *InstanceBuffer
	FirstInstance  int
	InstanceCount  int
	Pipeline       *wgpu.RenderPipeline
}

DrawCmd is a single instanced draw command.

type FrameStats

type FrameStats struct {
	DrawCalls int
	Instances int
	Triangles int
}

FrameStats tracks per-frame rendering metrics.

type GPU

type GPU struct {
	// contains filtered or unexported fields
}

GPU is the public facade over the render submission layer. It owns the per-frame render pass, the pipelines and the resources.

A frame has two phases. Begin captures the frame's command encoder without opening a render pass, so compute work has somewhere to go; the first draw opens the pass. A compute pass cannot be recorded inside a render pass, and the previous shape made the encoder reachable only once the pass was already open, so the cull dispatch was dropped and the indirect draw drew nothing.

gfx.Begin(dc, clear)
gfx.SetCamera(vp, w, h)
h := gfx.CullSprites(batch, bounds)   // compute, before any draw
gfx.DrawSpritesCulled(h)              // opens the pass
gfx.End()

func New

func New(dp DeviceProvider) *GPU

New creates a GPU facade from a device provider.

func (*GPU) Begin

func (g *GPU) Begin(dc *gogpu.Context, c Clear) error

Begin captures the frame's command encoder and records that the pass will clear to c. It does not open the render pass; the first draw does, which is what leaves room for CullSprites in between.

func (*GPU) BeginLoad

func (g *GPU) BeginLoad(dc *gogpu.Context) error

BeginLoad is Begin for a frame that keeps what is already on the surface.

func (*GPU) CameraLayout

func (g *GPU) CameraLayout() *wgpu.BindGroupLayout

CameraLayout is the bind group layout of the engine's shared camera, for a pipeline built outside the engine. List it first in the pipeline layout and read the camera at group(0) binding(0); the engine binds and updates it.

func (*GPU) CullSprites

func (g *GPU) CullSprites(batch *SpriteBatch, viewBounds ViewBounds) Culled

CullSprites encodes a compute pass that tests every instance in the batch against viewBounds and the camera frustum, compacting the survivors and writing the draw count the GPU will use.

It must be called after Begin and before the first draw of the frame, because a compute pass cannot be recorded inside a render pass. Calling it once the pass is open is refused rather than silently dropped, which is what used to happen.

Several culls a frame are allowed, up to the pipeline's slot count. Each takes its own region of the output buffer and its own draw command, so they do not overwrite one another.

func (*GPU) CullSpritesRange

func (g *GPU) CullSpritesRange(batch *SpriteBatch, first, count int, viewBounds ViewBounds) Culled

CullSpritesRange is CullSprites over one range of the batch, which is what a scene submits: only the chunks a view covers are worth testing per instance.

func (*GPU) Device

func (g *GPU) Device() *wgpu.Device

Device returns the wgpu device.

func (*GPU) DrawSprites

func (g *GPU) DrawSprites(batch *SpriteBatch)

DrawSprites draws all sprites in the batch (no culling).

func (*GPU) DrawSpritesCulled

func (g *GPU) DrawSpritesCulled(c Culled)

DrawSpritesCulled draws the survivors of a cull encoded earlier this frame. It opens the render pass, so everything the cull needed is already recorded.

func (*GPU) DrawSpritesRange

func (g *GPU) DrawSpritesRange(batch *SpriteBatch, first, count int)

DrawSpritesRange draws one range of a batch. A layer's buffer is divided into chunks of world space, so a view is a few ranges of it rather than all of it.

func (*GPU) DrawStrokes

func (g *GPU) DrawStrokes(segments *StrokeBuffer)

DrawStrokes submits GPU-expanded stroke segments. Flushes pending dirty ranges automatically.

func (*GPU) DrawSubcells

func (g *GPU) DrawSubcells(cells *SubcellBuffer)

DrawSubcells draws the cell layer.

func (*GPU) DrawSubcellsRange

func (g *GPU) DrawSubcellsRange(cells *SubcellBuffer, first, count int)

DrawSubcellsRange draws one range of the cell layer, which is what a grid submits: the chunks a view covers rather than the whole world.

func (*GPU) DrawVertices

func (g *GPU) DrawVertices(pipe *wgpu.RenderPipeline, vertexBuffer *wgpu.Buffer, vertexCount uint32)

DrawVertices submits a non-indexed draw with a pipeline created outside the Renderer. The camera is bound at group 0 from the engine's buffer, so such a pipeline is built against CameraLayout and owns no camera of its own.

func (*GPU) DrawVerticesRange

func (g *GPU) DrawVerticesRange(pipe *wgpu.RenderPipeline, vertexBuffer *wgpu.Buffer, vertexCount, firstVertex uint32)

DrawVerticesRange submits a non-indexed draw for a sub-range of a vertex buffer.

func (*GPU) End

func (g *GPU) End()

End closes the current render pass.

func (*GPU) Queue

func (g *GPU) Queue() *wgpu.Queue

Queue returns the wgpu queue.

func (*GPU) Release

func (g *GPU) Release()

Release releases all GPU resources.

func (*GPU) SetCamera

func (g *GPU) SetCamera(viewProj [16]float32, viewportW, viewportH float32)

SetCamera writes the camera every pipeline draws through, including any built outside the engine against CameraLayout. It is one buffer and one write: a pipeline added later needs no line here, which is what the four separate camera buffers this replaced each cost.

func (*GPU) SetCellSize

func (g *GPU) SetCellSize(w, h float32)

SetCellSize sets the world-space size every cell is drawn at. A grid shares one size, so it is a uniform rather than bytes on every instance.

func (*GPU) SetRamp

func (g *GPU) SetRamp(table *RampTable)

SetRamp uploads the palette the compact instances index into. Upload it once; every cell reads it, which is what makes the index cheaper than the colour.

func (*GPU) Sprites

func (g *GPU) Sprites(capacity int) *SpriteBatch

Sprites creates a sprite batch with the given capacity. The batch wraps a shared unit quad mesh and an InstanceBuffer.

func (*GPU) Stats

func (g *GPU) Stats() FrameStats

Stats returns per-frame rendering metrics.

func (*GPU) Subcells

func (g *GPU) Subcells(capacity int) *SubcellBuffer

Subcells creates a buffer of compact 16-byte instances for the dense cell layer, where the colour is a palette index rather than an RGBA value.

func (*GPU) SurfaceFormat

func (g *GPU) SurfaceFormat() gputypes.TextureFormat

SurfaceFormat returns the surface texture format.

type IndirectCmd

type IndirectCmd struct {
	IndexCount    uint32
	InstanceCount uint32
	FirstIndex    uint32
	BaseVertex    int32
	FirstInstance uint32
}

IndirectCmd matches WebGPU's DrawIndexedIndirectCommand layout (20 bytes).

type InstanceBuffer

type InstanceBuffer = instanceBufferOf[InstanceData]

InstanceBuffer holds the 64-byte sprite instances.

func NewInstanceBuffer

func NewInstanceBuffer(dev *wgpu.Device, capacity int) *InstanceBuffer

NewInstanceBuffer creates a sprite instance buffer. The buffer carries Storage usage so the compute cull pass can read it.

type InstanceData

type InstanceData struct {
	Position [2]float32 // offset 0,  8 bytes
	Scale    [2]float32 // offset 8,  8 bytes
	Rotation float32    // offset 16, 4 bytes

	Color    [4]float32 // offset 32, 16 bytes
	UVOffset [2]float32 // offset 48, 8 bytes
	Layer    float32    // offset 56, 4 bytes
	Pad      float32    // offset 60, 4 bytes padding (16-byte struct alignment)
	// contains filtered or unexported fields
}

InstanceData is the per-instance data layout for instanced rendering.

The stride is 64 bytes: the WGSL struct ends with `layer: f32` after a `vec2`, so in a `array<InstanceData>` storage buffer the struct alignment rounds the size up to a multiple of 16 (60 -> 64). The trailing Pad keeps the Go struct, the vertex stride, and the WGSL storage stride identical.

type Mesh

type Mesh struct {
	VertexBuffer *wgpu.Buffer
	IndexBuffer  *wgpu.Buffer
	IndexCount   uint32
}

Mesh is a shared, immutable geometry (vertex + index buffers).

func NewMesh

func NewMesh(dev *wgpu.Device, queue *wgpu.Queue, vertices []MeshVertex, indices []uint32) *Mesh

NewMesh creates a Mesh from raw vertex and index data. queue is used for the initial upload; if nil, dev.Queue() is used.

func NewUnitQuad

func NewUnitQuad(dev *wgpu.Device, queue *wgpu.Queue) *Mesh

NewUnitQuad creates a 1×1 quad centred at the origin (vertices -0.5 to +0.5). queue may be nil (falls back to dev.Queue()).

func (*Mesh) Release

func (m *Mesh) Release()

Release releases GPU resources.

type MeshVertex

type MeshVertex struct {
	X, Y       float32
	R, G, B, A float32
}

MeshVertex is the per-vertex data for the mesh geometry itself.

type Pipeline

type Pipeline struct {
	// contains filtered or unexported fields
}

Pipeline manages a single instanced render pipeline. The camera it draws through is the renderer's, bound at group 0, so the pipeline owns no uniform of its own.

func NewPipeline

func NewPipeline(dev *wgpu.Device, format gputypes.TextureFormat, camBGL *wgpu.BindGroupLayout) *Pipeline

NewPipeline creates an instanced render pipeline against the engine's shared camera layout.

func (*Pipeline) Pipeline

func (p *Pipeline) Pipeline() *wgpu.RenderPipeline

Pipeline returns the underlying render pipeline.

func (*Pipeline) Release

func (p *Pipeline) Release()

Release releases GPU resources.

type RampTable

type RampTable struct {
	Colors [RampEntries][4]float32
}

RampTable is the palette the compact instances index into.

Materials are numbered from zero and a cell carries PaletteOf(material), which is one more than the material. The shift is what reserves a value for "draws nothing": a cell carrying 0 is skipped, so it can be cleared without being taken out of the buffer. Entry zero of the table is an ordinary colour, the one material zero draws with.

Use Set rather than writing Colors directly, and the shift stops being something to remember.

How materials and rows map onto a flat material number belongs to the game, not here.

func (*RampTable) Set

func (t *RampTable) Set(material int, r, g, b, a float32)

Set gives a material its colour. Materials are numbered from zero.

type Renderer

type Renderer struct {
	// contains filtered or unexported fields
}

Renderer owns the per-frame render pass and all draw pipelines.

func NewRenderer

func NewRenderer(dp DeviceProvider) *Renderer

NewRenderer creates a renderer from a device provider.

func (*Renderer) BeginFrame

func (r *Renderer) BeginFrame(dc *gogpu.Context) error

BeginFrame captures the encoder for a frame that loads existing content rather than clearing.

func (*Renderer) CameraBuffer

func (r *Renderer) CameraBuffer() *wgpu.Buffer

CameraBuffer returns the shared camera uniform buffer, which the compute cull binds so it tests against the same camera the draw uses.

func (*Renderer) CameraLayout

func (r *Renderer) CameraLayout() *wgpu.BindGroupLayout

CameraLayout is the bind group layout of the engine's camera, at group 0. A pipeline built outside the engine lists it first in its own layout and reads the camera at group(0) binding(0); the Renderer binds it for every draw.

func (*Renderer) ClearAndBeginFrame

func (r *Renderer) ClearAndBeginFrame(dc *gogpu.Context, cr, cg, cb, ca float32) error

ClearAndBeginFrame captures the frame's command encoder and records that the render pass will clear to the given colour. It does not open the pass: the first draw does that, which leaves room between here and there for compute work to be encoded.

func (*Renderer) CommandEncoder

func (r *Renderer) CommandEncoder() *wgpu.CommandEncoder

CommandEncoder returns the frame's command encoder, valid only during the current draw callback. Used by the GPU facade for compute cull dispatch.

func (*Renderer) Device

func (r *Renderer) Device() *wgpu.Device

func (*Renderer) DrawInstanced

func (r *Renderer) DrawInstanced(mesh *Mesh, instances *InstanceBuffer)

DrawInstanced submits an instanced draw command. It flushes pending dirty ranges first, so callers do not need a separate Flush call (an explicit Flush beforehand is still fine and not duplicated: Flush is a no-op when no dirty ranges remain).

func (*Renderer) DrawInstancedIndirect

func (r *Renderer) DrawInstancedIndirect(mesh *Mesh, cull *CullPipeline, slot int)

DrawInstancedIndirect submits an indirect instanced draw over culled data. The cull compute pass compacted survivors into this slot's region of the output buffer, which is bound as the instance stream, and wrote the draw count into this slot's indirect command.

func (*Renderer) DrawInstancedRange

func (r *Renderer) DrawInstancedRange(mesh *Mesh, instances *InstanceBuffer, first, count int)

DrawInstancedRange submits an instanced draw over one range of the buffer. The range is a run of chunks a view covers; the instances outside it are not drawn and not culled, which is the work the chunking exists to skip.

func (*Renderer) DrawStrokes

func (r *Renderer) DrawStrokes(segments *StrokeBuffer)

DrawStrokes submits an instanced draw for stroke segments. Each segment is expanded into a screen-space quad by the vertex shader. Flushes pending dirty ranges automatically.

func (*Renderer) DrawStrokesN

func (r *Renderer) DrawStrokesN(segments *StrokeBuffer, n int)

DrawStrokesN draws the first n segments. Map strokes are ordered by rank when they are built, so a level of detail is a smaller n.

func (*Renderer) DrawSubcells

func (r *Renderer) DrawSubcells(cells *SubcellBuffer)

DrawSubcells submits the compact instanced draw for the cell layer.

func (*Renderer) DrawSubcellsRange

func (r *Renderer) DrawSubcellsRange(cells *SubcellBuffer, first, count int)

DrawSubcellsRange submits one range of the cell layer. The range is a run of chunks a view covers; the cells outside it are not drawn.

func (*Renderer) DrawVertices

func (r *Renderer) DrawVertices(pipe *wgpu.RenderPipeline, vertexBuffer *wgpu.Buffer, vertexCount uint32)

DrawVertices submits a non-indexed draw with a pipeline built outside the Renderer. The camera is bound at group 0 from the engine's own buffer, so a custom pipeline is built against CameraLayout and owns no camera.

func (*Renderer) DrawVerticesRange

func (r *Renderer) DrawVerticesRange(pipe *wgpu.RenderPipeline, vertexBuffer *wgpu.Buffer, vertexCount, firstVertex uint32)

DrawVerticesRange submits a non-indexed draw for a sub-range of a vertex buffer, through the engine's camera at group 0.

func (*Renderer) EndFrame

func (r *Renderer) EndFrame()

EndFrame closes the render pass if one was opened, and drops the borrowed encoder. A frame that drew nothing opened no pass and has nothing to close.

func (*Renderer) PassOpen

func (r *Renderer) PassOpen() bool

PassOpen reports whether the render pass is already recording, which is what makes it too late to encode compute work for this frame.

func (*Renderer) Queue

func (r *Renderer) Queue() *wgpu.Queue

func (*Renderer) Release

func (r *Renderer) Release()

Release releases GPU resources.

func (*Renderer) Stats

func (r *Renderer) Stats() FrameStats

func (*Renderer) SubcellPipeline

func (r *Renderer) SubcellPipeline() *SubcellPipeline

SubcellPipeline returns the compact cell pipeline, building it on first use. It is built on demand because a game that draws no cell layer should not pay for its ramp table.

func (*Renderer) SurfaceFormat

func (r *Renderer) SurfaceFormat() gputypes.TextureFormat

func (*Renderer) UpdateCamera

func (r *Renderer) UpdateCamera(viewProj [16]float32, viewportW, viewportH float32)

UpdateCamera writes the camera every pipeline draws through. One buffer, one write, whatever is drawn this frame.

type Scene

type Scene struct {
	// contains filtered or unexported fields
}

Scene draws what a world holds. It is the layer between the ECS and the GPU: a game spawns entities and writes components, and nothing it touches names a buffer, a pipeline or a bind group.

The shape is one instance buffer per layer, divided into chunks of world space (see chunks.go). An entity that has not moved is not written and not walked; a view is the handful of buffer ranges its chunks cover; and what those ranges hold is handed to the GPU cull, which drops the instances that are inside a visible chunk but outside the view. Coarse on the CPU, fine on the GPU, and never per entity in Go.

What has changed is told, not discovered, and what tells it is the ECS's own awake partition: a Transform that is awake is one whose instance has to be written again. Sync writes those and puts them back to sleep. A still world is a world with nothing awake, and costs nothing per frame.

The scene invents no vocabulary of its own for this. Waking already means "this needs visiting" everywhere else in the engine, and a second way of saying it is how two subsystems end up disagreeing about what changed.

func NewScene

func NewScene(gfx *GPU, w *ecs.World, comps Components, cfg SceneConfig) *Scene

NewScene builds the scene for a world. The component handles are the ones RegisterECS returned; a scene reads through them and registers nothing of its own.

func (*Scene) Cull

func (s *Scene) Cull(view ViewBounds)

Cull encodes the compute half of a frame and draws nothing.

It exists for a frame that draws something other than this scene. A cull is a compute pass and a compute pass cannot be recorded inside a render pass, so every cull in a frame has to be encoded before whatever opens that pass. A caller drawing only this scene never needs it: Draw does both halves in the right order. A caller drawing a Cells layer under the scene does, because the cells open the pass, and without this the scene's culls arrive too late, are refused one per range per frame, and every range is drawn unculled.

scene.Cull(view)   // compute, before anything opens the pass
cells.Draw(view)   // opens the pass
scene.Draw(view)   // submits what Cull planned

Call it between Begin and the first draw. The plan it leaves is used by the next Draw for the same view and discarded by a Draw for any other.

func (*Scene) Damaged

func (s *Scene) Damaged() []ViewBounds

Damaged is the world rectangles the last Sync wrote into, one per chunk. It is what a compositor wants to be told about, and what a partial redraw would have to cover.

A rebuilt layer damages nothing in particular, so the whole of it counts: after a Sync that reports Rebuilt, treat the view as damaged entirely.

func (*Scene) Draw

func (s *Scene) Draw(view ViewBounds)

Draw submits the layers in order, through the camera already set on the GPU.

It follows the frame's two phases on its own: every cull it needs is encoded before the first draw opens the render pass. Call it between Begin and End, or after Cull when something else opens the pass first.

func (*Scene) Drop

func (s *Scene) Drop(e ecs.Entity)

Drop takes an entity out of the scene. Its slot is blanked rather than reclaimed, so dropping is a write and not a relayout.

Call it before destroying an entity. A destroyed entity cannot be Touched - an ecs.Set refuses a handle whose generation has moved on - so the scene has no way of being told after the fact. What it has instead is the sweep, which finds a dead entity within a bounded number of frames; Drop is what makes it immediate.

func (*Scene) Release

func (s *Scene) Release()

Release frees the layer buffers.

func (*Scene) Spawn

func (s *Scene) Spawn(t Transform, c Color, sp Sprite) ecs.Entity

Spawn creates a drawable entity and wakes it, so the next Sync writes it.

It exists because setting a component does not wake it, and an entity that is asleep is in nothing the scene walks. That was found from the outside: the first game on this engine fell back to visiting every row because of it.

func (*Scene) Stats

func (s *Scene) Stats() SceneStats

Stats is what the last Sync and Draw did.

func (*Scene) Sync

func (s *Scene) Sync()

Sync writes the awake transforms into the layer buffers and puts them back to sleep. Wake an entity when you move, recolour or re-layer it; leave it asleep and it keeps the instance it already has.

It reads Transform, Sprite and Color and writes none of them, so a Schedule can declare it as Reads(transform, sprite, color) and run it beside anything that does not write them. What it does write is the awake partition of Transform, which belongs to the renderer.

type SceneConfig

type SceneConfig struct {
	// ChunkSize is the side of a chunk in world units. Too small and a view
	// covers many chunks; too large and a chunk is redrawn for one moving
	// entity. Defaults to 64.
	ChunkSize float32

	// Layers is how many draw buckets there are. An entity's bucket is its
	// Sprite.Layer, clamped. Defaults to 1.
	Layers int
}

SceneConfig is what a scene needs to know that it cannot work out.

type SceneStats

type SceneStats struct {
	// Written is instances rewritten by the last Sync, which is the number a
	// still world drives to zero.
	Written int
	// Rebuilt is layers laid out again by the last Sync, which is the
	// expensive case: every instance in the layer written.
	Rebuilt int
	// Submitted is instances the last Draw covered, before the GPU cull.
	Submitted int
	// Draws is draw calls the last Draw issued.
	Draws int
}

SceneStats is what the last frame did, which is what a change to the scene is argued with.

type Sprite

type Sprite struct {
	Layer float32
	UVX   float32
	UVY   float32
	Flags uint32
}

Sprite holds rendering metadata: draw layer, UV offset, and flags.

type SpriteBatch

type SpriteBatch struct {
	// contains filtered or unexported fields
}

SpriteBatch is a convenience wrapper around InstanceBuffer for sprite rendering. It hides the raw mesh + buffer management behind a simple Set/SetAll interface. Created via GPU.Sprites().

func NewSpriteBatch

func NewSpriteBatch(dev *wgpu.Device, queue *wgpu.Queue, mesh *Mesh, capacity int) *SpriteBatch

NewSpriteBatch creates a sprite batch with the given capacity. mesh is the shared quad geometry; if nil, a unit quad is created.

func (*SpriteBatch) BeginFrame

func (sb *SpriteBatch) BeginFrame()

BeginFrame releases buffers left behind by earlier growth, once enough frames have passed that no submitted frame can still be reading them.

func (*SpriteBatch) Capacity

func (sb *SpriteBatch) Capacity() int

Capacity is how many instances the batch holds without growing. Writing past it grows the buffer rather than panicking, so this is a hint for a caller that would rather cap its own work than reallocate.

func (*SpriteBatch) Count

func (sb *SpriteBatch) Count() int

Count returns the number of sprites written this frame.

func (*SpriteBatch) Release

func (sb *SpriteBatch) Release()

Release frees GPU resources.

func (*SpriteBatch) Reset

func (sb *SpriteBatch) Reset()

Reset clears the batch for the next frame.

func (*SpriteBatch) Set

func (sb *SpriteBatch) Set(index int, sprite InstanceData)

Set writes a single sprite at the given index.

func (*SpriteBatch) SetAll

func (sb *SpriteBatch) SetAll(sprites []InstanceData)

SetAll replaces all sprites in the batch (dense write, one GPU upload).

type StrokeBuffer

type StrokeBuffer struct {
	// contains filtered or unexported fields
}

StrokeBuffer is a long-lived GPU buffer for stroke segment instances with dirty-range tracking. It follows the same two-mode upload model as InstanceBuffer: sparse Write for individual segment changes, or dense WriteAll for full polyline rebuilds.

func NewStrokeBuffer

func NewStrokeBuffer(dev *wgpu.Device, capacity int) *StrokeBuffer

NewStrokeBuffer creates a stroke buffer with the given capacity.

func (*StrokeBuffer) Buffer

func (sb *StrokeBuffer) Buffer() *wgpu.Buffer

Buffer returns the underlying GPU buffer.

func (*StrokeBuffer) CPUData

func (sb *StrokeBuffer) CPUData() []StrokeSegment

CPUData returns the CPU-side segment data slice (for direct access).

func (*StrokeBuffer) Capacity

func (sb *StrokeBuffer) Capacity() int

Capacity returns the maximum number of segments.

func (*StrokeBuffer) Count

func (sb *StrokeBuffer) Count() int

Count returns the number of valid segments written this frame.

func (*StrokeBuffer) Flush

func (sb *StrokeBuffer) Flush(queue *wgpu.Queue)

Flush uploads all dirty ranges to the GPU buffer. Must be called once per frame before drawing (DrawStrokes does this automatically when called through Renderer).

func (*StrokeBuffer) Reset

func (sb *StrokeBuffer) Reset()

Reset clears the segment count and dirty ranges for the next frame.

func (*StrokeBuffer) SetCount

func (sb *StrokeBuffer) SetCount(n int)

SetCount manually sets the segment count without writing data.

func (*StrokeBuffer) Write

func (sb *StrokeBuffer) Write(index int, seg *StrokeSegment)

Write marks a single segment slot for upload. Use for sparse updates where only a few segments change per frame.

func (*StrokeBuffer) WriteAll

func (sb *StrokeBuffer) WriteAll(src []StrokeSegment)

WriteAll copies src into the buffer starting at slot 0, sets the count, and marks [0, len(src)) dirty in one range. Use for full polyline rebuilds.

func (*StrokeBuffer) WriteAt

func (sb *StrokeBuffer) WriteAt(start int, src []StrokeSegment)

WriteAt copies src into the buffer starting at start, sets the count, and marks one dirty range.

type StrokePipeline

type StrokePipeline struct {
	// contains filtered or unexported fields
}

StrokePipeline renders screen-space-width polylines via vertex-shader expansion of segment instances. Each segment is one instance; the vertex shader expands it into a quad using adjacency for miter joins.

Bindings:

group(0) binding(0) = the engine's shared camera uniform (viewProj + viewport)
slot(0)            = quad vertex buffer (4 corners, 8 bytes each)
slot(1)            = segment instance buffer (StrokeSegment, 48 bytes)
index buffer       = quad indices (6 x uint16)

func NewStrokePipeline

func NewStrokePipeline(dev *wgpu.Device, format gputypes.TextureFormat, camBGL *wgpu.BindGroupLayout) *StrokePipeline

NewStrokePipeline creates the stroke render pipeline against the engine's shared camera layout.

func (*StrokePipeline) Pipeline

func (sp *StrokePipeline) Pipeline() *wgpu.RenderPipeline

Pipeline returns the underlying render pipeline.

func (*StrokePipeline) QuadIndexBuffer

func (sp *StrokePipeline) QuadIndexBuffer() *wgpu.Buffer

QuadIndexBuffer returns the unit quad index buffer (6 x uint16).

func (*StrokePipeline) QuadVertexBuffer

func (sp *StrokePipeline) QuadVertexBuffer() *wgpu.Buffer

QuadVertexBuffer returns the unit quad vertex buffer (4 corners, 8 bytes each).

func (*StrokePipeline) Release

func (sp *StrokePipeline) Release()

Release releases GPU resources.

type StrokeQuadVertex

type StrokeQuadVertex struct {
	X, Y float32
}

StrokeQuadVertex is a minimal vertex for the stroke quad mesh.

type StrokeSegment

type StrokeSegment struct {
	P0     [2]float32 // endpoint 0
	P1     [2]float32 // endpoint 1
	Prev   [2]float32 // neighbor before P0 (for miter)
	Next   [2]float32 // neighbor after P1  (for miter)
	Color0 [4]float32 // color at P0
	Color1 [4]float32 // color at P1
	Width  float32    // stroke width
	Flags  uint8      // bit 0: 0=pixels, 1=world
	Layer  uint8      // draw order layer
	// contains filtered or unexported fields
}

StrokeSegment is one line segment instance: two endpoints with neighbor context for miter joins. The vertex shader expands each segment into a quad (4 vertices, index-buffered as 2 triangles).

Layout (72 bytes, 16-byte aligned for WGSL storage):

Offset  Size  Field
 0       8    p0       (vec2<f32>)
 8       8    p1       (vec2<f32>)
16       8    prev     (vec2<f32>)
24       8    next     (vec2<f32>)
32      16    color0   (vec4<f32>)
48      16    color1   (vec4<f32>)
64       4    width    (f32)
68       1    flags    (u8: bit 0 = width mode)
69       1    layer    (u8)
70       2    pad

func BuildSegments

func BuildSegments(points [][2]float32, color [4]float32, width float32, flags uint8, layer uint8) []StrokeSegment

BuildSegments converts a polyline (ordered points) into StrokeSegments with adjacency for miter joins. End segments duplicate the endpoint as the missing neighbor.

width is the stroke width; flags encodes WidthModePixels/WidthModeWorld. color applies uniformly to all segments; per-vertex colors in the output are all set to this value.

type SubcellBuffer

type SubcellBuffer = instanceBufferOf[SubcellInstance]

SubcellBuffer holds the 16-byte palette-indexed instances.

func NewSubcellBuffer

func NewSubcellBuffer(dev *wgpu.Device, capacity int) *SubcellBuffer

NewSubcellBuffer creates a compact instance buffer for the palette path.

type SubcellInstance

type SubcellInstance struct {
	X, Y    float32 // offset 0, world position of the cell centre
	Palette uint32  // offset 8, index into the ramp table, plus one; 0 draws nothing
	// contains filtered or unexported fields
}

SubcellInstance is the compact per-instance format for the layer that carries by far the most instances: loose material drawn as one coloured cell each.

The colour is a palette index rather than an RGBA value. A cell's colour is a material identity, not an arbitrary number, so the instance carries a four-byte index into a ramp table uploaded once, against the sixteen bytes an RGBA colour costs. With the size and rotation a grid does not need either, the instance is 16 bytes rather than 64, which is a quarter of the per-frame bandwidth on that layer.

It also changes what a moving light costs: a flashlight cone sweeping over cells becomes a change of row index per cell rather than a recolour.

type SubcellPipeline

type SubcellPipeline struct {
	// contains filtered or unexported fields
}

SubcellPipeline draws the compact instances. The camera is the engine's, at group 0; what this pipeline owns is the ramp table and the cell size, which sit at group 1 because group 0 belongs to the engine.

func NewSubcellPipeline

func NewSubcellPipeline(dev *wgpu.Device, queue *wgpu.Queue, format gputypes.TextureFormat, camBGL *wgpu.BindGroupLayout) *SubcellPipeline

NewSubcellPipeline builds the compact instanced pipeline against the engine's shared camera layout.

func (*SubcellPipeline) Mesh

func (p *SubcellPipeline) Mesh() *Mesh

func (*SubcellPipeline) ParamsBindGroup

func (p *SubcellPipeline) ParamsBindGroup() *wgpu.BindGroup

ParamsBindGroup is the pipeline's own bindings - the ramp table and the cell size - which the draw sets at group 1.

func (*SubcellPipeline) Pipeline

func (p *SubcellPipeline) Pipeline() *wgpu.RenderPipeline

func (*SubcellPipeline) Release

func (p *SubcellPipeline) Release()

Release frees the pipeline's GPU resources.

func (*SubcellPipeline) SetCellSize

func (p *SubcellPipeline) SetCellSize(queue *wgpu.Queue, w, h float32)

SetCellSize sets the size every cell is drawn at, in world units. A grid shares it, so it is a uniform rather than sixteen bytes on every instance.

func (*SubcellPipeline) SetRamp

func (p *SubcellPipeline) SetRamp(queue *wgpu.Queue, table *RampTable)

SetRamp uploads the palette. It is uploaded once and read by every instance, which is what makes the index cheaper than the colour it replaces.

type Transform

type Transform struct {
	X, Y   float32
	Rot    float32
	SX, SY float32
}

Transform stores the 2D world-space position, rotation, and scale of a sprite entity. Default scale is (1, 1).

type ViewBounds

type ViewBounds struct {
	MinX, MinY float32
	MaxX, MaxY float32
}

ViewBounds defines an axis-aligned bounding box for GPU culling.

func ViewOf

func ViewOf(camX, camY, zoom, vpW, vpH float32) ViewBounds

ViewOf is the world rectangle a camera covers, which is what Scene.Draw takes and what the cull tests against. It is here because otherwise every game writes the same four lines of arithmetic.

scene.Draw(render.ViewOf(cam.X, cam.Y, cam.Zoom, vpW, vpH))

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL