Conversation
dzolotov
commented
Oct 5, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.