f-compute-shaders #14
No reviewers
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Ade9/Adetonics!14
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "f-compute-shaders"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Compute shader implementation speeds up generation greatly.
@ -0,0 +32,4 @@if (platebuffer.plateid[localpoint] != -1 && platebuffer.plateid[neighborbuffer.neighbors[localpoint*6+neighbor]] == -1){// Neighbor unclaimed, and we can claim it.platebuffer.plateid[neighborbuffer.neighbors[localpoint*6+neighbor]] = platebuffer.plateid[localpoint];(lines 32-35)
⚫️ [PROBLEM] ⚫️ [PROBLEM] Unsynchronized test-and-claim on shared plate buffer — nondeterministic plate ownership
Lines 32-35 are a classic read-then-write race on a storage buffer shared by all workgroups: one workgroup can read
plateid[n] == -1and write plate X while another workgroup does the same for plate Y on the same target vertex, and there are nomemoryBarrier()/groupMemoryBarrier()calls or atomics anywhere inmain(). The sequential CPU version this mirrors made the test-and-claim deterministic. Here plate assignment varies across runs/GPUs: a vertex can be claimed by whichever workgroup's write happens to win, propagation depth varies per dispatch, and since this buffer feeds the read-back that drives BorderSearch/EdgeStress/Height, the whole planet becomes unreproducible run-to-run. UseatomicCompSwapon the owner slot so exactly one claimant wins.(lines 25-35)
⚫️ [PROBLEM] ⚫️ [PROBLEM] No bounds check on localpoint — tail invocations read/write outside the storage buffers
localpoint(line 25) is derived fromgl_WorkGroupID.x * gl_WorkGroupSize.xwith no comparison against the actual vertex count, but the C# side dispatchesxgroups = ceil(points.Length / 25.0)(PlanetHelper.cs:355), so the last workgroup iterates indices pastpoints.Length - 1. Those tail invocations readneighbors[localpoint*6+neighbor]out of bounds, and when that garbage read is not -1 they readplateid[localpoint]and even writeplateid[garbage_neighbor](line 35) — an out-of-bounds write to unrelated buffer memory. On the D3D12/Vulkan drivers this PR now targets (thegl_compatibilityfallback was removed inproject.godot), that is undefined behaviour: spurious plate assignments or validation-device aborts in debug builds. Pass the vertex count into the shader (push constant or uniform) and guard before any indexing.@ -40,2 +43,4 @@AxialTiltChanged(GetNode<LineEdit>("%AxialTilt").Text);UpdateTime();_planetHelper.InitializeGeneration();⚫️ [PROBLEM] ⚫️ [PROBLEM] Eager init in _Ready is wiped by the first AdvanceStage — seeded plate and color are discarded
InitializeGeneration()here ends withCompleteStage(), which setsStageComplete = truebut leavesStage = NotStarted(PlanetHelper.cs:173). On the first advance,AdvanceStage()movesStagetoInitializationand clearsStageComplete, soProcess()'scase GenerationStage.Initialization:(PlanetHelper.cs:272) callsInitializeGeneration()a second time — whose first statement isPlates = new();(PlanetHelper.cs:147). The re-seeded plates and this line's manually assigned land color are thrown away before generation is ever visible. Compounding this, the PR removedColorVertex(vertex.Id, color)fromInitializeGenerationand the only code that would paint the seeded color onto the mesh (the commented block below) never runs, so the initial-color intent is silently lost. Either make the seed stick (e.g. advanceStageinsideInitializeGenerationso the duplicate run doesn't wipePlates) or assign the color at the point where it is actually consumed.@ -318,0 +307,4 @@int[] points = Enumerable.Range(0, Mdt.GetVertexCount()).ToArray();var sizePoints = points.Length * sizeof(int);int[] neighbors = Vertices.SelectMany(v => v.Neighbours.Count < 6 ? [..v.Neighbours.ToArray(),-1] : v.Neighbours.ToArray()).ToArray();🚨 [CRITICAL] 🚨 [CRITICAL] Neighbor buffer is not padded to a fixed stride of 6
The shader indexes the neighbor buffer with a fixed stride of 6 (
neighborbuffer.neighbors[localpoint*6+neighbor],PlateExpansion.glsl:30/34/37), so the contract requires exactly 6 ints per vertex. But the flattening here appends a single-1only when a vertex has fewer than 6 neighbours and does NOT truncate when it has more:v.Neighbours.Count < 6 ? [..v.Neighbours.ToArray(),-1] : v.Neighbours.ToArray(). A degree-4 vertex emits 5 entries and a degree-7 vertex emits 7, so every vertex after it is shifted off its 6-slot boundary. Neighbours come from the imported mesh viaMdt.GetVertexEdges, so degree is not guaranteed to be exactly 6. If the total length also ends up shorter thanvertexCount*6, the shader reads past the end of the storage buffer. The failure is silent and data-dependent — it will look like wrong/random continent shapes rather than a crash. Pad to exactly 6 slots and assert the invariant before dispatch.@ -325,0 +369,4 @@{Mdt.SetVertexColor(index, Plates[plate].Color);Vertices[index].PlateId = plate;}(lines 366-372)
🚨 [CRITICAL] 🚨 [CRITICAL] GPU read-back never repopulates Plates[x].Vertices — area logic runs on seed-only lists
The removed CPU expansion performed
plateData.Vertices.Add(expandTo)for every newly claimed vertex. This read-back loop only setsVertices[index].PlateIdand the colour; it never adds the newly-claimed index toPlates[plate].Vertices. EachPlateData.Verticestherefore keeps only the single seed created inInitializeGeneration(new PlateData(i, color, false, [vertex.Id]),PlanetHelper.cs:167), and nothing else repopulates it. Consequences:AssignOceanPlates(areas.Sum(a => a.Vertices.Count * a.PlateExpansion)and the per-plateforeach (int v in areas[i].Vertices)recolouring) seesVertices.Count == 1for every plate, so land/ocean selection is driven purely byPlateExpansion, the area readout inMain.cs:255shows ~0%, and only the seed vertices can be recoloured. Add the newly-claimed index to the owning plate during read-back.@ -326,0 +380,4 @@_rd.FreeRid(neighborBuffer);AssignOceanPlates(Plates);CompleteStage();(lines 382-383)
⚫️ [PROBLEM] ⚫️ [PROBLEM] Single dispatch expands plates only one hop, then unconditionally completes the stage
The removed CPU loop was re-entered every
Process()tick and only firedAssignOceanPlates/CompleteStageonce!availableVerts.Any(), i.e. it kept expanding waves until the sphere filled. HerePlateGeneration()runs a single dispatch (one claim wave around the 12 seeds) and these two calls fire unconditionally at lines 382-383. Result: plates claim roughly one neighbour-hop per seed instead of filling the sphere, so every downstream stage (BorderSearch, EdgeStress, Height, ocean assignment) operates on an almost entirely unclaimed planet, and land/ocean ratios derived from plate sizes are meaningless. Loop the dispatch until no reachable-1vertex remains (the read-back plate array is the convergence signal) and keepAssignOceanPlates/CompleteStagebehind that condition — or move the iteration axis into the shader as its comments intend.Hey! Nice ambition on this one — moving plate expansion onto the GPU via Godot's RenderingDevice is a meaty refactor, and the CPU-side RD plumbing (buffers, submit/sync, cleanup) is put together correctly. The catch is that the GPU version doesn't yet reproduce the semantics the CPU loop provided, so the generated planet will come out very differently (and unreproducibly) from before.
a. Summary
This PR offloads tectonic plate expansion from a per-tick CPU loop to a GLSL compute shader (
PlateExpansion.glsl), adds eager generation bootstrap inMain._Ready, and switches the octree containment test to explicit inclusive per-component comparisons (Oct.cs). It also drops thegl_compatibilityrenderer fallback, making a compute-capable driver (D3D12/Vulkan) a hard requirement.The biggest risks, in order: (1) the neighbor buffer is not padded to the fixed 6-int stride the shader assumes, which silently misaligns every lookup after the first non-6-degree vertex; (2) the read-back never repopulates
Plates[x].Vertices, so all area-weighted land/ocean logic and per-plate recolouring now run on seed-only lists; (3) the single dispatch plus unconditionalCompleteStage()means plates expand one hop and the stage is "done"; (4) the shader has no bounds guard and no atomic claim, giving out-of-bounds reads/writes and run-to-run nondeterminism. I'd treat this as work-in-progress against a merge until the stride fix, theVerticesrepopulation, and the multi-wave dispatch loop are in.b. Good points
ComputeListEnd()→Submit()→Sync()beforeBufferGetData(src/PlanetHelper.cs:358-361) — so the CPU read-back can never race the dispatch.src/PlanetHelper.cs:375-379), keeping VRAM flat across repeated generations sinceMakeGo()constructs a freshPlanetHelper.IsInsideinsrc/Oct.cs:140-146tiles consistently withWhichSubOct/GetSubStartand removes a silent data-loss path where boundary-exact vertices were dropped and could never be returned bySearchNearest(pointer/projection picking).GetNeighboringVerticesnow treatsNotStartedlikeInitialization(src/PlanetHelper.cs:178), which is exactly the boundary the new eagerMain._Readyinit needed so seeding reads mesh-derived neighbours instead of empty lists.Main Issues
src/PlanetHelper.cs:310. The shader indexesneighbors[localpoint*6+neighbor], but a degree-4 vertex emits 5 entries and a degree-7 vertex emits 7, shifting every subsequent vertex off its 6-slot boundary; short totals also cause out-of-bounds reads. Wrong plate propagation with no error.Plates[x].Vertices—src/PlanetHelper.cs:366-372. OnlyPlateIdand colour are restored; the removed CPU loop'splateData.Vertices.Add(expandTo)has no replacement, soAssignOceanPlatesseesVertices.Count == 1per plate and the area readout (Main.cs:255) shows ~0%.src/PlanetHelper.cs:382-383. The CPU version looped perProcess()tick until the sphere filled; hereAssignOceanPlates/CompleteStagefire after a single wave, leaving the planet ~unclaimed for all downstream stages.localpoint—shaders/compute/PlateExpansion.glsl:25-35.ceil(vertexCount/25)dispatch leaves the last workgroup indexing past the end; tail invocations can writeplateid[garbage_neighbor]— UB on the D3D12/Vulkan drivers this PR now targets.shaders/compute/PlateExpansion.glsl:32-35. Concurrent read-then-write with no atomics/barriers makes plate ownership nondeterministic across runs and GPUs;atomicCompSwapis the drop-in fix._Readyis wiped by the first advance —src/Main.cs:46-47.AdvanceStage()transitions toInitialization, which re-runsInitializeGeneration()starting withPlates = new(), discarding the seeded plates and the manually assigned land colour before the user ever sees them.Additional Notes
src/PlanetHelper.cs:306, 356: thepointsarray /pointBuffer/ binding 0 is uploaded but never read by the shader, andyGroups: 50withlocal_size_y = 1launches ~50x more invocations than the shader actually uses — consider dropping both until the y/iteration axis is implemented.src/PlanetHelper.cs:306: the comment "We use floats in the shader" is stale — the buffers areint.src/PlanetHelper.cs:139-142:GD.Load<RDShaderFile>(...)/ShaderCreateFromSpirV(...)run unvalidated; a nullshaderFileis an immediate NRE and an unsupported device only surfaces later as a cryptic RD error — worth explicit null/valid checks with a clear message, especially now thatproject.godotremoves thegl_compatibilityfallback and compute is a hard requirement.src/Main.cs:52-81: the commented-out CPU expansion block references variables that no longer exist in scope and duplicates logic that now lives in the shader — delete it; git history preserves it.src/Main.cs:4, 8, 10: the addedSystem.Collections.Generic,System.Threading.Tasks, andArray = System.Arrayusings are unused in the file (they supported the commented-out block).src/Oct.cs:55-66:Inserthas no depth cap — two nodes at the identical scaled position recurse forever (stack overflow); the widened inclusiveIsInsideadmits boundary-exact points into this path. A coincident-position guard or max-depth bail withGD.PrintErrwould make it diagnosable.@beepster Please review after changes.
@Ade9
Re-reviewed after the push. All four blockers from my last pass are addressed, and the plumbing looks right. Remaining items are mostly semantics/perf rather than the GPU sequence itself.
Verified fixed
EnsurePlateExpansionResourcesnow allocatesvertexCount * 6andArray.Fill(neighbors, -1)before copying, so every row stays on its 6-slot boundary (src/PlanetHelper.cs:452-461). Matchesneighbors[localpoint*6+neighbor]in the shader.Plates[x].Verticesrepopulated on read-back — newly GPU-claimed vertices are appended (src/PlanetHelper.cs:386-402), and your reasoning in the comment holds: the shader only ever does-1 -> myPlate, so an owned vertex can't change owners and no stale list entries are possible.AssignOceanPlates' area math and theMainarea readout are no longer running on seed-only lists.Process()tick, read-back fed straight back in,unclaimed/newlyClaimedas the convergence signal, andAssignOceanPlates+CompleteStageonly after the sphere fills (src/PlanetHelper.cs:404-435).MaxPlateWaves = 4096plusClaimOrphans()gives a belt-and-braces exit.vertexCountgoes in as a push constant and tail invocations bail before any read/write (PlateExpansion.glsl:33-40,src/PlanetHelper.cs:372-374).atomicCompSwapon the claim — one winner per contested vertex (PlateExpansion.glsl:47).InitializeGenerationsetsStage = Initializationitself (src/PlanetHelper.cs:172-178), so the firstAdvanceStage()lands onPlateGenerationrather than re-running the method and clobbering the seeded plates/colours.yGroups: 1(no more ~50x phantom invocations), the stale "We use floats" comment, the commented-out CPU expansion block and the unusedSystem.Collections.Generic/System.Threading.Tasks/Array =usings inMain.cs. Caching the pipeline + buffers per vertex count is the right call — no per-hop pipeline creation.Remaining / new
⚫️ The land seed in
_Readyis transient, and plate 0 can't actually be land.Main.cs:46paints plate 0 land, butPlateGenerationends withAssignOceanPlates(Plates)(src/PlanetHelper.cs:434), which overwritesIsLandformandColorfor every plate. Worse, the mask enumeration only visits multiples of 4:bits 0 and 1 are therefore always clear, so
IsLandform = (bestMask & (1 << i)) != 0is always false for plates 0 and 1. Net effect: the eager land seed survives only until the expansion stage finishes, then flips to ocean. What's the intent here — "plate 0 must start as land" (thenAssignOceanPlatesneeds a forced-land constraint / the quantization needs fixing), or just "show something coloured on the mesh immediately" (then it's working as designed and worth a comment saying so)?⚫️ Neighbours beyond 6 are dropped silently —
Math.Min(vertexNeighbors.Count, neighborsPerVertex)(src/PlanetHelper.cs:460). On the current icosphere-derived mesh that's benign (only the twelve degree-5 vertices, which under-fill rather than overflow), but any mesh with a degree ≥ 7 vertex loses adjacency with no diagnostic, andClaimOrphansonly runs when the GPU stops making progress — so the loss would show up as subtly different plate boundaries, not an error. Cheap fix:GD.PushWarningwhenvertexNeighbors.Count > 6.⚫️
pointbuffer/ binding 0 is still dead weight — grep forpointbuffer.inshaders/compute/*.glslreturns nothing; the shader never reads it. The binding has to stay to match the SPIR-V layout unless you also delete the block from the shader, but theEnumerable.Range+ upload (src/PlanetHelper.cs:448-449,476-479) is pure waste. Either drop the binding entirely or allocate it without payload.⚫️ Shader/device bootstrap is still unvalidated —
src/PlanetHelper.cs:164-167. A failedGD.Load<RDShaderFile>is an NRE inside the constructor, and a null/unsupported localRenderingDevicesurfaces later as an opaque RD error. This matters more now that thegl_compatibilityfallback is gone. Worth an explicit null/IsUsingDrawCalls-style guard with a clear "compute-capable driver required" message.⚫️
project.godotstill declares"GL Compatibility"— line 16:config/features=PackedStringArray("4.6", "C#", "GL Compatibility"). The diff removedrenderer/rendering_method/.mobile, so the project actually runs forward_plus, but the feature tag still claims GL Compatibility. That tag feeds import-time/feature checks and will mislead anyone reading the project settings. Drop it (or setrendering_methodexplicitly toforward_plus) so declared features match reality.Small — misleading log on the wave cap:
if (unclaimed > 0)printswave N claimed nothing(src/PlanetHelper.cs:417-422), but that branch is also reached when the wave was progressing and simply hitMaxPlateWaves. Split the message so a cap hit doesn't point debugging at the wrong thing.Small —
_rd/_shaderlifecycle:MakeGo()(src/Main.cs:232) constructs a freshPlanetHelper, which means a fresh localRenderingDeviceper generation, released only whenever GC collects the old one;_shaderis never explicitly freed (ReleasePlateExpansionResourcesfrees pipeline/uniform set/buffers only). A deterministicCleanup()that frees_shaderand destroys the local RD would keep repeatedMakeGo()clicks from piling up devices.Perf note — the wall clock is now dominated by the per-tick
Submit()+Sync()+BufferGetData(), not by the shader. Since the buffers persist across waves, you could run K waves per tick and pay one sync instead of K — check whether your Godot version exposes a memory barrier inside the compute list first, since consecutive dispatches hitting the same storage buffer need one to be correct. Worth measuring before bothering, though: at ~60 fps a couple hundred waves is only a few seconds.Still open from last time (not a blocker for this PR's goal):
Oct.Inserthas no depth cap (src/Oct.cs:41-66), so two nodes at an identical scaled position recurse forever — and the widened inclusiveIsInsidemakes boundary-exact coincident points reachable. A coincident-position guard or max-depth bail withGD.PrintErrwould make it diagnosable instead of a stack overflow.Also confirmed the
.mtldeletion is safe — nothing referencesPlanetLowanywhere in the tree; the scene usesres://assets/PlanetBase.obj(scenes/MainScene.tscn:4).So: GPU side is good to go. I'd still want #1 resolved (or explicitly declared intentional) before merge, since it changes the generated world; #4/#5 are cheap and are the ones most likely to bite someone else on different hardware.
@beepster I have addressed your concerns. Please re-validate 🙂
@Ade9
@Ade9 Re-validated after the push — all nine items from my last pass are resolved (or explicitly declared intentional where the semantics question allowed it). Verified against the current head:
Confirmed fixed
AssignOceanPlatesnow enumerates the full mask space (Parallel.For(0, 1 << n, ...)atsrc/PlanetHelper.cs:937), so bits 0/1 are no longer pinned and plates 0 and 1 can come out as land from the area math. TheMain._Readyseed is now documented as transient-by-design (src/Main.cs:44-49): it paints plate 0 green for immediate feedback andAssignOceanPlateslegitimately re-derives land/ocean at the end ofPlateGeneration. That's the "working as designed + comment says so" branch, which is what I asked for. ✅GD.PushWarningwhenvertexNeighbors.Count > neighborsPerVertexbefore the truncatingArray.Copy(src/PlanetHelper.cs:513). Adjacency loss is now visible instead of silent. ✅PointBufferblock is gone from the shader (PlateExpansion.glsl:6-8) and is no longer allocated/uploaded; the C# builds bindings 1 and 2 only, with a comment to keep them matched to the SPIR-V layout (src/PlanetHelper.cs:522-523,UniformSetCreateat:558). No moreEnumerable.Range+ payload for a buffer nothing reads. ✅CreateLocalRenderingDevice, null check onGD.Load<RDShaderFile>, and_shader.IsValidcheck with_rd.Dispose()before throwing, each with a "compute-capable renderer required" message (src/PlanetHelper.cs:166-189). No more bare NRE on a failed import. ✅project.godotfeature tags (#5) —config/features=PackedStringArray("4.7", "C#")(line 16); the"GL Compatibility"tag is dropped, andrendering_device/driver.windows="d3d12"matches the actual forward_plus + D3D12 setup. Declared features now match reality. ✅:471) and the cap-hit branch (:479) are now separate messages, so hittingMaxPlateWaveswhile still progressing no longer masquerades as a reachability bug. ✅Cleanup()frees the_shaderRid and disposes the localRenderingDevice, idempotent via the_rd == nullguard (src/PlanetHelper.cs:197-214). Called from bothMakeGo()(src/Main.cs:244) and_ExitTree()(src/Main.cs:56), so repeated regenerations no longer stack up devices waiting on the GC. ✅MaxDepth = 48with aGD.PushWarningbail inInsert(src/Oct.cs:45,:55-59), and the recursive calls passdepth + 1. Coincident-position infinite recursion is now a diagnosable drop instead of a stack overflow. ✅The previously-verified plumbing (6-int stride with
Array.Fill(-1),Plates[x].Verticesrepopulation on read-back, one-wave-per-tick convergence loop, push-constant bounds guard,atomicCompSwapclaim,InitializeGenerationsettingStageitself) all still holds atsrc/PlanetHelper.cs:445,:456,:458-481,PlateExpansion.glsl:33-40/:47,:221.Remaining (non-blocking)
Submit()/Sync()/BufferGetData()round-trip per tick. Fine to leave; measure first as I said.Parallel.For(0, 1 << n, ...)is O(2^n) with alockon every candidate improvement (src/PlanetHelper.cs:937-953). Perfectly fine at_plateCount = 12(4096 masks), but it becomes pathological if_plateCountever grows past ~20, and1 << noverflows at n ≥ 31. Worth a comment capping the exhaustive search or a note that_plateCountmust stay small — no change needed for this PR.The one thing I flagged as a merge condition (#1, since it changes the generated world) is now both fixed in the quantization and declared intentional in the seed comment. LGTM — no further blockers from me.
The flood was first-arrival-wins: a vertex that had been written once returned early, so the second wave to reach it was discarded at the door. Measured on the shipped field, 0 of 10 242 reached vertices carried influence from more than one direction -- meeting waves produced a hard seam rather than mixing, and plate interiors were a pure nearest-border field with no intermixing at all. Replace the cached travelling value with a relaxed weighted sum: out[v] = source[v] + f * mean(in[neighbours]) // per sweep value = out.U / out.D // rates = U, weight = D source is the constant border injection; U accumulates seeded rate components scaled by attenuation, D the matching weights. The fixed point is (I - f*M)^-1 * s, so every source reaches every vertex with IDW weight f^d and the contributions superpose. The border stops being special: it is just where source != 0, which is what makes total coverage fall out rather than have to be engineered. Attenuation changes meaning along the way. Under claim-once it set the *reach*. Now it sets the *blend width*, and coverage is independent of it -- the reason raising 0.9 -> 0.96 visibly softened the falloff without ever fixing the seam. Two bugs found while verifying this, both caught by measuring instead of assuming: * SpreadMinInfluence = 1e-12 looked like a divide-by-zero guard but was a reach limit. D fades about 0.72 per hop, crossing 1e-12 near 91 hops while float32 is sound to about 272. U and D fade together and the ratio keeps roughly float32's full relative precision all the way down, so the epsilon was silently discarding real, representable far-field answers. Lowered to 1e-30, which sits just above the float32 normal floor and so can only ever catch a denominator that is genuinely zero. * One sweep carries influence exactly one hop, so the tolerance-derived sweep count was simultaneously capping the blend's geography. At 0.9 retention it asks for 88 sweeps while the mesh reaches 92 hops: 1 175 vertices received nothing at all, not filtered but never reached. Sweep count is now max(toleranceSweeps, requiredReach), with the reach measured by a BFS from the seed set and logged, so "how far does this go" is a stated quantity. Verified on GPU (RTX 5080, forward_plus): full coverage with 0 unreached, 0 errors and 0 warnings, and the EdgeStress CPU mirror still agrees to 0.00004% with 0 type mismatches. The blended map now shows genuine superposition -- where compression meets tension the result is near-black cancellation, not a wall. Type balance moves to roughly 64% shear. That is the expected consequence of superposition rather than a defect: tension and compression are opposite signs of the same channel and annihilate at range, leaving shear comparatively prominent. Left as-is pending a look at how it reads in motion.@beepster Implemented the rest of the stages as compute shaders - and changed some implementations for better ones both visually and logically.
Please review 🙂
@Ade9
Reviewed the compute-shader port. Overall this reads well — the batching story (chain N waves/sweeps per submit, poll a 16-byte progress counter instead of a full read-back) is the right shape, and the CPU fallback paths in
PlateGeneration/SeedSpreadStressare honest about what they're papering over.Things I verified as correct:
ComputeWorkgroupSize = 25matcheslocal_size_x = 25inPlateExpansion.glsl,BorderSearch.glsl,EdgeDistance.glsl,EdgeStress.glsl,SpreadStress.glslandHeightCalculation.glsl.Ensure*Resourcesbuilder (PlateExpansion 1–3, BorderSearch 1–4, EdgeDistance 1–4, EdgeStress 1–6, SpreadStress 1–4, HeightCalculation 1–8).SpreadStress's read-back parity is right: sweep 0 runs forward (A→B), sosweeps % 2 == 1 → AccumB, even →AccumA— and the batched loop keeps the global sweep index, so batch boundaries can't flip the parity.Cleanup()/_ExitTree()and theMakeGo()pre-cleanup close the leaked-RenderingDevice-per-click hole, and the release helpers are idempotent.ClaimOrphans()parking stranded vertices on the smallest plate keeps the unguardedPlates[PlateId]lookups downstream safe.A few things worth a look:
Workgroup size vs. warp size.
local_size_x = 25doesn't divide 32, so on NVIDIA each workgroup straddles warp boundaries and ~7 lanes/warp sit idle in these mostly-uniform loops. Bumping to 64 or 128 (in the shaders andComputeWorkgroupSize) is usually a free win on these full-sphere dispatches. Not a correctness issue.Land/ocean semantics flipped in
AssignOceanPlates. The old code treated set mask bits as ocean and targetedtotalArea * _landRatioas the ocean area; the newFindBestLandMasktreats set bits as land againsttargetLand = totalArea * _landRatio. With_landRatio = 0.4fthat means the generator now produces ~40% land where it used to produce ~40% ocean. The name matches the new behaviour, so I assume it's intentional — just confirming, because it's an easy silent change to bake into screenshots/tuning.Unreached vertices go slightly negative after normalisation. In
FinishEdgeDistanceCalculation, vertices the flood never reached keepEdgeDistance = -1, but when some vertex reaches a distance > 1 the divide bymaxDistanceturns that marker into-1/max— a small negative thatHeightCalculationthen feeds into the coastline term with the landform sign applied. Worth parking unreached vertices explicitly (e.g. skip them in the divide, or clamp the term shader-side) so "no measurement" never becomes a tiny-but-real signal.Missing read-back length check in
PlateGeneration.Buffer.BlockCopy(outputBytes, 0, output, 0, seedBytes.Length)will throw on a short buffer. Every other stage guards this (if (flagBytes.Length < expectedBytes) ...inBorderSearch, same inEdgeDistance/EdgeStress/SpreadStress/HeightCalculation) — worth matching here for consistency.Minor: leaked shader Rids on a mid-bootstrap compile failure. In
LoadComputeShader, the failure path disposes_rdand throws, but shaders already compiled in earlierLoadComputeShadercalls from the constructor are never freed. Harmless in practice (the process is failing anyway), just noting it.None of these block the PR — 2 and 3 are the only ones I'd want a conscious answer on. Nice work on the meet-in-the-middle land split; the truncation-preserving comment on
weight[i]is exactly the right thing to call out, since that's what keeps the chosen mask identical to the old enumeration.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.