Add native touch controls with live layout editor (FYI / optional merge)#1
Add native touch controls with live layout editor (FYI / optional merge)#1mgrz18 wants to merge 77 commits intoizzy2lost:portfrom
Conversation
Crosshair Edge Deadzone slider and fixed `insightaim` Camera Movement
Due to a Windows Update regression: RawInput driver has a issue related to Xbox Controllers' trigger input only being detected. For safety reasons, `useRawInput` will be disabled by default until the bug is fixed on Microsoft's end. for those who still needs it: you can head over to `pd.ini` and revert `useRawInput` back to `1`
Temporarily disables RawInput driver by default
This reverts commit 857e755.
… with struct flag
Fix light transparency for tinted glass doors
fix link to fast
Fix for background lights
Exclude laser doors from lighting line-of-sight test
…csim Limit 'Start Armed' option to Combat Simulator
Addresses the issue with Crosshair Swivel conflciting with each other when using both Mouse and Camera at the same time.
fixed Crosshair Swivel conflicting when using Mixed Input
…clobber Prevent Combat Simulator 'Load Settings' clobber
bcachefs directories return stat.st_size=0
last I check: Xbox Controller doesn't refer "Left Stick Click" as "L3". PlayStation and Steam Deck/Steam Controller (2026) actually refers their variant as "L3".
fix homeDir check
corrected Xbox pad name (LS Click) on Crouch cycle over at `README.md` Controls page
Mipmaps and Anisotropic Filtering
Replace the dead GameView/TouchControls scaffolding (which was wired against a second GLSurfaceView that MainActivity never actually used) with a real touch pad that sits on top of the SDL surface and feeds OSContPad directly: - port/src/touch.c: JNI bridge that publishes the current virtual-pad snapshot (two analog axes + CONT_* button bitmask) with seqlock-style double buffering so the game thread never sees a torn state. - port/src/input.c: inputReadController() calls touchApplyToPad() for player 0 so rebinds keep working and a real controller can still override the on-screen pad. - TouchOverlayView: draws the two sticks and the buttons, handles the multi-touch logic, and exposes an edit mode where elements drag around (or resize, when the RESIZE toggle is on). - TouchLayout: persistent layout (normalized coordinates) stored in SharedPreferences so the user's tweaks survive rotation and reboots. - LayoutEditorActivity: fullscreen editor with Save / Reset. - LauncherActivity: always shows the menu with Play + ROM picker + "Configure Touch Controls" instead of auto-launching the game. - .github/workflows/android.yml: CI job that installs NDK 26 + CMake 3.22 and uploads a debug APK artifact on every push.
The early return at `if (!pads[idx])` meant the on-screen pad only worked when an Xbox-style controller was also plugged in, which is the opposite of what we want on Android.
NDK clang chokes on os_cont.h alone because os_message.h (transitively included) references OSThread. input.c already does this; touch.c/h needed to mirror it.
The overlay now exposes a floating EDIT pill in the top-right corner: tapping it freezes input, shows Save/Resize/Reset/Cancel pills across the top, and turns every element into a draggable/resizable handle. Save persists to SharedPreferences, Cancel restores the snapshot taken when edit started. Saved layouts survive quits and reboots. A new LOOK_PAD element kind replaces the right virtual stick. Within its rectangle, any drag accumulates into rstick velocity scaled by screen size, with a 16 ms decay tick so releasing the finger stops the camera immediately — the same feel as CoD Mobile or Fortnite. LayoutEditorActivity keeps its own widget-based HUD and now calls setInternalHudVisible(false) to avoid double UI.
Replaces the fixed LEFT_STICK with a MOVE_PAD — a rectangular zone where the stick center anchors to wherever you touch down. Drag from that anchor deflects the stick up to its max radius (a per-element field, default 0.09 of the shorter screen dimension). Release zeroes the stick so the character stops immediately. Same ideia as the LOOK_PAD: you get the whole right half to move, not a tiny circle to aim your thumb at. Buttons float over the pad and win hit-test thanks to their later order in the element list.
The rstick is a velocity signal — set it to 0.3 and the camera keeps rotating as long as the value holds, even after the finger stops. That is exactly the 'palanca' behavior the user was trying to avoid. Switch to the same channel real mice use: bondmove.c already calls inputMouseGetScaledDelta() under allowmlook (which is on during gameplay), and the default mouseaimmode is MOUSEAIM_CLASSIC. Now every move-event on the LOOK_PAD adds the pixel delta (scaled by LOOK_SENS_PX = 0.4) into a native accumulator that inputUpdateMouse drains once per frame into mouseDX/mouseDY. Consumed, not held — releasing the finger stops the camera immediately.
Default layout is now the CoD Mobile standard: MOVE pad on the left, LOOK pad on the right, shooting buttons clustered around the LOOK pad. Weapon helpers sit over the MOVE pad. Sensitivity of the look pad moved from a Java compile-time constant to TouchLayout.lookSens so it persists with the rest of the layout. The live-edit HUD now has SENS - / SENS + pills around a readout of the current value, stepping by 0.05 between 0.05 and 2.0.
PD's default CONTROLMODE_PC (enabled when extcontrols is true, which is the default in PLAYER_EXT_CFG_DEFAULT) routes stick1 to camera turn/pitch and stick2 to walk/strafe. Our MOVE_PAD was writing to stick1 (left), which meant dragging the movement pad was turning the camera and you could never walk anywhere. Swap MOVE_PAD to feed the right stick instead. LOOK_PAD is unaffected — it still injects mouse delta, which bondmove already adds as freelookdx/freelookdy on top of the camera.
- Split lookSens into lookSensX / lookSensY, persisted separately. The legacy single _lookSens pref is used as fallback so existing layouts keep their current feel. - Bumped range to 0.05..5.0 (was 0.05..2.0) and step to 0.1, so you can reach useful high-turn speeds without 80+ taps. - Edit HUD is now two rows: actions on top (SAVE / RESIZE / RESET / CANCEL), per-axis sens on the bottom (X- 0.40 X+ Y- 0.40 Y+). - Shrunk default movepad/lookpad hw from 0.25 to 0.23 so the two rectangles no longer overlap across the screen midline. Before, any touch in that 4%-wide strip went to the lookpad (drawn last) instead of the movepad, which looked like a control bug. Hit RESET in the editor to pick up the new rectangles.
- Edit HUD now has a ▲ / EDIT ▼ pill in the top-right corner that collapses the two-row bar down to just that pill, so you can line up buttons under the top row without the bar in the way. Tap the pill again to bring the bar back. State resets on each new entry into edit mode. - Move and look pads now default to each covering exactly half the screen (cx=0.25 / 0.75, cy=0.50, hw=0.25, hh=0.50). Tap RESET in the editor to pick up the new defaults. - Pad labels moved from the top of the pad to its center so they never collide with the START/CANCEL row at the top or the CROUCH button at the bottom.
- Tapping a button in edit mode now selects it and shows a per-button resize bar (− / r=0.050 / +) right below it (or above, if that would fall off-screen). Step is 0.005 per tap over 0.03..0.15. Tap anywhere else to clear the selection. - MOVE_PAD and LOOK_PAD no longer render their rectangle, fill or label. They are simply the left and right halves of the screen and adapt to whatever surface the phone has. The floating stick knob still draws while the user touches the move pad, as feedback. - Pads are also skipped by the edit hit test (hitTestEditable), so the user can't accidentally drag or resize them. - Removed the global RESIZE toggle (pill in the HUD + setResizeMode API + resizeMode field in LayoutEditorActivity). Per-button bar replaces it entirely.
Each button now renders an icon that looks like what it does: fire - orange ring around a red bullseye aim - scope reticle (ring + crosshair + center dot) use - open-box arrow reload - 3/4 circular arrow with arrowhead altfire - hollow ring + center dot wprev - left-pointing filled triangle wnext - right-pointing filled triangle radial - 3x3 dot grid crouch - down chevron over a floor bar start - pause bars cancel - X mark Icons auto-scale with the button's radius so they look right at any size the user picks via the per-button resize bar. Unknown ids still fall back to a text label.
Solves a issue when using Input Remapper's Mouse-based Reset to Horizon camera function (e.g. Steam Input), where the recenter couldn't stay centered.
To prepare for fgsfdsfgs#627, this commit added Accept/Cancel Swap for Menu Navigation. This function only applies when using a Nintendo Switch controller, but it can be forcefully enabled for all Controller Types within `pd.ini`'s `swapAcceptCancelButtons`
You can now recenter the camera vertically (similar to id Tech-based games, FEAR and Splatoon). Additionally, for Mouse Input users (and soon: Gyro users, hence this PR uses `cidx`): they can now reset the crosshair to the center!
To differenciate between current and future Input Method or Styles: the Mouse Settings's "Crosshair Speed" now has a "Mouse" at a start of the name. That way: you can easily can tell that this specific slider is specifically for Mouse Input.
this might mess up existing mouse crosshair sens setup in the process
Cherry-picked PR fgsfdsfgs#676 added #include "data.h" after SDL.h, which works on Linux but chokes on Android because Android NDK's libc ships overloadable attributes on memcpy / memset / memcmp that clash with the PR/os_libc.h prototypes data.h transitively pulls in. Placing data.h first lets the N64 declarations define the overload set up front so the NDK headers can be pulled in on top without conflict.
If the ROM already exists in app-scoped storage and its MD5 matches the recommended NTSC v1.1 build, skip the launcher menu and hand off straight to MainActivity on app start. The menu with Play / Pick ROM / Configure Touch Controls is still the fallback when no valid ROM is detected; touch-layout editing also remains available in-game via the EDIT pill.
The overlay (buttons + EDIT pill + floating-stick feedback) now dims to ~12% alpha after 3s of no touches, snapping back to full on the next tap. Edit mode disables the fade so the user always sees what they are editing. The behavior is persisted on TouchLayout.idleFade (default on) and toggled from a new FADE: ON / FADE: OFF pill next to the RESET button in the edit HUD. Animation is driven by postInvalidateOnAnimation only while the alpha is actively transitioning, so there is no idle-time battery cost once the fade has settled.
Bumped the inactivity threshold from 3 s to 7 s and dropped the settled alpha from 0.12 to 0.0 so the overlay becomes fully invisible once it finishes fading out. Hit-testing still works through the invisible overlay, so the first tap snaps it back to full opacity and registers as a button / pad press in the same gesture.
Spanish, user-facing. Covers: features, requirements, install and play flow, default layout and the in-game editor, credits to the upstream decomp team, fgsfdsfgs, izzy2lost, relevant PR authors, Rare/Nintendo, and Claude. Adds an issues / PR pointer and a license + known-state section.
There was a problem hiding this comment.
Code Review
This pull request introduces native Android touch controls for the Perfect Dark port, including a live-editable overlay, custom input handling, and necessary adjustments to the input and video systems to support the new platform. My review identified several critical and high-severity issues, including a flawed button-swap logic that forces an A/B swap, a race condition in the Seqlock implementation for touch state, and performance concerns regarding off-screen buffer usage in the overlay. Additionally, I noted issues with input responsiveness due to rounding errors, incorrect global disabling of Raw Input, and confusing UI navigation changes.
| static inline s32 inputBindPressed(const s32 idx, const u32 ck) | ||
| s32 inputBindPressed(const s32 idx, const u32 ck) | ||
| { | ||
| u32 swapped_ck = ck; |
There was a problem hiding this comment.
Hay un error crítico en la lógica de intercambio de botones A/B. La función inputConfirmCancelButtonSwap devuelve el valor de ck si no hay un intercambio activo o si ck no coincide con los botones de UI. Como ck es un valor distinto de cero (un índice de botón), la condición if siempre se evalúa como verdadera, provocando que los botones A y B se intercambien siempre, independientemente de la configuración del usuario. Además, se está pasando un contkey a una función que espera un bitmask de botón de UI.
if (inputConfirmCancelSwapActive(idx)) {| atomic_store_explicit(&liveSeq, seq + 1, memory_order_release); | ||
|
|
||
| snaps[slot].lx = lx; | ||
| snaps[slot].ly = ly; | ||
| snaps[slot].rx = rx; | ||
| snaps[slot].ry = ry; | ||
| snaps[slot].buttons = (u32)buttons; | ||
|
|
||
| // publish end (even seq); flip slot for next writer. | ||
| atomic_store_explicit(&writeSlot, 1 - slot, memory_order_release); | ||
| atomic_store_explicit(&liveSeq, seq + 2, memory_order_release); |
There was a problem hiding this comment.
La implementación del Seqlock tiene un riesgo de carrera (race condition) debido al reordenamiento de memoria. atomic_store_explicit con memory_order_release garantiza que las operaciones anteriores no se muevan después del store, pero no impide que las operaciones posteriores (como la escritura en snaps) se muevan antes del primer store a liveSeq. Si esto ocurre, un lector podría leer datos parcialmente escritos mientras liveSeq todavía parece estable (par).
Para una implementación robusta en C11, se recomienda usar memory_order_seq_cst para las actualizaciones de la secuencia o insertar barreras explícitas.
atomic_store_explicit(&liveSeq, seq + 1, memory_order_seq_cst);
snaps[slot].lx = lx;
snaps[slot].ly = ly;
snaps[slot].rx = rx;
snaps[slot].ry = ry;
snaps[slot].buttons = (u32)buttons;
// publish end (even seq); flip slot for next writer.
atomic_store_explicit(&writeSlot, 1 - slot, memory_order_release);
atomic_store_explicit(&liveSeq, seq + 2, memory_order_seq_cst);| float dx = (x - lookLastX) * layout.lookSensX; | ||
| float dy = (y - lookLastY) * layout.lookSensY; | ||
| lookLastX = x; lookLastY = y; | ||
| int idx = Math.round(dx), idy = Math.round(dy); | ||
| if (idx != 0 || idy != 0) { | ||
| nativeAddLookDelta(idx, idy); |
There was a problem hiding this comment.
La conversión directa de float a int mediante Math.round en cada evento de movimiento provoca una pérdida de precisión significativa, especialmente con sensibilidades bajas. Si el movimiento en un frame es pequeño (por ejemplo, 0.4 píxeles), se redondea a 0 y el movimiento se pierde por completo. Esto hace que los controles se sientan poco responsivos o "stuttery" al realizar movimientos lentos.
Se recomienda acumular el residuo decimal y sumarlo al siguiente evento.
| overlayLayer = canvas.saveLayerAlpha(0, 0, w, h, | ||
| (int) (fadeFactor * 255f)); |
There was a problem hiding this comment.
El uso de canvas.saveLayerAlpha es costoso en términos de rendimiento ya que requiere la creación de un buffer fuera de pantalla (off-screen buffer). Dado que los elementos del overlay (círculos, líneas, iconos) no suelen solaparse de forma compleja, es mucho más eficiente aplicar el factor de transparencia directamente a los objetos Paint utilizados para dibujar cada elemento.
| static s32 useHIDAPI = 1; | ||
| #endif | ||
| static s32 useRawInput = 1; | ||
| static s32 useRawInput = 0; |
There was a problem hiding this comment.
Desactivar useRawInput de forma global afecta a todas las plataformas (Windows, Linux, etc.). En plataformas de escritorio, el uso de Raw Input es preferible para evitar la aceleración y el escalado del sistema operativo en el mouselook. Este cambio debería estar restringido a Android si es necesario para esa plataforma.
#ifdef ANDROID
static s32 useRawInput = 0;
#else
static s32 useRawInput = 1;
#endif| } else if (key == VK_ESCAPE) { | ||
| return -1; |
There was a problem hiding this comment.
Se ha eliminado el soporte para la tecla VK_RETURN (Enter) para confirmar la entrada de texto, y en menuitem.c se ha cambiado el texto de ayuda a "ESC: OK". Esto es extremadamente contraintuitivo para los usuarios, ya que la tecla Escape se asocia universalmente con la acción de cancelar, no con confirmar. Se recomienda mantener el comportamiento estándar de Enter para confirmar.
| s32 displayKey = menuSwapConfirmCancel(binds[data->dropdown.value], menuBinds[idx].ck); | ||
|
|
||
| strncpy(keyname, inputGetKeyName(binds[data->dropdown.value]), sizeof(keyname) - 1); |
There was a problem hiding this comment.
La variable displayKey se calcula pero no se utiliza en la llamada a inputGetKeyName. Esto provoca que el nombre del botón mostrado en el menú de configuración no refleje el intercambio de botones A/B si este está activo.
| s32 displayKey = menuSwapConfirmCancel(binds[data->dropdown.value], menuBinds[idx].ck); | |
| strncpy(keyname, inputGetKeyName(binds[data->dropdown.value]), sizeof(keyname) - 1); | |
| s32 displayKey = menuSwapConfirmCancel(binds[data->dropdown.value], menuBinds[idx].ck); | |
| strncpy(keyname, inputGetKeyName(displayKey), sizeof(keyname) - 1); |
|
It's a start. There's a lot I would change though. On screen controls is something I don't like doing. I find it to be difficult. Especially making it editable. |
Hola Izzy 👋 — no es un PR urgente, es más bien un FYI por si quieres integrar / cherry-pickear / tomar ideas. No hay problema si lo cierras sin mergear.
Qué trae
Partí de tu fork (que me dejó prácticamente resuelto el andamiaje Gradle + SDL2 +
LauncherActivitycon SAF, gracias por eso) y añadí controles táctiles nativos al estilo CoD Mobile / Fortnite encima del overlay de SDL. El resumen:inputUpdateMousepara que el juego lo trate como mouselook y se detenga al soltar, no como stick analógico).SharedPreferences.fgsfdsfgs/perfect_dark): fix de SIGBUS en ARM 32-bit (nivel 3), vertical camera wrap, UI Accept/Cancel swap, Camera Recenter action, separación de sensibilidad crosshair/cámara..github/workflows/android.yml) que compila APK en cada push.Detalles técnicos relevantes
GameView.javayTouchControls.javaoriginales los borré — quedaban desconectados porqueMainActivity extends SDLActivityrenderea por su cuenta, así que ese camino no se usaba.port/src/touch.ccon un seqlock simple de doble buffer para que el thread del juego nunca lea un snapshot a medio escribir.TouchOverlayViewes unViewque se agrega aSDLActivity.mLayoutcomo sibling de la SDL surface; los touches los consume el overlay primero y solo pasa al SDL surface cuando no hay hit.rstick_x/y(que enCONTROLMODE_PCcontrola strafe), sino que acumula pixeles en un atomic quetouchConsumeLookDeltadrena una vez por frame haciamouseDX/mouseDY. Eso da la sensación correcta: arrastras, gira; sueltas, se detiene. Es exactamente lo que la gente espera viniendo de Quake Mobile / CoD Mobile.rstick_x/y(porque enCONTROLMODE_PCstick2 es walk/strafe).Release + APK
Hay release público con APK debug en el fork: https://github.com/mgrz18/perfect_dark/releases/tag/touch-v0.1.0
Créditos
El README nuevo (mgrz18/perfect_dark) te lista explícitamente como autor del andamiaje inicial de Android, junto con
fgsfdsfgspor el port a plataformas modernas yn64decomppor el decomp original.Gracias por el trabajo que ya tenías — ahorró muchísimo tiempo. Si quieres integrar esto de vuelta o usarlo como referencia, eres libre de hacerlo. Si prefieres cerrarlo sin más, también está bien 🙏
Este PR fue preparado con asistencia de Claude (Anthropic) para acelerar la parte de ingeniería.