refactor: externalize SPX module and streamline builds - #331
Conversation
There was a problem hiding this comment.
FennoAI Review — PR #331
Threading runtime_abi and a lock-driven toolchain (scons/emsdk/android_ndk/jdk) through the SPX build workflows, incorporating engine-toolchain-sha256 into cache keys, and restructuring the macOS universal template + web minigame/miniprogram builds to reuse artifacts. The diff is well-constructed: the embedded lock/profile validators are strict (duplicate-key rejection, exact schema/ABI/field-set checks, orchestration-key denylist), producer/consumer artifact names match, cache restore-key chains preserve useful partial hits, and the macOS/web restructures remove genuinely redundant compiles. Two minor findings below (both inline); no blocking issues.
Adjacent observations (non-blocking, not in this diff):
engine-toolchain-sha256is hashed from all four tools and embedded in every platform's cache key, so bumping e.g.emsdkwill cold-invalidate the Linux/Windows/macOS/iOS/Android SCons caches that don't use emsdk. This over-invalidation is correctness-safe and the tradeoff is documented in the cache-contract comment — noted only for awareness.- The
android_ndk→ setup-ndk alias map insetup-spx-module/action.ymlcurrently has a single entry; bumpingtoolchain.android_ndkin the lock hard-fails until that map is also updated. Worth keeping the coupling discoverable.
ci: centralize SPX runtime inputs and deduplicate builds ci: build external SPX module from shared profile refactor: decouple SPX implementation from Godot
c2d4039 to
a24bff6
Compare
There was a problem hiding this comment.
Review summary
This is a large, well-structured refactor that removes the custom SPX module, the OBS/hybrid movie-recorder runtime, the standalone audio recorders/importers, and the bundled lunasvg third-party sources, replacing the SPX lifecycle hooks with a self-contained MainLoopPhaseCallbackBus. The new bus is cleanly designed (single-owner movie lease, snapshot iteration for re-entrancy safety) and comes with a thorough test suite. Most web/JS changes are reverts back toward upstream Godot.
No build-breaking dangling references to the removed subsystems were found (headers, class XML, docs, and web APIs are all consistently cleaned up). The findings below are the concrete, actionable items; inline comments cover diff-line findings.
Performance (main/main_loop_phase_callback_bus.cpp)
The bus is invoked every frame (notify_update / notify_fixed_update from main.cpp), and each dispatch:
- Per-frame heap allocation —
_snapshot_registration_ids()allocates and frees a freshLocalVector<RegistrationID>on every phase, every frame, purely to tolerate re-entrant register/unregister. Consider iteratingregistrationsdirectly in the common (no-mutation) case, or reusing aclear()-ed member buffer that retains capacity. - O(N²) dispatch with full struct copy — after snapshotting IDs, each ID is re-found via
_get_callbacks(a linear scan) which copies the entire ~72-byteCallbacksstruct by value. For each phase this is O(N²) plus a struct copy per subscriber per frame, when the IDs were just read from the same vector. In the non-re-entrant path, iteratingregistrationsdirectly avoids both the round-trip and the copy.
For the expected tiny registration count these are not functional bugs, but they add avoidable per-frame churn to a hot path; at minimum a comment documenting the intentional simplicity would help future maintainers.
CI hardening (non-blocking)
.github/actions/godot-build/action.yml(inline) — untrusted${{ github.event.number }}is interpolated directly into a shellrun:block; move it intoenv:and reference"${PR_NUMBER}"to follow the pattern used elsewhere in the same file. (Only reachable via maintainer-controlledworkflow_call/workflow_dispatch, so low practical risk, but it is the injection sink the new SPX build path now depends on.)- The new SPX web workflows (
spx_module_checks.yml,a_depoly_web.yml) reference the third-party actionmymindstorm/setup-emsdk@v14by mutable tag; pinning to a full commit SHA is recommended for supply-chain safety. .github/actions/setup-spx-modulechecks out and compiles code fromspx_repository@spx_ref(defaultgoplus/spx@dev, a mutable branch). Since triggers are maintainer-controlled this is by design, but pinningspx_refto a commit SHA is worth considering for reproducible/trusted builds.
Minor
main/main_loop_phase_callback_bus.cpp:179-182— inside themovie_requires_live_audiobranch,callbacks_after_policyis re-fetched only to guard thecontinueon a self-unregistering policy callback; the fetched struct is otherwise unused. This works but reads as incomplete next to themovie_claimpath — a short comment clarifying intent would help.scene/resources/audio_stream_wav.cpp:36-37— new file-scope constantsTRIM_DB_LIMIT/TRIM_FADE_OUT_FRAMESuse generic unprefixed names; considerstatic constexprin an anonymous namespace (and-50.0ffor the float literal).
Nothing here is blocking. The two clearest code-level items are the WAV importer error swallowing and the JS _playbackPosition default regression (inline).
| Ref<AudioStreamWAV> sample; | ||
| auto err = import_asset(sample, p_source_file, p_options, r_platform_variants, r_gen_files, r_metadata); | ||
|
|
||
| Ref<AudioStreamWAV> sample = AudioStreamWAV::load_from_file(p_source_file, options); |
There was a problem hiding this comment.
import() now discards both results and always returns OK. AudioStreamWAV::load_from_file can return a null/invalid Ref (it ERR_FAIL_COND_Vs on empty file data) and ResourceSaver::save returns an Error that is ignored. The editor will report a successful import even when the WAV failed to load or the .sample failed to write, leaving a broken/empty resource with no diagnostic (the previous code propagated err).
Suggest checking sample.is_null() and returning the ResourceSaver::save result, e.g.:
Ref<AudioStreamWAV> sample = AudioStreamWAV::load_from_file(p_source_file, options);
ERR_FAIL_COND_V(sample.is_null(), ERR_FILE_CANT_READ);
return ResourceSaver::save(sample, p_save_path + ".sample");Also note r_platform_variants, r_gen_files, and r_metadata are now unused — worth confirming that's intended.
| @@ -404,7 +423,7 @@ class SampleNode { | |||
| /** @type {number} */ | |||
| this.offset = options.offset ?? 0; | |||
| /** @type {number} */ | |||
| this._playbackPosition = this.offset; | |||
| this._playbackPosition = options.offset; | |||
There was a problem hiding this comment.
this._playbackPosition = options.offset; drops the ?? 0 default that the line just above (this.offset = options.offset ?? 0;) and the base version (this._playbackPosition = this.offset;) apply. When options.offset is undefined, _playbackPosition becomes undefined instead of 0, which then flows into getPlaybackPosition() and the this.offset + ... arithmetic elsewhere. Recommend this._playbackPosition = this.offset; to preserve the default.
| p_image->set_data(bitmap_width, bitmap_height, false, Image::FORMAT_RGBA8, result); | ||
| p_image->set_data(width, height, false, Image::FORMAT_RGBA8, buffer); | ||
|
|
||
| res = sw_canvas->clear(true); |
There was a problem hiding this comment.
res = sw_canvas->clear(true); assigns to res but the function returns OK on the next line, so the result is never checked — unlike every other ThorVG call in this function which uses ERR_FAIL_V_MSG. Either check it or drop the assignment (sw_canvas->clear(true);).
No description provided.