Conversation
A tipped tube's liquid stood out through the glass along its long sides: its meniscus was the upright tube's, by distance from the axis, lifted straight up off a wall that leans. Tipped, the meniscus is now the flat wall's, by distance from the glass and with its mean taken out so it adds no liquid, and the liquid mesh keeps every lifted point inside the vessel. Drops are drawn the colour of what is dissolved in them, rather than near white, and as spheres of forty-two points.
Each labelled solution's colour now comes from Beer-Lambert with its molar absorption coefficients at about 610, 550 and 465 nm and the strength on its label, A = εcl, rather than from a swatch: permanganate at 0.02 mol/L is nearly opaque in a tube, copper sulphate a deep clear blue, dichromate orange, nickel chloride pale green, hydrochloric acid clear. Particles that touch are drawn as one drop of all their liquid, so a stream breaking up shows drops of its own size, not millimetre dots.
The tabletop reflected the liquids and labels but not the glass round them, a choice made while the glass was nearly invisible. The glass's tint is what one surface lets through, and light crosses four surfaces of a tube: at 0.96 an empty tube's shadow let through seven tenths of the sun. It is nearly white now, and Fresnel alone darkens the shadow's edges.
The tilt slider set a glass's lean at once, and its lift jumped by centimetres the frame a tipping glass first came over a neighbour. The liquid felt jolts of several g, its surface turned over, and a tube leaned a tenth of a radian further emptied itself in a step. The slider now asks for a lean; the hand eases towards it and lifts first, both with no jolt at the start or the end. Also pours briskly, so the stream is wide enough to reach the clean tube whole, and lets a tool read the bench's frames through ext.flutter3d.render.
The tabletop cast no shadow, so it was not in the shadow map the engine's photons search for somewhere to land; they found nothing and were lost, and every solution threw the same grey shadow. It is in the map now. The hand tipped to where the tube held the next eighth of a second's worth less, which on a tube nearly on its side stood the surface a fraction of a millimetre over the lip and ran a trickle. It now tips to where the lip's weir runs the flow it wants, and a tube pours half in about a second with a stream that reaches the clean tube whole. The liquid meshes are thirty-two by twenty-four, which halved what a pour's frame cost.
Once the bench took part in the shadow map, every photon found somewhere to land, and the ones leaving a liquid's rim edge-on were drawn as quads over most of the caustic tile. Their light is too thin to see, so both the GLSL pass and its CPU port skip them; camera orbits are smooth again.
A drop that came down on the bench kept its speed along it and slid off for good, ending up under another glass where it looked as if it were inside. A viscous liquid has no velocity along a wall at rest, and a drop smaller than the capillary length is held whole by its pinned edge, so particles touching a wall keep only what leaves it, and such drops stand still. A drop that touches a vessel's glass from inside runs down into its liquid, as a stream's parcels already do.
A floor given to the fluid world now takes what reaches it as puddles: one body each, with its volume and what is dissolved in it, instead of thousands of particles. A spread puddle is 2lc*sin(theta/2) deep and a small one is a cap at the contact angle; it spreads as a viscous gravity current and runs together with the ones it meets. capMesh draws them. A tube emptied on the bench used to leave 7500 particles that took a quarter of an hour to step.
A tube can now be tipped past the lean it holds its liquid at, mouth down at the most, and what runs out lies on the bench as a puddle drawn in its own colour. The hand lifts it out of the row by the height either side needs, so the tube at the end of the row no longer lies down on the bench when it leans off it.
Once the lip stops feeding a stream, its upper end eats the thread ahead of it at Keller's sqrt(sigma/(rho*r)) and gathers it into a bulb. The tail of a pour used to fall whole, as wide at its end as along it, like a bent rod of glass.
The bank kept the end of every run of drops, up to a particle's worth, counted and nowhere. It now leaves as a last, smaller particle where the liquid last came in, and lands as the amount it is.
Only the liquids in the glasses stood in the mirror; a stream poured over the bench had no reflection. Each stream, drop and puddle node now has one sharing its mesh. Drops are sized and centred by the liquid each particle carries.
The light a glass or a liquid lets through to its shadow was read from the shadow atlas in one tap, relying on a bilinear step the atlas does not get, so a translucent shadow's edge stepped a texel at a time. It is now averaged over the same 3x3 texels as the opaque edge, in the GLSL and in the CPU port.
The clean tube's liquid material was made with the first drops, where its round bottom is nearly no width, and never remade, so what was poured into it showed a fraction of its colour. It is remade whenever the width at the level changes. A vessel emptied and filled again now fills with what it last held rather than plain water. The bench's sun has its quarter-degree size, so shadow edges get their penumbra instead of stepping along the map's texels.
A pour into a tube spent most of its time working out the surface's modes again: each time the level rose half a cell, conjugate gradients solved the same Laplacian afresh, seventy milliseconds a cross-section. The modes are now cached by the cells a cross-section takes, so a straight-sided glass solves once, and the Laplacian is factored once as a band, three milliseconds a solve. Drops and streams look only at the walls they can reach, and the drops' pair loops no longer make an object for every difference, which had the collector pausing whole frames. A four-second pour costs a tenth of the CPU it did.
A FluidWorld now has an Atmosphere, room air by default, its density, viscosity and vapour diffusivity worked out from temperature, pressure and humidity. Drops fall against a sphere's drag at their own size, streams bend in a crosswind, and open liquid evaporates: a tube up Stefan's column and out of its mouth, a puddle by Hu and Larson or a flat plate's boundary layer in a wind, a drop by Ranz and Marshall. Only the solvent leaves, so solutions grow stronger, and the vapour is kept in the world's accounts. Only water has a vapour curve so far.
Pipes between vessels now lose to Darcy friction: 64/Re laminar, Haaland's form of Colebrook turbulent for the bore's roughness, and a straight line through the transition. A stream leaving a lip above a Reynolds number of 2300 parts at Grant and Middleman's turbulent intact length rather than its laminar one.
Walls now say what they are made of: a SolidSurface with water's contact angle and its hysteresis. A drop stays where it is while its weight along the wall is no more than Furmidge's sigma*w*(cos receding - cos advancing) across the circle it wets, and slides when it is: a microlitre hangs on a pane, twenty run down. Puddles take their surface's angle, so water beads on the bench's lacquer and spreads on glass.
A floating body's spin was never touched by the liquid round it, and a ball spun in glycerol spun on for ever. The part under the surface now feels 8*pi*mu*R^3*omega on the sphere of its volume, taken implicitly about the body's own axes.
A FluidWorld can now take a GravityField, a uniform part and any number of attractors pulling as mu/r^2 outside and as a uniform ball inside. Each vessel levels across the gravity where it stands, and each drop and parcel of a stream falls by the gravity at its own place.
…ffers The loops over neighbour pairs in a Position Based Fluids substep, density, normals, forces, the density constraint and XSPH, now live behind PbfKernels and work on Float64List positions and velocities and compressed neighbour rows, so another implementation can take them as they are. The Dart one is the reference and adds in the same order as before: a run of drops with walls and wind comes out bit for bit the same. Walls, receivers and drops stay with ParticleFluid between calls.
flutter3d_physics now builds src/pbf_kernels.c with a build hook and binds it through @Native, passing the fluid's own buffers by address. A fluid made with native: true, or a FluidWorld with nativeKernels: true, runs its pair loops and its neighbour search in C, summing two neighbours at a time with the compiler's vector extensions; held to the Dart kernels within a part in 10^12 per kernel. Without a compiler the hook builds nothing and the fluid runs on Dart, as it does on the web. The neighbour search is now a sort by cell instead of a map of lists, in Dart too, with the same rows in the same order, so the Dart path steps exactly as before.
A drop in flight does not feel its own weight, and the air breaks it up only past an aerodynamic Weber number of about twelve, which drops off a pour are a fifteenth of. So a drop is now one body: a sphere's drag and evaporation, Furmidge's hold or slide on a wall, and drops that touch run together. A drop wider than the capillary length spreads as particles where it meets a wall, and particles that leave every wall gather into a drop again. A free block of particles used to ring until it flew apart; a pour now flies ten bodies at most instead of thirty-eight, and costs two fifths less.
Filling a tube up its round bottom wanted a new meniscus every other step, and each took ten milliseconds: sixty halvings of a shot of 512 Runge-Kutta steps. The curvature is now found by Illinois false position and the shot takes two steps a sample, which against a reference eight times finer is as close as before, at 1.5 ms. Menisci are shared by every vessel of the same liquid and gravity, and a stream asks only the receivers its way passes near. A four-second pour, its caches warm, now steps in 0.3 s with no frame over 14 ms.
The tolerance's power of two came from math.pow, which a step may not ask the platform for; it is now the exact constant. The bench's golden moves by one pixel with the new meniscus solve.
A FluidWorld made with background: true hands what would stop a frame, the modes of a cross-section not met before and a meniscus not solved before, to a long-lived isolate, and the vessel carries on its old grid or the nearest meniscus until the answer comes. The answers are the same numbers, checked to the bit; only when they arrive differs, so it is off unless asked for. On the web there are no isolates and nothing changes. The bench asks for it and for the native kernels: the first pour of a session no longer has frames of 40 ms.
A world with nativeKernels: true now runs the heavy half of a cross-section's mode solve in C too: the banded Cholesky factor and the Lanczos steps with full reorthogonalisation, a millisecond where Dart takes three or four. The tridiagonal is finished in Dart from the same start vector. Native and Dart modes are cached apart, so a world that must replay never reads the other's, and the background isolate solves natively when asked to.
TubeMeniscus takes native: true and runs the same Illinois search over Runge-Kutta shots in C, on the platform's sine and cosine. It agrees with the Dart solve to about 1e-16 and takes 0.34 ms where Dart takes 0.56. A world with nativeKernels sets it on its vessels; native and Dart menisci are cached apart, and the background isolate solves the kind it is asked for.
The native neighbour search read each candidate's position through its index, nearly always a cache miss, and with one search per cell. It now copies the positions out in sorted order and finds each row of three cells with one search. Rows and their order are unchanged. On 576 particles on a pane the search takes half the time it did, and a step 4.3 ms instead of 5.4.
Finding which particles make one drop walked each union-find chain to its end, and blocks of particles on a wall made chains hundreds long. The three copies of it are now one helper that halves the path on the way; roots, and so every result, are the same. The Dart neighbour search reads candidates in cell order and finds each row of three cells with one search, as the native one does, with the same rows. A pour replays bit for bit against the previous code. On 576 particles on a pane a Dart step takes 7.3 ms instead of 8.3, a native one 3.4 instead of 4.3.
The liquids page now says that nativeKernels also runs the surface mode solve and the meniscus solve natively.
All six native pair loops now come from one source, pbf_lanes.h, built at two, four and eight lanes. Which one runs is chosen on first use by what the processor reports: AVX-512F takes eight, AVX2 four, SSE2 and NEON two. The hook builds on the machine that builds the app, so the wider widths are compiled with a target attribute and only called where cpuid allows; nothing is built for the build machine's own processor. No fused multiply-adds, so each lane rounds as the scalar loop does. Every width agrees with the scalar loop to about 1e-15, checked on arm64 and on x86-64 under Rosetta with AVX2. A new CI job builds the check with gcc and clang on an x86-64 runner and times each width there. On arm64, where two lanes measured fastest, a PBF step on 576 particles takes 3.1 ms instead of 3.4.
The CI runner, which has AVX-512F, measured eight lanes as fast as four with gcc and slower with clang: the native loops wait on gathering each neighbour's numbers, not on arithmetic. Four lanes stay the default wherever AVX2 is; eight remain for a test to ask for.
On the web there is neither dart:ffi nor a second isolate, and the code rightly works everything out in Dart at once there; the tests that check the native kernels and the background isolate now say @teston('vm'). The width test's name now carries the width instead of a literal $lanes.
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.
Two problems from the macOS app:
liquidMeshesprojects lifted points back inside aRevolvedVessel. New test: no vertex of a tipped tube's liquid is outside its glass.ParticleFluid.concentrations, and are drawn as smoother spheres.mixture_testnow tips the glass to 1.3 rad instead of 1.2. At 1.2 rad, with the wall meniscus, the glass comes to rest at its lip with a film of oil still in it.