Skip to content

0.9.0 - #79

Draft
dzolotov wants to merge 170 commits into
mainfrom
0.9.0
Draft

dzolotov wants to merge 170 commits into
mainfrom
0.9.0

Conversation

@dzolotov

@dzolotov dzolotov commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator
  • The showcase shows what the bridge gained in 0.8.4
  • 0.9 has a plan: parity first, then past it
  • The 0.9 plan answers its own open questions
  • A hot reload reaches the picture
  • An edited model lands in the nodes the game holds
  • The editor plays the project it is editing
  • A level edit knows whether the run has to replay
  • A level saved in the editor lands in the running game
  • A tuned number is on the tape, and new code can relive the last seconds
  • A material dragged in the inspector changes the next frame
  • The river's land reaches the sky, and its gunners are seen firing
  • A river craft only moves where there is water to cross
  • A level saved in the editor lands in the platformer as it runs
  • A dragged slider shows in the running game, and a reload replays itself
  • The Medium article on the Flame bridge has its pictures
  • The Medium article calls River Sortie a game along the lines of River Raid
  • The Medium article calls River Sortie a River Raid-style shooter
  • The Medium article takes its title and points back to the first one
  • The Medium article reads less like a template
  • The Medium article takes the serial comma
  • An agent can play the level it is editing
  • The Medium article credits Carol Shaw for River Raid
  • The Carol Shaw paragraph claims only what it can back
  • The Carol Shaw paragraph says it once
  • The Medium article ends with where to write
  • A texture repainted under a running game is drawn at its new size
  • A decal box paints its picture onto the geometry inside it
  • The root's own test run checks the structure rules
  • The README counts the golden scenes again, and a decal's scissor is pinned
  • Edges can be smoothed by SMAA as well as FXAA
  • The lens bends the picture, flares, and takes a grading tool's table
  • A mirror in the floor, and a camera drawing into a texture
  • The sky becomes air, with stars at night and fog that lies low
  • A draw can read one part of its index buffer
  • A material written in the material language now builds into a shader bundle
  • A sky panorama edited under a running game lights it again
  • A save in the editor reaches the game as a patch, and a saved run replays it
  • A shader parameter dragged in the editor changes the running game
  • The editor plays a running game it attaches to by address
  • An actor decides by a tree that is written down
  • A paused run can be scrubbed, branched from, and bisected against another
  • A recorded run is a game's regression test, with no GPU
  • A save is migrated, written on pause, and kept in the cloud only with consent
  • A player's run can be sent with consent and drawn as a heatmap
  • A paused game can be photographed at any size, filtered and shared
  • A tool attached to a running game can read the frame it drew
  • A body can turn, when it is built to
  • The level editor has Windows and Linux runners, and Stop ends the tool there
  • A level bakes into a navigation mesh beside its grid
  • An animation graph picks the clip in the fixed step
  • A level can be shared behind a short code
  • A material channel can be shown in place of the light, beside it
  • An orthographic camera stops treating its eye as a point
  • Nodes that share a material can be put in an order
  • See-through casters shade the sun by what their material lets through
  • The bench's glass and liquids cast shadows by what they let through
  • The bench's shadows take the colour of what is in the tubes
  • A transmitting caster's shadow takes the colour of its depth
  • A see-through caster can paint what it lets through, past one
  • The bench works its shadows out with geometric optics
  • Light through a refracting caster is followed to where it lands
  • The bench can be lit by the engine's photons as well as its own optics
  • A masked edge can be smoothed as multisample coverage where the device can
  • A material written in the language draws on all four backends from one source
  • CI walks past packages/education, photons use the particles' varyings
  • A material written in the language takes values a game sets while it runs
  • A material edited under a running game is drawn by the next frame
  • The bench's glass is as visible as its shadow is dark
  • Thin glass composites over what is behind it; convex volumes
  • chemlab: liquid that keeps level, sloshes and refracts the bench
  • chemlab: pour a solution into a clean tube until both hold the same
  • Reflected light is lost to a shadow only as far as it is turned away
  • chemlab: run on macOS; controls on two lines; stream placeholder Metal accepts
  • chemlab: glass you can see; the engine's photons always on
  • chemlab: glass halfway between a ghost and a wall
  • chemlab: hold the sliders to their range during a pour
  • chemlab: the stream worked out from the flow, poured lip to lip
  • chemlab: the stream keeps to continuity and runs down the glass
  • chemlab: pour by the lean, lean in the picture
  • Liquids in flutter3d_physics: vessels, outflow, free surface
  • Liquids: a stream as parcels, held by walls, parting into drops
  • Liquids: capillarity, communicating vessels, a fluid world
  • Liquids: solutions and layers that do not mix
  • Liquids: drops, splashes and spills as particles
  • Liquids: buoyancy, drag and the load on a vessel
  • Meshes for liquids, streams and particles in flutter3d_core
  • feat: Run chemlab on the fluid module at life size
  • fix: Rename River's jet plane mesh off the engine's jetMesh
  • fix: Stamp a vessel's place with the frame's time
  • fix: Make the pour work on macOS and the web, and look like one
  • perf: Let a liquid standing still sleep
  • A scene can be written as widgets over the same graph
  • The article's collision snippet matches the example it quotes
  • Shadow cascades follow an orthographic camera's view
  • Particles and splats fog by depth through an orthographic camera
  • An orthographic near plane can stand behind the camera
  • Splats sort by depth through an orthographic camera
  • The overlay reads its pixel off the camera's own projection
  • A level brush can say where it draws
  • A renderer draws materials from more than one bundle
  • A hot edit that changes how a material binds reaches it
  • A WebGPU stage loaded from a bundle says what it declares
  • A material can answer each light itself
  • Each copy of an instanced batch carries numbers of the game's own
  • The rest of a scene can be written as widgets
  • The physics core starts in C
  • An overfull flask no longer floods the bench
  • The native world turns, burns and feels the wind
  • Native bodies find where they touch
  • Native crates rest, stack, slide and roll
  • The native world keeps a tree of where things are
  • Native bodies come in any convex shape
  • Native levels can be made of triangles
  • Native bodies can be joined
  • Fast native bodies stop at thin walls
  • A native bullet's turn is swept with it
  • The native world answers rays, and walks a character
  • A thin native rod no longer flies apart
  • Particles fall on the GPU as they do on the CPU
  • Rubble heaps on the GPU a frame late
  • Cloth hangs and drapes on the GPU as on the CPU
  • Water slumps, splashes and settles on the GPU too
  • A world steps on threads and lands on the same bits
  • Contacts solve a colour at a time in the fast mode
  • The broadphase remembers its pairs between steps
  • The fast mode solves contacts four to a vector
  • The core's Dart side reaches it by address, not by pointer
  • The physics core runs in the browser on the same API
  • The core steps on Web Workers in the browser
  • A ragdoll falls, tumbles and sleeps on its joints
  • The core's own tests run under flutter test again
  • The core steps to the same bits on every platform it reaches

Three pages for flame_flutter3d 0.8.4. Two keyboard layouts claim seats by
pressing, keys read from the keyboard rather than the focus. Ninety-six
simulated monsters take slots in one instanced batch and each chases the
nearer of two players, drawn between their steps. A game moves between two
level scenes with replaceScene3d while a view camera keeps the whole party
in frame.

Each page has a guide and a test that fails when its feature is taken away.
The showcase moves to 0.8.4+1 so its pages may name that release.
tasks/0.9-engine-roadmap.md records the owner's twenty decisions and the
research behind them: hot reload of shaders, assets and levels with the game
kept alive, rotating bodies and joints in pure Dart, a declarative widget
API, the renderer's missing rows, user materials compiled for every backend,
an editor that runs the game, and twelve features the simulation makes
possible, from an animation graph to replay tests for game developers. Hot
reload and editor Play come first.
Four more decisions: lighting hooks in user materials land in 0.9.0 after
surface outputs, the authoritative network model replicates through
flame_multiplayer's own wire and snapshots, sharing and telemetry get
client adapters plus a reference server in cloud/, and the level editor
ships as a web build and as desktop downloads with full Play.
Flutter reinitializes the engine's shader bundle on hot reload, but the
pipelines linked from the old code kept drawing it. HotSwap in
flutter3d_app relinks every registered renderer from SceneSurface's
reassemble (the Flame bridge draws through it too), refreshes bundles an
application loaded from bytes, keeps the last bundle that loaded when a
new one is refused, and answers ext.flutter3d.hotSwap.

Renderer.relinkShaders now reaches contributors through
PassContributor.relinkShaders: particles, mesh particles, splats and the
debug overlay drop their pipelines too. Named a swap because reload is a
weapon's word in the packages.
ModelInstance.adopt matches surfaces by node name and slot and hands each
MeshNode the new mesh and material, so transforms, animation and anything
hung from the nodes survive; what the new file adds or drops is reported.

HotSwap.loadModel reads and fingerprints the converted file, and a swap after
it changed rebuilds the asset and has every tracked instance adopt it.
ext.flutter3d.assets.put does the same from bytes sent over the VM service,
for a device the editor cannot write to. A model that does not build keeps
the version that did.
A play button runs the project above the open level with flutter run
--machine. PlayScreen shows the console and drives start, stop, hot reload
and hot restart through the daemon protocol, and opens the timeline panel
on the VM service the run reports. The run belongs to the editor, so
closing the panel leaves the game running.

The protocol shapes were checked against a real run on macOS; the tests
drive a fake process that speaks them.
diffLevel splits a change between two versions of a level: lights,
materials, fog and music can be patched into a running scene; brushes,
entities, the ground, recipes and the next level are the simulation's.

RunTimeline.swapLevel takes the simulation's half: it restores the last
keyframe, lets the caller swap the level, replays the recorded input to the
present and rebases the rewind buffer there. TimelineLevelSwapped records
the step and the level digest, so a replay that swaps at the same step
arrives where the run did.
Saving while the project runs sends the document and its digest to
ext.flutter3d.level.apply. The game's LiveLevel refuses a document whose
digest does not match, patches the look in place, and sends brushes,
entities and the ground through RunTimeline.swapLevel so the run replays
under the new level instead of jumping. A game with no timeline is rebuilt
in place and the answer says so.

Checked end to end over a real VM service in the timeline extensions test.
InputState.tune sets a tunable for one step; the recorder writes it and
playback applies it, and Tunables takes it before the step reads it and
keeps it in the snapshot. ext.flutter3d.cvar.set goes through the same
door, so a run tuned while played replays like any other.

RunTimeline.replayUnderNewCode lives the last seconds again from the
nearest keyframe under the code running now, compares each keyframe the
old run left and the present, and names the first field that parted and
the steps it parted between. firstDifferingPath moves to flutter3d_sim for
it; flutter3d_net still exports it.
HotSwap.setMaterial, and ext.flutter3d.material.set over the VM service,
sets colour, emissive, roughness, metallic, normal scale or alpha cutoff on
every material of that name in the scenes SceneSurface registers. The
values stay as overrides and are put back after every model swap, so a
colour still being dragged does not snap back when the model is saved.
The grass was drawn only 46 units either side of the river's middle, while
the stretches ahead reach a few hundred units up it; on a wide window the
sky showed in both top corners. The outer quads of each row, and the roads
to the bridges, now run to the camera's far plane. Trees keep to the old
band, so the course is laid out as before.

A helicopter's shot was a cube the size of an explosion shard and came once
per pass: it is now a red streak along its flight with a flash at the nose,
and a gunner fires from 46 units ahead every 1.1 seconds, which is three
shots at cruise rather than one. More tankers and helicopters move once
woken, from 55% on the first level to 90% on the last; the rest still hold
still.
A tanker or helicopter put on a channel barely longer than itself turned
at each bank several times a second and looked like it was shaking in
place. One now gets a speed only when its channel leaves it at least three
metres to travel, and a course test holds every mover to that.

The test that a woken target moves measured where it ended a second later,
and a mover that met a bank and turned back could end where it began; it
now looks at the furthest it got.
The platformer builds the edited document ahead of the swap, textures,
meshes and a simulation of its own, then puts it in the running level's
place. When the edit reaches the simulation the swap happens inside the
timeline's replay, so the run is lived again to now under the new level;
the widget hears of it only afterwards, like any load, and starts a new
demo there. A document for another level, or one that does not build, is
refused and the level on screen stays.

LiveLevel gains prepare and applyWhenReady for levels that cannot be built
synchronously, and a settable level for a game that moved on by itself.
RunSession.replaceLevel swaps a new build of the level being played in and
carries the run over through the game's own snapshot and restore.
The editor's material panel, which had never been mounted, opens from a
palette button beside the step panel. Every value one of its sliders passes
through, and every value written, goes to the game Play is running as
ext.flutter3d.material.set, in the engine's words: a level's emissive is
emissiveStrength, and a new base colour is the glow's too. One VM service
connection is kept for it, and values are merged and sent at most thirty
times a second, the last one always.

RangeSliderField and FieldRow gain onPreview for the values between the
start and the end of a drag. replayAfterHotSwap lives the last seconds
again under the new code after every HotSwap and says where the two runs
part; the platformer calls it.
An English write-up of how flame_flutter3d joins Flame and flutter3d, with
a step-by-step tutorial (Buoy Run) checked against the published packages
and run on macOS. The frame diagram is redrawn in English, without the
claim that the 3D layer draws a frame late.
Play leaves the editor application for a package of its own,
flutter3d_editor_play: plain Dart with dart:io, since it starts a process
and flutter3d_editor_core is written never to. FlutterRun, projectRootFor
and pushLevel move there with Watched in place of ValueNotifier, beside
flutterDevices, which reads flutter devices --machine, and a fake
flutter run for tests.

The editor's MCP server runs the same Play: play waits until the game is up
or has failed, and play_status, play_swap, play_stop and play_devices follow
it; a save while the game runs sends it the level. The editor's Play panel
picks the device between runs.
HotSwap keeps a texture with the image file it came from. A swap after the
file changed uploads the new picture as a texture of its own, puts it in
every slot of every material in the registered scenes and in their
environment, and releases the old one through the renderer once no frame in
flight samples it. Size, format and mip count may all change; writing into
the old handle would have left its smaller mips showing the old picture.

LevelLoader watches every map a level names and keeps materialTextures in
step with the swap, and LoadedLevel.dispose stops watching them, so nothing
is released twice. ext.flutter3d.assets.put takes an image too.
P3. A DecalNode is its node's unit cube, stamped down its y axis: a picture
or none, an sRGB tint with an opacity, an emission, an atlas region, an
order between overlapping decals, and an angle limit past which a surface
turned away from the box is left alone. RenderSettings.decals switches them
on and is off by default.

A depth attachment cannot be sampled, so the new Decal stage finds the point
under each pixel in the surface buffer, as reflections and occlusion do. The
light the point was lit by is read back as the lit colour over the albedo
buffer, and the decal's colour is laid under that light, so a decal sits in
shadows and under coloured lamps as the surface did; recomputing the light
would have meant a second lighting model with every map bound. A pixel needs
a factor and a term, and one blend cannot do both, so each batch of sixteen
decals and four pictures is two fullscreen draws, scissored to the boxes on
screen, per view. A surface that wrote no albedo gets the decal at its own
colour. The pass runs between the opaque and the transparent halves of the
scene and splits the frame as glass does, so glass in front of a decal is
drawn over it; it needs three colour attachments and turns MSAA off.

The CPU mirror chooses each mip level from neighbouring texels of the surface
buffer, as the GLSL does, rather than from derivatives it does not have. The
decal-floor golden is recorded in all four sets: WebGL and WebGPU draw it 0 of
172800 pixels from Impeller, the software set 0.067%. Every other Impeller
golden is unchanged. What is left (normals and roughness, decals on glass,
exact order across batches, the level format and the editor) is in the 0.9
plan.
The root's test directory held only reference pictures, so running the
test runner there found no test and exited 79, and a check that runs it at
the root read that as a failure. test/structure_test.dart runs the
detectors' self-proof and every rule in allRules as a test of its own: the
same list tool/structure.dart runs, not a second copy of it. The root takes
package:test as a dev dependency for it; the lock file only moves test from
transitive to direct.

The root is not one of the directories the test count scans, so the counts
in the documents do not move.
…inned

The README still said 78 scenes after decal-floor made it 79. The rule that
holds the documents to the count never read the README, so it is in the
list now, and the old sentence fails it.

The decal pass hands its scissor to the backend from the top left, with no
adjustment for the framebuffer origin, while it adjusts the matrix. That is
the engine's contract: WebGL2's encoder turns every rectangle upside down
itself, and the scene pass hands over its view rectangles the same way; the
matrix is adjusted because the stage reads texture rows, which no encoder
translates. Flipping the scissor for a bottom-left origin as well was run:
the WebGL2 decal-floor golden then differs by 602 pixels. The code says so
beside the scissor, and a new software test keeps a decal in the top rows of
the frame, which a scissor measured from the wrong edge cuts away entirely.
Brings the relaxed Dart, Flutter, vector_math and clock floors, the
flutter3d_audio_core split and the dependabot ignores. Each package's
Unreleased notes stay above the release entry main added, and the new
flutter3d_editor_play takes the same SDK floor as its siblings.
Phase 5 of the C physics core: cylinders, cones, convex hulls, and any
shape rounded by a radius. A hull is built from points, weighed as a solid
and moved so its centre of mass is the body's origin; the world keeps its
hulls in snapshots. Inertia is a full tensor now, so a hull spun off its
axes keeps its angular momentum.

Pairs without a closed form go through GJK between the shapes' cores and
EPA where the cores overlap, and their manifold comes from the faces and
edges facing each other: a cylinder rests on its rim or along its side, a
cone on its base or slant, a hull on its face. Depth is measured from the
reference face's own plane, so a slightly tilted normal over a wide floor
no longer floats a body above it. A cylinder rolls down a slope at
2/3 g sin θ.
Phase 6 of the C physics core: indexed triangle meshes for fixed bodies,
one sided, each with a tree of its triangles. A ball or capsule meets a
triangle by its closest points, a lying capsule on both ends, and every
other shape by GJK and EPA against the triangle; the contacts of all the
triangles a body touches merge into one manifold.

An edge two triangles share across a flat or hollow fold is internal, and
a contact on it takes the face's normal, so a ball rolls down a mesh at
5/7 g sin θ instead of catching on its seams, while a ridge keeps its
edge. Meshes are in snapshots and their trees are built again.
Phase 7 of the C physics core: fixed, spherical, hinge, slider and
distance joints, solved in the same soft substeps as the contacts and
warm-started from step to step. A joint's locked directions are solved
together as one block, so a light ball on a long lever keeps its hinge's
plane; solved one after another it drifted out of it.

Hinges and sliders take limits, a bounded motor and a spring. A distance
joint is a rod, a spring between a least and a most length, or a rope that
is slack until it is taut; changing between them starts its impulses
afresh. Joined bodies do not collide unless told to, joints join islands,
and a body's joints go with it. A pendulum swings at its period and a rod
holds a weight with m g. The core has its own arctangent for a hinge's
angle, and joints are in snapshots.
Phase 8 of the C physics core: continuous collision. Softly, by default, a
body's leaf in the tree covers where it will be after the step and a
pair's contact reaches as far as the two can close in it, so a ball at
three hundred metres a second stops at a wall a centimetre thick, while
one passing close to a box is not slowed. Hard, for bullets: after the
solve a bullet is swept along its path by conservative advancement and put
back at its first impact; touching where the step began, or moving away,
is left to the contact solver.

Building it found a solver fault: a contact's gap was measured by
carrying the touched point round with the body, so a ball rolling fast
read a gap where it rested and sank two centimetres. The point's motion is
taken to first order now.
The path a bullet takes through a step is recorded substep by substep, and
the sweep follows it, place and turn, bounding how fast the turn can close
a gap by the bullet's fastest point. A bar spun at three hundred radians a
second, five a step, which the step's two ends alone read the short way
round and backwards, now stops at a wall beside it and is thrown back
instead of turning through it.
Phase 9 of the C physics core: queries and a character controller. Rays
walk the broadphase tree nearest box first and cut it at each hit,
meeting balls and boxes in closed form, mesh triangles from their front,
and every other shape by conservative advancement; they meet what
flutter3d_physics' rays meet, at the same distance. Shape overlaps and
shape casts go through the narrow phase, each query with a layer mask and
a body to ignore.

The character is a kinematic capsule moved by casting and sliding: it
stands on ground no steeper than its slope, slides along anything steeper
without climbing it, climbs a step up to its height, and keeps to the
ground walking down a slope. Its slope is given as a cosine, so a step
asks no platform library for an answer.
A body's turn is held to an eighth of a turn a substep, before the turn
and after it. A rod two metres by two centimetres struck at its end spins
about its length, where it is five thousand times easier to turn, at
thousands of radians a second; turned tens of radians in a substep, its
momentum read back through that tiny inertia grew without end. Bounded,
it is thrown back and spins, and nothing blows up.
The core gains f3d_particles: gravity, a drift towards the wind, a floor
to bounce and slide on, a life that runs out, slots filled round and
round. The same step runs as a WGSL compute shader in f3d_gpu, a second
library linked statically with wgpu-native, which the build hook
downloads for the target from a pinned release, checks by sha256 and
caches. Without it the core builds alone and NativeGpu.open() is null.
A thousand particles over two seconds agree with the CPU to 3e-5 m.
The core gains f3d_debris: balls of any size and mass that fall, knock
together, roll and settle on still planes and boxes. Contacts come from
a hashed grid with a list per cell and are solved all at once from the
same velocities, each body splitting its mass among its contacts, and a
pair's impulse is always worked out from its lower slot so both bodies
get it to the bit. The GPU library runs the same passes in WGSL and is
read back without waiting, a step behind the one queued; it is now a
file per pass with the device, kernels and readback shared. Until bodies
meet the GPU matches the CPU to the bit; a poured heap then settles to
the same mean height within a part in a thousand.
The core gains f3d_cloth: points held at their distances by constraints
with a compliance each, XPBD in small steps, falling on balls and a floor,
drifting in the wind, pins that can be carried. The constraints are
coloured when the cloth is made, no two of a colour sharing a point, and
f3d_cloth_edges hands the GPU that same order, so it solves a colour a
dispatch, chosen by a uniform at a dynamic offset, and sums nothing in
an order of its own. Where nothing folds the two agree point for point;
a hanging sheet agrees until its wrinkles diverge, then hangs as low.
The C tests now build at -O1 under the sanitisers.
The core gains f3d_fluid: position-based fluids in a tank, the density
constraint only pushing apart, with XSPH viscosity. Each tank wall counts
as still layers of particles past it, so the bottom layer is no longer
pressed flat against the floor and a dam settles with its centre of mass
at half its depth. The GPU runs the same passes, read a frame late, and
agrees particle for particle until the water splashes.

GPU kernels are now built inside a validation error scope, so WGSL that
does not compile fails the constructor instead of ending the process.
Three C tests that handed CHECK_NEAR an already scaled tolerance now
check what they claim to.
A world can now be given a pool of up to 64 threads, POSIX or Windows',
the caller one of them; the WebAssembly build keeps to the caller. The
tree queries gather pairs into a lane per worker, sorted together after,
and the narrow phase makes each pair's manifold in a slot of its own,
closed up in key order, so a world steps to the same snapshot on any
number of threads. The joined-pairs table is built before the queries
instead of lazily inside one. On four thousand heaped bodies the
collision stage drops from 13.3 ms to 3.8 ms on six threads.
A world can be switched to a fast mode that colours its contacts each
step, no two of a colour sharing a moving body, and solves each colour's
contacts at once on every thread, the stages of a step meeting at a spin
barrier inside one pass of the pool and joints solved on one thread
between them. It lands on other bits than the deterministic mode but on
the same bits on any number of threads, and the mode is kept in a
snapshot. Contacts no longer write bodies that do not move, which changes
no bit in either mode. The C tests now link libm.
Every awake body used to query the tree every step. The world now keeps
the pairs whose leaves overlap, drops those whose leaves parted, queries
only the leaves that moved, and takes the step's pairs from the kept ones
by the same tests the queries made. A leaf always holds its body's swept
box, so the pairs, and every body's motion, stay the same to the bit.
Swept boxes are now worked out once a step. On four thousand heaped
bodies the collision stage drops from 13.3 ms to 2.2 ms on one thread.
A colour's contacts now go in batches of four, one to a lane of a
vector, every lane doing what one contact solved alone does, operation
for operation, with branches turned into masks; MSVC gets the same
operations a lane at a time. The fast mode lands on the bits it landed
on before, with vectors or without, and the C tests build it both ways
and compare. The contacts read the bodies from a tight array gathered
before each stage, the turn worked out once a body. On four thousand
heaped bodies the fast solver on one thread drops from 7.7 ms to 4.5.
The C tests get five minutes each for a machine busy with other work.
The wrappers no longer hold dart:ffi pointers. They call the core
through lib/src/core/, where every call is a function and every pointer
an address: natively a pointer, in the browser an offset into the
WebAssembly module's memory. Typed blocks of the core's memory stand
where pointers stood, and the settings structs are written by offset.
tool/gen_core.dart writes both sets of calls and the struct layouts from
f3d_physics.h, and a test runs it in check mode and holds each layout
against the C compiler's offsetof. The GPU passes stay native; in the
browser NativeGpu.open() is null. Every native test passes as before,
and the package now compiles with dart2js and dart2wasm.
loadPhysicsCore() fetches the WebAssembly module the package now ships
as web/f3d_physics.wasm, and NativeWorld and the CPU systems run on it
through dart:js_interop under dart2js or dart2wasm. A 64-bit handle
crosses into the module as two 32-bit halves through a generated shim.
A shared scene steps in Chrome to the snapshot the native library steps
it to, and the shipped module is held to the native library byte for
byte. A body's slot and generation now come from its handle by division,
since dart2js shifts see only 32 bits.
A threads build of the module, on a shared memory its host makes, ships
beside the single-threaded one. loadPhysicsCore(threads: n) on a page
that can share memory loads it and starts n - 1 Web Workers, each an
instance of the module on that memory with its own stack, waiting in the
core for its share of each pass; elsewhere it falls back to one thread.
The module's allocator is now locked, since workers allocate mid-step,
and memory is read through a DataView that takes a SharedArrayBuffer.
On four node workers and on four Web Workers in Chrome the module steps
a scene to the native library's bytes.
The ball joint takes a cone on its swing, the hinge's limits on its twist
and friction against its turn, which is what a ragdoll needs to come to
rest. The twist limit pushes along the axes' sum over one plus their
cosine; pushed about the first axis alone, a far-swung shoulder overshot
its limit by half a radian. A ragdoll of eleven bodies falls to a floor
and sleeps, takes a bullet and sleeps again, and tumbles down a flight of
stairs, its joints held throughout and its bits the same on one thread
and four. Dart gains setJointCone, setJointFriction and jointSwing; the
wasm modules are rebuilt. ABI 18, snapshot version 8.
A dart_test.yaml that defines a browser platform makes flutter test
refuse the whole package, so since the browser's threads came in none of
the package's tests ran that way. Chrome is now asked for
SharedArrayBuffer by tool/chrome_sab.sh, which the runner starts through
CHROME_EXECUTABLE, and the file is gone. The shared scene's hash follows
the joint's larger slot in the snapshot, the same in Chrome as natively.
test_digest.c steps seven scenes and fails unless they hash to numbers
taken on macOS arm64: a world of every shape, the same world fast on
three threads, the ragdoll down its stairs, debris, cloth, water and
particles. They hold, in both precisions, on Linux arm64, x86-64 and
armv7 under gcc and clang, on the iOS simulator and on Android arm64;
tool/digest_platforms.sh repeats those runs. The C tests build with MSVC
on Windows, and CI runs the package there. On 32-bit x86 the x87 lands
elsewhere, so the hook asks for SSE2 there and the core refuses the x87.
The browser tests join tool/ci.sh, and the ragdoll's builder moves to a
header both tests share.
Win32 has InitializeSRWLock and no InitializeSRWLockExclusive, so the
core never linked on Windows; nothing had compiled its Win32 branch until
CI built it with MSVC. The core and every C test now build and link with
mingw-w64 too.
tool/digest_platforms.sh gains a windows target: the core and its tests
built with mingw-w64 and run under wine, the digests and the pool on
Win32 threads, in both precisions.
On Windows, git checks the header and the generated files out with CRLF,
and gen_core --check called every generated file out of date though the
text was the same; it now compares with Unix line ends. With it, CI's
Windows job had 103 of 104 passing under MSVC, the digests among them.
RigidDynamics is what WorldStep, the games, the crates and the Flame
bridge now hold: the world, gravity, the bodies, add, remove, bodyOf,
step and push. Dynamics implements it unchanged, and stays what every
game builds; the native core can stand behind the same bodies next,
without anything past the line that builds the world knowing which.
NativeDynamics is a RigidDynamics: a game builds it where it built a
Dynamics, and its crates, sweeps and character controller stay as they
were. The core holds the bodies and each RigidBody mirrors it. What a
game changed since the last step is found by comparison and written in,
and everything is written back after. The rest of the world stands in
the core as fixed bodies: wedges as hulls, height fields as meshes, and
moving colliders with the velocity they move at, so a lift carries its
crate. The push a character gives is now Pusher, shared by both
backends. Snapshots carry the warm starts for a rollback.
stage() takes the dynamics to build, the reference unless told
otherwise, and a test plays the shipped level on NativeDynamics: its
crates land where the reference lands them and sleep, the runner's route
is the same to the bit, a push moves a crate, and two runs agree. The
game itself still builds Dynamics; the core is a dev dependency here, so
no build of the game compiles it. Nine of the level's crates start
inside its brushes, which the reference pushes them out of silently.
A scene of flutter3d_physics' bodies on NativeDynamics (crates, a ball
down a wedge, a lift and a push) hashes the same in Chrome, under
dart2js and dart2wasm, as natively; the numbers are hashed by their
float64 bytes, in arithmetic JavaScript keeps exact. The hook now dates
wgpu-native's unpacked headers in the past: dated now, they counted as
files modified during the build, and a fresh checkout said the build
must be rerun.
The game now builds NativeDynamics, loading the core first in the
browser and letting it go with the level; FLUTTER3D_PHYSICS=dart builds
the reference instead. Saves carry what the dynamics hold beyond the
bodies, nothing for the reference, so a rewind or a demo restored
mid-level repeats the run on the core too. The core's restore puts right
what came and went since its snapshot, and resumes each standing
collider where it is: a barge restored elsewhere had read as a jump and
been given that speed. The shipped tape, the site's sample and a golden
are recorded again on the core.
pub.dev decides a package's platforms by its imports, and listed
flutter3d without the web while it ran there: loadModelAsset imported
dart:io to name an exception, and flutter3d_core imported dart:io for its
file source and dart:isolate for its decoders. Each is now chosen at
compile time by dart.library.js_interop: isolates and files natively, in
place and a refusal in the browser, with native behaviour unchanged. Both
pubspecs declare the six platforms, and a structure rule holds any
package that says it runs on the web to what the browser's build can
import.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant