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]
}