Skip to content

bring execution model into the native core - #631

Draft
sargunv-bot wants to merge 64 commits into
mainfrom
t3code/native-executor
Draft

bring execution model into the native core#631
sargunv-bot wants to merge 64 commits into
mainfrom
t3code/native-executor

Conversation

@sargunv-bot

Copy link
Copy Markdown
Collaborator

Summary

Replaces the thread-affinity and host-pumping architecture with a native-owned executor, then revises the execution forms per review: unified command/snapshot generations, snapshot fields for unkeyed reads, NOT_FOUND remove commands, a layer-info aggregate, and synchronous projections, applied across the C API and all eight bindings.

Test plan

C API, Kotlin (JVM+native), Rust, Zig, Python, Swift, .NET, Go, and Dart suites all pass on linux-x64, with new coverage for the generation fence, layer info, NOT_FOUND removals, and synchronous projections.

AI assistance

  • Tools: Claude Code
  • Context: Continues bring execution model into the native core #433 (frozen by a head-branch deletion; review history lives there). Claude implemented the execution-form revision from the maintainer-approved per-function evaluation, swept the bindings with per-language agents, and ran a multi-agent audit whose confirmed findings are fixed in the final commit.

Replaces #433, which GitHub permanently detached from this branch after a head-branch deletion/recreation; all review threads and history remain there.

sargunv-bot and others added 15 commits August 14, 2026 03:08
…to it

Committed map commands now publish a map snapshot and report its
generation in COMMAND_FINISHED, replacing the disjoint style-generation
counter, so snapshot readers can fence on any commit. The snapshot
gains debug_options, rendering_stats_view_enabled, tile, bounds, and
free_camera, and collapses loading/fully_rendered into fully_loaded.
The eleven operations that duplicated snapshot state or existence
checks already reported by info getters are removed.
mln_map_remove_style_layer/source/image carried only an out_removed
bool, so they become commands like their sibling mutations. A missing
id reports COMMAND_FINISHED as failed with the new MLN_STATUS_NOT_FOUND
code, and an in-use source keeps reporting MLN_STATUS_INVALID_STATE.
mln_map_get_style_layer_info reports type, zoom bounds, visibility,
and the source id/layer byte sizes with out_found, mirroring the
source-info pattern, replacing four scalar operations per layer read.
mbgl::MapProjection copies the transform state at construction and
never touches the map again, so reads, setters, and close now run on
the calling thread under a per-projection mutex instead of queuing
through the runtime. Creation stays an ordered operation so the
capture observes prior commands. The projection command plumbing and
MLN_RUNTIME_EVENT_SOURCE_PROJECTION are gone.
c-conventions gains the decision rule for immediate, command, snapshot,
and operation forms, including the generation fence and the both-forms
criterion. Feature-state ownership moves from the map to the render
session in concepts, matching where MapLibre stores it.
All eight bindings drop the eleven snapshot-covered read operations and
the four per-layer scalar getters, reshape their snapshot types around
debug options, load state, tile, bounds, and free camera, adopt the
layer-info aggregate and the NOT_FOUND remove commands, and expose the
projection as a synchronous, internally serialized object. Generated
FFI layers were regenerated; every binding's host test suite is green.
A committed map command whose snapshot publish threw could skip its
terminal event and hang the caller; the publish now runs inside the
guarded region like every sibling command site. Go events gain a typed
Err on the command-finished payload so NOT_FOUND is observable through
the public API. The Kotlin jextract includes drop the retired
projection event source, layer-info copies degrade to null when the
layer disappears mid-read, and the Swift and .NET projections use
their bindings' live-handle helpers. BND-190 scopes submission order
to the calls that still queue, and two racy C tests are made
order-tolerant.
@greptile-apps

greptile-apps Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review (681 files, 250 file limit).

The hand-maintained manifest was a second source of truth for every
export. Names and signatures already encode the execution form, so the
checker now derives the category from each declaration and verifies
the boundary conventions across every public header, with a one-entry
exception table for the frame demand. The classification table joins
c-conventions as the naming rule for new functions.
Deletes dead code left behind by the executor rearchitecture and the
execution-form revision: unused render-session virtuals and helpers,
orphaned map and test-support getters, dead Kotlin downcall aliases and
test-only registry hooks, orphaned Go cgo and darwin test scaffolding,
stale Zig test fixtures, and a trivial Rust status-mapping test. The
move-the-camera guide now teaches the one camera command and its three
update modes, the frame-loop snippet is embedded again, and the
examples shed their queue-era Channels names and enqueue wrappers.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

@sargunv
sargunv marked this pull request as draft August 15, 2026 16:13
@sargunv-bot

This comment was marked as resolved.

@jfberry

This comment was marked as resolved.

Semantic union of main's #616-#630 with the native-owned execution
model: prepared GeoJSON data lands as any-thread create plus command
installs, the render-update repaint flag surfaces as needs_repaint on
frame results, tvOS targets and test enablement come through as-is, and
the pump drain budget is superseded because no host pump exists. The
now-unused run-loop process-gate patch is dropped.

Also lands the verified review fixes: panic containment in the Go
notification trampoline, an ABI-aware emulator reuse check, an
atomic close-vs-attach claim, an attachment recheck under the
driver-queue lock, session-publication rollback, fail-fast unsupported
consumer sync that never recycles an unwaited slot, a coherent
update/generation snapshot for frame results, and render-if-needed
gating that honors MLN_RENDER_RESULT_NO_UPDATE. The execution
conventions checker's exception table is now empty: request_frame
classifies as immediate because it hands back no completion identity.
Semantic union of #632 with the operation-based query surface: rendered-
and source-feature queries keep their _start form and now complete with
mln_render_query_features_take_result, which hands out an owned
mln_queried_feature_list; feature-extension queries keep the JSON buffer
take. Bindings materialize the list to native collections and destroy
the handle after the copy.
@jfberry

This comment was marked as resolved.

sargunv-bot and others added 24 commits August 18, 2026 10:52
Reconciles seventeen main commits with the completion model:

- Adopts the mbgl-to-mln namespace rename from the submodule bump and moves
  the pin to 550f64b.
- Keeps the converged synchronous projection design; creation stays an
  ordered completion and everything after it is synchronous and any-thread.
- Moves feature state onto the map as a copied command, an ordered read, and
  a remove command, with the render session pushing coalesced snapshots into
  its renderer; the session-level feature-state entry points are gone.
- Ports custom MVT vector sources onto completion-based commands, backed by
  a kind-aware callback-source registry shared with custom geometry sources.
- Keeps prepared GeoJSON data and the synchronous-tiling override, carrying
  main's replace-during-async-tiling fix and its stress test, rewritten for
  frame demands and made locale-proof.
- Delivers queued worker results without a rendering demand: the session
  scheduler's repaint hook now wakes the driver, and a demand drains before
  the render-if-needed check, so still images no longer strand behind
  keep-alive demands. Covered by a regression test that fails without the
  fix. Retires the renderer outside control_mutex in ordered resizes, which
  the new hook turned into a deadlock cycle.
- Fixes two latent JVM binding bugs the merge exposed: attach never set
  mln_wake.size, and render struct sizes were stale hardcodes.
- Sweeps all eight bindings, docs, snippets, and examples onto the merged
  API, restoring main's new coverage in completion form.
mln_runtime_release now takes a completion that runs after every earlier
accepted submission, including released maps' teardown, has finished and the
runtime's threads are gone. The invoking thread touches no library state
afterward, so a host that waits for it can exit the process without racing
MapLibre's process-wide singletons, which previously segfaulted short-lived
hosts about once in eight runs. Every binding exposes the teardown report in
its own completion idiom: Kotlin close() returns a cached Deferred and use{}
awaits it, .NET gains CloseAsync and IAsyncDisposable, Dart's close() future
now resolves after teardown, Go returns the command Future, Zig returns a
completion future, Swift close() is async, and Rust returns a NativeFuture
while Drop stays non-blocking.

Waiting immediately exposed two teardown bugs, both fixed: cancelling a
pending still image leaked its submission lease, because the mbgl callback
that releases it only fires at map destruction, after teardown already waits
for submissions — the cancel path now releases the stored lease directly;
and the test helper's rejection path skipped the caller-owned release marker
that completion destroy waits for.

mln_render_session_abandon now also drains the map's in-flight tile work
before returning, because tile workers can still hold the quarantined
renderer's atlas and through it the host's graphics device; after abandon
returns the host may destroy its graphics objects immediately.

Also restores the Python render-session coverage lost in the cleanup sweep
(3 to 14, 3 to 18, and 3 to 14 tests across the Vulkan, EGL, and Metal
suites), fixes the Dart async-close path caching a synchronously rejected
close forever, adds Dart's missing busy, target-lost, and not-ready status
mappings, sweeps the stale Python type stubs, documents the Dart render
surface against the C headers, and updates every example, snippet, and guide
to wait for teardown before exit.
@jfberry

jfberry commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Follow-up from the field: surfacing mln_wake in the Go binding would retire the last host-side polling — with numbers on what polling costs

Closing the loop on the two earlier reports (core-worker for dedicated EGL; the demand fast-path stranding): rampardos's Go renderer now runs the full shape this PR converged on — core-worker driver, private EGL context, still-image demands, and a host resource provider serving mbtiles tiles — pinned at cf27aa58. Production results are good: warm stills complete in 12–16 ms where the previous architecture measured ~20 ms, with the drain fix behaving exactly as designed. One structural gap remains, and it's binding-side rather than C-API-side.

The gap. The C API grew per-session wakes in the async rewrite — frame_wake / driver_work_wake in mln_render_session_attach_options, with a contract explicitly designed for gradual adoption ("a zeroed descriptor disables waking; polling remains valid"). The canonical still-image.c is wake-driven, and Kotlin's generated layer already carries the fields. The Go binding, however, zeroes them in RenderSessionAttachOptions.toC() and exposes no way to supply one — so frame results are observable only by polling DrainFrameResults.

What polling costs, traced. A static-mode still is many progressive passes, each authorized by a demand and observed by a drain. With nothing able to interrupt the host's pacing park, every pass (or pipeline-batch of passes) pays up to one tick of pure observation latency. Traced on production before tuning, a ~33 ms render spent ~12–15 ms in exactly that: pipeline-pairs of demands resolving ~1.15 ms apart, each round gated on a 1 ms tick rather than on result arrival. We clawed most of it back host-side — a four-deep demand pipeline (distinct coalescing_boundary per demand so the supersede rule doesn't collapse the queue) plus a 250 µs tick — which amortizes the tax to ~1–2 ms per render. But that's a workaround with a timer spinning at 4 kHz while parked, not a design; a surfaced frame wake deletes both the residual latency and the polling entirely.

Sketch of the Go surface (mirroring the C contract; the binding already has the cgo-handle + release-callback pattern in its completion shim):

type RenderSessionAttachOptions struct {
    Driver                    RenderDriver
    RequestedTextureRingDepth uint32
    // FrameWake, when non-nil, is installed as the session's frame_wake:
    // invoked whenever a frame result becomes drainable. Native may call
    // it from any thread and calls may coalesce; it must only schedule
    // receiver work. Released per mln_wake_release after the session
    // can no longer invoke it.
    FrameWake func()
    // DriverWorkWake likewise surfaces driver_work_wake for
    // caller-graphics-thread hosts. Core-worker sessions need only
    // FrameWake (the worker wakes itself).
    DriverWorkWake func()
}

A host then parks on a channel fed by FrameWake instead of a tick — the still await becomes fully event-driven with no API changes beyond the two optional fields.

Not merge-blocking, same as before — the polling contract works and we're shipping on it. But it's the last place a Go host has to spin instead of listen, and per the pattern of the previous two: happy to validate a build here within a day of it landing, with the same production tracing that produced the numbers above.

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.

3 participants