The latest from Osavus
Small beginnings.
A place that’s yours.
Start with a table in a sunlit Addis Ababa courtyard. Merge ingredients, serve your neighbors and grow a café, one good cup at a time.
Beans
Ground
Coffee
A place for practical things and unfamiliar worlds. Software made with curiosity and love, from Estonia.
Step insideFrom the things you collect to the places you imagine. Find something that speaks to you.
The latest from Osavus
Small beginnings.
A place that’s yours.
Start with a table in a sunlit Addis Ababa courtyard. Merge ingredients, serve your neighbors and grow a café, one good cup at a time.
Beans
Ground
Coffee
Every piece has a story.
Keep yours with it.
The coin you found abroad. The note you inherited. A home for your coins, banknotes and the details that make them yours.
A little history.
A place in your collection.
Pilot a small ship into a universe of restless stars, distant worlds and strange encounters. Pick a direction. See what’s out there.
Game · In developmentBe in the conversation. Let your Mac keep the transcript and the things to follow through.
Five difficulty levels. Hints that help you learn. A quieter kind of exploration.
Beyond the horizon
New tools, unfamiliar languages, unexpected algorithms. A few ideas we’d like to explore by making something useful—or simply worth playing with.
A calmer way to understand where your money goes, and what comes next.
Designed to work entirely offline, syncing with family nearby through a shared ledger.
Local sync · Bluetooth · Blockchain & consensusPlant a tiny ecosystem. Change the conditions. See what finds a way to live there.
Artificial life · Cellular automataHum a phrase or tap the table. Hear an instrument answer with something you didn’t quite expect.
Generative sound · Signal processingCapture a place you love. Keep a little 3D memory you can look around again.
Spatial capture · Gaussian splattingA trip, a move, a crowded week. Set the limits, change one thing, and see what becomes possible.
Constraint solving · Search algorithmsBehind the projects
Osavus is an independent software studio in Estonia. We follow the ideas that stay with us: practical tools, thoughtful details, and the occasional journey far from everyday life.
Get in touchEngineering mode / Osavus field notes
Same worlds. A different way in.
Pull apart the pictures. Follow the equations. Meet the algorithms that make it all feel alive.
01 / The portal · discrete fields
The landscape is an image. The water is state: height and velocity, stored in two alternating GPU textures. Your pointer injects a disturbance; neighboring samples carry it outward.
A five-point stencil approximates the spatial Laplacian. Each fixed step updates velocity from local curvature, then height from velocity. The textures exchange roles, so no draw reads the field it is writing.
The visible surface is a second computation. Centered differences estimate ∇h; a bounded displacement bends the image lookup. Moving slopes sample the luminous rim for reflections. A zero field reproduces the original image.
float laplacian = texelFetch(uField, clamp(p + ivec2(1, 0), lo, hi), 0).r
+ texelFetch(uField, clamp(p - ivec2(1, 0), lo, hi), 0).r
+ texelFetch(uField, clamp(p + ivec2(0, 1), lo, hi), 0).r
+ texelFetch(uField, clamp(p - ivec2(0, 1), lo, hi), 0).r
- 4.0 * state.r;
// Mask data and image data have top-left origins; field textures have the
// framebuffer's bottom-left origin. Only these two samples need a Y flip.
float domain = texture(uMask, vec2(vUv.x, 1.0 - vUv.y)).r;
float velocity = (state.g + laplacian * uWaveSpeed) * mix(0.7, 0.991, domain);
float height = state.r + velocity;
Production shader excerpt. Field velocity is a height increment per simulation step; these are normalized display units.
The circular opening defines the wave domain. Foreground mountains only occlude the displayed surface; they do not become artificial obstacles in the simulation. Edge damping and a separate refraction mask protect the illustrated rim and ridge.
Pointer paths are integrated with midpoint samples no more than 0.0075 opening units apart. Impulse amplitude scales with path length, reducing dependence on event density. Each step accepts at most 32 queued impulses; long jumps and field amplitudes are bounded.
This is a damped height-field wave model. Its five-point stencil, damping, impulses and clamps are designed for a responsive surface, with no incompressible flow, volumetric water or Navier–Stokes solve. The coefficient scales as N² to retain approximately the same propagation speed in normalized image space when the grid changes.
The field fades during the final eight seconds of a 16-second settling window, then clears and stops drawing. A stalled frame discards excess elapsed time. Unsupported GPU capabilities retain the source image.
Read the implementation: wave solver and surface shader ↗ · domain and occlusion masks ↗
02 / BUNNA CAFÉ · STATE & DURABILITY
A drag becomes a command. A command proposes a new world. The satisfying part happens after that world has been saved.
The shared Kotlin engine is a deterministic reducer. It validates the incoming state, applies the command, then validates the result. An invalid move returns the original state.
A coroutine actor serializes commands with at most one unacknowledged storage transaction. The candidate can appear while saving; merge sounds, haptics and presentation receipts wait for acknowledgement.
(St+1, Et) = reduce(St, Ct)
A storage error does not prove a transaction failed. If the outcome is unknown, the session reads the saved head and compares it with the proposed head, including its transaction identifier. A match acknowledges the write; otherwise the session restores its last durable state and blocks further changes until recovery.
Queued commands carry the café run identifier. Restarting a café cannot cause an old command to mutate its replacement. Foreground state and a presentation epoch also gate delayed feedback.
Implementation reviewed: GameEngine.kt · GameSession.kt. The diagram and code are explanatory reductions of the current shared engine.
candidate, effects = reduce(durable, command)
head = nextRevision(candidate)
result = await commit(head, candidate)
if result == UNKNOWN:
result = await readHead() == head
if acknowledged(result):
durable = candidate
publishIfCurrent(effects)
else:
restore(durable)
pauseUntilRecovered()
A save acknowledgement is part of the game loop.
03 / EXERGUE · PARAMETRIC DESIGN
The native app’s guilloché motif begins with a circle rolling inside another circle. The pen does the rest.
Exergue draws its engraved rosette from four layered hypotrochoids. Each layer has its own radii, pen offset, rotation and scale. The path is generated once, then fitted to the drawing surface.
Here, R = 150 and r = 105 come from the outer native layer. Change the pen offset d to see the same construction move from restrained loops to a denser engraving.
For integer radii, both angular terms return to their starting phase after r / gcd(R, r) revolutions. These radii have gcd = 15, so the complete trace spans seven revolutions: 0 ≤ θ ≤ 14π.
The native implementation samples each of its four layers at 1,400 intervals. Its other (R, r, d) triples are (150, 90, 66), (140, 60, 84) and (90, 36, 60). Compose caches the paths with remember and draws them with a scale transform.
Implementation reviewed: Guilloche.kt, used by the native wordmark and empty states. This lab reconstructs one motif layer; it does not generate or analyze the photographed banknote.
Reconstruction of the native motif · Source-derived teaching visualization
04 / COSMIC FRONTIERS · PROCEDURAL WORLDS
Before stars have surfaces, they need somewhere to exist. A geometric graph supplies the skeleton.
A seeded generator places cluster centres. Two centres are connected when their diameter circle contains no third centre: the Gabriel graph. Each accepted edge becomes a bowed quadratic Bézier filament.
The same sampled polyline guides star placement and nebula geometry. Sampling by cumulative length gives a long filament proportionally more stars, avoiding the bias of sampling uniformly in the curve parameter.
The current implementation tests every pair against all other centres: O(K³) work, deliberately bounded to at most 16 clusters. The output is a sparse geometric graph, but that does not make the construction itself linear.
A filament uses B(t) = (1−t)²a + 2(1−t)tm + t²b, with its control point displaced from the edge midpoint. A cumulative polyline-length table maps a random distance to a segment and interpolation fraction. It approximates arc length rather than solving the curve integral.
The population counts are rounded to integers; the field receives the remainder. This is an authored procedural game universe, not an N-body cosmological simulation.
Implementation reviewed: universe_layout.lua. World-generation notes ↗
for each pair (a, b):
midpoint = (a + b) / 2
radius² = distance²(a, b) / 4
if no other centre is inside:
connect(a, b)
distance = random() * totalFilamentLength
position = interpolateByArcLength(distance)
Schematic input points · Exact empty-disc adjacency · Pseudocode
05 / CALL ASSISTANT · SIGNAL PROCESSING
Two known audio streams keep “You” and “Remote” separate. Each becomes 16 kHz mono, then a deterministic voice-activity segmenter decides when to send an utterance to local Whisper.
if not speech:
α = 0.10 if E < η else 0.01
η = (1 − α) * η + α * E
keep 200 ms before onset
end after 600 ms of silence
force a boundary at 12 seconds
If speech trained the noise floor, a long monologue could raise its own threshold until it disappeared. Only non-speech frames update η; downward adaptation is faster than upward adaptation. Non-finite energy values leave the estimate untouched.
A 200 ms pre-roll protects word onsets. Timestamps come from sample indices, so transcription latency does not move them. This is an energy heuristic: channel labels distinguish the two capture streams, not individual remote speakers.
Implementation reviewed: audio/vad.rs and audio/capture.rs. Equations and pseudocode summarize the production segmenter.
06 / SUDOKU · CONSTRAINT SEARCH
The exact Rust solver counts solutions with backtracking. To establish uniqueness, it only needs to distinguish zero, one and at least two; the second solution ends the search.
cell = argmin popcount(candidates(cell))
for digit in candidates(cell):
place(cell, digit)
search(limit = 2)
undo(cell)
if solutions == 2: return
unique = solutions == 1
R, K and B denote the digits already used in a row, column and box. The implementation scans those peers to clear bits from a nine-bit mask. MRV selects the empty cell with the fewest candidates; an empty mask rejects a branch immediately.
The generator removes a clue, tests uniqueness, and restores it if the test fails. For non-Master targets, removal also has to remain solvable by the human-technique solver within the requested difficulty. A unique answer alone says little about how a person will reach it.
Implementation reviewed: solver_exact.rs, grid.rs and digger.rs. Compact notation and pseudocode preserve the algorithm, not its full control flow.
On the drawing board / research questions
The next ideas, expressed as technical questions. These are possible directions, waiting to become implementations.
The studio / an engineering disposition
Osavus is an independent software studio in Estonia. The projects travel in different directions; the questions underneath them keep recurring.
What is the state? Which quantities should stay invariant? What happens when a write fails, a frame stalls, or the model reaches its limit?
This mode is an invitation to inspect those decisions. Code excerpts come from the implementations; compact pseudocode and reconstructed diagrams are labeled. The assumptions belong beside the equations.
Compare notes ↗07 / The founder’s black hole · from equations to pixels
The light beside “stay curious, keep exploring” follows a set of equations. Here, you can change their inputs. Follow one ray through curved spacetime, then take apart the image it helps create.
Every pixel launches a ray from the camera. Integrate its path through Schwarzschild spacetime.
If the ray meets the flat disk, that intersection supplies the pixel’s material and frequency shift.
Procedural texture, color, exposure and bloom turn the computed geometry into the finished visual.
Experiment 01 / the path
Change the mass or the incoming ray’s impact parameter. The upper plot follows the light; the lower plot explains whether it can turn back. Try increasing the mass while keeping the impact parameter fixed.
The moving point links the two plots. For an ideal ray, a turning point occurs where U = 1. Above that line, radial motion is forbidden. Animation advances the computed path in affine parameter λ, not clock time.
More mass enlarges the capture cross-section.
The incoming ray’s angular momentum per unit energy, b = L / E.
Scenarios restore M = M₀.
L = |x × dx/dλ| is conserved. In reference units Rs0 = 1, M = μ/2. The numerical solver advances position and affine velocity together; it never normalizes velocity after a step.
The barrier peaks at r = 1.5μ. Its maximum is [b / bcrit]², where bcrit = (3√3 / 2)μ. The event horizon is smaller: r = μ.
At the critical impact parameter, the turning points merge at the photon sphere. An ideal incoming ray approaches that unstable circular orbit asymptotically. Slightly larger b scatters; slightly smaller b captures. A finite step size and iteration budget only approximate this boundary.
The barrier’s horizontal line is the normalized energy level 1. An interpolated marker can briefly enter a classically forbidden part of the chart because the numerical trajectory has integration and interpolation error. The reported maximum energy drift comes from the solver’s stored integration steps.
The axes use the reference Schwarzschild radius Rs0 = 2GM₀/c². At mass ratio μ, the current horizon radius is μRs0. We solve at normalized impact b/μ, then scale positions and λ by μ; affine velocities and E² are unchanged. The finite camera plane stays at z = 32μ in reference units: 32 current Schwarzschild radii along the incoming direction.
The animation shows the portion of the trajectory crossing the viewing window. The full path is still solved: capture is reported below r = 1.001μ, escape beyond r = 46μ while moving outward, and exhausted step budgets remain unresolved. Neighboring rays use the same mass and numerical resolution.
Read the implementation: interactive model ↗ · production reference integrator ↗
Geodesic reduction: Belbruno & Pretorius, equations 11–13 ↗
Experiment 02 / the image
The upper arch is the far side of the same disk, seen along curved light paths. Nothing bends the disk mesh. Change the viewing elevation, reverse its rotation, and step through the rendering layers.
The image experiment needs WebGL. The ray laboratory and mathematical explanation above remain available.
0° is edge-on; 90° looks down on the disk.
· changes orbital brightness asymmetry.
Final image: frequency-shifted disk emission, mapped to the display with the production bloom pipeline.
In Intersections, cyan marks first disk-plane crossings and amber marks second crossings. Third and higher crossings are suppressed. Follow the arch as you raise the camera: it is an image of the emitting surface, not a torus of gas standing upright.
In Radiance, the same intersections pick up the procedural disk texture and emitter frequency shifts. Reversing rotation changes which side is enhanced. Final image adds the production glow.
Visual explanation: NASA’s warped-disk visualization ↗
g is the observed-to-emitted frequency ratio in the distant-observer approximation. The shader omits finite-observer redshift normalization. Gravity, transverse motion and orbital direction contribute; ℓ is taken from the backward-traced ray. The material uses g1.15 for brightness rather than the physical bolometric g⁴, and applies an artistic color palette.
float dt = min(radius * 0.055, 1.6);
vec3 next = position + velocity * dt + a * dt * dt * 0.5;
vec3 nextAcceleration = acceleration(next, h2);
vec3 nextVelocity = velocity + (a + nextAcceleration) * dt * 0.5;Velocity-Verlet with a radius-dependent affine step. The shader uses Rs = 1, so M = 1/2. It allows 240 steps; the reference solver increases that budget as the step fraction is reduced.
The image traces an orthographic camera in a nonrotating Schwarzschild spacetime. A thin disk emits over 3 ≤ r ≤ 10.5 in current Schwarzschild units; plane crossings are interpolated between numerical steps. The separate ray laboratory disables the disk to isolate the geodesic equation. It does not claim to track a selected pixel in this image.
The image pipeline stores radiance in RGBM-encoded RGBA8 targets, extracts highlights, applies six separable blur passes, and composites them with the scene. The same geometry is used in every layer; changing bloom changes the display, not the light paths.
The original renderer dims secondary images and suppresses unresolved higher-order emission. Palette, disk density, animation rate and exposure are art directed. This model does not solve Kerr rotation, magnetohydrodynamics or full radiative transfer. Reversing the disk’s orbital direction does not add black-hole spin.
Read the implementation: reference integrator ↗ · ray, material and composite shaders ↗ · image laboratory ↗
Physics references: circular-emitter frequency shifts ↗ · shadows, lensing rings and emission geometry ↗
stay curious, keep exploring.
Indrek Pari · Founder, Osavus