diff --git a/ENHANCEMENTS.md b/ENHANCEMENTS.md index 5c7dfff2..88b4f0ae 100644 --- a/ENHANCEMENTS.md +++ b/ENHANCEMENTS.md @@ -209,8 +209,8 @@ Findings (tracked for the next work cycle): has no sprite-tag ws hooks (ws work parked at 4:3). **Policy gates confirmed already in-tree (no code change needed for the merge):** -- Vulkan is hidden by default: launcher offers only Software<->OpenGL - (launcher.cpp:347/776); PSX_ENABLE_VULKAN defaults OFF (runtime.cmake:484); +- Vulkan is hidden by default: the shared launcher offers only + Software<->OpenGL; PSX_ENABLE_VULKAN defaults OFF (runtime.cmake); runtime downgrades renderer=vulkan -> opengl when not compiled (main.cpp:2463). Vulkan stays a dev/CLI-only backend (--renderer vulkan on a VK-enabled build). - Widescreen carries an EXPERIMENTAL tag in the launcher. diff --git a/ISSUES.md b/ISSUES.md index 2dd155d5..d494c6ee 100644 --- a/ISSUES.md +++ b/ISSUES.md @@ -513,45 +513,6 @@ sites aren't, it's an emit gap (fix in code_generator.cpp). If neighbors are absent too, it's a discovery gap (fix in function discovery seeds). Either way, then close Issue #3 alongside. -## Issue #6 — Launcher art has rough cutout edges (memory cards + controllers) - -**Status:** open, cosmetic — deferred -**Date opened:** 2026-06-12 -**Phase:** Launcher initiative (UI polish) - -### Symptom - -This issue tracked cosmetic problems in the removed in-tree launcher asset -pipeline. It is retained only as historical context. - -### Cause - -`FloodTransparent` uses a binary alpha decision (`max(R,G,B) < thresh` -→ alpha 0, else keep). There is no feathering of the boundary band, so -the object silhouette inherits the threshold's hard step. The memory -card and (grey) controller bodies are closer in luminance to the dark -mockup background than the disc is, so the same threshold leaves more -fringe on them. - -### Fix options (later) - -- Soft alpha ramp across a luminance band (`t_lo..t_hi`) instead of a - hard cutoff, applied to the flooded boundary pixels. -- Or supersample: crop at 2–4× from the mockup, knock out, then - downscale with high-quality bicubic so the edge anti-aliases. -- Or hand-mask the four assets once in an image editor (cleanest, but - manual). -- Best long-term: replace the mockup-derived crops with proper source - renders (transparent PNGs) — the `decorator: image(...)` pipeline is - already in place, so it's a drop-in. - -### Notes - -Tooling: this historical note referred to the removed in-tree launcher asset -pipeline. The active launcher path is downstream `recomp-ui`. - ---- - ## Issue #7 — sljit live execution is unvalidated (pure-live save-load wedge) **Status:** open, root-caused — fix in progress (branch `feat/sljit-backend`) diff --git a/docs/BUILDING.md b/docs/BUILDING.md index ac97a05e..9e014382 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -110,7 +110,7 @@ On Windows with MSVC or plain MinGW makefiles, swap `-G Ninja` for your generato |---|---|---| | `PSX_DEBUG_TOOLS` | ON for Debug/RelWithDebInfo, OFF for Release | TCP debug server + heartbeat + per-block recording | | `PSX_STATIC_RUNTIME` | ON for MinGW Release | Self-contained exe (statically links SDL2 + libgcc/libstdc++) | -| `PSX_LAUNCHER` | ON | Allow downstream game projects to wire recomp-ui launcher support | +| `PSX_RECOMP_UI` | ON | Wire a downstream game's pinned recomp-ui launcher; set OFF for headless/generated builds | | `PSX_ENABLE_VULKAN` | OFF | Build the experimental Vulkan renderer | | `PSX_BUILD_COSIM` | OFF | Build the first-divergence co-sim oracle target | diff --git a/docs/RECURSION_BUG.md b/docs/RECURSION_BUG.md index a084d4fa..4bc4e387 100644 --- a/docs/RECURSION_BUG.md +++ b/docs/RECURSION_BUG.md @@ -203,7 +203,7 @@ cmake --build ../psxrecomp/recompiler/build --target psxrecomp-game psxrecomp-bi (cd ../psxrecomp && ./recompiler/build/psxrecomp-bios.exe --config bios/SCPH1001.toml) ../psxrecomp/recompiler/build/psxrecomp-game.exe --config tomba_soak_e.toml # build: -cmake -S . -B -G Ninja -DCMAKE_BUILD_TYPE=Release -DPSX_DEBUG_TOOLS=ON -DPSX_LAUNCHER=OFF \ +cmake -S . -B -G Ninja -DCMAKE_BUILD_TYPE=Release -DPSX_DEBUG_TOOLS=ON -DPSX_RECOMP_UI=OFF \ -DCMAKE_C_COMPILER=C:/msys64/mingw64/bin/gcc.exe -DCMAKE_CXX_COMPILER=C:/msys64/mingw64/bin/g++.exe cmake --build --target psx-runtime -j8 # run (per-instance card; taskkill first): diff --git a/docs/beetle-linux.md b/docs/beetle-linux.md index 3134315e..a1feb3b0 100644 --- a/docs/beetle-linux.md +++ b/docs/beetle-linux.md @@ -38,7 +38,7 @@ make platform=unix STATIC_LINKING=1 HAVE_LIGHTREC=0 -j"$(nproc)" cp mednafen_psx_libretro.so libmednafen_psx.a cd ../runtime -cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPSX_LAUNCHER=OFF -DPSX_DEBUG_TOOLS=ON +cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPSX_RECOMP_UI=OFF -DPSX_DEBUG_TOOLS=ON ninja -C build psx-beetle # Headless run (picks the next free port from 4380 if taken; watch stderr) diff --git a/docs/internal/FAITHFUL_TIMING_PLAN.md b/docs/internal/FAITHFUL_TIMING_PLAN.md index 6c06956b..b2dfa3ed 100644 --- a/docs/internal/FAITHFUL_TIMING_PLAN.md +++ b/docs/internal/FAITHFUL_TIMING_PLAN.md @@ -304,9 +304,9 @@ on a fixed region -> next. re-enumerates gamecontrollers, keeps selection by GUID. Offline + netplay. - **2026-07-20 (launcher: lobby lock emoji via symbol fallback font):** - Password lobbies showed □ for 🔒 — LatoLatin has no emoji. Load bundled - `NotoSansSymbols2-Regular.ttf` (system Segoe UI Symbol on Windows) with - RmlUi `fallback_face=true` so missing glyphs resolve. + Password lobbies showed □ for 🔒 because the primary face has no emoji. + Load a symbol fallback face through the shared Dear ImGui font atlas so + missing glyphs resolve. - **2026-07-20 (launcher lobbies: button order + dblclick join):** Lobbies actions: Return to Launcher → Change Player Name → Host Game → @@ -559,7 +559,7 @@ on a fixed region -> next. - **2026-07-19 (netplay lobby server + launcher menus):** Lobby WS+JSON owned by proprietary `recomp-net-server` (was C `servers/lobby/` under open recomp-net); - `psx_lobby_client` + RmlUi home → Offline / Netplay → lobbies table + `psx_lobby_client` + shared launcher home → Offline / Netplay → lobbies table (host/join/password). Launch hands `PsxNetplayConfig` to `psx_netplay_start` (LAN endpoints from lobby). ICE relay stubbed. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 56d97100..5cdee5d1 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -33,6 +33,12 @@ psxrecomp_add_runtime_target(psx-runtime ) if(BUILD_TESTING) + add_executable(launcher_device_roundtrip_test + tests/test_launcher_device_roundtrip.cpp) + target_include_directories(launcher_device_roundtrip_test PRIVATE include) + add_test(NAME launcher_device_roundtrip_test + COMMAND launcher_device_roundtrip_test) + add_executable(psx_cyc_batch_test tests/test_psx_cyc_batch.c) target_include_directories(psx_cyc_batch_test PRIVATE include) target_compile_definitions(psx_cyc_batch_test PRIVATE diff --git a/runtime/include/disc_identity.h b/runtime/include/disc_identity.h index 83ff3385..6fa92740 100644 --- a/runtime/include/disc_identity.h +++ b/runtime/include/disc_identity.h @@ -6,7 +6,7 @@ // serial prefix, and optionally compares against an expected serial / CRC32. // // Used both by the runtime's launch-time disc check (runtime/src/main.cpp) -// and by the integrated launcher's "Disc verified" badge (launcher.cpp), so +// and by the shared launcher's "Disc verified" badge, so // the two never drift apart. #pragma once diff --git a/runtime/include/launcher_device.h b/runtime/include/launcher_device.h new file mode 100644 index 00000000..bc7ca194 --- /dev/null +++ b/runtime/include/launcher_device.h @@ -0,0 +1,47 @@ +// launcher_device.h - controller-source conversion at the recomp-ui C ABI. + +#pragma once + +#include +#include +#include + +namespace PSXRecompV4 { + +inline std::string trim_launcher_device(const std::string& device) { + const auto first = std::find_if_not(device.begin(), device.end(), + [](unsigned char c) { return std::isspace(c) != 0; }); + const auto last = std::find_if_not(device.rbegin(), device.rend(), + [](unsigned char c) { return std::isspace(c) != 0; }).base(); + return first < last ? std::string(first, last) : std::string(); +} + +inline std::string normalize_launcher_device(const std::string& device) { + std::string normalized = trim_launcher_device(device); + std::transform(normalized.begin(), normalized.end(), normalized.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return normalized; +} + +inline int launcher_source_from_device(const std::string& device) { + const std::string normalized = normalize_launcher_device(device); + if (normalized.empty() || normalized == "none") return 0; + if (normalized == "keyboard") return 1; + return 2; +} + +inline std::string launcher_device_from_source( + int source, const std::string& previous_device) { + if (source <= 0) return "none"; + if (source == 1) return "keyboard"; + + // recomp-ui's C ABI currently returns a source category, not the selected + // controller GUID. Preserve an existing gamepad/GUID assignment; when the + // user switched from None/Keyboard, persist the runtime's first-pad alias. + if (launcher_source_from_device(previous_device) == 2) { + return trim_launcher_device(previous_device); + } + return "gamepad"; +} + +} // namespace PSXRecompV4 diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 6cdf33cb..ffdd28b3 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -57,6 +57,7 @@ extern "C" void psx_event_step_conservative_env_init(void); #include "crc32.h" #include "disc_identity.h" #include "iso_reader.h" /* text-image guard: extract the boot EXE from the disc */ +#include "launcher_device.h" /* recomp-ui controller-source round-trip */ #include "psx_keybinds.h" /* configurable keyboard->DualShock keybinds (keybinds.ini) */ #if defined(RECOMP_LAUNCHER) #include "recomp_launcher.h" /* shared recomp-ui Dear ImGui launcher */ @@ -3865,6 +3866,7 @@ namespace { bool g_lnch_hosting_lan = false; bool g_lnch_joined_lan = false; std::string g_lnch_lan_endpoint; + std::string g_lnch_lan_guest_bind; struct AeLanLobbyState { std::string name; @@ -3926,6 +3928,7 @@ namespace { g_lnch_hosting_lan = true; g_lnch_joined_lan = false; g_lnch_lan_endpoint = state.endpoint; + g_lnch_lan_guest_bind.clear(); } int ae_np_read_lan_lobby(RecompLauncherCNetplayLobby* out) { @@ -4203,6 +4206,7 @@ namespace { } g_lnch_joined_lan = false; g_lnch_lan_endpoint.clear(); + g_lnch_lan_guest_bind.clear(); if (host_endpoint) std::snprintf(host_endpoint, 96, "%s", endpoint); return psx_lobby_create(lobby_name && lobby_name[0] ? lobby_name : "Netplay Lobby", @@ -4214,6 +4218,17 @@ namespace { * (prefer 7778); never advertise :0 to the lobby. */ int ae_np_join(void*, const char* lobby_id, const char* password, char* guest_bind) { + char bind_buf[64]; + const char* bind = guest_bind; + const char* colon = (bind && bind[0]) ? std::strrchr(bind, ':') : nullptr; + const unsigned port = (colon && colon[1]) + ? static_cast(std::strtoul(colon + 1, nullptr, 10)) : 0u; + if (!bind || !bind[0] || port == 0u) { + std::snprintf(bind_buf, sizeof(bind_buf), "0.0.0.0:7778"); + bind = bind_buf; + if (guest_bind) + std::snprintf(guest_bind, 64, "%s", bind_buf); + } if (lobby_id && strncmp(lobby_id, "lan:", 4) == 0) { AeLanLobbyState state; if (!ae_np_read_lan_state(&state) || !state.joiner_name.empty()) return -1; @@ -4225,19 +4240,9 @@ namespace { g_lnch_hosting_lan = false; g_lnch_joined_lan = true; g_lnch_lan_endpoint = state.endpoint; + g_lnch_lan_guest_bind = bind; return 0; } - char bind_buf[64]; - const char* bind = guest_bind; - const char* colon = (bind && bind[0]) ? std::strrchr(bind, ':') : nullptr; - const unsigned port = (colon && colon[1]) - ? static_cast(std::strtoul(colon + 1, nullptr, 10)) : 0u; - if (!bind || !bind[0] || port == 0u) { - std::snprintf(bind_buf, sizeof(bind_buf), "0.0.0.0:7778"); - bind = bind_buf; - if (guest_bind) - std::snprintf(guest_bind, 64, "%s", bind_buf); - } return psx_lobby_join(lobby_id, password ? password : "", bind); } @@ -4256,6 +4261,7 @@ namespace { } g_lnch_joined_lan = false; g_lnch_lan_endpoint.clear(); + g_lnch_lan_guest_bind.clear(); g_lnch_pending_direct_launch = {}; return psx_lobby_leave(); } @@ -4340,7 +4346,9 @@ namespace { "0.0.0.0:%s", port); } else { std::snprintf(g_lnch_pending_direct_launch.bind_hostport, - sizeof(g_lnch_pending_direct_launch.bind_hostport), "0.0.0.0:0"); + sizeof(g_lnch_pending_direct_launch.bind_hostport), "%s", + g_lnch_lan_guest_bind.empty() + ? "0.0.0.0:0" : g_lnch_lan_guest_bind.c_str()); std::snprintf(g_lnch_pending_direct_launch.peer_hostport, sizeof(g_lnch_pending_direct_launch.peer_hostport), "%s", state.endpoint.c_str()); @@ -5296,8 +5304,20 @@ int main(int argc, char** argv) { ls.enable_audio = 1; ls.audio_freq = 44100; ls.volume = 100; - ls.player_src[0] = (p1_device == "keyboard") ? 1 : (p1_device == "none") ? 0 : 2; - ls.player_src[1] = (p2_device == "keyboard") ? 1 : (p2_device == "none") ? 0 : 2; + ls.player_src[0] = PSXRecompV4::launcher_source_from_device(p1_device); + ls.player_src[1] = PSXRecompV4::launcher_source_from_device(p2_device); +#if defined(RECOMP_LAUNCHER_HAS_PLAYER_GAMEPAD_GUID) + if (ls.player_src[0] == 2) { + std::snprintf(ls.player_gamepad_guid[0], + sizeof(ls.player_gamepad_guid[0]), "%s", + p1_device.c_str()); + } + if (ls.player_src[1] == 2) { + std::snprintf(ls.player_gamepad_guid[1], + sizeof(ls.player_gamepad_guid[1]), "%s", + p2_device.c_str()); + } +#endif { int rui_deadzone_pct = seed.deadzone * 100 / 32767; ls.deadzone[0] = rui_deadzone_pct; @@ -5484,8 +5504,18 @@ int main(int argc, char** argv) { * is the legacy fallback field for consoles without the cap and is * left unused here. */ seed.texture_filter = ls.texture_filter ? 1 : 0; seed.has_texture_filter = true; - p1_device = (ls.player_src[0] == 1) ? "keyboard" : (ls.player_src[0] == 0) ? "none" : p1_device; - p2_device = (ls.player_src[1] == 1) ? "keyboard" : (ls.player_src[1] == 0) ? "none" : p2_device; + std::string p1_launcher_device = p1_device; + std::string p2_launcher_device = p2_device; +#if defined(RECOMP_LAUNCHER_HAS_PLAYER_GAMEPAD_GUID) + if (ls.player_gamepad_guid[0][0]) + p1_launcher_device = ls.player_gamepad_guid[0]; + if (ls.player_gamepad_guid[1][0]) + p2_launcher_device = ls.player_gamepad_guid[1]; +#endif + p1_device = PSXRecompV4::launcher_device_from_source( + ls.player_src[0], p1_launcher_device); + p2_device = PSXRecompV4::launcher_device_from_source( + ls.player_src[1], p2_launcher_device); seed.p1_device = p1_device; seed.has_p1_device = true; seed.p2_device = p2_device; seed.has_p2_device = true; seed.deadzone = ls.deadzone[0] * 32767 / 100; seed.has_deadzone = true; diff --git a/runtime/tests/test_launcher_device_roundtrip.cpp b/runtime/tests/test_launcher_device_roundtrip.cpp new file mode 100644 index 00000000..2f1657c8 --- /dev/null +++ b/runtime/tests/test_launcher_device_roundtrip.cpp @@ -0,0 +1,24 @@ +#include "launcher_device.h" + +#include + +int main() { + using PSXRecompV4::launcher_device_from_source; + using PSXRecompV4::launcher_source_from_device; + + assert(launcher_source_from_device("") == 0); + assert(launcher_source_from_device(" none ") == 0); + assert(launcher_source_from_device("Keyboard") == 1); + assert(launcher_source_from_device("gamepad") == 2); + assert(launcher_source_from_device("030000005e0400008e02000000000000") == 2); + + assert(launcher_device_from_source(0, "keyboard") == "none"); + assert(launcher_device_from_source(1, "none") == "keyboard"); + assert(launcher_device_from_source(2, "keyboard") == "gamepad"); + assert(launcher_device_from_source(2, "none") == "gamepad"); + assert(launcher_device_from_source(2, " auto ") == "auto"); + assert(launcher_device_from_source( + 2, " 030000005e0400008e02000000000000 ") == + "030000005e0400008e02000000000000"); + return 0; +} diff --git a/tools/crop_launcher_assets.ps1 b/tools/crop_launcher_assets.ps1 deleted file mode 100644 index 5ed50c6c..00000000 --- a/tools/crop_launcher_assets.ps1 +++ /dev/null @@ -1,81 +0,0 @@ -# Crop the real launcher art out of the design mockup and knock out the dark -# background to transparent, so each asset blends seamlessly on any panel. -# -# Photo assets (disc/controllers/memcard): edge flood-fill — flood the dark -# background inward from the borders; bright object pixels are walls, so -# interior dark details (disc PlayStation logo, controller buttons, the centre -# hole) are preserved. Logo: simple global luminance key (thin bright glyphs). -Add-Type -AssemblyName System.Drawing -$src = "/ef772e04-a7db-4ecd-98bb-eb75a01de0a6.png" -$out = "runtime\launcher\assets\img" -$mock = [System.Drawing.Bitmap]::FromFile($src) - -function Get-Crop($x,$y,$w,$h) { - $rect = New-Object System.Drawing.Rectangle($x,$y,$w,$h) - return $mock.Clone($rect, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb) -} - -# Returns the byte[] + BitmapData for in-place editing. -function Lock($bmp) { - $rect = New-Object System.Drawing.Rectangle(0,0,$bmp.Width,$bmp.Height) - $data = $bmp.LockBits($rect, [System.Drawing.Imaging.ImageLockMode]::ReadWrite, - [System.Drawing.Imaging.PixelFormat]::Format32bppArgb) - $len = $data.Stride * $bmp.Height - $bytes = New-Object byte[] $len - [System.Runtime.InteropServices.Marshal]::Copy($data.Scan0, $bytes, 0, $len) - return @{ data=$data; bytes=$bytes; stride=$data.Stride; w=$bmp.Width; h=$bmp.Height } -} -function Unlock($bmp, $ctx) { - [System.Runtime.InteropServices.Marshal]::Copy($ctx.bytes, 0, $ctx.data.Scan0, $ctx.bytes.Length) - $bmp.UnlockBits($ctx.data) -} - -function FloodTransparent($bmp, $thresh) { - $c = Lock $bmp - $b = $c.bytes; $stride = $c.stride; $w = $c.w; $h = $c.h - $visited = New-Object bool[] ($w*$h) - $stack = New-Object System.Collections.Generic.Stack[int] - for ($x=0; $x -lt $w; $x++) { $stack.Push($x); $stack.Push(($h-1)*$w+$x) } - for ($y=0; $y -lt $h; $y++) { $stack.Push($y*$w); $stack.Push($y*$w+($w-1)) } - while ($stack.Count -gt 0) { - $p = $stack.Pop() - if ($visited[$p]) { continue } - $visited[$p] = $true - $px = $p % $w; $py = [int][Math]::Floor($p / $w) - $i = $py*$stride + $px*4 - $mx = [Math]::Max($b[$i], [Math]::Max($b[$i+1], $b[$i+2])) - if ($mx -ge $thresh) { continue } # bright object pixel = wall - $b[$i+3] = 0 # background -> transparent - if ($px -gt 0) { $n=$p-1; if(-not $visited[$n]){$stack.Push($n)} } - if ($px -lt $w-1) { $n=$p+1; if(-not $visited[$n]){$stack.Push($n)} } - if ($py -gt 0) { $n=$p-$w; if(-not $visited[$n]){$stack.Push($n)} } - if ($py -lt $h-1) { $n=$p+$w; if(-not $visited[$n]){$stack.Push($n)} } - } - Unlock $bmp $c -} - -function GlobalKey($bmp, $thresh) { - $c = Lock $bmp - $b = $c.bytes - for ($i=0; $i -lt $b.Length; $i+=4) { - $mx = [Math]::Max($b[$i], [Math]::Max($b[$i+1], $b[$i+2])) - if ($mx -lt $thresh) { $b[$i+3] = 0 } - } - Unlock $bmp $c -} - -function Process($x,$y,$w,$h,$name,$mode,$thresh) { - $bmp = Get-Crop $x $y $w $h - if ($mode -eq "flood") { FloodTransparent $bmp $thresh } else { GlobalKey $bmp $thresh } - $bmp.Save((Join-Path $out $name), [System.Drawing.Imaging.ImageFormat]::Png) - $bmp.Dispose() - Write-Output "$name ($mode t=$thresh)" -} - -Process 28 20 70 70 "logo.png" "key" 60 -Process 74 120 288 294 "disc.png" "flood" 74 -Process 50 506 346 198 "pad_digital.png" "flood" 80 -Process 740 506 344 200 "pad_analog.png" "flood" 80 -Process 56 794 148 164 "memcard.png" "flood" 80 -$mock.Dispose() -Write-Output "done"