Gododin Gododin Documentation

Linked repository source

examples/ripple_pond/subsystem/pond.odin

This read-only view comes from the file in the Gododin repository. It retains its design, example or evidence context.

Download the original file · Return to the guide

// Package pond owns the ripple pond's CPU state: two f64 height grids and the
// packed RGBA8 frame the game paints. No Godot/Gododin/Raylib imports.
//
// This is the "one substantial batch operation" the adoption tutorial moves
// out of GDScript: the per-frame wave-equation step. All arithmetic is
// deterministic f64 in a fixed evaluation order, chosen so the game's
// GDScript reference implementation (gd_ripples.gd) can run the identical
// expression sequence and produce byte-identical frames — that equivalence
// is what the example's oracle proves. Stepping, splashing, and painting
// allocate nothing after pond_create.
//
// One capacity is allocated up front (MAX_DIMENSION square); configure only
// selects the active rectangle inside it and zeroes state, so a reconfigure
// never reallocates and every instance's memory is bounded.
package pond

import "core:mem"

MAX_DIMENSION :: 256
// Heights outside ±EXTENT clamp in the color pass; typical play stays inside.
EXTENT :: 4.0
// Splash energy falloff by Manhattan radius; index 0 is the center cell.
FALLOFF :: [3]f64{1.0, 0.5, 0.25}
SPLASH_RADIUS :: 2

Pond :: struct {
	width, height: i32,
	damping:        f64,
	// One allocation backs both height grids; curr/prev are the active
	// window into it and swap on pond_step.
	grids:          []f64,
	curr, prev:     []f64,
	pixels:         []u8,
	steps:          i64,
	allocator:      mem.Allocator,
}

pond_create :: proc(allocator: mem.Allocator) -> ^Pond {
	if allocator.procedure == nil do return nil
	capacity := MAX_DIMENSION * MAX_DIMENSION
	grids, grids_err := make([]f64, 2*capacity, allocator)
	if grids_err != nil do return nil
	pixels, pixels_err := make([]u8, 4*capacity, allocator)
	if pixels_err != nil {
		delete(grids, allocator)
		return nil
	}
	p, owner_err := new(Pond, allocator)
	if owner_err != nil {
		delete(grids, allocator)
		delete(pixels, allocator)
		return nil
	}
	p^ = {
		allocator = allocator,
		grids     = grids,
		pixels    = pixels,
	}
	return p
}

// pond_destroy consumes and clears the owner's pointer; repeated cleanup is
// safe through that same owner.
pond_destroy :: proc(owner: ^^Pond) -> bool {
	if owner == nil || owner^ == nil do return true
	p := owner^
	allocator := p.allocator
	if p.grids != nil {
		if delete(p.grids, allocator) != nil do return false
		p.grids = nil
		p.curr = nil
		p.prev = nil
	}
	if p.pixels != nil {
		if delete(p.pixels, allocator) != nil do return false
		p.pixels = nil
	}
	if mem.free(p, allocator) != nil do return false
	owner^ = nil
	return true
}

// pond_configure selects the active rectangle and zeroes all state. The pond
// is then in its deterministic rest state: flat water, no pending energy.
pond_configure :: proc(p: ^Pond, width, height: i32, damping: f64) -> bool {
	if width < 8 || height < 8 || width > MAX_DIMENSION || height > MAX_DIMENSION {
		return false
	}
	if damping <= 0.0 || damping > 1.0 {
		return false
	}
	p.width = width
	p.height = height
	p.damping = damping
	p.steps = 0
	p.curr = p.grids[:int(width)*int(height)]
	p.prev = p.grids[int(width)*int(height) : 2*int(width)*int(height)]
	mem.zero_slice(p.curr)
	mem.zero_slice(p.prev)
	mem.zero_slice(p.pixels)
	return true
}

// pond_splash adds an impulse at the given cell, clamped to the interior so
// the fixed zero border always stays fixed.
pond_splash :: proc(p: ^Pond, x, y: i32, strength: f64) {
	falloff := FALLOFF
	for dy in i32(-SPLASH_RADIUS) ..= i32(SPLASH_RADIUS) {
		for dx in i32(-SPLASH_RADIUS) ..= i32(SPLASH_RADIUS) {
			nx := x + dx
			ny := y + dy
			if nx < 1 || ny < 1 || nx > p.width - 2 || ny > p.height - 2 {
				continue
			}
			radius := abs(dx) + abs(dy)
			if radius > SPLASH_RADIUS {
				continue
			}
			p.curr[ny*p.width + nx] += strength * falloff[radius]
		}
	}
}

// pond_step advances the damped wave equation by one fixed step over the
// interior. The border cells are a fixed zero frame. The evaluation order is
// the exact f64 sequence the GDScript reference runs:
//
//	next = (left + right + up + down) * 0.5 - previous, then * damping
//
// computed left-to-right with one rounding per operation.
pond_step :: proc(p: ^Pond) {
	w := p.width
	damping := p.damping
	grid := p.curr
	previous := p.prev
	for y in 1 ..< p.height - 1 {
		row := y * w
		for x in 1 ..< w - 1 {
			i := row + x
			sum := grid[i-1] + grid[i+1] + grid[i-w] + grid[i+w]
			previous[i] = (sum * 0.5 - previous[i]) * damping
		}
	}
	p.curr = previous
	p.prev = grid
	p.steps += 1
}

// pond_frame paints the current heights into the retained RGBA8 frame and
// returns it. The returned slice aliases the pond's own storage and stays
// valid until the next pond_frame call; the bridge copies it exactly once.
pond_frame :: proc(p: ^Pond) -> []u8 {
	count := int(p.width) * int(p.height)
	for i in 0 ..< count {
		t := (p.curr[i] + EXTENT) / (2.0 * EXTENT)
		if t < 0.0 {
			t = 0.0
		} else if t > 1.0 {
			t = 1.0
		}
		j := i * 4
		p.pixels[j + 0] = u8(16.0 + t * t * 60.0)
		p.pixels[j + 1] = u8(30.0 + t * 150.0)
		p.pixels[j + 2] = u8(60.0 + t * 170.0)
		p.pixels[j + 3] = 255
	}
	return p.pixels[:count * 4]
}