2V0-f: run the real system.py app loop in Godot
- system.py runs app.Loop() on the script fiber; each UIUpdate is one CPythonApplication::Process(). Logo -> Login, popup ESC/click and app.Exit teardown (launcher cleared first, as in 40250 Main) work. - Image commands are base-texture quads with UVs (a .sub shares its parent texture); ImageInstance/ExpandedImageInstance OnRender and SetRenderingRect ported verbatim, so scale/rotation/rendering rect are drawn. - CGraphicBase::GetColor / grp.GenerateColor ported (ThinBoard background). - port.app_loop test, python_host_test app-loop checks, MT_TEST_MODE=python_ui. - TODO: .fnt/CGraphicText font pipeline, ExpandedImage blend modes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5590714b16
commit
141206cb69
@@ -2,6 +2,7 @@
|
||||
// These modules expose names for script loading; calls record that they still need the real 2V1-2V3 implementation.
|
||||
#include "ScriptLib/StdAfx.h"
|
||||
#include <cstdio>
|
||||
#include "../EterLib/UIRenderCommands.h"
|
||||
|
||||
namespace {
|
||||
PyObject* stub_call_named(const char* name) {
|
||||
@@ -1629,8 +1630,19 @@ PyMethodDef methods_snd[] = {
|
||||
const char* ints_snd[] = {NULL};
|
||||
const char* strings_snd[] = {NULL};
|
||||
// 40250 UserInterface/PythonSystemModule.cpp: systemSetting export names.
|
||||
PyObject* stub_systemSetting_GetWidth(PyObject*, PyObject*) { return stub_call_named("systemSetting.GetWidth"); }
|
||||
PyObject* stub_systemSetting_GetHeight(PyObject*, PyObject*) { return stub_call_named("systemSetting.GetHeight"); }
|
||||
// 40250 CPythonSystem::GetWidth/GetHeight return metin2.cfg's WIDTH/HEIGHT (default 800x600), which
|
||||
// RunApp passes to wndMgr.SetScreenSize and app.Create. PORT (2V0-f): the Godot canvas is the window,
|
||||
// so report its size; the rest of systemSetting stays a stub until CPythonSystem is ported.
|
||||
PyObject* stub_systemSetting_GetWidth(PyObject*, PyObject*) {
|
||||
unsigned width = 0;
|
||||
UIRenderGetSize(&width, nullptr);
|
||||
return Py_BuildValue("i", width ? int(width) : 800);
|
||||
}
|
||||
PyObject* stub_systemSetting_GetHeight(PyObject*, PyObject*) {
|
||||
unsigned height = 0;
|
||||
UIRenderGetSize(nullptr, &height);
|
||||
return Py_BuildValue("i", height ? int(height) : 600);
|
||||
}
|
||||
PyObject* stub_systemSetting_SetInterfaceHandler(PyObject*, PyObject*) { return stub_call_named("systemSetting.SetInterfaceHandler"); }
|
||||
PyObject* stub_systemSetting_DestroyInterfaceHandler(PyObject*, PyObject*) { return stub_call_named("systemSetting.DestroyInterfaceHandler"); }
|
||||
PyObject* stub_systemSetting_ReserveResource(PyObject*, PyObject*) { return stub_call_named("systemSetting.ReserveResource"); }
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
#include "ScriptLib/Resource.h"
|
||||
#include "UserInterface/StdAfx.h" // initpack and the rest of the module initializer list
|
||||
#include "EterPythonLib/StdAfx.h"
|
||||
#include "EterBase/Timer.h"
|
||||
#include "PythonBoot.h"
|
||||
#include "PythonHost.h"
|
||||
#include "../EterLib/UIRenderCommands.h"
|
||||
#include "../EterBase/LogBox.h"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <pthread.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -18,6 +22,8 @@ namespace
|
||||
std::unique_ptr<CPythonLauncher> g_launcher;
|
||||
std::unique_ptr<UI::CWindowManager> g_window_manager;
|
||||
std::unique_ptr<CPythonResource> g_resource;
|
||||
// 40250 CPythonApplication::m_timer; CSlotWindow's cool time and Process() read it via Instance().
|
||||
std::unique_ptr<CTimer> g_timer;
|
||||
|
||||
bool fail(std::string* error, const std::string& text)
|
||||
{
|
||||
@@ -25,6 +31,78 @@ bool fail(std::string* error, const std::string& text)
|
||||
*error = text;
|
||||
return false;
|
||||
}
|
||||
|
||||
// PORT: 40250 RunApp() blocks inside app.Loop() (CPythonApplication::Loop, PythonApplication.cpp:980)
|
||||
// and only returns to run mainStream.Destroy() when the window closes. Godot owns the frame loop here,
|
||||
// so system.py runs on its own thread and the two threads pass a baton: exactly one of them runs at
|
||||
// any time. Loop() hands the baton back after every Process(), and the host hands it over once per
|
||||
// Godot frame (AppFrame). The script keeps its original control flow — including the code after
|
||||
// app.Loop() — without ever blocking the Godot main thread for more than one Process().
|
||||
struct ScriptFiber
|
||||
{
|
||||
enum Turn { Host, Script };
|
||||
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
Turn turn = Host;
|
||||
pthread_t thread{};
|
||||
bool alive = false; // the thread exists and has not been joined
|
||||
bool finished = false; // RunMainScript's body returned
|
||||
bool looping = false; // parked inside CPythonApplication::Loop
|
||||
bool quit = false; // app.Exit / app.Abort / Stop
|
||||
bool ok = false;
|
||||
std::string error;
|
||||
std::string command_line;
|
||||
};
|
||||
ScriptFiber g_fiber;
|
||||
thread_local bool t_on_fiber = false;
|
||||
|
||||
// Caller holds the GIL. Returns with the GIL held again after the script has given the baton back.
|
||||
void hand_to_script()
|
||||
{
|
||||
PyThreadState* state = PyEval_SaveThread();
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(g_fiber.mutex);
|
||||
g_fiber.turn = ScriptFiber::Script;
|
||||
g_fiber.cv.notify_all();
|
||||
g_fiber.cv.wait(lock, [] { return g_fiber.turn == ScriptFiber::Host; });
|
||||
}
|
||||
PyEval_RestoreThread(state);
|
||||
}
|
||||
|
||||
void join_fiber()
|
||||
{
|
||||
if (!g_fiber.alive)
|
||||
return;
|
||||
PyThreadState* state = PyEval_SaveThread();
|
||||
pthread_join(g_fiber.thread, nullptr);
|
||||
PyEval_RestoreThread(state);
|
||||
g_fiber.alive = false;
|
||||
}
|
||||
|
||||
bool run_main_script_body(const char* lpCmdLine, std::string* error);
|
||||
|
||||
void* fiber_main(void*)
|
||||
{
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(g_fiber.mutex);
|
||||
g_fiber.cv.wait(lock, [] { return g_fiber.turn == ScriptFiber::Script; });
|
||||
}
|
||||
t_on_fiber = true;
|
||||
PyGILState_STATE gil = PyGILState_Ensure();
|
||||
std::string error;
|
||||
const bool ok = run_main_script_body(g_fiber.command_line.c_str(), &error);
|
||||
PyGILState_Release(gil);
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_fiber.mutex);
|
||||
g_fiber.ok = ok;
|
||||
g_fiber.error = error;
|
||||
g_fiber.finished = true;
|
||||
g_fiber.looping = false;
|
||||
g_fiber.turn = ScriptFiber::Host;
|
||||
g_fiber.cv.notify_all();
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void init_2v0_gameplay_stubs();
|
||||
@@ -63,20 +141,32 @@ void UIMouseButton(int button, bool pressed, int x, int y)
|
||||
}
|
||||
}
|
||||
|
||||
// 40250 CPythonApplication::OnKeyDown/OnKeyUp (PythonApplicationEvent.cpp:130-146): ESC first goes
|
||||
// to RunPressEscapeKey (the OnPressEscapeKey chain every dialog closes on), then to RunKeyDown.
|
||||
void UIKey(int key, bool pressed)
|
||||
{
|
||||
if (!g_window_manager) return;
|
||||
if (pressed) g_window_manager->RunKeyDown(key);
|
||||
else g_window_manager->RunKeyUp(key);
|
||||
if (pressed)
|
||||
{
|
||||
if (DIK_ESCAPE == key)
|
||||
g_window_manager->RunPressEscapeKey();
|
||||
g_window_manager->RunKeyDown(key);
|
||||
}
|
||||
else
|
||||
g_window_manager->RunKeyUp(key);
|
||||
}
|
||||
|
||||
void UIUpdate()
|
||||
{
|
||||
// While system.py is inside app.Loop(), one host frame is one CPythonApplication::Process(),
|
||||
// which updates and renders the window tree itself.
|
||||
if (IsAppLooping()) { AppFrame(); return; }
|
||||
if (g_window_manager) g_window_manager->Update();
|
||||
}
|
||||
|
||||
void UIRender()
|
||||
{
|
||||
if (IsAppLooping()) return;
|
||||
UIRenderBeginFrame();
|
||||
if (g_window_manager) g_window_manager->Render();
|
||||
}
|
||||
@@ -94,6 +184,8 @@ bool Start(const char* stdlib_path, std::string* error)
|
||||
PythonHost::Configure();
|
||||
|
||||
g_launcher = std::make_unique<CPythonLauncher>(); // Py_Initialize
|
||||
// The script fiber (RunMainScript) is a second OS thread; 2.7 creates the GIL lazily.
|
||||
PyEval_InitThreads();
|
||||
if (!g_launcher->Create())
|
||||
{
|
||||
Stop();
|
||||
@@ -106,17 +198,19 @@ bool Start(const char* stdlib_path, std::string* error)
|
||||
Stop();
|
||||
return fail(error, "InstallStdLib(" + stdlib + "): " + stdlib_error);
|
||||
}
|
||||
g_timer = std::make_unique<CTimer>();
|
||||
g_resource = std::make_unique<CPythonResource>();
|
||||
g_window_manager = std::make_unique<UI::CWindowManager>();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 40250: RunMainScript
|
||||
bool RunMainScript(const char* lpCmdLine, std::string* error)
|
||||
{
|
||||
if (!g_launcher)
|
||||
return fail(error, "PythonBoot::Start has not run");
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// Mirrors 40250 RunMainScript. Runs on the script fiber.
|
||||
bool run_main_script_body(const char* lpCmdLine, std::string* error)
|
||||
{
|
||||
CPythonLauncher& pyLauncher = *g_launcher;
|
||||
|
||||
// The module initializers follow 40250's order; missing modules arrive in later 2V steps.
|
||||
@@ -189,6 +283,81 @@ bool RunMainScript(const char* lpCmdLine, std::string* error)
|
||||
NANOEND
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
namespace PythonBoot
|
||||
{
|
||||
|
||||
bool RunMainScript(const char* lpCmdLine, std::string* error)
|
||||
{
|
||||
if (!g_launcher)
|
||||
return fail(error, "PythonBoot::Start has not run");
|
||||
if (g_fiber.alive)
|
||||
return fail(error, "RunMainScript is already running");
|
||||
|
||||
g_fiber.turn = ScriptFiber::Host;
|
||||
g_fiber.finished = g_fiber.looping = g_fiber.quit = g_fiber.ok = false;
|
||||
g_fiber.error.clear();
|
||||
g_fiber.command_line = lpCmdLine ? lpCmdLine : "";
|
||||
|
||||
// 40250's main thread has the default 1 MB Windows stack; Python frames are deep during
|
||||
// uiScriptLocale/ui imports, so give the script thread more than a secondary thread's default.
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
pthread_attr_setstacksize(&attr, 16u * 1024u * 1024u);
|
||||
const int rc = pthread_create(&g_fiber.thread, &attr, fiber_main, nullptr);
|
||||
pthread_attr_destroy(&attr);
|
||||
if (rc != 0)
|
||||
return fail(error, "cannot start the script thread");
|
||||
g_fiber.alive = true;
|
||||
|
||||
hand_to_script(); // until system.py parks in app.Loop() or returns
|
||||
if (g_fiber.finished)
|
||||
{
|
||||
join_fiber();
|
||||
if (!g_fiber.ok)
|
||||
return fail(error, g_fiber.error);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsAppLooping()
|
||||
{
|
||||
return g_fiber.alive && g_fiber.looping && !g_fiber.finished;
|
||||
}
|
||||
|
||||
bool AppFrame()
|
||||
{
|
||||
if (!IsAppLooping())
|
||||
return false;
|
||||
hand_to_script();
|
||||
if (g_fiber.finished)
|
||||
join_fiber();
|
||||
return IsAppLooping();
|
||||
}
|
||||
|
||||
bool AppLoopWait()
|
||||
{
|
||||
if (!t_on_fiber)
|
||||
return false;
|
||||
g_fiber.looping = true;
|
||||
if (g_fiber.quit)
|
||||
return false;
|
||||
PyThreadState* state = PyEval_SaveThread();
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(g_fiber.mutex);
|
||||
g_fiber.turn = ScriptFiber::Host;
|
||||
g_fiber.cv.notify_all();
|
||||
g_fiber.cv.wait(lock, [] { return g_fiber.turn == ScriptFiber::Script; });
|
||||
}
|
||||
PyEval_RestoreThread(state);
|
||||
return !g_fiber.quit;
|
||||
}
|
||||
|
||||
void AppRequestQuit()
|
||||
{
|
||||
g_fiber.quit = true;
|
||||
}
|
||||
|
||||
bool RunLine(const char* source, std::string* error)
|
||||
{
|
||||
@@ -205,9 +374,21 @@ void Stop()
|
||||
{
|
||||
if (!g_launcher)
|
||||
return;
|
||||
// In 40250, closing the window ends Loop(); RunApp() then runs mainStream.Destroy() and returns.
|
||||
if (g_fiber.alive)
|
||||
{
|
||||
AppRequestQuit();
|
||||
while (g_fiber.alive && !g_fiber.finished)
|
||||
hand_to_script();
|
||||
join_fiber();
|
||||
}
|
||||
g_fiber.finished = false;
|
||||
// 40250 Main(): pyLauncher.Clear() runs before app->Destroy()/delete app, so the window manager
|
||||
// (a CPythonApplication member) is still alive while Py_Finalize runs the windows' __del__.
|
||||
g_launcher->Clear();
|
||||
g_window_manager.reset();
|
||||
g_resource.reset();
|
||||
g_launcher->Clear(); // Main() calls Clear() explicitly before the launcher leaves scope
|
||||
g_timer.reset();
|
||||
g_launcher.reset();
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,18 @@ bool IsRunning();
|
||||
// __COMMAND_LINE__ and RunFile("system.py"). 2V0-e registers temporary observable gameplay modules;
|
||||
// they are replaced by the real modules in later 2V slices.
|
||||
// False leaves *error set to what the launcher reported through LogBox (platform/EterBase/LogBox.h).
|
||||
//
|
||||
// PORT: system.py runs on a script thread. RunMainScript returns once prototype.RunApp() has reached
|
||||
// app.Loop() (IsAppLooping) or the script has ended; from then on every AppFrame() runs exactly one
|
||||
// CPythonApplication::Process() while the host thread waits, so the two never run concurrently.
|
||||
bool RunMainScript(const char* lpCmdLine, std::string* error);
|
||||
bool IsAppLooping();
|
||||
bool AppFrame();
|
||||
|
||||
// Used by the platform CPythonApplication: Loop() calls AppLoopWait() before each Process() and stops
|
||||
// when it returns false; Exit()/Abort() call AppRequestQuit() (40250: PostQuitMessage).
|
||||
bool AppLoopWait();
|
||||
void AppRequestQuit();
|
||||
|
||||
// CPythonLauncher::RunLine, with the reported text handed back rather than only shown. The host's own
|
||||
// check uses it to prove the interpreter and the standard library work in-process on a device that has
|
||||
@@ -42,6 +53,7 @@ void SetUISize(int width, int height);
|
||||
void UIMouseMove(int x, int y);
|
||||
void UIMouseButton(int button, bool pressed, int x, int y);
|
||||
void UIKey(int key, bool pressed);
|
||||
// While IsAppLooping(), UIUpdate is AppFrame() and UIRender keeps the commands that Process() drew.
|
||||
void UIUpdate();
|
||||
void UIRender();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user