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
- Variables
- func BlendAlpha() *gputypes.BlendState
- func Clamp255(v float32) uint32
- func InstanceSlice[T any](ib *instanceBufferOf[T], n int) []T
- func MergeRects(rects []image.Rectangle, max int) []image.Rectangle
- func PaletteOf(material int) uint32
- func ScreenRect(r ViewBounds, camX, camY, zoom, vpW, vpH, scale float32) image.Rectangle
- func SimplifyPolyline(coords []int32, closed bool, minSegPx float32) [][2]float32
- type CameraUniform
- type CellStats
- type Cells
- type CellsConfig
- type Clear
- type Color
- type Components
- type CullParams
- type CullPipeline
- func (cp *CullPipeline) BeginFrame()
- func (cp *CullPipeline) Claim(n int) (int, bool)
- func (cp *CullPipeline) EncodeDispatch(enc *wgpu.CommandEncoder, slot int, inputBuf *wgpu.Buffer, ...)
- func (cp *CullPipeline) Fits(n int) bool
- func (cp *CullPipeline) IndirectBuffer() *wgpu.Buffer
- func (cp *CullPipeline) IndirectOffset(slot int) uint64
- func (cp *CullPipeline) OutputBuffer() *wgpu.Buffer
- func (cp *CullPipeline) OutputOffset(slot int) uint64
- func (cp *CullPipeline) Release()
- func (cp *CullPipeline) ResetIndirect(slot int, indexCount uint32)
- func (cp *CullPipeline) SlotsLeft() int
- type Culled
- type DeviceProvider
- type DrawCmd
- type FrameStats
- type GPU
- func (g *GPU) Begin(dc *gogpu.Context, c Clear) error
- func (g *GPU) BeginLoad(dc *gogpu.Context) error
- func (g *GPU) CameraLayout() *wgpu.BindGroupLayout
- func (g *GPU) CullSprites(batch *SpriteBatch, viewBounds ViewBounds) Culled
- func (g *GPU) CullSpritesRange(batch *SpriteBatch, first, count int, viewBounds ViewBounds) Culled
- func (g *GPU) Device() *wgpu.Device
- func (g *GPU) DrawSprites(batch *SpriteBatch)
- func (g *GPU) DrawSpritesCulled(c Culled)
- func (g *GPU) DrawSpritesRange(batch *SpriteBatch, first, count int)
- func (g *GPU) DrawStrokes(segments *StrokeBuffer)
- func (g *GPU) DrawSubcells(cells *SubcellBuffer)
- func (g *GPU) DrawSubcellsRange(cells *SubcellBuffer, first, count int)
- func (g *GPU) DrawVertices(pipe *wgpu.RenderPipeline, vertexBuffer *wgpu.Buffer, vertexCount uint32)
- func (g *GPU) DrawVerticesRange(pipe *wgpu.RenderPipeline, vertexBuffer *wgpu.Buffer, ...)
- func (g *GPU) End()
- func (g *GPU) Queue() *wgpu.Queue
- func (g *GPU) Release()
- func (g *GPU) SetCamera(viewProj [16]float32, viewportW, viewportH float32)
- func (g *GPU) SetCellSize(w, h float32)
- func (g *GPU) SetRamp(table *RampTable)
- func (g *GPU) Sprites(capacity int) *SpriteBatch
- func (g *GPU) Stats() FrameStats
- func (g *GPU) Subcells(capacity int) *SubcellBuffer
- func (g *GPU) SurfaceFormat() gputypes.TextureFormat
- type IndirectCmd
- type InstanceBuffer
- type InstanceData
- type Mesh
- type MeshVertex
- type Pipeline
- type RampTable
- type Renderer
- func (r *Renderer) BeginFrame(dc *gogpu.Context) error
- func (r *Renderer) CameraBuffer() *wgpu.Buffer
- func (r *Renderer) CameraLayout() *wgpu.BindGroupLayout
- func (r *Renderer) ClearAndBeginFrame(dc *gogpu.Context, cr, cg, cb, ca float32) error
- func (r *Renderer) CommandEncoder() *wgpu.CommandEncoder
- func (r *Renderer) Device() *wgpu.Device
- func (r *Renderer) DrawInstanced(mesh *Mesh, instances *InstanceBuffer)
- func (r *Renderer) DrawInstancedIndirect(mesh *Mesh, cull *CullPipeline, slot int)
- func (r *Renderer) DrawInstancedRange(mesh *Mesh, instances *InstanceBuffer, first, count int)
- func (r *Renderer) DrawStrokes(segments *StrokeBuffer)
- func (r *Renderer) DrawStrokesN(segments *StrokeBuffer, n int)
- func (r *Renderer) DrawSubcells(cells *SubcellBuffer)
- func (r *Renderer) DrawSubcellsRange(cells *SubcellBuffer, first, count int)
- func (r *Renderer) DrawVertices(pipe *wgpu.RenderPipeline, vertexBuffer *wgpu.Buffer, vertexCount uint32)
- func (r *Renderer) DrawVerticesRange(pipe *wgpu.RenderPipeline, vertexBuffer *wgpu.Buffer, ...)
- func (r *Renderer) EndFrame()
- func (r *Renderer) PassOpen() bool
- func (r *Renderer) Queue() *wgpu.Queue
- func (r *Renderer) Release()
- func (r *Renderer) Stats() FrameStats
- func (r *Renderer) SubcellPipeline() *SubcellPipeline
- func (r *Renderer) SurfaceFormat() gputypes.TextureFormat
- func (r *Renderer) UpdateCamera(viewProj [16]float32, viewportW, viewportH float32)
- type Scene
- type SceneConfig
- type SceneStats
- type Sprite
- type SpriteBatch
- type StrokeBuffer
- func (sb *StrokeBuffer) Buffer() *wgpu.Buffer
- func (sb *StrokeBuffer) CPUData() []StrokeSegment
- func (sb *StrokeBuffer) Capacity() int
- func (sb *StrokeBuffer) Count() int
- func (sb *StrokeBuffer) Flush(queue *wgpu.Queue)
- func (sb *StrokeBuffer) Reset()
- func (sb *StrokeBuffer) SetCount(n int)
- func (sb *StrokeBuffer) Write(index int, seg *StrokeSegment)
- func (sb *StrokeBuffer) WriteAll(src []StrokeSegment)
- func (sb *StrokeBuffer) WriteAt(start int, src []StrokeSegment)
- type StrokePipeline
- type StrokeQuadVertex
- type StrokeSegment
- type SubcellBuffer
- type SubcellInstance
- type SubcellPipeline
- func (p *SubcellPipeline) Mesh() *Mesh
- func (p *SubcellPipeline) ParamsBindGroup() *wgpu.BindGroup
- func (p *SubcellPipeline) Pipeline() *wgpu.RenderPipeline
- func (p *SubcellPipeline) Release()
- func (p *SubcellPipeline) SetCellSize(queue *wgpu.Queue, w, h float32)
- func (p *SubcellPipeline) SetRamp(queue *wgpu.Queue, table *RampTable)
- type Transform
- type ViewBounds
Constants ¶
const ( WidthModePixels = 0 // width in screen pixels (constant under zoom) WidthModeWorld = 1 // width in world units (scales with zoom) )
const CameraUniformSize = 80
CameraUniformSize is the byte size of the CameraUniform struct (80 bytes).
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 ¶
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).
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).
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.
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 InstanceSlice ¶
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 ¶
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 ¶
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.
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) Sync ¶
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.
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 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 ¶
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) 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 ¶
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 (*GPU) Begin ¶
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) 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) DrawSprites ¶
func (g *GPU) DrawSprites(batch *SpriteBatch)
DrawSprites draws all sprites in the batch (no culling).
func (*GPU) DrawSpritesCulled ¶
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) SetCamera ¶
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 ¶
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 ¶
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) 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 ¶
Mesh is a shared, immutable geometry (vertex + index buffers).
func NewMesh ¶
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 ¶
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()).
type MeshVertex ¶
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.
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.
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 ¶
BeginFrame captures the encoder for a frame that loads existing content rather than clearing.
func (*Renderer) CameraBuffer ¶
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 ¶
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) 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 ¶
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) 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 ¶
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 ¶
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) Spawn ¶
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) 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 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 ¶
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) 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.
type Transform ¶
Transform stores the 2D world-space position, rotation, and scale of a sprite entity. Default scale is (1, 1).
type ViewBounds ¶
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))