From 2ef883574a32823e47bbc4b16d4b24e0aac38343 Mon Sep 17 00:00:00 2001 From: Vincent Hengel Date: Mon, 27 Jul 2026 22:01:22 +0200 Subject: [PATCH 1/3] android: fix the NDK build and enable the SPIR-V recompiler The recompiler was compiled out of every Android build: delta/gpu gates it on pkg_check_modules finding SPIRV-Tools, which never resolves against the NDK sysroot, so DELTA_HAVE_SPIRV_BACKEND was undefined and every guest draw was dropped. Point PKG_CONFIG_LIBDIR at an NDK-built SPIRV-Tools to enable it; delta_gpu is a static library so only the library list propagates to the final link, and pkg-config hands back bare -l names when cross compiling, so resolve each entry to an absolute path. memfd_create is only declared by bionic at API 30 and we target 29, and the Android gfx backends were missing setTitle/overlaySetPerf. --- delta/gfx/gfx_android.cpp | 6 ++++++ delta/gfx/gfx_headless.cpp | 3 +++ delta/gpu/CMakeLists.txt | 29 +++++++++++++++++++++++++---- delta/kern/ps4/dev/dma_dev.cpp | 9 +++++++++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/delta/gfx/gfx_android.cpp b/delta/gfx/gfx_android.cpp index 7a80a23b..262fdacf 100644 --- a/delta/gfx/gfx_android.cpp +++ b/delta/gfx/gfx_android.cpp @@ -28,6 +28,7 @@ #include "gfx.h" #include "gfx_android.h" +#include "overlay.h" namespace gfx { namespace { @@ -900,6 +901,11 @@ bool pollKeyboardPad(PadKeys &out) { // service; no-op until that's wired through the NativeActivity. void setRumble(uint8_t, uint8_t) {} +// The activity is fullscreen and has no title bar; the perf numbers already +// reach logcat through the renderer's [fps] line. +void setTitle(const char *) {} +void overlaySetPerf(float, float, float) {} + void shutdown() { teardown(); } void setAndroidWindow(ANativeWindow *window) { diff --git a/delta/gfx/gfx_headless.cpp b/delta/gfx/gfx_headless.cpp index 655d843a..531b05e2 100644 --- a/delta/gfx/gfx_headless.cpp +++ b/delta/gfx/gfx_headless.cpp @@ -15,6 +15,7 @@ #if defined(__ANDROID__) && !defined(DELTA_ANDROID_APP) #include "gfx.h" +#include "overlay.h" namespace gfx { @@ -28,6 +29,8 @@ bool pumpEvents() { return true; } bool pollKeyboardPad(PadKeys &) { return false; } void setRumble(uint8_t, uint8_t) {} void shutdown() {} +void setTitle(const char *) {} +void overlaySetPerf(float, float, float) {} } // namespace gfx diff --git a/delta/gpu/CMakeLists.txt b/delta/gpu/CMakeLists.txt index e5ec0587..8c2c2929 100644 --- a/delta/gpu/CMakeLists.txt +++ b/delta/gpu/CMakeLists.txt @@ -35,10 +35,31 @@ endif() if(SPIRVTOOLS_FOUND AND SPIRVHEADERS_FOUND) # PRIVATE: SPIRV-Tools is an implementation detail of the recompiler; no # header on the public surface needs it, and keeping it off dependents' - # include paths stops accidental use outside the module. LINK_LIBRARIES - # (absolute paths) instead of LIBRARIES+link_directories so the link - # requirement still reaches the final executable through the static lib. - target_link_libraries(delta_gpu PRIVATE ${SPIRVTOOLS_LINK_LIBRARIES}) + # include paths stops accidental use outside the module. + # + # delta_gpu is a static library, so only the library list propagates to the + # final executable -- link directories do not. Absolute paths are therefore + # the only form that works, and a cross build (the Android NDK sysroot) gets + # bare -l names out of pkg-config, so resolve each entry against the .pc + # libdir ourselves. + set(_spirv_libs "") + foreach(_lib IN LISTS SPIRVTOOLS_LIBRARIES) + set(_found "") + foreach(_dir IN LISTS SPIRVTOOLS_LIBRARY_DIRS) + set(_cand + "${_dir}/${CMAKE_STATIC_LIBRARY_PREFIX}${_lib}${CMAKE_STATIC_LIBRARY_SUFFIX}") + if(EXISTS "${_cand}") + set(_found "${_cand}") + break() + endif() + endforeach() + if(_found) + list(APPEND _spirv_libs "${_found}") + else() + list(APPEND _spirv_libs "${_lib}") + endif() + endforeach() + target_link_libraries(delta_gpu PRIVATE ${_spirv_libs}) target_include_directories(delta_gpu PRIVATE ${SPIRVTOOLS_INCLUDE_DIRS} ${SPIRVHEADERS_INCLUDE_DIRS}) target_compile_definitions(delta_gpu PRIVATE DELTA_HAVE_SPIRV_BACKEND=1) diff --git a/delta/kern/ps4/dev/dma_dev.cpp b/delta/kern/ps4/dev/dma_dev.cpp index fc53e5ab..3f896cea 100644 --- a/delta/kern/ps4/dev/dma_dev.cpp +++ b/delta/kern/ps4/dev/dma_dev.cpp @@ -19,6 +19,15 @@ #include #include +#ifdef __ANDROID__ +// bionic only declares memfd_create at API >= 30; we target 29. The syscall +// itself is present on every kernel the emulator runs on. +#include +static int memfd_create(const char *name, unsigned flags) { + return static_cast(::syscall(__NR_memfd_create, name, flags)); +} +#endif + #include "dma_dev.h" #include "kern/ps4/lv2/sys_mem.h" #include "kern/proc.h" From 5f713d2acf6996131a4dd7dbaa42bde4698cd353 Mon Sep 17 00:00:00 2001 From: Vincent Hengel Date: Mon, 27 Jul 2026 22:01:35 +0200 Subject: [PATCH 2/3] android: letterbox the frame and redraw the touch controls The present blit stretched the framebuffer over the whole surface, so 16:9 content came out ~20% too wide on a 21:9 panel. The fit has to be solved in the landscape space the player sees and mapped back into image space for the blit: the swapchain image is portrait while the window is landscape, and the compositor scales each axis on its own rather than rotating. The controls were four hard-edged discs alpha-blended at a flat 0.40. They are now signed distance fields resolved with a pixel of coverage, and cover the pad the games actually need (twin sticks, d-pad, face buttons, shoulders, touchpad, options) instead of the four Isaac used. Layout is in height units so it keeps its proportions on any panel. --- delta/gfx/gfx_android.cpp | 537 +++++++++++++++++++++++++++++--------- 1 file changed, 419 insertions(+), 118 deletions(-) diff --git a/delta/gfx/gfx_android.cpp b/delta/gfx/gfx_android.cpp index 262fdacf..ea9b7a7d 100644 --- a/delta/gfx/gfx_android.cpp +++ b/delta/gfx/gfx_android.cpp @@ -121,71 +121,140 @@ bool g_windowChanged = false; constexpr int kMaxTouch = 8; Touch g_touches[kMaxTouch]; int g_touchCount = 0; -uint32_t g_surfaceW = 0, g_surfaceH = 0; - -// On-screen virtual gamepad, in normalised [0,1] surface coords. The present -// blit stretches the whole game framebuffer across the whole surface, so a -// point at (fx,fy) draws and hit-tests at the same place. Buttons use a square -// zone of half-size `r` (in height units); sticks use a generous side region. -struct Btn { - float cx, cy, r; + +// Rect the presented image occupies inside the surface (landscape px). The +// frame is letterboxed rather than stretched, so this is what maps a touch to +// a point in the image. +float g_viewX = 0.0f, g_viewY = 0.0f, g_viewW = 0.0f, g_viewH = 0.0f; + +// On-screen virtual gamepad. Laid out in "height units": y runs 0..1 down the +// presented image and x runs 0..aspect across it, so the pad keeps its +// proportions on any panel and a circle stays a circle. Drawing and hit-testing +// share the units, so a control is exactly where it looks. +struct Pill { + float cx, cy; // centre + float r; // half-height / corner radius + float hw; // half-width between the caps; 0 => circle }; -const Btn kCross{0.70f, 0.86f, 0.06f}; // use item / confirm -const Btn kCircle{0.80f, 0.66f, 0.06f}; // cancel -const Btn kBomb{0.70f, 0.62f, 0.06f}; // bomb (R1) -const Btn kOptions{0.95f, 0.10f, 0.05f}; // pause / start -struct Stick { - float cx, cy, r; + +enum class Mark { Cross, Circle, Square, Triangle, Bars, Pad, L1, L2, R1, R2 }; + +struct Button { + Pill p; + bool PadKeys::*bit; + Mark mark; }; -const Stick kLStick{0.12f, 0.70f, 0.14f}; // move -const Stick kRStick{0.88f, 0.70f, 0.14f}; // aim / shoot -bool inBtn(float nx, float ny, const Btn &b, float aspect) { - // square zone; scale x by aspect so the zone is square on screen, not - // stretched - return std::fabs((nx - b.cx) * aspect) <= b.r && std::fabs(ny - b.cy) <= b.r; +constexpr float kStickR = 0.155f; + +struct Layout { + Button btns[11]; + int n = 0; + Pill lstick, rstick; + float dpadCx, dpadCy, dpadArm; +}; + +// Thumbs sit at the bottom corners, so the sticks anchor there and the d-pad / +// face diamond stack above them; shoulders run along the top edge. +Layout buildLayout(float aspect) { + const float A = aspect; + Layout l; + auto add = [&](float cx, float cy, float r, float hw, bool PadKeys::*bit, + Mark m) { l.btns[l.n++] = Button{{cx, cy, r, hw}, bit, m}; }; + + const float faceCx = A - 0.24f, faceCy = 0.30f; + const float spread = 0.105f, faceR = 0.058f; + add(faceCx, faceCy - spread, faceR, 0.0f, &PadKeys::triangle, Mark::Triangle); + add(faceCx + spread, faceCy, faceR, 0.0f, &PadKeys::circle, Mark::Circle); + add(faceCx, faceCy + spread, faceR, 0.0f, &PadKeys::cross, Mark::Cross); + add(faceCx - spread, faceCy, faceR, 0.0f, &PadKeys::square, Mark::Square); + + const float shY = 0.075f, shR = 0.042f, shHw = 0.080f; + add(0.19f, shY, shR, shHw, &PadKeys::l1, Mark::L1); + add(0.40f, shY, shR, shHw, &PadKeys::l2, Mark::L2); + add(A - 0.19f, shY, shR, shHw, &PadKeys::r1, Mark::R1); + add(A - 0.40f, shY, shR, shHw, &PadKeys::r2, Mark::R2); + + add(A * 0.5f, shY, shR, 0.130f, &PadKeys::touchpad, Mark::Pad); + add(A * 0.5f + 0.26f, shY, 0.038f, 0.060f, &PadKeys::options, Mark::Bars); + + l.lstick = {0.26f, 0.74f, kStickR, 0.0f}; + l.rstick = {A - 0.26f, 0.74f, kStickR, 0.0f}; + l.dpadCx = 0.24f; + l.dpadCy = 0.30f; + l.dpadArm = 0.075f; + return l; +} + +// Distance test against a pill (a circle when hw == 0), grown by `grow` so the +// touch target is a little more forgiving than the drawn shape. +bool inPill(float hx, float hy, const Pill &p, float grow) { + float dx = std::fabs(hx - p.cx) - p.hw; + if (dx < 0.0f) + dx = 0.0f; + const float dy = hy - p.cy; + const float r = p.r + grow; + return dx * dx + dy * dy <= r * r; +} + +// Surface pixel -> height units inside the presented image. False when the +// point falls in the letterbox margin. +bool toUnits(float sx, float sy, float &hx, float &hy) { + hx = (sx - g_viewX) / g_viewH; + hy = (sy - g_viewY) / g_viewH; + return hx >= 0.0f && hy >= 0.0f && hx <= g_viewW / g_viewH && hy <= 1.0f; } // Map the down touches to a DS4 pad against the layout above. PadKeys computePad() { PadKeys k; // neutral (sticks centred at 128) - if (!g_surfaceW || !g_surfaceH) + if (g_viewW <= 0.0f || g_viewH <= 0.0f) return k; - float aspect = float(g_surfaceH) / float(g_surfaceW); + const float A = g_viewW / g_viewH; + const Layout l = buildLayout(A); for (int i = 0; i < g_touchCount; i++) { - float nx = g_touches[i].x / g_surfaceW; - float ny = g_touches[i].y / g_surfaceH; - if (inBtn(nx, ny, kOptions, aspect)) { - k.options = true; + float hx, hy; + if (!toUnits(g_touches[i].x, g_touches[i].y, hx, hy)) continue; - } - if (inBtn(nx, ny, kCross, aspect)) { - k.cross = true; + + bool claimed = false; + for (int b = 0; b < l.n && !claimed; b++) + if (inPill(hx, hy, l.btns[b].p, 0.012f)) { + k.*(l.btns[b].bit) = true; + claimed = true; + } + if (claimed) continue; - } - if (inBtn(nx, ny, kCircle, aspect)) { - k.circle = true; + + // D-pad: 8-way, from the offset within the cross. + const float ddx = hx - l.dpadCx, ddy = hy - l.dpadCy; + if (std::fabs(ddx) <= l.dpadArm * 1.5f && std::fabs(ddy) <= l.dpadArm * 1.5f) { + const float dz = l.dpadArm * 0.32f; + k.left = k.left || ddx < -dz; + k.right = k.right || ddx > dz; + k.up = k.up || ddy < -dz; + k.down = k.down || ddy > dz; continue; } - if (inBtn(nx, ny, kBomb, aspect)) { - k.r1 = true; + + // Sticks: a generous zone around each ring, deflection from its centre. + const bool leftSide = hx < A * 0.5f; + const Pill &s = leftSide ? l.lstick : l.rstick; + float dx = (hx - s.cx) / kStickR, dy = (hy - s.cy) / kStickR; + if (std::fabs(dx) > 1.9f || std::fabs(dy) > 1.9f) continue; - } - // Sticks: left half moves, right half aims. Deflection from the ring - // centre. - const Stick &s = (nx < 0.5f) ? kLStick : kRStick; - float dx = (nx - s.cx) / s.r, dy = (ny - s.cy) / s.r; dx = std::clamp(dx, -1.0f, 1.0f); dy = std::clamp(dy, -1.0f, 1.0f); - uint8_t vx = uint8_t(std::clamp(128.0f + dx * 127.0f, 0.0f, 255.0f)); - uint8_t vy = uint8_t(std::clamp(128.0f + dy * 127.0f, 0.0f, 255.0f)); - if (nx < 0.5f) { + const uint8_t vx = uint8_t(std::clamp(128.0f + dx * 127.0f, 0.0f, 255.0f)); + const uint8_t vy = uint8_t(std::clamp(128.0f + dy * 127.0f, 0.0f, 255.0f)); + if (leftSide) { k.lx = vx; k.ly = vy; - k.left = dx < -0.4f; - k.right = dx > 0.4f; - k.up = dy < -0.4f; - k.down = dy > 0.4f; // d-pad for menus + // Menus that only read the d-pad still follow the movement stick. + k.left = k.left || dx < -0.4f; + k.right = k.right || dx > 0.4f; + k.up = k.up || dy < -0.4f; + k.down = k.down || dy > 0.4f; } else { k.rx = vx; k.ry = vy; @@ -194,10 +263,25 @@ PadKeys computePad() { return k; } -// --- helper overlay (CPU alpha-blend into the present framebuffer) ---------- -// Drawn in the game framebuffer, which the present blit stretches across the -// whole surface, so normalised (fx,fy) lands at the same on-screen spot. Radii -// are corrected by the surface aspect so glyphs stay round on screen. +// --- control overlay (CPU alpha-blend into the present framebuffer) -------- +// Everything is a signed distance field resolved with one pixel of coverage, +// so the pad has soft edges instead of the stair-stepped discs a hard test +// gives. Height units scale by the framebuffer height, which is exactly how +// the letterboxed blit maps them to the panel. + +struct Paint { + uint8_t r, g, b; + float a; +}; + +constexpr Paint kNone{0, 0, 0, 0.0f}; +constexpr Paint kGlass{12, 14, 20, 0.30f}; // resting button body +constexpr Paint kGlassOn{58, 66, 84, 0.58f}; // held +constexpr Paint kRim{224, 231, 245, 0.26f}; +constexpr Paint kRimOn{255, 255, 255, 0.80f}; +constexpr Paint kShadow{0, 0, 0, 0.20f}; + +inline float clamp01(float v) { return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v); } inline void blendPx(uint8_t *buf, int w, int h, bool bgra, int x, int y, uint8_t r, uint8_t g, uint8_t b, float a) { @@ -210,86 +294,275 @@ inline void blendPx(uint8_t *buf, int w, int h, bool bgra, int x, int y, p[bi] = uint8_t(p[bi] * (1 - a) + b * a); } -// Round-on-screen ellipse in the framebuffer. band<=0 => filled disc, else a -// ring of that normalised-radius thickness. -void glyph(uint8_t *buf, int w, int h, bool bgra, float xr, float yr, float fx, - float fy, float sr, float band, uint8_t r, uint8_t g, uint8_t b, - float a) { - float cx = fx * w, cy = fy * h; - float rx = sr * xr, ry = sr * yr; // px radii (round on screen) - int x0 = std::max(0, int(cx - rx - 1)), - x1 = std::min(w - 1, int(cx + rx + 1)); - int y0 = std::max(0, int(cy - ry - 1)), - y1 = std::min(h - 1, int(cy + ry + 1)); +// Filled and/or stroked pill in framebuffer pixels. strokeW is the full width. +void drawPill(uint8_t *buf, int w, int h, bool bgra, float cx, float cy, + float hw, float r, const Paint &fill, const Paint &stroke, + float strokeW) { + const float pad = r + strokeW + 2.0f; + const int x0 = std::max(0, int(cx - hw - pad)); + const int x1 = std::min(w - 1, int(cx + hw + pad)); + const int y0 = std::max(0, int(cy - pad)); + const int y1 = std::min(h - 1, int(cy + pad)); + for (int y = y0; y <= y1; y++) + for (int x = x0; x <= x1; x++) { + float dx = std::fabs(x + 0.5f - cx) - hw; + if (dx < 0.0f) + dx = 0.0f; + const float dy = y + 0.5f - cy; + const float d = std::sqrt(dx * dx + dy * dy) - r; + if (fill.a > 0.0f) { + const float cov = clamp01(0.5f - d); + if (cov > 0.0f) + blendPx(buf, w, h, bgra, x, y, fill.r, fill.g, fill.b, fill.a * cov); + } + if (stroke.a > 0.0f && strokeW > 0.0f) { + const float cov = clamp01(strokeW * 0.5f - std::fabs(d) + 0.5f); + if (cov > 0.0f) + blendPx(buf, w, h, bgra, x, y, stroke.r, stroke.g, stroke.b, + stroke.a * cov); + } + } +} + +// Capsule segment; the building block for the ✕ / □ / △ marks and the d-pad. +void drawSeg(uint8_t *buf, int w, int h, bool bgra, float ax, float ay, + float bx, float by, float halfW, const Paint &p) { + const int x0 = std::max(0, int(std::min(ax, bx) - halfW - 2)); + const int x1 = std::min(w - 1, int(std::max(ax, bx) + halfW + 2)); + const int y0 = std::max(0, int(std::min(ay, by) - halfW - 2)); + const int y1 = std::min(h - 1, int(std::max(ay, by) + halfW + 2)); + const float vx = bx - ax, vy = by - ay; + const float len2 = vx * vx + vy * vy; for (int y = y0; y <= y1; y++) for (int x = x0; x <= x1; x++) { - float ex = (x - cx) / rx, ey = (y - cy) / ry; - float d = std::sqrt(ex * ex + ey * ey); - if (band <= 0.0f ? (d <= 1.0f) : (d <= 1.0f && d >= 1.0f - band)) - blendPx(buf, w, h, bgra, x, y, r, g, b, a); + const float px = x + 0.5f - ax, py = y + 0.5f - ay; + const float t = + len2 > 0.0f ? clamp01((px * vx + py * vy) / len2) : 0.0f; + const float dx = px - vx * t, dy = py - vy * t; + const float cov = + clamp01(halfW - std::sqrt(dx * dx + dy * dy) + 0.5f); + if (cov > 0.0f) + blendPx(buf, w, h, bgra, x, y, p.r, p.g, p.b, p.a * cov); + } +} + +// 3x5 cells, MSB-first per row; just enough for the shoulder labels. +const uint8_t kGlyphL[5] = {4, 4, 4, 4, 7}; +const uint8_t kGlyphR[5] = {6, 5, 6, 5, 5}; +const uint8_t kGlyph1[5] = {2, 6, 2, 2, 7}; +const uint8_t kGlyph2[5] = {7, 1, 7, 4, 7}; + +void drawCell(uint8_t *buf, int w, int h, bool bgra, float x0, float y0, + float s, const Paint &p) { + for (int y = int(y0); y <= int(y0 + s); y++) + for (int x = int(x0); x <= int(x0 + s); x++) { + const float cx = clamp01(std::min(x0 + s, x + 1.0f) - std::max(x0, float(x))); + const float cy = clamp01(std::min(y0 + s, y + 1.0f) - std::max(y0, float(y))); + const float cov = cx * cy; + if (cov > 0.0f) + blendPx(buf, w, h, bgra, x, y, p.r, p.g, p.b, p.a * cov); } } +void drawChar(uint8_t *buf, int w, int h, bool bgra, const uint8_t rows[5], + float x, float y, float s, const Paint &p) { + for (int ry = 0; ry < 5; ry++) + for (int rx = 0; rx < 3; rx++) + if (rows[ry] & (4 >> rx)) + drawCell(buf, w, h, bgra, x + rx * s, y + ry * s, s, p); +} + +void drawLabel(uint8_t *buf, int w, int h, bool bgra, const uint8_t a[5], + const uint8_t b[5], float cx, float cy, float s, + const Paint &p) { + const float width = 7.0f * s; // 3 + gap + 3 + drawChar(buf, w, h, bgra, a, cx - width * 0.5f, cy - 2.5f * s, s, p); + drawChar(buf, w, h, bgra, b, cx - width * 0.5f + 4.0f * s, cy - 2.5f * s, s, + p); +} + +void drawMark(uint8_t *buf, int w, int h, bool bgra, Mark m, float cx, + float cy, float s, const Paint &p) { + const float t = std::max(1.2f, s * 0.17f); + switch (m) { + case Mark::Cross: { + const float d = s * 0.60f; + drawSeg(buf, w, h, bgra, cx - d, cy - d, cx + d, cy + d, t, p); + drawSeg(buf, w, h, bgra, cx - d, cy + d, cx + d, cy - d, t, p); + break; + } + case Mark::Circle: + drawPill(buf, w, h, bgra, cx, cy, 0.0f, s * 0.62f, kNone, p, t * 2.0f); + break; + case Mark::Square: { + const float d = s * 0.55f; + drawSeg(buf, w, h, bgra, cx - d, cy - d, cx + d, cy - d, t, p); + drawSeg(buf, w, h, bgra, cx + d, cy - d, cx + d, cy + d, t, p); + drawSeg(buf, w, h, bgra, cx + d, cy + d, cx - d, cy + d, t, p); + drawSeg(buf, w, h, bgra, cx - d, cy + d, cx - d, cy - d, t, p); + break; + } + case Mark::Triangle: { + const float d = s * 0.70f; + const float bx = d * 0.87f, by = d * 0.5f; + drawSeg(buf, w, h, bgra, cx, cy - d, cx + bx, cy + by, t, p); + drawSeg(buf, w, h, bgra, cx + bx, cy + by, cx - bx, cy + by, t, p); + drawSeg(buf, w, h, bgra, cx - bx, cy + by, cx, cy - d, t, p); + break; + } + case Mark::Bars: + for (int i = -1; i <= 1; i++) + drawSeg(buf, w, h, bgra, cx - s * 0.45f, cy + i * s * 0.42f, + cx + s * 0.45f, cy + i * s * 0.42f, t * 0.7f, p); + break; + case Mark::Pad: + drawPill(buf, w, h, bgra, cx, cy, s * 0.50f, s * 0.40f, kNone, p, t * 1.3f); + break; + case Mark::L1: + drawLabel(buf, w, h, bgra, kGlyphL, kGlyph1, cx, cy, s * 0.30f, p); + break; + case Mark::L2: + drawLabel(buf, w, h, bgra, kGlyphL, kGlyph2, cx, cy, s * 0.30f, p); + break; + case Mark::R1: + drawLabel(buf, w, h, bgra, kGlyphR, kGlyph1, cx, cy, s * 0.30f, p); + break; + case Mark::R2: + drawLabel(buf, w, h, bgra, kGlyphR, kGlyph2, cx, cy, s * 0.30f, p); + break; + } +} + +Paint markPaint(Mark m, bool on) { + const float a = on ? 1.0f : 0.62f; + switch (m) { + case Mark::Cross: + return {108, 160, 255, a}; + case Mark::Circle: + return {240, 106, 106, a}; + case Mark::Square: + return {232, 130, 200, a}; + case Mark::Triangle: + return {86, 210, 168, a}; + default: + return {226, 232, 245, a}; + } +} + void drawOverlay(uint8_t *buf, uint32_t w, uint32_t h, bool bgra) { Touch t[kMaxTouch]; int n; - uint32_t sw, sh; + float vx, vy, vw, vh; { std::lock_guard lk(g_inMutex); n = g_touchCount; std::memcpy(t, g_touches, sizeof(Touch) * (n < kMaxTouch ? n : kMaxTouch)); - sw = g_surfaceW; - sh = g_surfaceH; + vx = g_viewX; + vy = g_viewY; + vw = g_viewW; + vh = g_viewH; } - if (!sw || !sh) + if (vw <= 0.0f || vh <= 0.0f) return; - // Per-unit px radii so a glyph stays round after the fb is stretched to the - // surface: screen radius sr*sh maps to sr*h px in y and sr*(sh*w/sw) px in x. - float yr = float(h); - float xr = float(sh) * float(w) / float(sw); - float aspect = float(sh) / float(sw); - - auto pressed = [&](const Btn &btn) { - for (int i = 0; i < n; i++) - if (inBtn(t[i].x / sw, t[i].y / sh, btn, aspect)) + + const float A = vw / vh; + const Layout l = buildLayout(A); + const float U = float(h); // height unit -> framebuffer pixels + + // Touches in height units, so press state is tested exactly as computePad + // resolves it. + float hx[kMaxTouch], hy[kMaxTouch]; + int m = 0; + for (int i = 0; i < n; i++) { + const float ux = (t[i].x - vx) / vh, uy = (t[i].y - vy) / vh; + if (ux < 0.0f || uy < 0.0f || ux > A || uy > 1.0f) + continue; + hx[m] = ux; + hy[m] = uy; + m++; + } + auto held = [&](const Pill &p) { + for (int i = 0; i < m; i++) + if (inPill(hx[i], hy[i], p, 0.012f)) return true; return false; }; - // Face buttons: PS4 colours, brighter when held. - struct BC { - const Btn &b; - uint8_t r, g, bl; - } bcs[] = {{kCross, 70, 130, 255}, - {kCircle, 240, 70, 70}, - {kBomb, 220, 220, 220}, - {kOptions, 200, 200, 200}}; - for (auto &c : bcs) { - float a = pressed(c.b) ? 0.85f : 0.40f; - glyph(buf, w, h, bgra, xr, yr, c.b.cx, c.b.cy, c.b.r, 0.0f, c.r, c.g, c.bl, - a); - glyph(buf, w, h, bgra, xr, yr, c.b.cx, c.b.cy, c.b.r, 0.18f, 255, 255, 255, - 0.6f); - } - - // Sticks: outer ring + a dot at the current deflection. - const Stick *sticks[] = {&kLStick, &kRStick}; - for (auto *s : sticks) { - glyph(buf, w, h, bgra, xr, yr, s->cx, s->cy, s->r, 0.10f, 255, 255, 255, - 0.5f); - float dotx = s->cx, doty = s->cy; - for (int i = 0; i < n; i++) { - float nx = t[i].x / sw, ny = t[i].y / sh; - bool mine = (s == &kLStick) ? (nx < 0.5f) : (nx >= 0.5f); + for (int b = 0; b < l.n; b++) { + const Button &btn = l.btns[b]; + const bool on = held(btn.p); + const float cx = btn.p.cx * U, cy = btn.p.cy * U; + const float r = btn.p.r * U, hwp = btn.p.hw * U; + drawPill(buf, w, h, bgra, cx, cy + r * 0.10f, hwp, r, kShadow, kNone, 0.0f); + drawPill(buf, w, h, bgra, cx, cy, hwp, r, on ? kGlassOn : kGlass, + on ? kRimOn : kRim, std::max(1.5f, r * 0.075f)); + drawMark(buf, w, h, bgra, btn.mark, cx, cy, r * 0.60f, + markPaint(btn.mark, on)); + } + + // D-pad: a plus of capsules, each arm lit by its own direction. + { + const float cx = l.dpadCx * U, cy = l.dpadCy * U, arm = l.dpadArm * U; + const float t2 = arm * 0.40f; + float ddx = 0.0f, ddy = 0.0f; + bool active = false; + for (int i = 0; i < m; i++) { + const float ax = hx[i] - l.dpadCx, ay = hy[i] - l.dpadCy; + if (std::fabs(ax) <= l.dpadArm * 1.5f && std::fabs(ay) <= l.dpadArm * 1.5f) { + ddx = ax; + ddy = ay; + active = true; + } + } + const float dz = l.dpadArm * 0.32f; + const bool left = active && ddx < -dz, right = active && ddx > dz; + const bool up = active && ddy < -dz, down = active && ddy > dz; + drawSeg(buf, w, h, bgra, cx - arm, cy, cx + arm, cy, t2, kShadow); + drawSeg(buf, w, h, bgra, cx, cy - arm, cx, cy + arm, t2, kShadow); + drawSeg(buf, w, h, bgra, cx - arm, cy, cx, cy, t2 * 0.92f, + left ? kGlassOn : kGlass); + drawSeg(buf, w, h, bgra, cx, cy, cx + arm, cy, t2 * 0.92f, + right ? kGlassOn : kGlass); + drawSeg(buf, w, h, bgra, cx, cy - arm, cx, cy, t2 * 0.92f, + up ? kGlassOn : kGlass); + drawSeg(buf, w, h, bgra, cx, cy, cx, cy + arm, t2 * 0.92f, + down ? kGlassOn : kGlass); + const Paint tip{226, 232, 245, 0.55f}; + const float ts = arm * 0.16f; + drawSeg(buf, w, h, bgra, cx - arm * 0.70f, cy, cx - arm * 0.44f, cy, ts, + left ? kRimOn : tip); + drawSeg(buf, w, h, bgra, cx + arm * 0.44f, cy, cx + arm * 0.70f, cy, ts, + right ? kRimOn : tip); + drawSeg(buf, w, h, bgra, cx, cy - arm * 0.70f, cx, cy - arm * 0.44f, ts, + up ? kRimOn : tip); + drawSeg(buf, w, h, bgra, cx, cy + arm * 0.44f, cx, cy + arm * 0.70f, ts, + down ? kRimOn : tip); + } + + // Sticks: a recessed well with a knob that follows the thumb. + const Pill *sticks[] = {&l.lstick, &l.rstick}; + for (const Pill *s : sticks) { + const float cx = s->cx * U, cy = s->cy * U, r = s->r * U; + float kx = cx, ky = cy; + bool on = false; + for (int i = 0; i < m; i++) { + const float dx = (hx[i] - s->cx) / kStickR, dy = (hy[i] - s->cy) / kStickR; + if (std::fabs(dx) > 1.9f || std::fabs(dy) > 1.9f) + continue; + const bool mine = (s == &l.lstick) ? (hx[i] < A * 0.5f) : (hx[i] >= A * 0.5f); if (!mine) continue; - float dx = std::clamp((nx - s->cx) / s->r, -1.0f, 1.0f); - float dy = std::clamp((ny - s->cy) / s->r, -1.0f, 1.0f); - dotx = s->cx + dx * s->r; - doty = s->cy + dy * s->r; + kx = cx + std::clamp(dx, -1.0f, 1.0f) * r; + ky = cy + std::clamp(dy, -1.0f, 1.0f) * r; + on = true; } - glyph(buf, w, h, bgra, xr, yr, dotx, doty, s->r * 0.4f, 0.0f, 255, 255, 255, - 0.8f); + drawPill(buf, w, h, bgra, cx, cy, 0.0f, r, Paint{8, 10, 14, 0.22f}, + Paint{224, 231, 245, on ? 0.40f : 0.22f}, std::max(1.5f, r * 0.035f)); + drawPill(buf, w, h, bgra, kx, ky + r * 0.05f, 0.0f, r * 0.42f, kShadow, + kNone, 0.0f); + drawPill(buf, w, h, bgra, kx, ky, 0.0f, r * 0.42f, + on ? kGlassOn : Paint{18, 21, 28, 0.42f}, on ? kRimOn : kRim, + std::max(1.5f, r * 0.05f)); } } @@ -396,13 +669,6 @@ bool createSwapchain() { if (ext.width == 0 || ext.height == 0) return false; g.swapExtent = ext; - { - // Input + overlay work in the on-screen (landscape) space, which may differ - // from the swapchain extent when the compositor is rotating for us. - std::lock_guard lk(g_inMutex); - g_surfaceW = std::max(ext.width, ext.height); - g_surfaceH = std::min(ext.width, ext.height); - } uint32_t imgCount = caps.minImageCount + 1; if (caps.maxImageCount && imgCount > caps.maxImageCount) @@ -768,6 +1034,34 @@ void present(const void *pixels, uint32_t w, uint32_t h, uint32_t srcPitch, if (!waitForPresentFence(slot.fence, "vkWaitForFences")) return; + // Fit the frame into the panel without distorting it: phones are far wider + // than 16:9, and stretching to fill made everything ~20% too wide. + // + // The swapchain image is portrait while the window is landscape (the + // compositor scales each axis on its own rather than rotating), so the fit + // has to be solved in the landscape space the player actually sees and the + // resulting rect mapped back into image space for the blit. + const int32_t iw = (int32_t)g.swapExtent.width; + const int32_t ih = (int32_t)g.swapExtent.height; + const int32_t scrW = std::max(iw, ih), scrH = std::min(iw, ih); + int32_t vw = scrW, vh = (int32_t)((int64_t)scrW * h / w); + if (vh > scrH) { + vh = scrH; + vw = (int32_t)((int64_t)scrH * w / h); + } + const int32_t vx = (scrW - vw) / 2, vy = (scrH - vh) / 2; + { + std::lock_guard lk(g_inMutex); + g_viewX = float(vx); + g_viewY = float(vy); + g_viewW = float(vw); + g_viewH = float(vh); + } + const int32_t dx0 = (int32_t)((int64_t)vx * iw / scrW); + const int32_t dx1 = (int32_t)((int64_t)(vx + vw) * iw / scrW); + const int32_t dy0 = (int32_t)((int64_t)vy * ih / scrH); + const int32_t dy1 = (int32_t)((int64_t)(vy + vh) * ih / scrH); + auto *dst = static_cast(slot.stagingMap); auto *src = static_cast(pixels); for (uint32_t y = 0; y < h; y++) @@ -834,12 +1128,19 @@ void present(const void *pixels, uint32_t w, uint32_t h, uint32_t srcPitch, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); + if (dx0 > 0 || dy0 > 0 || dx1 < iw || dy1 < ih) { + const VkClearColorValue black{{0.0f, 0.0f, 0.0f, 1.0f}}; + const VkImageSubresourceRange all{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdClearColorImage(slot.cmd, g.swapImages[idx], + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &all); + } + VkImageBlit blit{}; blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; blit.srcOffsets[1] = {(int32_t)w, (int32_t)h, 1}; blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; - blit.dstOffsets[1] = {(int32_t)g.swapExtent.width, - (int32_t)g.swapExtent.height, 1}; + blit.dstOffsets[0] = {dx0, dy0, 0}; + blit.dstOffsets[1] = {dx1, dy1, 1}; vkCmdBlitImage(slot.cmd, slot.frameImg, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, g.swapImages[idx], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); From fb415ea7be33279e64a8aa317fb05244b6d8bc04 Mon Sep 17 00:00:00 2001 From: Vincent Hengel Date: Mon, 27 Jul 2026 22:02:38 +0200 Subject: [PATCH 3/3] android: read DELTA_* knobs from env.cfg and boot PS5 titles An adb-started Activity inherits no environment, so the DELTA_* knobs -- the emulator's whole debug interface -- were unreachable on Android, and with them DELTA_PS5_MODULES, without which no PS5 title can resolve a system module. android_main now reads /env.cfg and defaults DELTA_PS5_MODULES to the staged firmware. The launcher only browsed for .pkg, so PS5 titles could not be picked at all; it now takes .ffpkg and extracted game folders too. DELTA_ANDROID_LOGFILE tees the log pump to a file. logd drops bursts from a chatty uid and anything still in the pipe dies with the process, which is exactly when a crash dump is written. The activity was inset by the display cutout rather than using the whole panel. --- android/AndroidManifest.xml | 2 + .../com/prosperity/ps4/LauncherActivity.java | 43 +++++----- delta/main/android_main.cpp | 81 +++++++++++++++++-- 3 files changed, 103 insertions(+), 23 deletions(-) diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 98f53d51..23983830 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -46,6 +46,8 @@ android:process=":emu" android:screenOrientation="landscape" android:configChanges="orientation|keyboardHidden|screenSize|screenLayout" + android:resizeableActivity="false" + android:windowLayoutInDisplayCutoutMode="shortEdges" android:theme="@android:style/Theme.NoTitleBar.Fullscreen"> diff --git a/android/java/com/prosperity/ps4/LauncherActivity.java b/android/java/com/prosperity/ps4/LauncherActivity.java index 6fafc2f2..bb14c4b3 100644 --- a/android/java/com/prosperity/ps4/LauncherActivity.java +++ b/android/java/com/prosperity/ps4/LauncherActivity.java @@ -206,7 +206,8 @@ private void rebuildGameList() { JSONArray lib = loadLibrary(); if (lib.length() == 0) { TextView hint = new TextView(this); - hint.setText("No games yet. Tap the + button and pick a .pkg on your phone."); + hint.setText("No games yet. Tap the + button and pick a PS4 .pkg, " + + "a PS5 .ffpkg, or an extracted PS5 game folder."); hint.setTextColor(0xFF6A7178); hint.setPadding(dp(16), dp(24), dp(16), dp(16)); gamesContainer.addView(hint); @@ -270,29 +271,35 @@ private View makeGameRow(JSONObject g) { private void addGame() { if (!requireAccess()) return; - browse(Environment.getExternalStorageDirectory(), new String[]{".pkg"}, false, f -> { + // PS4 titles are .pkg; PS5 ones are a .ffpkg backup or an extracted app + // directory (eboot.bin + sce_sys), so folders are pickable too. + browse(Environment.getExternalStorageDirectory(), + new String[]{".pkg", ".ffpkg"}, true, f -> { runBusy("Reading " + f.getName() + " ...", () -> { String titleId = ""; String title = ""; - String info = safePkgInfo(f.getAbsolutePath()); - if (info != null && info.indexOf('\t') >= 0) { - String[] parts = info.split("\t", 2); - titleId = parts[0]; - title = parts.length > 1 ? parts[1] : ""; + String iconPath = ""; + // Title and cover art come from the outer PKG entry table, which + // only a .pkg has; the other layouts fall back to their name. + if (f.isFile() && f.getName().toLowerCase().endsWith(".pkg")) { + String info = safePkgInfo(f.getAbsolutePath()); + if (info != null && info.indexOf('\t') >= 0) { + String[] parts = info.split("\t", 2); + titleId = parts[0]; + title = parts.length > 1 ? parts[1] : ""; + } + File iconsDir = new File(getFilesDir(), "icons"); + iconsDir.mkdirs(); + File iconFile = new File(iconsDir, + Integer.toHexString(f.getAbsolutePath().hashCode()) + ".png"); + try { + if (NativeBridge.pkgIcon(f.getAbsolutePath(), iconFile.getAbsolutePath())) + iconPath = iconFile.getAbsolutePath(); + } catch (Throwable ignored) { + } } if (title.isEmpty()) title = f.getName(); - - String iconPath = ""; - File iconsDir = new File(getFilesDir(), "icons"); - iconsDir.mkdirs(); - File iconFile = new File(iconsDir, - Integer.toHexString(f.getAbsolutePath().hashCode()) + ".png"); - try { - if (NativeBridge.pkgIcon(f.getAbsolutePath(), iconFile.getAbsolutePath())) - iconPath = iconFile.getAbsolutePath(); - } catch (Throwable ignored) { - } addToLibrary(f.getAbsolutePath(), title, titleId, iconPath); return "Added " + title; }); diff --git a/delta/main/android_main.cpp b/delta/main/android_main.cpp index 85ff11e2..97061ebb 100644 --- a/delta/main/android_main.cpp +++ b/delta/main/android_main.cpp @@ -30,6 +30,11 @@ namespace { +// Optional unbuffered copy of the log. logd drops bursts from a chatty uid and +// anything still in the pipe when the process dies is lost with it, which is +// exactly the output a crash dump consists of; a plain file loses neither. +FILE *g_logFile = nullptr; + // Pump stdout/stderr (the emulator logs through both) into logcat so a plain // `adb logcat -s prosperity` shows the whole boot. void *logPump(void *arg) { @@ -41,11 +46,21 @@ void *logPump(void *arg) { --n; buf[n] = '\0'; __android_log_write(ANDROID_LOG_INFO, "prosperity", buf); + if (g_logFile) { + std::fputs(buf, g_logFile); + std::fputc('\n', g_logFile); + std::fflush(g_logFile); + } } return nullptr; } -void redirectStdioToLogcat() { +void redirectStdioToLogcat(const base::String &dataDir) { + if (std::getenv("DELTA_ANDROID_LOGFILE")) { + base::String path = dataDir; + path += "/emu.log"; + g_logFile = ::fopen(path.c_str(), "w"); + } static int pfd[2]; if (::pipe(pfd) != 0) return; @@ -92,6 +107,59 @@ base::String resolveBootPkg(const base::String &dataDir) { return fallback; } +// The DELTA_* knobs are the emulator's whole debug interface and an adb-started +// Activity inherits no environment, so read them from /env.cfg as +// KEY=VALUE lines ('#' comments). Existing variables win, so anything already +// exported (e.g. by the launcher) is not overridden. +void loadEnvConfig(const base::String &dataDir) { + base::String cfg = dataDir; + cfg += "/env.cfg"; + FILE *f = ::fopen(cfg.c_str(), "rb"); + if (!f) + return; + char line[1024]; + while (::fgets(line, sizeof(line), f)) { + char *p = line; + while (*p == ' ' || *p == '\t') + ++p; + if (*p == '#' || *p == '\n' || *p == '\r' || *p == '\0') + continue; + char *eq = std::strchr(p, '='); + if (!eq) + continue; + *eq = '\0'; + char *val = eq + 1; + for (char *e = eq; e > p && (e[-1] == ' ' || e[-1] == '\t');) + *--e = '\0'; + size_t vlen = std::strlen(val); + while (vlen > 0 && (val[vlen - 1] == '\n' || val[vlen - 1] == '\r' || + val[vlen - 1] == ' ' || val[vlen - 1] == '\t')) + val[--vlen] = '\0'; + if (*p) { + setenv(p, val, 0); + LOGI("env %s=%s", p, val); + } + } + ::fclose(f); +} + +// PS5 titles need a decrypted Prospero module set; point at the staged copy +// unless env.cfg already named one. +void defaultPs5Modules(const base::String &dataDir) { + if (std::getenv("DELTA_PS5_MODULES")) + return; + base::String common = dataDir; + common += "/ps5-modules/common_lib"; + if (::access(common.c_str(), R_OK) != 0) + return; + base::String paths = common; + paths += ":"; + paths += dataDir; + paths += "/ps5-modules/priv_lib"; + setenv("DELTA_PS5_MODULES", paths.c_str(), 1); + LOGI("ps5 modules = %s", paths.c_str()); +} + void bootOnce(AppState *s) { if (s->booted) return; @@ -157,14 +225,17 @@ int32_t onInput(android_app *, AInputEvent *ev) { } // namespace extern "C" void android_main(android_app *app) { - redirectStdioToLogcat(); - cpu::earlyInit(); // reserve the FEX heap before any large guest mapping - utl::createLogger(true); - AppState state; const char *ext = app->activity->externalDataPath; state.dataDir = base::String(ext ? ext : "/data/local/tmp/prosperity"); setenv("DELTA_DATA_DIR", state.dataDir.c_str(), 1); + // env.cfg first: it can ask for the log file the redirect is about to open. + loadEnvConfig(state.dataDir); + defaultPs5Modules(state.dataDir); + + redirectStdioToLogcat(state.dataDir); + cpu::earlyInit(); // reserve the FEX heap before any large guest mapping + utl::createLogger(true); LOGI("data dir = %s", state.dataDir.c_str()); app->userData = &state;