From 0ad52c2ceb2481190580b3a7903420a491cdea71 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sun, 19 Jul 2026 17:47:36 -0700 Subject: [PATCH] Add live JavaScript wedge to Xbox BIOS --- xbox/native-bios/App.cpp | 112 ++++++++++++++++++++++++++ xbox/native-bios/Package.appxmanifest | 7 +- xbox/native-bios/QuickJsEngine.cpp | 37 +++++++++ xbox/native-bios/pch.h | 2 + xbox/runtime/include/ac/runtime.hpp | 11 +++ 5 files changed, 167 insertions(+), 2 deletions(-) diff --git a/xbox/native-bios/App.cpp b/xbox/native-bios/App.cpp index c6df597bc..6511dcf50 100644 --- a/xbox/native-bios/App.cpp +++ b/xbox/native-bios/App.cpp @@ -6,6 +6,8 @@ using namespace Platform; using namespace Windows::ApplicationModel::Core; using namespace Windows::ApplicationModel::Activation; using namespace Windows::Gaming::Input; +using namespace Windows::Networking::Connectivity; +using namespace Windows::Storage; using namespace Windows::UI::Core; using namespace Windows::Foundation; @@ -28,6 +30,27 @@ function act(button){ function leave(){} )JS"; +static void LogTelemetry(const std::string& line) { + const auto folder = ApplicationData::Current->LocalFolder->Path; + std::wstring path(folder->Data()); + path += L"\\ac-native-bios.log"; + FILE* file = nullptr; + if (_wfopen_s(&file, path.c_str(), L"a") != 0 || !file) return; + std::fwrite(line.data(), 1, line.size(), file); + std::fwrite("\n", 1, 1, file); + std::fclose(file); +} + +static std::string Utf8(String^ value) { + if (!value || value->IsEmpty()) return {}; + const int size = WideCharToMultiByte(CP_UTF8, 0, value->Data(), value->Length(), + nullptr, 0, nullptr, nullptr); + std::string result(static_cast(size), '\0'); + WideCharToMultiByte(CP_UTF8, 0, value->Data(), value->Length(), result.data(), + size, nullptr, nullptr); + return result; +} + class HostGraphics final : public Graphics { public: std::function on_wipe; @@ -72,6 +95,7 @@ public: m_sound->on_stop = [this]() { if (m_voice) { m_voice->Stop(0); m_voice->FlushSourceBuffers(); } }; m_sound->get_rate = [this]() { return static_cast(m_sampleRate); }; m_api = std::make_unique(Api{{1920, 1080, 1}, {}, {}, {}, *m_graphics, *m_sound}); + RefreshCapabilities(true); m_engine = std::make_unique(); m_supervisor = std::make_unique(*m_engine); std::string error; @@ -80,6 +104,7 @@ public: OutputDebugStringA(("AC_NATIVE_BIOS_BOOT_ERROR " + error + "\n").c_str()); } else { OutputDebugStringA("AC_NATIVE_BIOS_READY engine=quickjs-ng piece=smoke\n"); + LogTelemetry("AC_NATIVE_BIOS_READY engine=quickjs-ng piece=smoke"); } } @@ -90,7 +115,9 @@ public: Render({0.025f, 0.02f, 0.04f, 1.0f}); while (!m_closed) { m_window->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent); + RefreshCapabilities(false); PollController(); + PollLivePiece(); if (m_supervisor && m_supervisor->active()) { try { m_supervisor->active()->sim(*m_api); @@ -239,6 +266,12 @@ private: }; for (const auto& named : names) { if (pressed & static_cast(named.bit)) { + LARGE_INTEGER now{}, frequency{}; + QueryPerformanceCounter(&now); + QueryPerformanceFrequency(&frequency); + LogTelemetry("AC_NATIVE_INPUT button=" + std::string(named.name) + + " qpc_us=" + std::to_string( + static_cast(now.QuadPart * 1000000 / frequency.QuadPart))); try { m_supervisor->active()->act(*m_api, {named.name, 1, 0}); } catch (const std::exception& error) { OutputDebugStringA((std::string("AC_NATIVE_BIOS_ACT_ERROR ") + error.what() + "\n").c_str()); @@ -265,6 +298,81 @@ private: m_needsIdleFrame = true; } + void RefreshCapabilities(bool force) { + const auto now = GetTickCount64(); + if (!force && now < m_nextCapabilityPollMs) return; + m_nextCapabilityPollMs = now + 1000; + m_api->system.online = NetworkInformation::GetInternetConnectionProfile() != nullptr; + m_api->gamepad.controllers.clear(); + for (auto raw : RawGameController::RawGameControllers) { + ControllerInfo info; + info.id = Utf8(raw->NonRoamableId); + info.name = Utf8(raw->DisplayName); + info.vendor_id = raw->HardwareVendorId; + info.product_id = raw->HardwareProductId; + info.axes = raw->AxisCount; + info.buttons = raw->ButtonCount; + info.switches = raw->SwitchCount; + info.gamepad = Gamepad::FromGameController(raw) != nullptr; + m_api->gamepad.controllers.push_back(std::move(info)); + } + std::string inventory = "online=" + std::to_string(m_api->system.online) + + " controllers=" + std::to_string(m_api->gamepad.controllers.size()); + for (const auto& controller : m_api->gamepad.controllers) { + inventory += " | " + controller.name + " vendor=" + + std::to_string(controller.vendor_id) + " product=" + + std::to_string(controller.product_id) + " axes=" + + std::to_string(controller.axes) + " buttons=" + + std::to_string(controller.buttons) + " switches=" + + std::to_string(controller.switches) + " gamepad=" + + std::to_string(controller.gamepad); + } + if (inventory != m_lastCapabilityInventory) { + m_lastCapabilityInventory = inventory; + LogTelemetry("AC_NATIVE_CAPABILITIES " + inventory); + } + } + + void PollLivePiece() { + const auto now = GetTickCount64(); + if (now < m_nextLivePollMs) return; + m_nextLivePollMs = now + 500; + + const auto folder = ApplicationData::Current->LocalFolder->Path; + std::wstring path(folder->Data()); + path += L"\\live-piece.js"; + struct _stat64 info{}; + if (_wstat64(path.c_str(), &info) != 0) return; + const auto signature = static_cast(info.st_mtime) ^ + (static_cast(info.st_size) << 32); + if (signature == m_livePieceSignature) return; + m_livePieceSignature = signature; + + if (info.st_size <= 0 || info.st_size > 2 * 1024 * 1024) { + LogTelemetry("AC_NATIVE_LIVE_REJECT reason=source-size"); + return; + } + FILE* file = nullptr; + if (_wfopen_s(&file, path.c_str(), L"rb") != 0 || !file) { + LogTelemetry("AC_NATIVE_LIVE_REJECT reason=open-failed"); + return; + } + std::string source(static_cast(info.st_size), '\0'); + const auto bytes = std::fread(source.data(), 1, source.size(), file); + std::fclose(file); + source.resize(bytes); + + std::string error; + if (!m_supervisor->stage({"live", std::to_string(signature), source, "local-dev"}, + *m_api, error) || !m_supervisor->activate(*m_api)) { + for (auto& character : error) if (character == '\n' || character == '\r') character = ' '; + LogTelemetry("AC_NATIVE_LIVE_REJECT reason=" + error); + return; + } + LogTelemetry("AC_NATIVE_LIVE_READY bytes=" + std::to_string(source.size()) + + " generation=" + std::to_string(m_supervisor->generation())); + } + void Render(const std::array& color) { if (!m_target) return; m_context->OMSetRenderTargets(1, m_target.GetAddressOf(), nullptr); @@ -279,6 +387,10 @@ private: unsigned m_previousButtons = 0; unsigned m_flashFrames = 0; unsigned m_rateIndex = 1; + unsigned long long m_livePieceSignature = 0; + unsigned long long m_nextLivePollMs = 0; + unsigned long long m_nextCapabilityPollMs = 0; + std::string m_lastCapabilityInventory; uint32_t m_sampleRate = 48000; std::array m_flashColor{1, 1, 1, 1}; diff --git a/xbox/native-bios/Package.appxmanifest b/xbox/native-bios/Package.appxmanifest index 10e02c646..d9a29f120 100644 --- a/xbox/native-bios/Package.appxmanifest +++ b/xbox/native-bios/Package.appxmanifest @@ -5,7 +5,7 @@ IgnorableNamespaces="uap mp"> + Version="1.0.0.2" /> @@ -31,5 +31,8 @@ - + + + + diff --git a/xbox/native-bios/QuickJsEngine.cpp b/xbox/native-bios/QuickJsEngine.cpp index f1cfae82a..662f53ec7 100644 --- a/xbox/native-bios/QuickJsEngine.cpp +++ b/xbox/native-bios/QuickJsEngine.cpp @@ -35,6 +35,41 @@ JSValue Synth(JSContext* context, JSValueConst, int argc, JSValueConst* argv) { return JS_UNDEFINED; } +JSValue Controllers(JSContext* context, JSValueConst, int, JSValueConst*) { + auto* scope = static_cast(JS_GetContextOpaque(context)); + if (!scope || !scope->api) return JS_EXCEPTION; + JSValue result = JS_NewArray(context); + uint32_t index = 0; + for (const auto& controller : scope->api->gamepad.controllers) { + JSValue item = JS_NewObject(context); + JS_SetPropertyStr(context, item, "id", JS_NewString(context, controller.id.c_str())); + JS_SetPropertyStr(context, item, "name", JS_NewString(context, controller.name.c_str())); + JS_SetPropertyStr(context, item, "vendorId", JS_NewInt32(context, controller.vendor_id)); + JS_SetPropertyStr(context, item, "productId", JS_NewInt32(context, controller.product_id)); + JS_SetPropertyStr(context, item, "axes", JS_NewInt32(context, controller.axes)); + JS_SetPropertyStr(context, item, "buttons", JS_NewInt32(context, controller.buttons)); + JS_SetPropertyStr(context, item, "switches", JS_NewInt32(context, controller.switches)); + JS_SetPropertyStr(context, item, "gamepad", JS_NewBool(context, controller.gamepad)); + JS_SetPropertyUint32(context, result, index++, item); + } + return result; +} + +JSValue Capabilities(JSContext* context, JSValueConst, int, JSValueConst*) { + auto* scope = static_cast(JS_GetContextOpaque(context)); + if (!scope || !scope->api) return JS_EXCEPTION; + JSValue result = JS_NewObject(context); + JS_SetPropertyStr(context, result, "platform", JS_NewString(context, "xbox-uwp")); + JS_SetPropertyStr(context, result, "quickjs", JS_NewBool(context, true)); + JS_SetPropertyStr(context, result, "direct3d11", JS_NewBool(context, true)); + JS_SetPropertyStr(context, result, "xaudio2", JS_NewBool(context, true)); + JS_SetPropertyStr(context, result, "internetClient", JS_NewBool(context, true)); + JS_SetPropertyStr(context, result, "privateNetworkClientServer", JS_NewBool(context, true)); + JS_SetPropertyStr(context, result, "online", JS_NewBool(context, scope->api->system.online)); + JS_SetPropertyStr(context, result, "liveLocalState", JS_NewBool(context, true)); + return result; +} + class QuickJsPiece final : public JsPiece { public: QuickJsPiece(const PieceBundle& bundle, const JsLimits& limits, std::string& error) @@ -49,6 +84,8 @@ class QuickJsPiece final : public JsPiece { JSValue global = JS_GetGlobalObject(context_); JS_SetPropertyStr(context_, global, "wipe", JS_NewCFunction(context_, Wipe, "wipe", 3)); JS_SetPropertyStr(context_, global, "synth", JS_NewCFunction(context_, Synth, "synth", 2)); + JS_SetPropertyStr(context_, global, "controllers", JS_NewCFunction(context_, Controllers, "controllers", 0)); + JS_SetPropertyStr(context_, global, "capabilities", JS_NewCFunction(context_, Capabilities, "capabilities", 0)); JS_FreeValue(context_, global); JSValue result = JS_Eval(context_, bundle.source.data(), bundle.source.size(), bundle.slug.c_str(), JS_EVAL_TYPE_GLOBAL); diff --git a/xbox/native-bios/pch.h b/xbox/native-bios/pch.h index de7a90257..be7643299 100644 --- a/xbox/native-bios/pch.h +++ b/xbox/native-bios/pch.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -15,4 +16,5 @@ #include #include #include +#include #include diff --git a/xbox/runtime/include/ac/runtime.hpp b/xbox/runtime/include/ac/runtime.hpp index 30ca51671..8dd5fb81c 100644 --- a/xbox/runtime/include/ac/runtime.hpp +++ b/xbox/runtime/include/ac/runtime.hpp @@ -23,10 +23,21 @@ struct Event { }; struct Screen { int width = 1920; int height = 1080; float scale = 1.0f; }; +struct ControllerInfo { + std::string id; + std::string name; + std::uint16_t vendor_id = 0; + std::uint16_t product_id = 0; + std::uint16_t axes = 0; + std::uint16_t buttons = 0; + std::uint16_t switches = 0; + bool gamepad = false; +}; struct GamepadState { float left_x = 0, left_y = 0, right_x = 0, right_y = 0; float left_trigger = 0, right_trigger = 0; std::unordered_set down; + std::vector controllers; [[nodiscard]] bool pressed(std::string_view button) const { return down.find(std::string(button)) != down.end(); } -- 2.51.2