Task 117 P2′: Image, PlaneMesh, StaticBody3D and the tweener family generated once — HAND_SHAPED down to the three roots - #275
Merged
Merged
Conversation
…Image members Both group-B "iOS hand copy is a strict subset of the shared draft" classes are generated once into src/sharedApi; all four per-platform copies are deleted and PER_PLATFORM_WRAPPERS loses both entries (32 -> 30). The shared draft is signature-identical to the deleted desktop file (only internal wrap's parameter goes MemorySegment -> the RawSegment alias), so nothing changes on desktop: iOS gains Image's 21 desktop-only members (blit*/blend*/ fillRect/getRegion, the load*/save*Buffer PackedByteArray family, setData, computeImageMetrics) and PlaneMesh.fromResource tightens to a non-null Resource. No Image shape was refused by the iOS renderer: the shape gap stays at 3 desktop-only members waiting across 2 classes and no Image.jvm.kt companion exists. 16 ObjectCalls helpers newly reach the shared tree and are actual on both platforms (expect 1415 -> 1431). HAND_SHAPED 7 -> 5, allowlist 103 -> 82, shared tree 1003 -> 1005. Verified: drift gate PASS (shared=1005), parity gate PASS (5 classes / 82), ObjectCalls parity PASS, PT-tag PASS, check_doc_claims OK, audit_claims PASS 6/6, ktfmtCheck, compileKotlinJvm + compileKotlinIosArm64 + :processor:test + :jvmTest BUILD SUCCESSFUL, all 11 demos compile unchanged. Assisted by AI Co-Authored-By: Claude <noreply@anthropic.com>
…ysics chain (D3) The generated desktop copy and the hand-written iOS `class StaticBody3D : Node3D` in IosGodotApi.kt are both deleted; the class is generated once into src/sharedApi and PER_PLATFORM_WRAPPERS loses the entry (30 -> 29, iOS collision classes 11 -> 10). On iOS the chain becomes StaticBody3D : PhysicsBody3D : CollisionObject3D : Node3D, which is task 117's D3 satisfied by construction rather than by a hand edit; the two re-declared collisionLayer/collisionMask properties now come from CollisionObject3D with the same Long type, the same uint32 ptrcall helpers and the same bind hashes, so no call site changes. iOS gains StaticBody3D's own 9 members (physicsMaterialOverride, constantLinearVelocity, constantAngularVelocity and their get/set pairs) plus the companion fromHandle/wrap, plus PhysicsBody3D's 14 and CollisionObject3D's 38 through the corrected chain; AnimatableBody3D (already shared as : StaticBody3D) inherits the fix. The shared draft is signature-identical to the deleted desktop file, so desktop is unchanged and no int width moved. HAND_SHAPED 5 -> 4, allowlist 82 -> 68 (the 14 StaticBody3D lines, including the `supertype | *` line that carried D3 as a known divergence since P0), shared tree 1005 -> 1006. Verified: drift gate PASS (shared=1006), parity gate PASS (4 classes / 68), ObjectCalls parity PASS, PT-tag PASS, check_doc_claims OK, audit_claims PASS 6/6, ktfmtCheck, compileKotlinJvm + compileKotlinIosArm64 + :processor:test + :jvmTest BUILD SUCCESSFUL, all 11 demos compile unchanged. Assisted by AI Co-Authored-By: Claude <noreply@anthropic.com>
…wn to the three roots Tweener, PropertyTweener, CallbackTweener and MethodTweener retire together into the shared tree. They had to go as a set: the iOS hand cluster in IosGodotApi.kt put setTrans/setEase on the BASE Tweener (Godot declares them on the subclasses) precisely because the generated subclass members would have clashed with it, which is also why MethodTweener was the `unsupported` cell iOS did not host at all. PER_PLATFORM_WRAPPERS 29 -> 25; IOS_UNSUPPORTED_CLASSES is down to DirAccess alone. Tween itself stays hand-written on both platforms (its iOS Variant tween_property runtime is outside task 117). Source break on desktop: the fluent setters now return the NULLABLE self type. The hand copies returned the non-null self through a private wrapOrThis that turned a null engine return into `this`; the generated self-return collapse keeps the reference-neutral part and ends in wrap(ret), which is nullable like every other generated object return. So PropertyTweener's from/fromCurrent/asRelative/setTrans/setEase/setCustomInterpolator/setDelay are `PropertyTweener?` and CallbackTweener.setDelay is `CallbackTweener?`; chains take `?.`. wrapOrThis is gone (no callers after the inline collapse); Tween's own wrapOrThis is untouched. Both constructors go internal -> public and both gain @JvmStatic fromHandle. Tweener and MethodTweener are otherwise identical to their deleted desktop copies. iOS gains the whole family: MethodTweener as a class it did not host, PropertyTweener's seven generated members (from(value: Any?) covers the hand from(Color) through packVariantDesc's PT_COLOR case), CallbackTweener.setDelay, and Tweener's Signals/companion. Tweener.setTrans / setEase move to the subclasses Godot declares them on. iOS Tween.tweenMethod returns MethodTweener? (was Tweener?) — the one hand edit that class needed. The iOS glue IosGodot.tweenerSetTrans/tweenerSetEase/propertyTweenerFromColor and their three cinterop imports are deleted; the C shim functions and their three static binds stay for a later cleanup. HAND_SHAPED 4 -> 3 (GodotObject, RefCounted, GodotCallable — the P3' roots), allowlist 68 -> 62, shared tree 1006 -> 1010. Verified: drift gate PASS (shared=1010, collision 7, unsupported 1), parity gate PASS (3 classes / 62), ObjectCalls parity PASS, PT-tag PASS, check_doc_claims OK, audit_claims PASS 6/6, ktfmtCheck, compileKotlinJvm + compileKotlinIosArm64 + :processor:test + :jvmTest BUILD SUCCESSFUL, example_project and templates/starter compile unchanged, 10/11 demos compile unchanged. Starter-Kit-Match3 needs the paired demos change (Tile.kt:85 and :91 chain setTrans(...).setEase(...) without `?.`). Assisted by AI Co-Authored-By: Claude <noreply@anthropic.com>
…D19) The generated shared tree calls Godot static methods through the ordinary instance-shape ObjectCalls helpers with NULL_SEGMENT as the instance (_null_segment() for method.is_static in scripts/generate_api_wrapper.py). Desktop/Android pass that MemorySegment.NULL straight to object_method_bind_ptrcall, which Godot accepts for a static bind. On iOS the same helpers all ended in the C entry point kanama_ios_godot_ptrcall, which deliberately early-returns when instance == 0 — the guard commit 30c949a kept when it added the separate kanama_ios_godot_ptrcall_static entry point. The per-platform iOS hand copies used the ptrcallStatic* helpers and were fine; the shared tree never did, so every shared-tree static call was a silent no-op on device (null / 0 / default) while passing on desktop. 72 call sites across 36 shared classes, present since 7a43afc (2026-09-15, task 104 step 3) and made visible by this PR moving Image.createFromData off the iOS hand copy. The fix is one dispatcher in the iOS Kotlin ObjectCalls layer, not the C shim and not the shared tree. ptrcallDispatch(...) is hand-written above the BEGIN GENERATED MEMBERS marker and sends a zero instance to kanama_ios_godot_ptrcall_static, everything else to kanama_ios_godot_ptrcall. All 1172 plain-ptrcall call sites in the file now go through it; the generated region does so because the generator emits ptrcallDispatch, so a regen keeps the fix (fixture and iOS region regenerated; check_wrapper_generator reports zero drift). ios/bootstrap/kanama_ios_shim.c, the ptrcallStatic* helpers and their hand users (FileAccess, ImageTexture) are untouched, and desktop needs no change: src/jvmMain/kotlin/binding/runtime/ObjectCalls.kt has no null-instance guard on the ptrcall path (line 194's guard is inside notifyPostinitialize). This repairs 59 of the 72 sites — the ones whose helper marshals through the generic dispatch. The other 13 return through specialized C entry points (..._no_args_ret_string, ..._no_args_ret_string_name, ..._no_args_ret_packed_string_array, ..._no_args_ret_typed_array_blob, ..._ret_array_blob, ..._ret_utf8, ..._ret_variant_scalar and kanama_ios_godot_object_call), each of which keeps its own null-instance guard and has no _static counterpart yet; they are called out in the changelog as still no-op on iOS and need shim work this ruling deliberately excludes. Device-visible probe: the iOS OBJECTCALLS SELFTEST grows from 205 to 212 checks. The seven new rows drive the shared-tree static path through the public wrapper API rather than the ptrcallStatic* helpers — Image.createFromData(2, 2, false, FORMAT_RGBA8, ByteArray(16)) (non-null, width 2, height 2), Image.create(4, 3, false, FORMAT_RGBA8) (non-null, width 4), Thread.isMainThread() and RegEx.createFromString("a+b", false). Text corrections: - CHANGELOG.md: the bullet claiming Image's four companion factories are "unchanged on both platforms" was false — on iOS createFromData moved from the static entry point to the null-instance path. Rewritten to say what is true after this fix, plus a new "### Fixed" entry describing the systemic defect, the dispatcher and the self-test additions, with recountable numbers. - docs/contributing/wrapper-maintenance.md: the sentence saying PropertyTweener.from reaches ptrcallWithVariantArgRetObject through a METHOD_CALL_SHAPE_OVERRIDES entry named the wrong mechanism — that table holds only the two ClassDB rows. The real source is the return-type-keyed special case in _candidate_for_impl (Object return over a single Variant arg when return_type is Node or PropertyTweener). - docs/contributing/wrapper-maintenance.md: documented the NULL_SEGMENT null-instance convention beside the existing "NULL_SEGMENT for a static" sentence — desktop passes it to Godot, iOS routes it to the static entry point through ptrcallDispatch. Assisted by AI Co-Authored-By: Claude <noreply@anthropic.com>
Commit 002e2c6 routed the 1172 plain ptrcall sites through ptrcallDispatch and repaired 59 of the 72 shared-tree static call sites. The residue was 13 sites in EditorExportPlatform, FileDialog, GLTFDocument, JSON, MultiplayerAPI, Node, Resource and ShaderIncludeDB whose ObjectCalls helper does not marshal through the generic entry point: they return through a specialised C entry (..._no_args_ret_string, ..._no_args_ret_string_name, ..._no_args_ret_packed_string_array, ..._no_args_ret_typed_array_blob, ..._ret_array_blob, ..._ret_utf8, ..._ret_variant_scalar and kanama_ios_godot_object_call), each carrying its own `instance == 0` early return and having no static sibling. kanama_ios_godot_ptrcall was never the only guarded entry point. The property this reaches is stronger than the 13: no Kotlin ObjectCalls helper can hand a zero instance to a guarded C entry point, so the next static the generator renders through any shape is covered by construction. C shim (ios/bootstrap/kanama_ios_shim.c, ios/include/kanama_ios.h). All 22 guarded kanama_ios_godot_* entry points an ObjectCalls helper calls with an instance now follow the split commit 30c949a established for kanama_ios_godot_ptrcall: the body moves into an unguarded `static <symbol>_dispatch(...)`, the existing exported symbol keeps its null-instance guard and calls it, and a new `<symbol>_static(...)` sibling without the instance parameter calls it with 0. Four entries zero their out-parameters before the guard (ret_callable, ret_variant_scalar, object_call, load_status_with_progress); that preamble is repeated in the guarded entry's reject branch, because their Kotlin callers decode the out-parameters without consulting the returned status. No existing guard was removed and no existing signature changed. The 22 new symbols are declared beside their siblings in ios/include/kanama_ios.h, which is what the kanama_ios.def cinterop reads. Guarded entries WITHOUT a method_bind (object_destroy, object_connect_bound, object_disconnect_bound, ...) are untouched on purpose: their zero check guards a live engine object handle, which the generator's static marker never reaches. Kotlin (src/iosMain/.../binding/runtime/ObjectCalls.kt, hand region). One private dispatcher per C entry point, next to ptrcallDispatch and following it: 22 `<entry>Dispatch` functions that pick the _static sibling when instance == 0. All 32 call sites of those entry points in the file now go through them, so each guarded symbol is named exactly once in the file, inside its dispatcher. `inline` is dropped from ptrcallDispatch (the compiler says it buys nothing) and the new ones are not inline. Gate: scripts/check_ios_static_dispatch.py, a local_ci.sh stage beside check_objectcalls_parity. It parses the shim for every exported kanama_ios_godot_* function that early-returns on a zero instance, derives the in-scope set (those that also take a method_bind — the ptrcall shape a static is rendered through) and fails if ObjectCalls.kt names one anywhere except inside a *Dispatch function that also calls its _static sibling. Findings are raw-call, dispatcher-no-static, missing-sibling and parse-error; the entry-point -> dispatcher table and the out-of-scope object-handle entries print on PASS. Red run (task 118: every gate ships with its negative test, documented in the docstring): replacing the ptrcallNoArgsRetStringDispatch call in ptrcallNoArgsRetString with the raw entry point yields [ios_static_dispatch] FAIL raw-call src/iosMain/kotlin/net/multigesture/kanama/binding/runtime/ObjectCalls.kt:1005 calls the guarded entry point kanama_ios_godot_ptrcall_no_args_ret_string from ptrcallNoArgsRetString(), not from a *Dispatch function — a static call site reaches its `instance == 0` guard and silently no-ops and exit 1; restoring the dispatcher call makes it green again. Device-visible probes: the iOS OBJECTCALLS SELFTEST grows from 212 to 218 checks. The six new rows drive the six specialised entry points through the wrapper API — a JSON.stringify/JSON.parseString round-trip (ret_utf8 and ret_variant_scalar), ShaderIncludeDB.listBuiltInIncludeFiles() non-empty (no_args_ret_packed_string_array), ShaderIncludeDB.getBuiltInIncludeFile() on a name from that list (object_call, via callWithVariantArgs), Resource.generateSceneUniqueId() non-empty (no_args_ret_string) and MultiplayerAPI.getDefaultInterface() == "SceneMultiplayer" (no_args_ret_string_name). Docs: the CHANGELOG "### Fixed" entry no longer carries the 59/13 residue — it reads 72/72, describes the _dispatch/_static split and names the new gate. docs/contributing/wrapper-maintenance.md gains the guarded-entry rule beside the null-instance paragraph; docs/exporting/ios.md says each instance-taking shim entry point has a _static sibling. docs/reference/generated/gates.md is regenerated for the new stage. Assisted by AI Co-Authored-By: Claude <noreply@anthropic.com>
Review of 002e2c6 + a30baf0 found seven items. All seven are fixed here. 1. BLOCKER: two `_dispatch` bodies called the guarded entry point, so their `_static` siblings were dead ends. `kanama_ios_godot_ptrcall_ret_raycast_dict_dispatch` (:4626) and `..._ret_object_handles_dispatch` (:10222) called kanama_ios_godot_ptrcall instead of kanama_ios_godot_ptrcall_dispatch. A `_dispatch` body is the UNGUARDED half of a split -- it is reached with a zero instance by construction -- so the callee's `instance == 0` guard returned before the result cell was written and a static routed through the raycast-dictionary or object-handle-list shapes still no-opped, one frame below the Kotlin dispatcher where nothing could see it. Both now call the unguarded body, as kanama_ios_ptrcall_encode_container (~:4330) already did. :10140 is inside kanama_ios_godot_ptrcall_ret_object_array, a guarded entry with no sibling that nothing calls, and is left alone. 2. The gate now reads the shim's own `_dispatch` bodies. The Kotlin rule was only half the property: for every `*_dispatch` body it fails (`shim-raw-call`) if the body names a guarded exported entry point instead of that entry's `_dispatch` body. 24 bodies scanned; zero parsed is itself a parse-error, because a half of the gate that reads nothing is not a gate. 3. Scope follows the SIGNATURE, not the name prefix. C_DEF_RE, C_DECL_RE and the call pattern required `kanama_ios_godot_`, which hid kanama_ios_classdb_instantiate_owned (:7898, guarded, takes method_bind + instance, called from ObjectCalls.kt:3774) for both previous follow-ups. Widened to `kanama_ios_[a-z0-9_]+`. The widened regex brings exactly two new exported symbols into scope in the shim -- kanama_ios_classdb_instantiate_owned and the _static sibling added here -- and no new call in ObjectCalls.kt beyond those two. classdb_instantiate_owned now has the same `_dispatch` / guarded entry / `_static` split, a declaration in ios/include/kanama_ios.h, and a classdbInstantiateOwnedDispatch in ObjectCalls.kt. Its one shared-tree caller (ClassDB.kt:93) passes the singleton, so this was latent, not live -- which is the point: the property now holds by construction. 4. KT_FUN_RE matches generic members. It could not match `internal inline fun <T> retTypedObjectList(` (ObjectCalls.kt:2997) -- nor ANY `fun <T>` member -- so every call inside a generic member was attributed to the member above it, and a raw guarded call there read as a call from whatever `*Dispatch` happened to precede it. An optional `<...>` after `fun` closes it. 5. C_DECL_RE matches both header forms. It required the opening `(` to end the line; 57 of kanama_ios.h's declarations put the parameter list on that line. A `_static` sibling written on one line would have been reported `missing-sibling` -- a false positive, which is how gates get ignored. 6. "Guarded" now means an early return: an `if (...)` whose condition tests `instance == 0` / `object == 0` and whose block returns, not a zero comparison anywhere in the body. The guarded set is unchanged by the tightening -- the same 25, plus classdb_instantiate_owned from item 3 = 26, printed on PASS (24 of them called from ObjectCalls.kt, each through its dispatcher). The gate also learned `--shim` / `--header` / `--objectcalls` (and the matching env vars) so its negative tests run against scratch copies without touching the tree, and it strips C comments and string literals before parsing, so a name in a comment is never read as a call. Red runs (task 118: every gate change ships with one), all verbatim in the handoff: the :4626 defect re-introduced on a scratch shim yields `shim-raw-call ...:4626`; a generic member inserted between a `*Dispatch` and a raw guarded call yields `raw-call ...:260 ... from scratchGenericMember()` where the pre-fix regex exited 0; and a one-lined `_static` declaration in a scratch header still PASSes where the pre-fix C_DECL_RE reported `missing-sibling`. 7. Docs and CHANGELOG. docs/exporting/ios.md no longer claims every instance-taking entry point has a `_static` sibling -- it is qualified to the ones an `ObjectCalls` helper can reach with the static marker, and names the three kinds that keep their single guarded form (ptrcall_string_arg, ptrcall_ret_object_array, the object-handle entries). The CHANGELOG `### Fixed` entry regains the qualifier from a30baf0's message ("that an `ObjectCalls` helper calls with an instance"), corrects 22 to 23 + classdb_instantiate_owned, moves the "72 of 72" claim after the dead-end fix that makes it true, and describes the gate's shim-side scan. docs/contributing/wrapper-maintenance.md gains the `_dispatch`-body rule. docs/reference/generated/gates.md is regenerated (the gate's docstring first sentence is what that page quotes). Gates: check_ios_static_dispatch, check_wrapper_generator, check_objectcalls_parity, check_pt_tag_tables, check_doc_claims, generate_gates_index --check, clang -fsyntax-only (same 7 pre-existing warnings as HEAD), ktfmtFormat (no reformat), compileKotlinIosArm64 and createIosDeviceDebugXcframework. Assisted by AI Co-Authored-By: Claude <noreply@anthropic.com>
…dispatch On device (Match3, iPhone, 0fca5ab) the PTRCALL matrix printed 70/70 and the Kotlin OBJECTCALLS self-test then SIGSEGV'd inside RenderingServer::_mesh_create_from_surfaces with a null `this`. The row is task 100 parcel 10's Array[Dictionary] argument probe. Its comment claimed "the RenderingServer exists before extensions initialise". It does not: Godot 4.7.2 main/main.cpp runs initialize_extensions(INITIALIZATION_LEVEL_SCENE) long before register_server_singletons() adds the RenderingServer singleton (servers/register_server_types.cpp). So getSingleton("RenderingServer") returned 0, the old C null-instance guard turned the call into a no-op returning RID 0, and the row asserted exactly RID(0L) -- it had passed for the wrong reason since the day it was written. D19 routes a zero instance to the static entry point, so the call finally reached Godot, with no instance. The dispatch did its job; the row was wrong. Fix the row. - requireSingleton(name) in the self-test resolves the singleton, records check("singleton-present(<name>)") and hands back the segment. All 13 getSingleton sites (Input, Time, OS, ClassDB x2, Geometry3D x2, ProjectSettings, InputMap, TranslationServer, RenderingServer, Engine, NativeMenu) go through it, and every dependent call in a row is now skipped when the singleton is absent -- recorded as a FAILED check with a reason, never silently passed, never executed with a zero instance. The redundant "input-singleton" row is subsumed by singleton-present(Input). - The RenderingServer row moves to a one-shot first-frame phase: kanamaIosRuntimeObjectCallsSelfTestFrame (@cname("kanama_ios_runtime_objectcalls_selftest_frame")), same check machinery, printing "[kanama][ios][kn] OBJECTCALLS SELFTEST (frame 1): N passed, M failed". kanama_ios_frame calls it exactly once, under KANAMA_IOS_DEBUG_VARIANT_CHECKS, when g_main_loop_callback_frame_count first reaches 1, before kanama_ios_runtime_frame(); the extern sits beside the scene-init one. The assertion is unchanged -- the engine rejects an empty surface list with ERR_FAIL_COND_V and returns an invalid RID -- but it now runs against a real instance. - Singleton lookups fail loudly on both platforms. iOS ObjectCalls.getSingleton prints [kanama][ios][kn] ERROR: getSingleton("<name>") returned null -- not registered at this initialization level (Godot's own error print for this does not reach the device console capture); desktop does the equivalent on System.err with the [kanama:kt] prefix the runtime already uses. Both return the null segment unchanged -- no throw, the return shape is public behaviour (task 124 decides that). - Audit of every other handle used as an instance in the self-test. Every constructObject-backed row asserts a round-trip value a zero instance cannot produce, so the row's own check is its guard -- except eight whose assertion is a DEFAULT (empty list, empty string, zero Rect2i, "no signal fired", "all components finite"), i.e. exactly the RenderingServer shape. Those now go through a matching requireObject(class) helper and skip on failure: TileMap.get_used_rect, AnimationPlayer.animation_get_next, GridMap.get_used_cells(+_in_octant), Camera3D.get_camera_projection, Camera3D.get_frustum, and the two lambda-Callable free-ordering rows (lamFreeReceiver, lamOrderEmitter/lamOrderReceiver). Object-returning helper results used as instances (imgHandle, Resource.duplicate, InputEventKey.create, the shared-static Image/RegEx returns, ctrl) already carry a non-null check or a short-circuiting assertion. Totals: OBJECTCALLS SELFTEST 218 -> 236 at scene init (-1 moved row, -1 redundant input-singleton, +12 singleton-present, +8 object-constructed) and 2 on frame 1. CHANGELOG ### Fixed records the false pass and both mitigations; docs/contributing/backends/ios.md gains a Rules bullet naming the two phases and their summary lines; docs/contributing/wrapper-maintenance.md's null-instance section says a failed singleton lookup now reaches Godot on iOS exactly as on desktop, hence the log line. Gates: check_ios_static_dispatch, check_wrapper_generator, check_objectcalls_parity, check_pt_tag_tables, check_doc_claims, generate_gates_index --check, clang -fsyntax-only (same 7 pre-existing warnings as HEAD), ktfmtFormat (no reformat), compileKotlinJvm, compileKotlinIosArm64 and createIosDeviceDebugXcframework (both selftest symbols exported). Assisted by AI Co-Authored-By: Claude <noreply@anthropic.com>
…e they broke The 4e9805d phone run (Match3, iPhone 12, GL Compatibility) did not crash and the runner passed, but the now-honest self-test failed four rows at scene init and one on frame 1. Three of the five could never have failed for the right reason. THE RULE, now in the self-test header and docs/contributing/backends/ios.md: a probe must assert a value the NO-OP PATH CANNOT PRODUCE. A static routed with a zero instance used to hit the C guard and return null / 0 / false / "" / an empty list / a zeroed struct; a probe whose EXPECTED value is any of those cannot tell the working call from the call that never happened. NativeMenu (2 rows) -- the same false pass as RenderingServer, MOVED to frame 1. Engine gets its "NativeMenu" entry from register_server_singletons() (servers/register_server_types.cpp:400), run at main/main.cpp:3864, long after initialize_extensions(INITIALIZATION_LEVEL_SCENE) at main/main.cpp:3793 -- so the scene-init lookup returned 0 and the guard answered with the null Callable the row asserted. The row moves rather than goes because the NativeMenu OBJECT exists far earlier on iOS: DisplayServerAppleEmbedded, which platform/ios inherits through drivers/apple_embedded, constructs one at drivers/apple_embedded/display_server_apple_embedded.mm:65, NativeMenu's constructor sets its own singleton (servers/display/native_menu.h:154), and DisplayServer::create runs at main/main.cpp:3368. ShaderIncludeDB (2 rows) -- DELETED; they were indistinguishable on this renderer. Godot registers the built-in include files only from the RenderingDevice renderer (servers/rendering/renderer_rd/renderer_scene_render_rd.cpp:1796-1798). Match3 runs GL Compatibility, so the honest answer is the empty list and the empty source -- which is exactly what the old no-op produced. Their two C entry points keep their coverage through replacements that assert a value a no-op cannot fake: * GLTFDocument.getSupportedGltfExtensions() -- same shared-tree ptrcallNoArgsRetPackedStringList helper, same NULL_SEGMENT static marker -- must be non-empty AND contain "KHR_lights_punctual". That set is hard-coded in GLTFDocument::get_supported_gltf_extensions_hashset (modules/gltf/gltf_document.cpp:6968) and bound by initialize_gltf_module at MODULE_INITIALIZATION_LEVEL_SCENE, which main/main.cpp runs immediately BEFORE extension SCENE init (main/main.cpp:3792-3793, 834-835 on the other setup path). It does not depend on the renderer. * ObjectCalls.ptrcallWithStringArgRetString(getSha256Bind, NULL_SEGMENT, "res://project.binary") must return 64 lowercase hex characters. Helper-level on purpose: FileAccess is per-platform on iOS and hosts no shared-tree static of this shape, but the route (objectCallDispatch -> kanama_ios_godot_object_call_static) is identical. Guarded by a new iOS FileAccess.fileExists -- the desktop member's exact shape, static bind through NULL_SEGMENT -- and a missing res://project.binary is a recorded FAILURE, not a skip. RenderingServer (frame 1) -- the EXPECTATION was wrong and had never been checked against 4.7.2. Follow-up 4 kept RID(0L) on the claim that the engine rejects an empty surface list with ERR_FAIL_COND_V. It does not: RenderingServer::_mesh_create_from_surfaces (servers/rendering/rendering_server.cpp:1996-2002) has no guard at all, and mesh_create_from_surfaces (servers/rendering/rendering_server_default.h:363) opens with mesh_allocate() -- mesh_owner.allocate_rid() in the GL Compatibility mesh storage (drivers/gles3/storage/mesh_storage.cpp:65-67) -- so an empty list yields a VALID, EMPTY mesh. An invalid RID was also the no-op answer, so the row could not tell the fix from the defect either way. It now asserts a valid RID and frees it with RenderingServer.free_rid, which also gives the frame-1 phase its non-default assertion. Camera3D.get_camera_projection -- a fourth row found by the item-4 audit and strengthened. It asserted isFinite() on the four diagonal cells, which 0.0f satisfies, so the zeroed 64-byte return buffer passed it. Its stated reason ("treeless camera projection values aren't deterministic") was also unverified and wrong: Camera3D::get_camera_projection opens with ERR_FAIL_COND_V_MSG(!is_inside_tree(), Projection(), ...) (scene/3d/camera_3d.cpp:300-303) and Projection is `= default` over member initialisers spelling the IDENTITY matrix (core/math/projection.h:55-60). The row now asserts the identity. The rest of the audit: every other default-valued assertion is admissible -- it is an INSTANCE row whose instance requireSingleton / requireObject has already proven non-zero (so the static route is not taken and the default is a real answer), or it sits in a row that also asserts a non-default value through the same helper. One is called out in place rather than fixed: plane-array-ret(get_frustum finite) is vacuously true because Camera3D::get_frustum returns an empty list outside the world tree (scene/3d/camera_3d.cpp:792-798) and a populated frustum needs a viewport that does not exist at scene-level extension init; its comment now says so and names the non-default row that does carry the Plane record decode. Expected device totals: OBJECTCALLS SELFTEST 236 passed, 0 failed (unchanged -- four rows out, four in) and OBJECTCALLS SELFTEST (frame 1) 4 passed, 0 failed (was 2). Assisted by AI Co-Authored-By: Claude <noreply@anthropic.com>
Independent review of 4e9805d + 4a7146a found no blocker. Applied: - CHANGELOG: the bold "236 / 2 on frame 1" totals in the follow-up-4 entry read as current; they are now marked as superseded by 236 / 4 in the same sentence. camera_3d.cpp citation range corrected to 299-302 (the ERR_FAIL_COND_V_MSG is line 300). - Self-test: `plane-array-ret(get_frustum finite)` was vacuously true (`all {}` on an empty list). It now asserts the documented off-world answer, an empty list (scene/3d/camera_3d.cpp:792-798), which is falsifiable. Row count unchanged (236 / 4). - getSingleton null log line (both platforms): says that the line is expected only for editor/web-only singletons on this platform, so a legitimately absent JavaScriptBridge / EditorInterface does not read as a bug. Assisted by AI Co-Authored-By: Claude <noreply@anthropic.com>
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.
What & why
Task 117 P2′ (kanama-tasks
117-hand-shaped-wrappers-expect-actual.md→ D18): the four classes that were generated on desktop buthand-written on iOS are generated once —
Image(iOS gains the 21 members its hand copy omitted:blit*/blend*/fillRect/getRegion, theload*FromBuffer/save*ToBufferfamily,setData,computeImageMetrics— the iOS renderer refused none of them),PlaneMesh,StaticBody3D(iOS gets the real: PhysicsBody3D : CollisionObject3Dchain — the task's D3 item — by construction),and the tweener family (
Tweener,PropertyTweener,CallbackTweener,MethodTweener; the iOS fluent glue inIosGodotApi.ktandits three
IosGodot.*declarations are deleted; iOS gainsMethodTweener).Tweenstays hand/collision; its iOStweenMethodnowreturns
MethodTweener?.HAND_SHAPEDis down to the three roots (GodotObject, RefCounted, GodotCallable); allowlist 103 → 62;shared tree 1003 → 1010; expect object 1415 → 1431 (16 desktop
actualmarkers).Desktop source change (documented): the generated
PropertyTweener/CallbackTweenerfluent setters return the nullable selftype like every generated Object return (the hand files returned the non-null self through a private
wrapOrThis); two chained calls inMatch3's
Tile.ktuse?.in the paired kanama-demos PR — the eleven-demo canary fails only on those two lines. iOS-side:PlaneMesh.fromResource(Resource)non-null (0 callers),Image.getData()without the size hint. Orphaned C entries (unreferencedfrom Kotlin now, left for a later cleanup):
kanama_ios_godot_property_tweener_from_color,…tweener_set_trans,…tweener_set_easeand their static binds.
Worker parcel (Opus) with a diff-first checkpoint on the tweener family (the nullability finding was reported before code; ruled: keep
the generator's shape, pair a demos fix); coordinator re-ran drift, parity, ObjectCalls-parity and PT-tag gates on the head and read
the generator, gate-script, iOS glue and seam diffs.
Follow-up in this PR: iOS static dispatch (D19) — found by the independent review
The review's blocker:
Image.createFromDatamoved from the iOS hand copy (ptrcallStatic…) into the shared file, where thegenerator renders every Godot static as
ObjectCalls.<helper>(bind, NULL_SEGMENT, …). Desktop passes the null straight toobject_method_bind_ptrcall; the iOS C entry points early-return on a null instance (the guard commit30c949a1kept when it addedkanama_ios_godot_ptrcall_static). Every shared-tree static — 72 sites in 36 classes (Image, JSON, Thread, RegEx, Resource, theGLTF factories, …) — had been a silent no-op on iOS since 7a43afc (2026-09-15).* Two follow-up commits fix it at the iOS
ObjectCallslayer, keep every C guard, and add a gate so it cannot come back:002e2c65:ptrcallDispatchin the iOSObjectCallshand region routes a zero instance tokanama_ios_godot_ptrcall_static; thegenerator template emits it, so all 1172 plain ptrcall sites (hand + generated) go through it. 59 of the 72 sites fixed.
a30baf0e: the 22 specialised guarded C entry points thatObjectCallscalls (…_no_args_ret_*,…_ret_variant_scalar,…_ret_utf8,…_ret_array_blob,…_ret_callable,kanama_ios_godot_object_call, …) get the same_dispatchbody / guardedinstance entry /
_staticsibling split askanama_ios_godot_ptrcall, declared inios/include/kanama_ios.h; one private*Dispatchper entry inObjectCalls.kt. The remaining 13 sites fixed → 72/72. New gatescripts/check_ios_static_dispatch.py(local_ci stage, gates index): parses the shim for every exported function that early-returns on a null instance and takes a
method_bind, and fails ifObjectCalls.ktcalls one outside a*Dispatchthat also calls its_staticsibling — red runrecorded in the script's docstring and re-executed by the coordinator (FAIL at the raw call, restored → PASS).
OBJECTCALLS SELFTEST205 → 218: 13 probes through the wrapper API (Image.createFromData/create,Thread.isMainThread,RegEx.createFromString, JSON round trip,ShaderIncludeDB.listBuiltInIncludeFiles/getBuiltInIncludeFile,Resource.generateSceneUniqueId,MultiplayerAPI.getDefaultInterface). Desktop: no code change.Text fixes from the review: the CHANGELOG claim that Image's factories were "unchanged on both platforms"; the
PropertyTweener.frommechanism sentence inwrapper-maintenance.md(a return-type special case in_candidate_for_impl, notMETHOD_CALL_SHAPE_OVERRIDES); the null-instance convention documented on both backends. Filed in kanama-tasks as 119 items 35–36and tasks 124–126 (no silent paths; API exercise derived from the API description; freeze the generator conventions at 0.5.0).
0fca5abe(from the independent review of the two commits above): two of the new_dispatchbodies (…_ret_raycast_dict,…_ret_object_handles) still called the guardedkanama_ios_godot_ptrcall, so their_staticsiblings were dead ends (latent: noshared static uses those shapes) — fixed, and the gate now scans the shim's own
_dispatchbodies (red run), scopes guarded entriesby signature instead of the name prefix (which brought
kanama_ios_classdb_instantiate_ownedinto scope: it got the same split anddispatcher), matches generic Kotlin members and one-line header declarations (red runs), and defines "guarded" as an early return
(guarded set unchanged: 26, 24 called). Docs/CHANGELOG qualifiers restored. Reviewer's recount of the earlier claims: 72/36 sites, 1172
plain sites, 205 → 218 checks, C bodies are moves (guard split only), desktop diff empty, generator fixture matches.
4e9805de(from the first phone smoke on 0fca5ab): Match3 crashed inside the Kotlin OBJECTCALLS SELFTEST — theRenderingServer.mesh_create_from_surfacesrow looked the singleton up at scene init, where Godot has not registered it yet(
main.cpp:initialize_extensions(SCENE)at 835,register_server_singletons()at 3864); the lookup returned 0, the old guardmade the call a no-op returning RID 0, and the row asserted exactly RID 0 — a false pass since task 100. With D19 the call reached
Godot with a null
this. Fix:requireSingleton/requireObjecthelpers (every singleton row and the eight default-assertingobject rows now assert presence and skip on absence instead of calling with zero), the RenderingServer row moved to a one-shot
first-frame phase (
OBJECTCALLS SELFTEST (frame 1): N passed, M failed, hooked inkanama_ios_frame), andgetSingletonlogs anerror line on both platforms when it returns null. Expected on device: level 2
236 passed, 0 failed, frame 12 passed, 0 failed(superseded below).4a7146a6(from the second phone smoke, on 4e9805d: no crash, runner PASS, but the now-honest self-test showed 4 FAILs): theNativeMenu rows were another false pass (same late registration as RenderingServer → moved to frame 1;
NativeMenu::get_singleton()is non-null on iOS via the apple_embedded DisplayServer); the two ShaderIncludeDB probes could not distinguish the fix from the no-op
on GL Compatibility (built-in includes are RD-only) → replaced by
GLTFDocument.getSupportedGltfExtensions()containingKHR_lights_punctualand a helper-levelFileAccess.get_sha256("res://project.binary")(64 lowercase hex chars) through theidentical null-instance route; the RenderingServer row's
RID(0)expectation was wrong (4.7.2 has no empty-listERR_FAIL; a validRID is allocated) → asserts a valid RID and frees it. Rule written into the self-test header and
backends/ios.md: a probe mustassert a value the no-op path cannot produce; every default-valued row audited (table in the commit). Expected on device: level 2
236 passed, 0 failed, frame 14 passed, 0 failed. iOSFileAccess.fileExistsadded (desktop parity).09506cf7: independent review of the two self-test commits (no blocker; every Godot fact re-verified at the cited lines; countsreconciled 218 → 236 / 4). Applied its findings: the follow-up-4 CHANGELOG totals marked as superseded, the
get_frustumrow nowasserts the documented off-world empty list instead of a vacuous
all {}, the null-singleton log line names the legitimateeditor/web-only case, one citation range corrected. Note recorded:
check_wrapper_parity.pycovers only the three roots, so thenew iOS
FileAccess.fileExistsis hand-verified against desktop, not gate-covered.ABI / ownership call-out:
ios/bootstrap/kanama_ios_shim.cgains 22_dispatch+ 22_staticfunctions (body moves, no guardremoved, no signature changed);
ObjectCalls.ktiOS gains 23 dispatchers. Both CIiosandlocal-ci (linux)jobs passed on a30baf0; re-run on 0fca5ab.Checklist
scripts/local_ci.sh <godot>to green — the full gate, not just the in-editor smoke (seeAGENTS.md→ Validation). — PASS on the head 850c543 (machine B, 2026-09-21, 58 stages;runtime_smokePASS incl. the Tweener ownership rows).main(branch protection also enforces this at merge time). — cut from 34f9c63, main unchanged since.src/jvmMain/kotlin/binding/,processor/, retain/release, marshalling), I've called it out below so it gets a careful review. —binding/runtime/ObjectCalls.kt: 16 existing helpers gainactualon desktop, none on iOS beyond the generated region; no body change. The tweener self-return collapse (releaseHandleon a same-address return) is the shared tree's standard form on both platforms;runtime_smokechecksTweener ownership … method_chained_same=true property_from_delta=0.AGENTS.md→ synchronized invariants). — CHANGELOG block per group;wrapper-maintenance.md,demo-porting-rules.md; generated pages incl. the staleios-backend-handwritten.md.Testing
Drift PASS (
shared=1010, hand desktop=23 → the roots), parity PASS (3 classes, 62), ObjectCalls parity PASS (1431), PT-tag PASS;compileKotlinJvm/compileKotlinIosArm64/:processor:test/:jvmTest/ktfmtCheckPASS per group;example_projectandtemplatescompile unchanged; demos canary 10/11 PASS, the failure being exactly the twoTile.ktchains. Falsifications:Imagebackin
HAND_SHAPED→ parity FAIL;Tweenerback inPER_PLATFORM_WRAPPERS→ drift gate FAIL.scripts/local_ci.shPASS on 850c543; PASS on a30baf0; re-run on 0fca5ab in progress. Pending before merge: independent review of the two follow-up commits (one Opus agent); iPhone 12 smokes (expected self-test lines:236 passed, 0 failedat level 2 and(frame 1): 4 passed, 0 failed) (Match3 +third-person through the demos runner, from the paired demos branch — third-person exercises StaticBody3D and the tween paths).
AI assistance
Written with Claude Code (Opus worker, Fable 5.1 coordinator), reviewed by the maintainer.
🤖 Generated with Claude Code