🐕

Cocos2dx-js v3でのSpiderMonkeyのVMの再起動のコツ

に公開

感想

正しいスレッドを使うこととメモリ解放は人類には早すぎる。

これなんですか

昔に作ったCocos2dx-jsアプリをメンテナンスしてる人で、Activityの再起動問題(実際はGLTreadの変更)に対応しようとしている少数の人の向け。きっと10年ぐらい前に作ったアプリをメンテナンスしてる人向けで、もう、とっくにフレームワークのメンテナンスも止まっている。

アプリが動いてると時代の流れに対応が必要になることもあって、最近、AndroidでAPI Level 35あたりからアクティビティの再起動への対応を促すため警告などでて対応への圧力が強まっている。36ではまだ回避できるのですが、Level 37あたりからは再起動対応をしないとに落ちる場合もありそう。(ゲームカテゴリーなら許されるフラグはできそう。)

アクティビティの再起動が起こる時などにGLSurefaceViewが再作成される。するとGLTreadが変わり、そうするとJavascript EngineであるSpiderMonkeyの再起動が必要になる。しないとちょっと動いてアプリが落ちる。

SpiderMonkeyの再起動に対応に成功したという記事もなく多くの人がうまくいかなかったと思うのですが、自分自身も長年、「これを直すは無理」と思っていました。3日、Claude CodeとChatGPTを問い詰めて直したところ、3.17.12のデフォルトのプロジェクトで対応できたので簡単な覚書です。

最大のはまりどころ

SpiderMonkeyのVMを再起動するにはどうすればいいのだろうか。
ネットにはよく、こうすればいいと書いてある。

auto engine = ScriptingCore::getInstance();
engine->cleanup();
engine->start();
engine->runScript("main.js");

これ自体は間違いでないのだが3つ問題がある。

スレッドの問題

メモリの取得と解放は同じスレッドで行う必要があります。

そのためGLTreadが変わる時に上記を一箇所で行うと落ちる。
①cleanup()は、元のGLThreadで行う必要がある。
取得したスレッドで解放が必要だから。
②start()は、新しいGLThreadで行う必要がある。
これからこのスレッドで管理するため。

よって、cleanup()はGLTreadの破棄前、Start()は再起動後。

cleanupの問題

GLTreadで作ったものをどの範囲で解放するか。

vmのStartの問題

どう起動するのか。

スレッドの問題について

cleanup()のタイミング

結論としては、surfaceDestroyed()にてcleanup()処理を行う。
実際、onPause()とsurfaceDestroyed()の後に新アクティビティが構築される場合がある。
(片方だけの時、両方呼ばれる時がある。)
onPause()はglSurfaceView.setPreserveEGLContextOnPause(true)としておいて、コンテキストを保持するようにすでに設定されている。(それでもpauseからアクティビティ再起動と場合があるようだが少数の機種なので無視する。)
また、前提としてできるだけアクティビティーの再起動が起きないように配慮しておく。
具体的には、minifestのandroid:configChangesでorientationやscreenSizeなど、できるだけ多く指定しておく。
※検証ではconfigChangesでorientationやscreenSizeを無効にしてアクティビティが変更されるようにしてonPauseの後に再起動が行われるようにしてテストを行った。

start()のタイミング

onSurfaceCreatedでGLThreadが変わったらstartすれば良い。

cleanupの問題

何を対象にメモリ解放するのか、JavaScriptで使ったものを全部解放すればいい。

細かい部分はバージョンにより異なるかも。
cocos2dx-jsの3.17.2では以下のcleanup関数とした。
なお、Scene graphのクリーンナップは各自実装ください。

void ScriptingCore::safeCleanup()
{
    cleanAllScript();
    removeAllRoots(_cx);

    if (_cx) {
        JS_DestroyContext(_cx);
        _cx = nullptr;
    }
    
    if (_rt) {
        JS_DestroyRuntime(_rt);
        _rt = nullptr;
    }

    _js_global_type_map.clear();

    // Clean scene graph
    auto director = Director::getInstance();
    if (director->getRunningScene()) {
        director->getActionManager()->removeAllActions();
        director->getScheduler()->unscheduleAll();
        director->popToRootScene();
    }
    
    director->getTextureCache()->removeAllTextures();
    FileUtils::getInstance()->purgeCachedEntries();
    _needCleanup = false;
}

vmのStartの問題

再起動のために起動処理を参考に作成。ソースだけ。

    JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRestartSpiderMonkeyVM(JNIEnv* env, jclass clazz) {
        LOGI("=== SpiderMonkey VM Restart Started ===");
        
        auto sc = ScriptingCore::getInstance();
        if (sc) {
            sc->start();
            sc->runScript("script/jsb_boot.js");
            sc->runScript("main.js");
            
            JSContext* cx = sc->getGlobalContext();
            if (cx) {
                ScriptingCore::forceGC(cx, 0, nullptr);
            }
            
            LOGI("=== SpiderMonkey VM Restart Completed ===");
        } else {
            LOGI("ScriptingCore instance not found");
        }
    }

最後に

後は、VM上のデーターをC++の領域かストレージに退避してvm再起動して戻してUIの状態を構築することが必要です。

と思っていましがまだ入り口にも到達していませんでした。

上のプログラムは間違えです。動きません。

サンプルプロジェクトでテストした時はうまく行ったと思ったのですが、実プロジェクトに適用するとうまく動きませんでした。

今見ると着眼点は良いのですが、色々、
①解放すべきものが足りてない。
②古いGLTreadの情報が残ってる。
などなど。

うまく動かない部分の真の原因をもとめてAIを使ってエラーログを解析しプログラムを書いてはテストを行い幾つもの障害を乗り越えて時には変な道に迷い込んで最終的には動くようになりました。

1週間ほどAIでなぜ動かないのか、仮説検証を数多く行い、コードを生成しては検証してうまくいかなければ破棄することを繰り返しました。とても人間業ではありません。さすがAI。

解説すると終わりそうにないので、修正点のドキュメントを以下、置いておきます。
AIに読ませて修正を再現できるように、コード差分がわかるように生成したドキュメントとなります。

Cocos2d-x 3.17.2 SpiderMonkey VM Restart 実装ガイド

📋 完全実装チェックリスト

  • Priority A: ActionManager復旧、GLThread検出、Thread-safe Context管理、GLProgramStateCache再初期化
  • Priority B: LayerColor対応、AppDelegate統合管理、Weak Reference設計
  • Priority C: JavaScript包括エラーハンドリング、再帰防止

Table of Contents

  1. 実装優先度マトリックス
  2. Priority A: 必須実装(クラッシュ回避)
  3. Priority B: 推奨実装(UX向上)
  4. Priority C: 安全性向上(運用安定)
  5. 完全実装チェックリスト
  6. 検証とトラブルシューティング

実装優先度マトリックス

🔥 Priority A: 必須実装(クラッシュ回避)

項目 効果 実装工数 影響度
ActionManager Scheduler再登録 Action実行システム復旧 1分 ⭐⭐⭐⭐⭐
GLThread変更検出システム VM restart トリガー 10分 ⭐⭐⭐⭐⭐
Thread-safe Context管理 SpiderMonkey制約対応 20分 ⭐⭐⭐⭐⭐
GLProgramStateCache再初期化 OpenGL状態同期 2分 ⭐⭐⭐⭐

Priority B: 推奨実装(UX向上)

項目 効果 実装工数 影響度
LayerColor EVENT_RENDERER_RECREATED対応 TransitionFade安定化 15分 ⭐⭐⭐⭐
AppDelegate包括管理 Surface/VM restart統合 30分 ⭐⭐⭐
Weak Reference設計 モジュール疎結合 10分 ⭐⭐⭐
※ LayerColor EVENT_RENDERER_RECREATED対応は必要ないかも?

🛡️ Priority C: 安全性向上(運用安定)

項目 効果 実装工数 影響度
Screen Size Change再帰防止 無限ループ防止 15分 ⭐⭐
包括エラーハンドリング 予期しない障害保護 45分 ⭐⭐

Priority A: 必須実装(クラッシュ回避)

1. ActionManager Scheduler再登録

問題: VM restart後、ActionManagerがSchedulerから外され、全Actionが停止

解決: AppDelegate でのVM restart時にActionManagerを再スケジューリング

注意: この問題はActionを多用するアプリで特に重要です。Actionを使用しないアプリでは影響が少ない場合があります。

1.1 AppDelegate.h 修正

ファイル: frameworks/runtime-src/Classes/AppDelegate.h

#include "cocos2d.h"

USING_NS_CC;

class AppDelegate : public cocos2d::Application
{
public:
    AppDelegate();
    virtual ~AppDelegate();
    
    virtual void initGLContextAttrs();
    virtual bool applicationDidFinishLaunching();
    virtual void applicationDidEnterBackground();
    virtual void applicationWillEnterForeground();
    
    // 🎯 VM Restart 包括管理メソッド追加
    static void handleSurfaceDestroyedCleanup();
    
private:
    
};

1.2 AppDelegate.cpp 修正

ファイル: frameworks/runtime-src/Classes/AppDelegate.cpp

必要なヘッダーをインクルード:

#include "AppDelegate.h"
#include "CCApplication.h"
#include "audio/include/SimpleAudioEngine.h"
#include "scripting/js-bindings/manual/ScriptingCore.h"
#include <thread>
#include "base/CCEventType.h"
#include "base/CCEventDispatcher.h"

VM restart処理関数を追加:

// 🎯 新規追加: VM restart処理関数
extern "C" void handleAppDelegateVMRestart() {
    auto sc = ScriptingCore::getInstance();
    if (sc) {
        sc->forceGLThreadRestart();
        sc->createGlobalContext();
        
        // 🎯 重要: ActionManager復旧
        auto director = Director::getInstance();
        if (director) {
            director->getScheduler()->scheduleUpdate(director->getActionManager(), Scheduler::PRIORITY_SYSTEM, false);
        }
        
        // JavaScript環境復旧
        if (sc->runScript("script/jsb_boot.js")) {
            sc->runScript("main.js");
        } else {
            CCLOG("AppDelegate VM restart - jsb_boot.js failed");
        }
    } else {
        CCLOG("AppDelegate VM restart - No ScriptingCore instance");
    }
}

void AppDelegate::handleSurfaceDestroyedCleanup() {
    auto sc = ScriptingCore::getInstance();
    if (sc) {
        sc->safeCleanup();
    }
}

// 🎯 JNI層から呼び出される関数
extern "C" void handleAppDelegateSurfaceDestroyed() {
    AppDelegate::handleSurfaceDestroyedCleanup();
}

2. Surface Lifecycle + ThreadID変更検出システム(重要な連携)

核心概念: Surface Destroyed時のsafeCleanup()とSurface Created時のThreadID変更検出によるVM restartの連携がシステムの心臓部

2.0 実装フロー概要

🎯 重要ポイント:

  1. Surface Destroyed → 必ずsafeCleanup()実行でJavaScript環境完全クリア
  2. Surface Created + ThreadID変更 → 必ずVM restart実行で環境再構築
  3. この2段階処理により画面回転時のクラッシュ完全回避

2.1 Surface Destroyed処理:safeCleanup()呼び出し実装

2.1.1 Java層:handleSurfaceDestroyed()

ファイル: frameworks/cocos2d-x/cocos/platform/android/java/src/org/cocos2dx/lib/Cocos2dxRenderer.java

メソッド: handleSurfaceDestroyed()に追加

public void handleSurfaceDestroyed() {
    if (!mNativeInitCompleted) {
        android.util.Log.i("Cocos2dxRenderer", "handleSurfaceDestroyed: Native not initialized, skipping");
        return;
    }
    
    String currentThreadName = Thread.currentThread().getName();
    android.util.Log.i("Cocos2dxRenderer", "handleSurfaceDestroyed: Calling native cleanup on thread: " + currentThreadName);
    
    // 🎯 Surface破棄時に必ずsafeCleanup()実行のためJNI呼び出し
    Cocos2dxRenderer.nativeOnSurfaceDestroyed();
    android.util.Log.i("Cocos2dxRenderer", "handleSurfaceDestroyed: Native cleanup completed");
}
2.1.2 JNI層:nativeOnSurfaceDestroyed()

ファイル: frameworks/cocos2d-x/cocos/platform/android/jni/Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp

メソッド: Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeOnSurfaceDestroyed()に追加

JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeOnSurfaceDestroyed() {
    cocos2d::log("nativeOnSurfaceDestroyed: Surface destroyed - checking for cleanup function");
    
    // 🎯 AppDelegate cleanup関数が利用可能な場合のみ呼び出し(Weak Reference設計)
    if (handleAppDelegateSurfaceDestroyed) {
        cocos2d::log("nativeOnSurfaceDestroyed: Calling AppDelegate cleanup");
        handleAppDelegateSurfaceDestroyed();
        cocos2d::log("nativeOnSurfaceDestroyed: AppDelegate cleanup completed");
    } else {
        cocos2d::log("nativeOnSurfaceDestroyed: No cleanup function available");
    }
}
2.1.3 AppDelegate層:handleSurfaceDestroyedCleanup()

ファイル: frameworks/runtime-src/Classes/AppDelegate.cpp

メソッド: handleSurfaceDestroyedCleanup()に追加

void AppDelegate::handleSurfaceDestroyedCleanup() {
    // 🎯 Step 1: JavaScript環境からのデータバックアップ収集
    CCLOG("AppDelegate - Starting backup data collection");
    collectBackupDataFromJS();
    
    // 🎯 Step 2: ScriptingCoreのsafeCleanup()実行(130行の完全クリーンアップ)
    auto sc = ScriptingCore::getInstance();
    if (sc) {
        sc->safeCleanup();
    }
    
    CCLOG("AppDelegate - Surface destroyed cleanup completed");
}

// 🎯 JNI層から呼び出される外部C関数
extern "C" void handleAppDelegateSurfaceDestroyed() {
    AppDelegate::handleSurfaceDestroyedCleanup();
}

2.2 Surface Created処理:ThreadID変更検出→VM restart実装

2.2.1 Java層:onSurfaceCreated()でThreadID変更検出

ファイル: frameworks/cocos2d-x/cocos/platform/android/java/src/org/cocos2dx/lib/Cocos2dxRenderer.java

追加するメンバー変数:

public class Cocos2dxRenderer implements GLSurfaceView.Renderer {
    // 🎯 静的変数で永続的なGLThread追跡(画面回転でも保持)
    private static long sLastKnownGLThreadId = -1;
    private boolean mNativeInitCompleted = false;
    
    // 既存メンバー変数...
    private int mScreenWidth;
    private int mScreenHeight;
    private long mLastTickInNanoSeconds;

メソッド: onSurfaceCreated()に追加(ThreadID変更検出→VM restart実行)

@Override
public void onSurfaceCreated(final GL10 GL10, final EGLConfig EGLConfig) {
    long currentThreadId = Thread.currentThread().getId();
    boolean glThreadChanged = false;

    // 🎯 GLThread変更検出(初回起動は除外)
    if (sLastKnownGLThreadId != -1 && sLastKnownGLThreadId != currentThreadId) {
        android.util.Log.i("VM_RESTART", "GLThread changed: " + sLastKnownGLThreadId + " -> " + currentThreadId);
        glThreadChanged = true;
    }
    
    // Cocos2d-x通常初期化
    Cocos2dxRenderer.nativeInit(this.mScreenWidth, this.mScreenHeight);
    this.mLastTickInNanoSeconds = System.nanoTime();
    mNativeInitCompleted = true;

    // 🎯 重要:GL初期化後にVM restart実行(ThreadID変更時のみ)
    if (glThreadChanged) {
        Cocos2dxRenderer.nativeRestartSpiderMonkeyVM();
    }
    sLastKnownGLThreadId = currentThreadId;
}
2.2.2 JNI層:nativeRestartSpiderMonkeyVM()

ファイル: frameworks/cocos2d-x/cocos/platform/android/jni/Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp

追加するWeak Reference宣言:

// 🎯 AppDelegate関数への弱参照(モジュール疎結合)
extern "C" __attribute__((weak)) void handleAppDelegateSurfaceDestroyed();
extern "C" __attribute__((weak)) void handleAppDelegateVMRestart();

メソッド: Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRestartSpiderMonkeyVM()に追加

JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRestartSpiderMonkeyVM() {
    cocos2d::log("nativeRestartSpiderMonkeyVM: Restarting SpiderMonkey VM - checking for restart function");
    
    // 🎯 AppDelegate VM restart関数が利用可能な場合のみ呼び出し(Weak Reference設計)
    if (handleAppDelegateVMRestart) {
        cocos2d::log("nativeRestartSpiderMonkeyVM: Calling AppDelegate VM restart");
        handleAppDelegateVMRestart();
        cocos2d::log("nativeRestartSpiderMonkeyVM: AppDelegate VM restart completed");
    } else {
        cocos2d::log("nativeRestartSpiderMonkeyVM: No VM restart function available");
    }
}
2.2.3 AppDelegate層:handleAppDelegateVMRestart()

ファイル: frameworks/runtime-src/Classes/AppDelegate.cpp

メソッド: handleAppDelegateVMRestart()に追加

// 🎯 新規追加: VM restart処理関数
extern "C" void handleAppDelegateVMRestart() {
    auto sc = ScriptingCore::getInstance();
    if (sc) {
        // Step 1: GLThread restart強制フラグ設定
        sc->forceGLThreadRestart();
        
        // Step 2: JavaScript Context再構築
        sc->createGlobalContext();
        
        // 🎯 Step 3: ActionManager復旧(重要)
        auto director = Director::getInstance();
        if (director) {
            director->getScheduler()->scheduleUpdate(director->getActionManager(), Scheduler::PRIORITY_SYSTEM, false);
            CCLOG("AppDelegate VM restart - ActionManager restored");
        }
        
        // Step 4: JavaScript環境復旧(jsb_boot.js → main.js)
        if (sc->runScript("script/jsb_boot.js")) {
            sc->runScript("main.js");
            CCLOG("AppDelegate VM restart - JavaScript environment restored");
        } else {
            CCLOG("AppDelegate VM restart - jsb_boot.js failed");
        }
    } else {
        CCLOG("AppDelegate VM restart - No ScriptingCore instance");
    }
}

2.3 実装における重要なメソッド一覧表

処理段階 ファイル メソッド 主な処理内容
Surface Destroyed Cocos2dxRenderer.java handleSurfaceDestroyed() nativeOnSurfaceDestroyed()呼び出し
Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeOnSurfaceDestroyed() handleAppDelegateSurfaceDestroyed()呼び出し
AppDelegate.cpp handleSurfaceDestroyedCleanup() collectBackupDataFromJS() + safeCleanup()実行
ScriptingCore.cpp safeCleanup() JavaScript Context完全クリーンアップ(130行処理)
Surface Created + ThreadID変更 Cocos2dxRenderer.java onSurfaceCreated() ThreadID変更検出 + nativeRestartSpiderMonkeyVM()呼び出し
Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRestartSpiderMonkeyVM() handleAppDelegateVMRestart()呼び出し
AppDelegate.cpp handleAppDelegateVMRestart() forceGLThreadRestart() + ActionManager復旧 + JavaScript再実行
ScriptingCore.cpp forceGLThreadRestart() + createGlobalContext() Thread-safe Context再構築

2.4 アプリケーション固有実装:データバックアップ・復元

⚠️ 重要: collectBackupDataFromJS() 関数はアプリケーション固有の独自実装が必要です。

実行タイミング: Surface Destroyed時(handleSurfaceDestroyedCleanup()内)、safeCleanup()実行前

実装: 必要に応じてJavaScript環境からアプリ状態を取得・保存。不要な場合は空実装でOK。


3. Thread-safe Context管理

問題: SpiderMonkey VM restartは異なるスレッド間でのJSContext操作によりJS_AbortIfWrongThreadエラーが発生

解決: Thread-safe な Context管理による安全なクリーンアップと再構築

3.1 ScriptingCore.h 修正

ファイル: frameworks/cocos2d-x/cocos/scripting/js-bindings/manual/ScriptingCore.h

必要なヘッダーをインクルード:

#include <thread>

クラス定義に追加:

class CC_JS_DLL ScriptingCore : public ScriptEngineProtocol
{
    // 既存のprivateメンバー...
private:
    std::thread::id _jsContextThreadId;       // 🎯 Context所有スレッド追跡
    bool _forceGLThreadRestart;             // 🎯 GLThread restart フラグ
    
    // 既存のpublicメンバー...
public:
    // 🎯 新規追加メソッド
    void safeCleanup();
    void cleanupExistingJSContext(bool forceGLThreadRestart);
    void forceGLThreadRestart() { _forceGLThreadRestart = true; }
    
    // 🎯 補助関数(safeCleanup内部で使用)
    void performPostVMRestartSceneCleanup();
    void resetSceneTransitionState();
    void cleanupJavaScriptEventCallbacks();
    void performPostVMRestartEventCleanup();
    
    // 既存メソッド...
};

3.2 ScriptingCore.cpp 修正

ファイル: frameworks/cocos2d-x/cocos/scripting/js-bindings/manual/ScriptingCore.h

必要なヘッダーをインクルード:

#include <thread>

クラス定義に追加:

class CC_JS_DLL ScriptingCore : public ScriptEngineProtocol
{
    // 既存のprivateメンバー...
private:
    std::thread::id _jsContextThreadId;       // 🎯 Context所有スレッド追跡
    bool _forceGLThreadRestart;             // 🎯 GLThread restart フラグ
    
    // 既存のpublicメンバー...
public:
    // 🎯 新規追加メソッド
    void safeCleanup();
    void cleanupExistingJSContext(bool forceGLThreadRestart);
    void forceGLThreadRestart() { _forceGLThreadRestart = true; }
    
    // 🎯 補助関数(safeCleanup内部で使用)
    void performPostVMRestartSceneCleanup();
    void resetSceneTransitionState();
    void cleanupJavaScriptEventCallbacks();
    void performPostVMRestartEventCleanup();
    
    // 既存メソッド...
};

3.2 ScriptingCore.cpp 修正

ファイル: frameworks/cocos2d-x/cocos/scripting/js-bindings/manual/ScriptingCore.cpp

必要なヘッダーをインクルード:

#include <thread>
#include "base/CCEventType.h"
#include "base/CCEventDispatcher.h"
#include "renderer/CCGLProgramCache.h"
3.2.1 Thread-safe Context Cleanup実装
void ScriptingCore::cleanupExistingJSContext(bool forceGLThreadRestart) {
    if (!_cx || !_rt) return;
    
    std::thread::id currentThreadId = std::this_thread::get_id();
    bool needsGLThreadRestart = forceGLThreadRestart || (_jsContextThreadId != currentThreadId);
    
    if (needsGLThreadRestart) {
        // 🎯 CRITICAL: 異なるスレッドからは JSContext を触らない
        // SpiderMonkey は JS_AbortIfWrongThread でクラッシュする
        LOGD("cleanupExistingJSContext: Abandoning old context safely");
        _cx = NULL;  // 安全に破棄
        _rt = NULL;
    } else {
        // 同一スレッド - 通常クリーンアップ
        LOGD("cleanupExistingJSContext: Normal cleanup on same thread");
        ScriptingCore::removeAllRoots(_cx);
        JS_DestroyContext(_cx);
        JS_DestroyRuntime(_rt);
        _cx = NULL;
        _rt = NULL;
    }
    
    _forceGLThreadRestart = false;
}
3.2.2 Context作成時スレッド追跡

createGlobalContext()に追加:

void ScriptingCore::createGlobalContext() {
    // 統合ヘルパーでContext cleanup
    cleanupExistingJSContext(_forceGLThreadRestart);

    // SpiderMonkey初期化
    if (!_jsInited && !JS_Init()) return;
    _jsInited = true;

    _rt = JS_NewRuntime(8L * 1024L * 1024L);
    _cx = JS_NewContext(_rt, 32 * 1024);
    
    // 🎯 Contextを作成したスレッドを記録
    _jsContextThreadId = std::this_thread::get_id();
    LOGD("JS context created on thread: %d", std::hash<std::thread::id>{}(_jsContextThreadId));
    
    // 既存初期化コード継続...
    JS_SetErrorReporter(_cx, ScriptingCore::reportError);
    // ... 他の初期化処理
}
3.2.3 完全safeCleanup実装
// 🎯 完全なsafeCleanup実装(130行以上の包括的処理)
void ScriptingCore::safeCleanup()
{
    if (!_cx) {
        LOGD("safeCleanup: No JavaScript context available, skipping cleanup");
        return;
    }
    
    LOGD("Starting safe cleanup for VM restart");
    
    // Step 1: Garbage collect while context is still valid
    try {
        if (_cx && _rt) {
            garbageCollect();
            LOGD("Garbage collection completed");
        }
    } catch (...) {
        LOGD("Garbage collection failed, continuing cleanup");
    }
    
    // Step 2: Free localStorage
    localStorageFree();
    LOGD("localStorage freed");
    
    // Step 3: Clean compiled scripts
    for (auto& s : filename_script) {
        CC_SAFE_DELETE(s.second); 
    }
    filename_script.clear();
    LOGD("Compiled scripts cleared");
    
    // Step 4: Remove JS roots and clear proxy maps
    removeAllRoots(_cx);
    LOGD("JS roots removed");
    
    // Clear native<->JS proxy maps
    _native_js_global_map.clear();
    
    auto it_js = _js_native_global_map.begin();
    while (it_js != _js_native_global_map.end()) {
        free(it_js->second);
        it_js = _js_native_global_map.erase(it_js);
    }
    _js_native_global_map.clear();
    LOGD("Native<->JS proxy maps cleared");
    
    // Step 5: Clear type map to prevent prototype reference issues
    for (auto iter = _js_global_type_map.begin(); iter != _js_global_type_map.end(); ++iter) {
        if (iter->second) {
            if (iter->second->jsclass) {
                free(iter->second->jsclass);
                iter->second->jsclass = nullptr;
            }
            free(iter->second);
        }
    }
    _js_global_type_map.clear();
    LOGD("JS global type map cleared");
    
    // Step 6: Clean autorelease pool
    PoolManager::getInstance()->getCurrentPool()->clear();
    LOGD("Autorelease pool cleared");
    
    // Step 7: Phase 1 Scene State Cleanup
    performPostVMRestartSceneCleanup();
    resetSceneTransitionState();
    
    // Step 8: Clean event dispatcher and custom events
    auto director = Director::getInstance();
    if (director) {
        // Clean all actions and scheduled callbacks
        if (director->getActionManager()) {
            director->getActionManager()->removeAllActions();
            LOGD("All actions removed from ActionManager");
        }
        if (director->getScheduler()) {
            director->getScheduler()->unscheduleAll();
            LOGD("All scheduled callbacks removed");
        }
        
        // Clean event dispatcher and custom events (Phase 2: Custom Event System Cleanup)
        auto eventDispatcher = director->getEventDispatcher();
        if (eventDispatcher) {
            LOGD("Starting comprehensive event dispatcher cleanup");
            
            // Phase 2.1: Remove all event listeners by type
            eventDispatcher->removeAllEventListeners();
            LOGD("All event listeners removed");
            
            // Phase 2.2: Clean specific event listener types
            eventDispatcher->removeEventListenersForType(EventListener::Type::KEYBOARD);
            eventDispatcher->removeEventListenersForType(EventListener::Type::TOUCH_ONE_BY_ONE);
            eventDispatcher->removeEventListenersForType(EventListener::Type::TOUCH_ALL_AT_ONCE);
            eventDispatcher->removeEventListenersForType(EventListener::Type::MOUSE);
            eventDispatcher->removeEventListenersForType(EventListener::Type::ACCELERATION);
            LOGD("Specific event listener types cleaned");
            
            // Phase 2.3: Clean common custom event names
            std::vector<std::string> customEventNames = {
                "game_state_changed", "user_input_received", "animation_completed",
                "network_response", "file_operation_complete", "ui_interaction",
                "scene_transition_start", "scene_transition_end"
            };
            
            for (const auto& eventName : customEventNames) {
                eventDispatcher->removeCustomEventListeners(eventName);
            }
            LOGD("Common custom events cleaned");
            
            // Phase 2.4: Clean application-specific custom events for pixel art maker
            std::vector<std::string> pixelArtEvents = {
                "canvas_updated", "color_selected", "tool_changed", "layer_modified",
                "export_completed", "save_state_changed", "undo_redo_state_changed",
                "brush_size_changed", "animation_frame_changed"
            };
            
            for (const auto& eventName : pixelArtEvents) {
                eventDispatcher->removeCustomEventListeners(eventName);
            }
            LOGD("Pixel art application events cleaned");
            
            // Phase 2.5: Reset event dispatcher to clean state
            eventDispatcher->setEnabled(false);
            eventDispatcher->setEnabled(true);
            LOGD("Event dispatcher reset to clean state");
            
            LOGD("Comprehensive event dispatcher cleanup completed");
        }
    }
    
    // Step 9: Phase 3 JavaScript-Native Bridge Cleanup
    cleanupJavaScriptEventCallbacks();
    
    LOGD("Safe cleanup completed successfully");
}
3.2.4 補助関数実装
// Phase 1: Scene State Cleanup for VM restart
void ScriptingCore::performPostVMRestartSceneCleanup()
{
    auto director = Director::getInstance();
    if (!director) return;
    
    LOGD("Starting post-VM restart scene cleanup");
    
    // Clear running scene and clean its resources
    auto runningScene = director->getRunningScene();
    if (runningScene) {
        LOGD("Cleaning running scene: %p", runningScene);
        // Recursively clean all child nodes
        runningScene->removeAllChildrenWithCleanup(true);
        // Clear scene-specific resources
        runningScene->cleanup();
    }
    
    // Pop all scenes from stack to clear scene stack completely
    // Use popToSceneStackLevel(0) to clear entire stack
    director->popToRootScene();
    LOGD("Cleared scene stack to root");
    
    // If there's still a running scene after popping, we need to replace it with null
    if (director->getRunningScene()) {
        // In Cocos2d-x 3.17.2, we can't directly set running scene to null
        // The safest approach is to end the director to clean everything
        LOGD("Running scene still exists after cleanup");
    }
    
    LOGD("Post-VM restart scene cleanup completed");
}

void ScriptingCore::resetSceneTransitionState()
{
    auto director = Director::getInstance();
    if (!director) return;
    
    LOGD("Starting scene transition state reset");
    
    // Reset animation properties
    director->setAnimationInterval(1.0f / 60.0f); // Default 60 FPS
    director->setDisplayStats(false);
    LOGD("Reset animation interval to 60 FPS and disabled display stats");
    
    // Clear paused status
    if (director->isPaused()) {
        director->resume();
        LOGD("Resumed director from paused state");
    }
    
    LOGD("Scene transition state reset completed");
}

// Phase 3: JavaScript-Native Bridge Cleanup for VM restart
void ScriptingCore::cleanupJavaScriptEventCallbacks()
{
    if (!_cx) {
        LOGD("cleanupJavaScriptEventCallbacks: No JavaScript context available");
        return;
    }
    
    LOGD("Starting JavaScript-Native Bridge cleanup");
    
    // Step 1: Clean up registered callbacks and JSFunction references
    try {
        // Remove all rooted objects to clear JS callback references
        removeAllRoots(_cx);
        LOGD("All rooted objects removed to clear callback references");
        
        // Step 2: Clean up proxy objects that might hold callback references
        // Clear native proxy to JS object mappings
        _native_js_global_map.clear();
        _js_native_global_map.clear();
        LOGD("Native-JS proxy mappings cleared");
        
        // Step 3: Clean up any component system callbacks
        auto director = Director::getInstance();
        if (director) {
            // Clean up any JS callbacks registered with native components
            auto scheduler = director->getScheduler();
            if (scheduler) {
                // Unschedule any JS callback functions
                scheduler->unscheduleAll();
                LOGD("All JS callback schedules cleared");
            }
            
            // Step 4: Clean up UI callback systems
            auto eventDispatcher = director->getEventDispatcher();
            if (eventDispatcher) {
                // Remove all event listeners as we can't target ScriptingCore specifically
                eventDispatcher->removeAllEventListeners();
                LOGD("All event listeners removed during JS callback cleanup");
            }
        }
        
        // Step 5: Clean up SpiderMonkey specific callback storage
        // Clear any stored JSFunction* references in callback systems
        if (_global) {
            // Clear global object references that might hold callbacks
            JS::RootedObject globalObj(_cx, _global->get());
            if (globalObj) {
                // Clear any global callback registrations
                JS::RootedValue undefined(_cx);
                undefined.setUndefined();
                
                // Clear common callback properties
                std::vector<std::string> callbackProps = {
                    "onUpdate", "onDraw", "onCreate", "onDestroy",
                    "onEnter", "onExit", "onPause", "onResume",
                    "onTouchBegan", "onTouchMoved", "onTouchEnded",
                    "onKeyPressed", "onKeyReleased"
                };
                
                for (const auto& prop : callbackProps) {
                    // Use simple property name setting for SpiderMonkey compatibility
                    JS_SetProperty(_cx, globalObj, prop.c_str(), undefined);
                }
                LOGD("Global JavaScript callback properties cleared");
            }
        }
        
        // Step 6: Clear any remaining JSFunction callback storage in native code
        // This includes any std::function<> objects that capture JS callbacks
        
        // Step 7: Clear cached compiled scripts that might hold callbacks
        for (auto& entry : filename_script) {
            if (entry.second) {
                // Clear the compiled script reference - proper SpiderMonkey cleanup
                delete entry.second;
                entry.second = nullptr;
            }
        }
        filename_script.clear();
        LOGD("Compiled script callback references cleared");
        
    } catch (const std::exception& e) {
        LOGD("Exception during JavaScript-Native Bridge cleanup: %s", e.what());
    } catch (...) {
        LOGD("Unknown exception during JavaScript-Native Bridge cleanup");
    }
    
    LOGD("JavaScript-Native Bridge cleanup completed");
}

4. GLProgramStateCache再初期化

ファイル: frameworks/cocos2d-x/cocos/renderer/CCGLProgramCache.cpp

void GLProgramCache::reloadDefaultGLPrograms()
{
    CCLOG("GLProgramCache::reloadDefaultGLPrograms() called - resetting all GL programs");
    
    // 🎯 GLProgramStateCacheを先にクリア - 既存のGLProgramStateは全て無効になる
    GLProgramStateCache::getInstance()->removeAllGLProgramState();
    
    // 既存のプログラムリセット・再読み込み(既存コード継続)
    // ... ここに既存のreloadDefaultGLPrograms()の実装を継続
}

Priority B: 推奨実装(UX向上)

5. LayerColor EVENT_RENDERER_RECREATED対応

5.1 CCLayer.cpp 修正

ファイル: frameworks/cocos2d-x/cocos/2d/CCLayer.cpp

ヘッダーインクルード:

#include "renderer/CCGLProgramCache.h"
#include "base/CCEventType.h"
#include "base/CCEventListenerCustom.h"
#include "base/CCEventCustom.h"

LayerColor自動復旧機能:

LayerColor::LayerColor()
{
    _blendFunc = BlendFunc::ALPHA_PREMULTIPLIED;
    
#if CC_ENABLE_CACHE_TEXTURE_DATA
    // 🎯 EVENT_RENDERER_RECREATED 対応追加
    CCLOG("create rendererRecreatedListener for LayerColor");
    _rendererRecreatedListener = EventListenerCustom::create(EVENT_RENDERER_RECREATED, 
        [this](EventCustom*) 
        {
            CCLOG("LayerColor: Reapplying GLProgramState after renderer recreation");
            // GLProgramState再設定
            setGLProgramState(GLProgramState::getOrCreateWithGLProgramName(GLProgram::SHADER_NAME_POSITION_COLOR_NO_MVP));
        });
    Director::getInstance()->getEventDispatcher()->addEventListenerWithFixedPriority(_rendererRecreatedListener, -1);
#endif
}

LayerColor::~LayerColor()
{
#if CC_ENABLE_CACHE_TEXTURE_DATA
    Director::getInstance()->getEventDispatcher()->removeEventListener(_rendererRecreatedListener);
#endif
}

5.2 CCLayer.h 修正

ファイル: frameworks/cocos2d-x/cocos/2d/CCLayer.h

class CC_DLL LayerColor : public Layer, public RGBAProtocol, public BlendProtocol
{
    // ... 既存メンバー ...
protected:
#if CC_ENABLE_CACHE_TEXTURE_DATA
    EventListenerCustom* _rendererRecreatedListener;  // 🎯 リスナー追加
#endif
    // ... 既存メンバー ...
};

6. Weak Reference設計(JNI層)

ファイル: frameworks/cocos2d-x/cocos/platform/android/jni/Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp

Weak Reference宣言:

// 🎯 AppDelegate関数への弱参照(モジュール疎結合)
extern "C" __attribute__((weak)) void handleAppDelegateSurfaceDestroyed();
extern "C" __attribute__((weak)) void handleAppDelegateVMRestart();

JNI VM Restart実装:

JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRestartSpiderMonkeyVM() {
    cocos2d::log("nativeRestartSpiderMonkeyVM: Restarting SpiderMonkey VM");
    
    // 🎯 AppDelegate VM restart関数が利用可能な場合のみ呼び出し
    if (handleAppDelegateVMRestart) {
        cocos2d::log("nativeRestartSpiderMonkeyVM: Calling AppDelegate VM restart");
        handleAppDelegateVMRestart();
        cocos2d::log("nativeRestartSpiderMonkeyVM: AppDelegate VM restart completed");
    } else {
        cocos2d::log("nativeRestartSpiderMonkeyVM: No VM restart function available");
    }
}

JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeOnSurfaceDestroyed() {
    cocos2d::log("nativeOnSurfaceDestroyed: Surface destroyed");
    
    // 🎯 AppDelegate cleanup関数が利用可能な場合のみ呼び出し
    if (handleAppDelegateSurfaceDestroyed) {
        cocos2d::log("nativeOnSurfaceDestroyed: Calling AppDelegate cleanup");
        handleAppDelegateSurfaceDestroyed();
        cocos2d::log("nativeOnSurfaceDestroyed: AppDelegate cleanup completed");
    } else {
        cocos2d::log("nativeOnSurfaceDestroyed: No cleanup function available");
    }
}

Priority C: 安全性向上(運用安定)

7. 包括的エラーハンドリング

ファイル: main.js

main.js初期化の安全化:

cc.game.onStart = function(){
    cc.log("=== main.js cc.game.onStart STARTED ===");
    
    try {
        // 🎯 全初期化処理をtry-catchでラップ
        
        // 基本的なCocos2d-x設定
        if(!cc.sys.isNative && document.getElementById("cocosLoading")) {
            document.body.removeChild(document.getElementById("cocosLoading"));
        }
        
        // 画面設定
        cc.view.enableRetina(cc.sys.os === cc.sys.OS_IOS ? true : false);
        cc.view.adjustViewPort(true);
        cc.director.setProjection(cc.Director.PROJECTION_2D);
        cc.view.resizeWithBrowserSize(true);

        // ファイルパス設定
        if (cc.sys.isNative) {
            var searchPaths = jsb.fileUtils.getSearchPaths();
            searchPaths.push("res");
            jsb.fileUtils.setSearchPaths(searchPaths);
        }

        // 🎯 ここにあなたのアプリケーション固有の初期化コードを記述
        // 例:データマネージャ初期化、設定読み込み、等
        
        // Scene作成・開始
        var scene = new YourMainScene();  // あなたのメインScene
        cc.director.runScene(scene);
        
        cc.log("=== main.js cc.game.onStart COMPLETED SUCCESSFULLY ===");
        
    } catch (error) {
        // 🎯 致命的エラーの完全キャッチ
        cc.log("main.js: CRITICAL ERROR in onStart - " + error.toString());
        cc.log("main.js: Error stack: " + (error.stack || "no stack available"));
        throw error;  // 再スローして開発者に通知
    }
};

8. Screen Size Change無限再帰防止

ファイル: main.js

// 🎯 Screen Size Change無限再帰防止システム
window._isScreenSizeChanging = false;

window.setScreenSizeChangedSafely = function(handler) {
    window.onScreenSizeChanged = function(width, height) {
        if (window._isScreenSizeChanging) {
            cc.log("RECURSION PREVENTION: Screen size change already in progress, ignoring call");
            return;
        }
        
        window._isScreenSizeChanging = true;
        
        try {
            handler(width, height);
        } catch (e) {
            cc.log("ERROR in screen size change handler: " + e.toString());
        } finally {
            // 短い遅延の後にフラグをリセット
            setTimeout(function() {
                window._isScreenSizeChanging = false;
            }, 200);
        }
    };
};

Discussion