Skip to content

feat: async API with executors, thread pool, mutex + region debug overlay - #3

Merged
PyXiion merged 14 commits into
masterfrom
feature/async-coroutines
Aug 1, 2026
Merged

feat: async API with executors, thread pool, mutex + region debug overlay#3
PyXiion merged 14 commits into
masterfrom
feature/async-coroutines

Conversation

@PyXiion

@PyXiion PyXiion commented Aug 1, 2026

Copy link
Copy Markdown
Owner
  • Coroutine suspension bridge
    • Lua coroutines can yield through continuable Java functions, pcall/xpcall, suspend functions, and event handlers.
    • Added resume handlers, continuation state, and error propagation.
  • Async Lua API
    • Replaced mc.task, mc.run, mc.prun, mc.sleep, and mc.fetch with require "async".
    • Added tasks, promises, all/allSettled, sleep, HTTP, executor selection, thread pool execution, mutexes, and serialized coroutine resumption.
    • Added scheduler thread-safety and server-stop cleanup.
  • Region debug overlay (WIP)
    • Added server/client region synchronization with interest-radius diffs, add/remove updates, admin gating, cap warnings, and /ignis debug regions.
    • Added client source sets and Fabric client entrypoints.
    • Wireframe rendering remains a documented no-op pending available Fabric/Yarn renderer APIs.
  • Reliability and compatibility
    • Fixed region payload ID collisions and development launch issues.
    • Added region diff, suspend bridge, Lua yield, and async tests.
    • Updated API/design documentation and changelog.

PyXiion added 13 commits June 28, 2026 01:56
- Add loom.splitEnvironmentSourceSets() and a second 'pxignis' mod entry
  pointing at sourceSets.client in build.gradle.
- Create src/client/java/ and src/version-*/client-kotlin/ for client code.
- Wire shadowJar to include both source set outputs and drop minimize()
  (it stripped PxIgnisClient since the main entrypoint doesn't reference it).
- Register client entrypoint ru.pyxiion.ignis.client.PxIgnisClient in
  fabric.mod.json.
- Add PxIgnisClient.kt entrypoint (logs 'loaded') and ClientCompat.kt stubs
  for 1.21.10 and 1.21.11.
…tests)

Server-side (src/main/java):
- Add RegionPackets.kt: 5 custom payloads sharing the pxignis:regions id —
  RegionSyncPayload (full snapshot), RegionUpsertPayload / RegionRemovePayload
  (incremental diff), RegionCapWarningPayload (one-time toast), and the
  C2S RegionInterestPayload. Each payload uses a per-class typed
  CustomPayload.Id<T> and a hand-rolled Box codec (6 doubles).
- Region.kt: add REGION_INTEREST_RADIUS_CHUNKS=4 and
  MAX_REGIONS_PER_PLAYER=256 constants; opt-in state (optInPlayers,
  lastSnapshots, warnedPlayers) and the per-tick tickClientSync that walks
  regionsByChunk within the player's 4-chunk radius, diffs against
  lastSnapshots via the pure diffRegionSnapshots, sends upserts/removes,
  and fires the cap warning once per opt-in. Pure diffRegionSnapshots
  treats bound changes as upserts to fix the stale-AABB bug. New
  setOptedIn / onPlayerLeft / closeAll(server) lifecycle methods.
- PxIgnis.kt: register all 4 S2C + 1 C2S payload types, gate the C2S
  handler with Compat.isAdmin (server-authoritative), call
  RegionManager.tickClientSync in END_SERVER_TICK, RegionManager.onPlayerLeft
  in DISCONNECT, and RegionManager.closeAll in SERVER_STOPPED.

Client-side (src/client/java):
- ClientRegionRegistry: thread-safe ConcurrentHashMap<Int, Box> with
  upsert/remove/replaceAll/clear; var enabled default false (off on join).
- ClientCommands: registers /ignis debug regions via
  ClientCommandRegistrationCallback; toggles registry.enabled, sends
  RegionInterestPayload.
- PxIgnisClient: registers 4 S2C receivers, hooks
  WorldRenderEvents.BEFORE_DEBUG_RENDER (runCatching the 1.21.10 stub so
  the unsupported stub doesn't log-spam every frame), and registers the
  client command.

Tests (src/test/kotlin):
- RegionInterestDiffTest: 6 pure-logic JUnit 5 tests covering empty/empty,
  add, remove, bounds-change-as-upsert, unbounded-when-under-cap, and
  capped-when-over-limit. No MC runtime.

Both ./gradlew build (1.21.11) and ./gradlew build -PtargetVersion=1.21.10
pass; all 93 unit tests pass. Wireframe rendering (drawWireframeBoxes)
remains a stub pending Phase 4.
- gradle.properties: bump yarn_mappings from 1.21.11+build.5 to +build.6.
  Build.5 was missing yarn classes that fabric-recipe-api-v1 8.2.4
  references, causing a Mixin 'resource invalid or could not be read'
  crash on game startup. Build.6 resolves it.

- RegionPackets.kt: each of the 5 payloads now gets its own
  CustomPayload.Id<...> with a unique identifier (pxignis:regions_sync,
  regions_upsert, regions_remove, regions_cap_warning, regions_interest).
  The previous version shared a single identifier across all 5 payloads,
  which collided in PayloadTypeRegistryImpl.register() because the
  registry is keyed by Identifier, not by Id<T> — it threw
  'Packet type Id[id=pxignis:regions] is already registered!' on
  main entrypoint. Also drops the unused internal REGIONS_ID shim.

- PxIgnisClient.kt: pass the WorldRenderContext from
  WorldRenderEvents.BEFORE_DEBUG_RENDER to the new
  ClientCompat.drawWireframeBoxes(ctx, boxes) signature.

- ClientCompat.kt (1.21.10): signature updated to match; the body is
  still an UnsupportedOperationException stub for parity with 1.21.11.

- ClientCompat.kt (1.21.11): real implementation is blocked because
  the renderer classes referenced by the fabric docs
  (com.mojang.blaze3d.vertex.BufferBuilder, ByteBufferBuilder, MeshData;
  MappableRingBuffer; RenderSystem.device, .dynamicUniforms,
  .projectionType) are not shipped in yarn 1.21.11+build.6 or the
  minecraft-client jar at the versions this mod depends on. The merged
  jar contains them under neoforge profiles, but the loom
  client source set has only minecraft-client. The fabric-renderer-indigo
  5.0.3 access widener does not widen any of these classes. Replaced the
  body with a one-time-warning no-op that documents the limitation; the
  network state machine, registry, command, and toggling flag are all
  live and will start drawing wireframes as soon as a future fabric-api
  or yarn build exposes the API.

Verified: ./gradlew build (1.21.11) green,
./gradlew build -PtargetVersion=1.21.10) green, all 93 unit tests pass,
./gradlew runClient boots singleplayer cleanly with the mod loaded.
Add LuaContinuableFunction — Java functions that let coroutine.yield
propagate through their call boundary. When an inner function yields,
the caller throws YieldContinuationException; OP_CALL stores the func,
callArgs and continuation on the frame and suspends. On resume the
continuable function is re-invoked with the continuation state.

- pcall/xpcall rework into continuable functions so yields pass through
  them and are not masked as errors.
- LuaThread.ResumeHandler: per-thread callback for async resume,
  inherited from parent/main thread; lua_resume_sync now sets/restores
  LuaState.current().
- FrameInterpreter: re-invoke continuable functions on resume, drop the
  "yield across C-call boundary" error for continuable calls.
- LuaClosure/DebugLib: guard against missing current LuaThread.
- pcall/xpcall yield propagation (single and multi-value yields)
- pcall errors not masked as yields
- closure errors without a current LuaThread no longer NPE
luaSuspendFunction/luaSuspendFunctionNil yield the Lua coroutine, run a
Kotlin suspend block on a CoroutineScope, and resume it via the thread's
LuaThread.ResumeHandler. Fails with a clear LuaError outside a coroutine,
on the main thread, or without a configured handler.

- IgnisRuntime owns modScope (SupervisorJob + Dispatchers.Default),
  cancelled on server stop.
- LuaMcApi.suspendFunction helper; main thread gets a resume handler that
  schedules resumes back on the server thread.
- EventBus runs LuaClosure handlers through a LuaThread, so mc.sleep /
  mc.fetch / suspend functions work inside event handlers; requires a
  stateProvider (regions use RegionManager.sharedStateProvider).
- SuspendBridgeTest covers sync/suspending suspend functions, pcall/xpcall
  propagation, error handling, and scope usage.
mc.task(fn, ...) runs a Lua function on a background thread and returns
a task userdata; mc.task:pwait() yields until it completes, returning
ok, result (pcall-like). mc.prun(fn) is a convenience for
task(fn):pwait().
- Changelog entry for the async coroutines release.
- docs/async-suspend-bridge.md design rationale (problem, thread model,
  deferred work); docs/index.md index.
- Split API surface and testing quirks out of AGENTS.md into
  agent_docs/api.md and agent_docs/testing.md.
…ises, all/allSettled

- mc.task/run/prun/sleep/fetch removed from mc table
- async.task runs body as LuaThread (can yield internally)
- async.promise() for manual settlement
- task:wait(), task:try(), promise:resolve(), promise:error()
- async.all() / async.allSettled() combinators
- async.sleep() / async.fetch() moved from mc
- 23 tests passing
… + docs

- AsyncExecutor registry + PxIgnis-async- thread pool
- coroutine-safe async.mutex with FIFO queuing
- SerializedResumer hardening for one-at-a-time coroutine resumes
- LuaThread.executionContext for executor propagation
- thread-safe Scheduler and SERVER_STOPPING lifecycle shutdown
- rewrite async-api.md reference around executors and I/O vs parallel work
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 448 complexity · 32 duplication

Metric Results
Complexity 448
Duplication 32

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@PyXiion
PyXiion merged commit eb16002 into master Aug 1, 2026
4 checks passed
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