Skip to content

Feature/device profiles - #751

Merged
maxjivi05 merged 13 commits into
WinNative-Emu:mainfrom
maxjivi05:feature/device-profiles
Sep 12, 2026
Merged

Feature/device profiles#751
maxjivi05 merged 13 commits into
WinNative-Emu:mainfrom
maxjivi05:feature/device-profiles

Conversation

@maxjivi05

Copy link
Copy Markdown
Contributor

No description provided.

…tton presses

WinNative shipped one set of bundled touch layouts for every device. Their
normalized coordinates are resolution-independent, but element size is derived
from view width alone (snappingSize = width/100), so on a 16:10 tablet the same
layout stretches vertically relative to its own buttons and the inboard controls
drift out of thumb reach.

Measured on a RedMagic Astra 2 (2400x1504 at 310.6 x 312.6 dpi = 195.0 x 123.0 mm,
9.08 in): with the grip pivot 5 mm outboard of the panel and 37 mm up from the
bottom, the stock layouts put the D-pad at 67 mm and the right stick at 66 mm from
the thumb pivot, and the inboard face button at 55 mm. Comfortable thumb travel on
a tablet held in two hands is roughly 42 mm neutral, 53 mm comfortable, 64 mm
stretch. Three of those controls were past the limit for their usage tier.

Device Profile

  DeviceProfile classifies the host from the marketing-name properties
  (ro.vendor.product.ztename, ro.vendor.feature.nubia.exif.module) and a Build
  token haystack, so all regional Astra 2 SKUs match (PQ85P01 board family, NP06J
  model family) while other RedMagic and nubia devices do not. Anything unmatched
  is Default. PluviaApp seeds the choice once, after the child-process guard;
  the user can override it from the new Settings > Device page, and the override
  is never overwritten by later detection.

  The profile is a global app setting built as the single seam for per-device
  defaults. Input control layouts and the adaptive-joystick recommendation are
  wired up now; library icon sizing, session menu button sizing and per-game
  recommended settings are listed on the page as Coming Soon and only need their
  resolver pointed at DeviceProfileSettings.

Bundled layouts

  Device variants live in a sibling asset directory (inputcontrols/profiles-astra2)
  rather than a subdirectory of profiles/. AssetManager.list returns directories
  too, and the sync loop opens every entry inside a single try block, so a nested
  variant directory would throw FileNotFoundException on the directory entry and
  abandon the rest of the loop - with the gate prefs already committed, that would
  silently skip the sync until the next app version. The listing now also filters
  to .icp, and the gate commits after the copy loop rather than before it.

  Switching device profile re-enters the sync through a third gate key. A working
  layout is replaced only when it still matches its pristine backup
  (FileUtils.contentEquals); an edited layout is left alone, while its backup is
  refreshed so Reset offers the layout bundled for the current device. Profile ids
  stay 1/2/3/6/7/8 in every variant because shortcuts persist the id and both the
  drawer and the shortcut dialog index by list position.

  All six Astra 2 layouts follow a DualSense split: D-pad above the left stick,
  ABXY above the right stick, triggers in the top outer corners for the index
  fingers, Start and Select centred out of both thumb arcs. L3 sits on the right
  half and R3 on the left, since the thumb owning a stick cannot also click it.
  Every gameplay control is inside its tier limit, no two elements overlap, and
  nothing sits closer than 4 mm to a neighbour or 6 mm to a screen edge.

Press semantics

  A BUTTON no longer releases when the finger slides outside its bounds; it is
  held until the pointer actually lifts, which is what the release-on-exit branch
  in handleTouchMove used to prevent. Bundled layouts never persist the swipeable
  flag, so its true default made every shipped button a swipe target and the old
  behaviour dropped a held button on any drift.

  A pointer now captures a list of elements instead of one, so stacked buttons
  both fire from a single touch. The first accepting element is captured whatever
  its type, further BUTTONs at the same point are added, and an expanded radial
  menu suppresses co-capture because its hit test balloons well past its box.
  ACTION_DOWN and ACTION_UP run inside the batching window so one pointer pressing
  several bindings still produces a single gamepad state send. Sliding a held
  button onto another swipe target now adds it rather than swapping, and the
  D-pad and radial hand-off path is unchanged.

  The L3/R3 minimum-hold release carried no pointer identity, so a re-press within
  300 ms was cancelled by the previous release's pending callback; it now checks a
  press generation. releaseActiveTouchElements also syncs the captured-pointer set,
  which it previously left stale for TouchpadView for one gesture.

Adaptive joysticks

  The four sites that resolved the container default to a hard-coded "0" now read
  the profile recommendation, so Astra 2 containers that have never set the extra
  default to adaptive. Containers with an explicit value are untouched.

Also back-fills library_games_store_none_owned, which was added to the base strings
without translations, and adds the 34 new Device Profile strings to all 22 locales.
The first pass placed the two sticks asymmetrically - left at 25.7 mm and high,
right at 23.4 mm and low - because it was reproducing an Xbox face, where the
sticks sit at different heights. On a touchscreen there is no handle offset to
justify that, and it left the clusters spread over the full height of the panel.

Both sticks are now identical and exactly mirrored: 23.4 mm, centred at x = 460
and x = 1940, both at y = 1150, both 43.1 mm from their grip pivot. The D-pad and
the ABXY diamond mirror each other too, at x = 301 and x = 2099, sharing y = 710.

Everything moved down and closer together. Measured from the pivot 5 mm outboard
of the panel and 37 mm up from the bottom:

  control            before     now
  face buttons       55.0 mm    50.5 mm
  D-pad              55.1 mm    40.6 mm
  right stick        49.7 mm    43.1 mm
  L3 / R3         29 / 61 mm    27.8 mm both

Every thumb control now lives in the bottom 78 mm of the 123 mm panel instead of
being spread over 100 mm, and the ABXY diamond is tighter - a 155 px radius with
11.7 mm buttons, so the thumb covers all four without leaving the cluster. L3 and
R3 moved into the bottom outer corners, the easiest reach on the panel.

The diamond radius also has to clear twice the button half-size or the square
bounding boxes overlap even when the drawn circles do not, and the engine hit
tests against the box. GameHub now shares the Virtual Gamepad geometry outright
rather than carrying its own scales, since VisualStyle only changes how elements
are drawn. RTS keys drop to scale 1.05 on a 190 px row pitch so the grid keeps a
5 mm gap and still clears the bottom edge.
…umb rest

The previous two passes were tuned against a guessed grip. Reviewing them turned
up three things that invalidated parts of that work.

First, the hardware. The Astra 2 body is 207.1 x 134.2 x 6.9 mm at 363 g, the
panel is 195.0 x 123.0 mm (310.6 x 312.6 dpi, 9.08 in), there is no display
cutout anywhere, and the tablet has NO shoulder triggers of any kind - physical
or capacitive - unlike RedMagic's phones. So every bumper and trigger has to be
on-screen, and the index fingers are not pinned to the top edge by hardware.
The top edge does carry the volume rocker and the red Magic Key, which jumps
straight into Game Space; an index finger curling over the top-right corner can
hit it, so the triggers moved inboard from the extreme corners (RT now has
21.5 mm of clearance from the right edge instead of 7 mm).

Second, the grip. Reach was being computed from a pivot assumed to sit 5 mm
outboard of the panel edge, 37 mm up. Published anthropometry puts the thumb MCP
on the glass, roughly 20-35 mm inboard, at or just below panel centre - a
difference larger than the entire design tolerance. Evaluating the shipped layout
both ways gave contradictory verdicts, so the pivot was measured instead:
25 seconds of raw contacts off /dev/input/event6 while the tablet was held
normally, 1030 samples splitting 523/507 into two clusters, both medians at
33 mm inboard and 35 mm up from the bottom edge. That is now the anchor, and it
is a measurement rather than a model.

Third, spacing. Adjacent face buttons were 17.8 mm centre to centre. A thumb
contact patch is about 25 mm wide, so independent presses need roughly 20 mm of
centre-to-centre separation; below that the thumb straddles two targets. The
diamond radius went from 155 to 174 px, which puts adjacent centres at exactly
20.0 mm, and the face buttons grew from 11.7 to 13.3 mm - above the measured
9.2-9.6 mm thumb plateau with margin for the +25% scatter seen away from screen
centre.

Every element is now inside its limit under all three candidate pivot models,
not just one: held controls (both sticks, D-pad) never exceed 45 mm and taps
never exceed 60 mm whether the grip is low, middle or high. Measured against the
captured thumb rest, nothing sits more than 40.5 mm away and most controls are
within 30 mm.

The sticks stay uniform and exactly mirrored at 23.4 mm. Worth noting that the
hand-edited Virtual Gamepad already on the tablet places its sticks 30-37 mm
inboard and 24 mm up, which is within a few millimetres of both the measured
thumb rest and where this layout puts them - three independent estimates
agreeing. That profile is left untouched by the sync because it differs from its
pristine backup.
Sticky press kept a button down when the finger drifted off it, but the move
handler still looked for another swipe target under that finger and pressed it
as well. Holding RT and drifting onto RB therefore ended up holding both, which
is worse than the drop it replaced: the finger never left the screen, so there
was no moment at which the player asked for the second press.

A pointer that already holds an element no longer acquires anything else. The
element stays down until the pointer actually lifts, and a second button needs a
genuine release and a fresh press. The branch removed here was the only path
that could add a capture mid-gesture for a BUTTON.

What this does not change: overlapping elements still both fire when the finger
lands on them together, since that press is deliberate; several fingers still
hold several buttons independently; and the D-pad and radial-menu hand-off is
untouched, because it is gated on the held element being a D_PAD or RADIAL_MENU
and is how a directional swipe is meant to work.
The diamond had been widened to 20.1 mm between adjacent buttons on the grounds
that a thumb contact patch is about 25 mm across, so independent presses needed
20 mm of separation. That reasoning does not apply to this engine: hit testing
compares the touch CENTROID against bounding boxes, so contact width cannot
cause a false press. The buttons were spread for no benefit and the cluster was
reported as too far apart to use.

The constraint that does bind is that bounding boxes are SQUARE. On a diamond,
diagonally adjacent boxes only clear when the radius is at least twice the
button half-size, so larger buttons force wider spacing - which is why the stock
phone layout pairs 11.7 mm buttons with 17.9 mm spacing.

Compressing the diamond vertically into a rhombus, 164 px wide by 110 px tall,
keeps the boxes clear at full button size. Vertical is the right axis to give up:
the thumb moves measurably slower in flexion and extension than in adduction and
abduction, so shortening the up-and-down hops costs the least. The cluster centre
was then placed to minimise the farthest button from the thumb rest measured off
the digitiser at 33 mm inboard and 35 mm up.

  adjacent travel          20.1 -> 16.1 mm
  farthest from the rest   40.5 -> 27.8 mm
  average from the rest    28.3 -> 21.3 mm
  button size              13.3 mm, unchanged

A now sits 11.2 mm from where the thumb rests. The D-pad mirrors the new centre,
which brings it from 26.5 to 19.2 mm on the left.

The layout check was wrong too. Its 4 mm edge-gap floor was invented rather than
derived, and it is the wrong test for a single cluster of related buttons. Face
button pairs are now checked on what actually matters: bounding boxes must not
overlap, since co-press would otherwise fire two buttons from one touch, and
centres must be at least 15 mm apart so a mis-hit needs more than a 2.5 sigma
targeting error. The visible gap is 2.8 mm, which is tight to look at and safe by
that test.
Changing the contents of a bundled .icp does nothing on its own. The sync gate is

  oldVersion == newVersion && oldSyncRevision >= REVISION && !deviceChanged

and a debug rebuild changes none of those three, so copyAssetProfilesIfNeeded
returns before the copy loop. The tightened face-button cluster was committed,
built, signed and installed, and the layouts on disk still read 20.1 mm between
adjacent buttons - the new assets never left the APK.

The revision goes to 10. Verified on device: the pref advanced to 10 and all five
bundled layouts carrying a face-button cluster now read 16.1 mm.

Any future edit to a file under assets/inputcontrols/profiles* has to bump this
constant in the same commit or it ships to nobody.
… rest

The previous passes optimised every control to sit as close to the thumb as
possible. That is not how a controller feels: on a real pad the thumb rests ON
the stick and the face buttons are a deliberate reach up and out from it. The
result had looked scattered rather than like a controller, and the sticks sat
13 mm below and 6 mm outboard of where the thumbs were measured to rest.

Each half is now built as a DualSense half scaled to the panel:

  stick on the measured rest        3.6 mm from it, was 14.6
  cluster centre from the stick     34.8 mm   (DualSense ~39)
  far button from the stick         47.0 mm   (DualSense ~55)
  face buttons                      12.2 mm at 17.5 mm, a perfect diamond
  D-pad                             26 mm, mirroring the cluster centre

A DualSense puts the cluster ~22 mm outboard of the stick. With the thumb rest
only 33 mm from the panel edge the cluster can move ~7 mm outboard before the far
button leaves the screen, so it goes up instead and every other proportion is
kept. The diamond is square again rather than the squashed rhombus of the last
revision, at r = 152 px with 75 px buttons, which keeps the square hit boxes
clear without shrinking the buttons below the thumb plateau.

L3 and R3 move to the gap between stick and cluster, up-inboard of the stick.
That is inside 25 mm of the measured rest and inside 60 mm from every candidate
grip, where the previous stick-height slot failed the low grip at 62 mm.

Every pair mirrors exactly: sticks, triggers, bumpers, L3/R3, Select/Start, and
the D-pad against the cluster centre, at identical scale on both sides.

Asset sync revision goes to 11 so the layouts install.
The triggers had been pulled 12 mm inboard, to x = 380 / 2020, on the theory that
an index finger curling over the top-right corner could hit the tablet's Magic
Key and drop into Game Space mid-game. In use they were reported as too far in
and awkward to reach. An index finger wrapping the top edge lands nearest the
corner it wraps, so that is where the buttons belong; the Magic Key concern was
hypothetical and the person holding the device is the better judge.

LT/LB and RT/RB move to x = 230 / 2170. The pair now spans 9 to 28 mm in from
its side edge instead of 21 to 40. Vertical placement is unchanged: the upper
button 6.5 to 16 mm from the top, the lower 21 to 30 mm. Every other element is
where the last revision put it.

Asset sync revision goes to 12 so the change installs.
Two more of the Device Profile's promised surfaces are wired up.

Library artwork. The grid capped every card at 1.25 times its width - a
portrait card - and drew the primary artwork into it with ContentScale.Crop.
Steam's primary slot resolves to the 460x215 header, so a 2.14:1 landscape image
was centre-cropped into a 0.8:1 card and lost most of its width; itch covers are
630x500 and Epic's wide banner and GOG's hero art are 16:9, all landscape too.
The Astra 2 profile now asks for 3:2 cards (height 0.67 of width; the title
strip takes the rest, leaving the image close to 16:9), three columns in
landscape and two in portrait, and a wide-artwork preference that picks Steam's
header explicitly, Epic's DieselStoreFrontWide banner ahead of the tall box, and
GOG's hero art ahead of the square tile. The Default profile keeps the stock
1.25 portrait card, the stock column ladder and the stock slot order, so phones
are untouched. The carousel and list paths keep their own slots.

Session drawer buttons. ActionCardGrid sized each card by dividing the leftover
drawer height by the row count, floored at 72 dp. On a phone that leftover is
small; on the Astra 2 the drawer is 668 dp tall, so six cards in two rows became
about 220 dp tall by 88 dp wide. The row height now also respects a per-profile
maximum aspect: Astra 2 caps cards at 1.0 so they are square (88 x 88 dp on the
300 dp drawer), Default keeps Float.MAX_VALUE and fills as before. The
computation is a pure function so the geometry is unit-tested at the Astra 2
numbers rather than eyeballed.

The Device page now reports both surfaces as live instead of Coming Soon, with
four new strings in the base and all 22 locales.
… Astra 2

The 0.67 card factor was a guess at "3:2 card minus a title strip" and it left
the image box at roughly 1.8:1. Steam headers are 460x215, 2.14:1, so with
ContentScale.Crop the sides went - "RAGON'S DOGM", "ONSTER HUNTER". The grid now
derives the row height from the image itself: column width divided by the
profile's image aspect, plus the 24 dp title strip (4 dp padding, one bodySmall
line, 4 dp padding). The Astra 2 profile's aspect is the Steam header exactly,
so headers fit edge to edge; Epic's wide banner and GOG's hero art lose a thin
band top and bottom. The Default profile reports no aspect and keeps the stock
1.25 card factor.

Landscape goes from three columns to four. On the 1067 dp panel that is about
245 dp per column and a 138 dp card, which fits three and a half rows in view.
Portrait stays at two.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@maxjivi05
maxjivi05 merged commit 9bc9dc7 into WinNative-Emu:main Sep 12, 2026
4 checks passed
@maxjivi05
maxjivi05 deleted the feature/device-profiles branch September 12, 2026 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant