Conversation
…draw keys
The config code was a bag of mutable fields whose setters wrote the file. That
put a full synchronous write to external storage on the main thread behind every
assignment, and forced an `isInitializing` flag to suppress it while loading.
Defaults were spelled out in three places that disagreed with each other, range
checks lived in the UI, and every failure was swallowed by a bare `runCatching {}`.
Model and storage
- MGConfig is an immutable data class. Each enum carries both the integer that
goes on disk and the label shown in the spinner, and the adapters are built
from the enums, so the option list and the value space cannot drift apart.
res/values/array.xml is gone.
- MGConfigCodec owns the key names and the fallback for every field. Decoding
never throws: a single broken field falls back on its own instead of
discarding the whole file. Keys the app does not know -- native reads
hideMGEnvLevel -- survive a save instead of being stripped.
- MGConfigStore is the only code that touches MG/config.json. An edit updates a
StateFlow at once and is written after a 300 ms debounce on Dispatchers.IO,
through temp file + fsync + rename so the game cannot read a torn file. It
writes only when something actually changed, which stops "open the app and
leave" from rewriting the file, and reports failures instead of hiding them.
- A damaged config is backed up and the user is asked what to do; it is never
silently replaced by defaults.
- customGLVersion is clamped the way settings.cpp clamps it, so a hand-edited 38
is no longer rewritten to 0 -- which native reads back as 4.0.
UI
- One-way rendering: render(config) is the only writer of the views, and every
callback returns early when the incoming value already equals the config.
That replaces isSpinnerInitialized, revertSpinner() and the post{} timing
tricks, which could not have worked anyway since AbsSpinner posts its
selection callback rather than firing it inline.
- Warning dialogs became suspend functions, so "ask, then apply" is a straight
line instead of a pair of callbacks.
- The GLSL cache size is a slider (off .. 1/16 of RAM) on a quadratic scale, and
clearing the cache is an explicit button that appears only while the cache is
off and the file still exists -- not a side effect of assigning -1, which used
to delete the cache on every load of such a config.
MultiDraw
- multidrawMode is dead: native ignores it and only warns when it is present. It
is replaced by one key per entry point carrying a backend NAME, plus the global
multidrawDisableBackends list. The per-entry allowed sets mirror k_md_entries,
because most backends are not a distinct strategy for most entry points.
- The section is collapsed by default behind a one-line summary; its rows and
chips are generated from the enums.
- The submodule moves to 55ef3c4, where these keys are read. The two changes
cannot be separated: the app no longer writes multidrawMode, so on the previous
native every multidraw choice would silently do nothing.
Adds unit tests for the on-disk contract and for the cases where the store must
not write, and declares kotlinx-coroutines and lifecycle-runtime-ktx explicitly
instead of relying on them arriving transitively through appcompat.
…g keys
MobileGlues renamed the two backends that were called "native" and "nativeext"
after the extensions that actually provide them:
native -> multibasevertex glMultiDrawElementsBaseVertexEXT
nativeext -> multiarrays glMultiDraw{Arrays,Elements}EXT
config.json carries these names rather than indices, so the app has to speak the
new spelling: on the current native every value it used to write is rejected as
an unknown backend name and silently falls back to auto.
Reorders MultidrawBackend to match md_backend_t. The order is not cosmetic -- it
groups the backends that issue one driver call per sub-draw (unroll, basevertex,
indirect) ahead of the ones that issue a single call for the whole batch
(multiarrays, multibasevertex, multiindirect), and it decides both the order of
the chips and the order of the names written to multidrawDisableBackends.
The Chinese labels for these two are dropped rather than retranslated. GLES core
has no multi-draw command at all, not even 3.2, which added only the singular
glDrawElementsBaseVertex; every batched backend here therefore comes from an
extension, and calling one of them "native" claimed a distinction that does not
exist. Both now show their technical name, the way BaseVertex, Indirect and
MultiIndirect already did.
Moves compute to the end of the glMultiDrawElementsBaseVertex list as well: it is
in that entry point's allowed set but not in its auto ladder, so it is reachable
only by an explicit choice and should not sit among the ones auto can pick.
Adds a test pinning the full list of backend names and entry-point keys, so a
later rename on the native side fails the build instead of quietly writing values
the loader throws away.
The submodule moves to 0a3d4f2, where the new names are read.
Kotlin 2.1.10 -> 2.3.20 with the compose compiler plugin, Compose BOM 2026.06.01, activity-compose, lifecycle-runtime-compose, documentfile and miuix-ui 0.9.0. Drops appcompat/constraintlayout/google-material and viewBinding, moves kotlinOptions to the compilerOptions DSL, and reduces the window theme to a plain NoActionBar platform theme now that Compose paints everything and the Activity owns edge-to-edge.
… stores MGConfigStore no longer touches java.io.File directly; every read, atomic write, corrupt-backup, cache measurement and delete goes through the new MgStorage interface, implemented by DirectMgStorage (all-files access) and SafMgStorage (DocumentFile/ContentResolver). AuthController owns the auth method plus its live validity as a StateFlow, so a revoked SAF grant falls back to the ungranted state instead of failing at write time. Also adds PluginConfigStore (ui_style, auth_method, saf_tree_uri, launch_count, donated), the sponsor-prompt modulo rule, the GLSL slider power curve as pure functions, and DeviceInfoProvider, which takes over the EGL GPU/GLES query and memory lookup that used to live in MainActivity.
MainActivity degrades to a shell: setContent, edge-to-edge and the ActivityResult launchers for the three auth routes (all-files access, SAF tree, legacy runtime permissions). AppController holds every behaviour the two skins share -- navigation, the suspending confirm() with its 41s/10s countdowns, the auth and corrupt-config dialog state machines, all setting actions with their existing guard rails, sponsor prompting, GL info and the MobileGlues removal flow -- so the skins only decide how to draw it. Adds the MD3 building blocks (theme with dynamic color, confirm/text dialogs, preference rows, single-choice dialog) and the strings for navigation, home, both auth methods, the settings groups, privacy and the sponsor prompt. Work in progress: the MD3 pages and the Miuix skin AppRoot dispatches to are not written yet, so the branch does not compile at this commit.
The layouts, the overflow menu and showAppInfoDialog all belonged to the Activity that no longer exists; two icons still referenced Material theme attributes that left with appcompat, so resource linking failed on them.
Miuix 0.9.0 requires compileSdk 37, which AGP 8.12.3 does not support, so the dependency stays on 0.8.8 -- the component API is the same. The artifact is also named `miuix` at that version, not `miuix-ui`. activity-compose has to be 1.13+: Miuix hosts its popups through NavigationBackHandler, which throws unless ComponentActivity provides a LocalNavigationEventDispatcherOwner. On 1.10 the app died the moment any dropdown or dialog appeared.
Five pages behind one Scaffold: the minimal home (large wordmark, auth pill, GPU/GLES/memory, a one-line config summary), the settings page whose plugin group sits above the permission gate, the info page that now carries the MobileGlues removal instead of an overflow menu, and the GL-info and privacy sub-pages. Pages cross-fade with a small directional shift; sub-pages slide in from the right and take the navigation bar away with them. The dialog host lives at the skin's root, not inside a page: a confirm, an auth prompt or the sponsor question may outlive the page that started it. Seven vector icons are added instead of the material-icons artifact -- the nav bar, the back arrow, copy, chevron and external-link marks are all this app needs, and both skins share them.
The same five pages and the same dialogs, expressed the way HyperOS does it: SmallTitle plus Card for a group, SuperArrow/SuperSwitch/SuperDropdown for the rows, SuperDialog for everything modal. Picking a value opens a popup anchored to its row rather than a dialog -- identical logic, idiomatic gesture. Both skins read the same AppController, so a warning, a countdown or a guard rail exists exactly once no matter which skin is drawing it. Note that Miuix inverts the Material convention: the page is `surface` and the cards are `surfaceContainer`. Painting the page with `background` makes the group cards vanish into it in dark mode.
Until now the permission gate only opened for an authorization this app had recorded itself. A user who had granted all-files access in system settings -- or who kept the permission across a reinstall -- was told they were not authorized and had to walk the whole flow again to record something the system already knew. AuthController now adopts an existing grant on refresh. Two smaller corrections from the same pass: removing MobileGlues with no storage attached reported "removal complete" while deleting nothing, and the home summary read "Disable · Prefer Enabled · 32 MiB" -- three values with no subject. It now names what each one is and only mentions the GL version when it has actually been customised.
MGConfigStoreTest builds its store on DirectMgStorage and reads the corrupt backup by name. New cases pin down the three pieces of logic the UI leans on: the sponsor prompt's modulo rule (a missed prompt must not shift the schedule), the SAF folder check (native reads a fixed /sdcard/MG, so only a folder named MG will do), and the cache slider's scale -- it must never run backwards, and a value has to survive the round trip through a position or the knob jumps under the user's finger.
The pointer was still on 0a3d4f2, the multidraw backend rename this branch's config keys were written against. Everything since -- per-context GL state, the EGL context table fixes, the multi-draw defects found on device, and the 1.3.6 development bump -- now comes with it.
# Conflicts: # MobileGlues # app/build.gradle.kts
Dropped "you don't have to take 'we value your privacy' on faith" -- stating
the disclaimer it's refuting was more defensive than reassuring, and the page
already backs up "we value your privacy" with specifics right below it.
Reworked the sponsor dialog's Chinese copy: it read like a counter readout
("this is your Nth launch") followed by a stiff disclaimer ("no feature
difference either way") that nobody had asked for. Also switched every
second-person 你 to the more formal 您 across the Chinese strings, matching
the tone the rest of the copy already uses in the warning dialogs.
Opening the settings app was never a launch -- the renderer running inside a game is. The count now comes from MG/stats.json, which libMobileGlues writes when a game loads it (submodule moves to feat/launch-count), and this app only reads it. MGInfoGetter sets MG_SKIP_LAUNCH_COUNT before dlopening the library for GL info, since that is this app talking to itself. The prompt's rule changes with the source of the number. Modulo worked while the app owned the counter and moved it one at a time; a count this app does not control can jump several launches between two visits and skip straight past the multiple. It now asks once the count has moved a full interval since the last time it asked, and records that point the moment the dialog appears rather than when a button is pressed -- otherwise "maybe later" would be answered again on the next start. Reading happens through MgStorage so SAF grants can see the file too, and only once storage is available: the count lives behind the permission gate, so the home page re-checks after authorization lands instead of giving up.
This app asks for all-files access, which is a lot to ask before saying anything about itself. The first launch now shows the privacy policy and does not proceed until the user agrees; declining exits, since every function here needs to read and write the MG folder. The dialog carries the full policy rather than a summary: what the user sees at the moment of agreeing should be the same words they can go back and read later, so both it and the privacy page now render one shared list of sections. It cannot be dismissed by the back key or a tap outside -- neither is an answer -- and nothing else, the sponsor prompt included, appears until it has been answered.
They were the only pair on the page without one, which read as leftovers below "About" rather than a section of their own.
A revoke row in the danger zone releases the storage authorization and the acceptance of the privacy policy together. No file is touched -- the MG folder keeps its configuration -- but the policy dialog comes straight back, because accepting it is the condition for using the app at all. All-files access is a system permission this app cannot hand back, so revoking also records that the user asked us to stop using it. Without that, the next refresh would adopt the still-granted permission again (the path added so a permission granted in system settings is not asked for twice) and the revoke would silently undo itself. Going through any grant flow clears the record.
The two rows locked each other out. Revoking left the removal row disabled because deleting needs the access that was just given up, and removing revoked the authorization on its way out, leaving nothing for the other row to do. They were never siblings: deleting the files is revoking plus more. The danger zone now has a single entry that opens a choice -- revoke only, or revoke and delete everything -- with the delete option unavailable, and saying why, when there is no access to delete with. Removal also gives back the privacy acceptance now, since after it there is nothing left to have consented to.
Revoking used to drop the user straight back onto the privacy dialog: they had just taken back their consent, so the app immediately demanded it again. Both paths now end the same way -- a word of thanks, and exit as the only button. Removal already worked like that; revoking now joins it, sharing one farewell dialog that says which of the two just happened. It is rendered before everything else in the host, or the consent dialog it just invalidated would cover it.
Deleting /sdcard/MG left a full copy of the user's renderer configuration sitting in the app's own cache -- MGInfoGetter exports one there every time GL info is queried -- along with every preference this app stores: the chosen authorization method, when sponsorship was last mentioned, whether the user donated. "Delete everything" has to mean it. Revoking clears the exported copy as well. The files in MG belong to the user and stay, but that copy is ours, and we just gave up the right to have it. The UI style is cleared from storage but left alone in memory: the farewell dialog is on screen at that moment, and repainting it in the other skin would read as a glitch. The next launch starts on the default.
…em allows A full reset with all-files access came back authorized. The reset cleared every preference, all-files access is a system permission this app cannot hand back, and refresh() would infer authorization from the permission still being on -- so the next launch adopted it again, and the reset had undone itself. The fix is to stop inferring. Authorization is now only what this app recorded the user choosing; without that record there is no access, whatever the system happens to permit. That also removes the flag added to defend the revoke path, since there is no longer an inference for it to defend against. The cost is one round trip: a user who turns the permission on in system settings by hand is still told they are not authorized, and has to walk the grant flow -- which returns immediately, because the permission is already on. Better than a revoke that quietly reverses itself.
Each section was a card with its own heading inside, which read as five unrelated boxes. Headings now sit above the cards, the way every group on the settings page already works, so the page scans as one document.
They inherited the group-label colour from the settings page -- the accent in Material, the muted grey in Miuix -- which reads as "name of a group of controls". These are headings of a document, so both skins now draw them in the plain foreground colour, and the group components take the colour as a parameter instead of hardcoding one.
Follows the renderer inverting its flag (submodule). Counting is now off unless MG_COUNT_LAUNCH says otherwise, so the launcher environments in the plugin manifest -- the ones Boat and Pojav hand to the game process -- set it, and that single case is what a launch means. MGInfoGetter no longer has to opt out. Loading the library to ask it one question was never a launch, and now nothing has to remember that.
There are four pages now -- the project's and one per developer -- and putting four rows in the info page would have made a donation menu out of a section that is mostly credits. One row opens a picker instead, and the sponsor prompt routes through the same picker, so "buy them a coffee" leads to the same list rather than picking a favourite on the user's behalf. Each entry shows its URL. 爱发电 is reachable on three different domains here and each page belongs to someone different; the platform name alone would not tell the user where their money is going. Choosing a channel from the prompt still ends in the gentle "did you donate?" question, but backing out of the picker does not -- nothing happened, so there is nothing to ask about.
"GitHub 发布页" pointed at one of three repositories as though it were the only one. It is now a source entry that opens the same picker the sponsor links use, listing the release repo, MobileGlues itself and this plugin. The about section also gains a page for third-party open source, split by where each component is used: a reader looking at SPIRV-Cross should be able to tell it belongs to the renderer the game loads, not to this settings screen. Every entry opens its own home page rather than reciting a licence here, where the text could drift out of step with upstream. The plugin README gains the components the release README does not cover -- Miuix above all, which is what the second skin is made of.
The info page ends on a wall of faces now -- the contributors of all three repositories, grouped by repository, each avatar opening its owner's GitHub profile. It sits below the danger zone because it is the page's sign-off, not a feature to scroll past. The list is baked in rather than fetched. Reading it from the GitHub API at runtime would mean declaring the INTERNET permission, and the privacy page tells the user there is none and invites them to check -- a credits list is not worth spending that promise on. tools/fetch_contributors.py regenerates the names and the avatars when they have moved on; it is run by hand, never during the build, because a build that needs the network cannot be reproduced offline. Thirty-three avatars at 96px come to 50 KiB.
It sat among ANGLE and the error mode, which are settings people change; this one opens a 41-second warning and belongs next to the MultiDraw backends, with the rest of what you touch only when you know why.
Body text drawn by hand missed the convention the library sets in DialogContentLayout: one size too small, a shade too grey, and left where Miuix centres. It now matches -- body1, onSurfaceSecondary, centred -- so the dialogs stop looking almost-but-not-quite HyperOS. Bulleted warnings and the privacy policy stay left-aligned: centring a column of "• ..." is unreadable, whatever the convention says.
Per-context GL state, the EGL and pixel-path audits, and the glslang resource limits.
…create 「撤销并删除全部文件」used to nuke the whole MG directory. Anything a user manually kept in there would go with it. Now it only touches the known file set (config.json, glsl_cache.tmp, stats.json, latest.log, glcalls.txt, and their transient/backup variants) and removes the MG folder itself only once it is left empty.
…hmark Advanced gets a MultiDraw section: one global order for all glMultiDraw entry points, plus a per-function exception for the cases where one function wants something different. Rows are reordered by dragging the handle on the right -- the list itself never reshuffles mid-drag, the rows just slide out of the way and the commit happens once, on release. "Benchmark and sort" runs the measurement that now lives in the renderer and offers the result as an order; nothing is written until the user adopts it. The result carries how many rounds each strategy got and how much it moved between them, and says so plainly when the device was too busy for the ranking to mean anything -- an order that is really noise should not look like a measurement.
… slot Dropping a dragged row played the lift-and-settle animation at the index the row came from, while the row itself was already at its new one. Column reuses composables by position, so the animation state behind the raised look -- the tint and the shadow -- belongs to a slot rather than to a row. Move A from slot 0 to slot 2 and slot 0 keeps the "raised" value it was left with, so that is where the fade back plays. Keying each row by its item makes the state follow the row. That in turn means onSizeChanged no longer reports: a row whose size did not change stays silent when it moves, and the height table would keep describing the old arrangement, throwing off the next drop. Positions are reported through onGloballyPositioned instead, which fires every frame during a drag, so heights are only written back when they actually differ. Also held the reordered list locally until the new order comes back through the config: the write goes out through a StateFlow and reaches composition a frame or two later, and in that gap the row would sit at its old index with the offset already cleared.
…g a shaky one Follows the renderer's re-measuring: the running dialog now says when a pass is being repeated with longer batches and how far into it we are, so a progress bar that restarts reads as a retry rather than a stall. When four passes still cannot get the spread under 15%, the result is presented as a question -- how many times it was measured, how far the steadiest of them still moved, and "adopt anyway" on the button. An order that is really noise should not be offered in the same words as one that was measured.
…page Dropped the global benchmark. One order has to serve five entry points whose absolute costs differ by orders of magnitude, so any weighting produces a sequence that is optimal for none of them -- and the measurement was per function all along. "Benchmark every function" now runs once and reports a separate ranking per function; adopting gives each measured function its own exception and leaves the global order alone. The global order stays what it is: the fallback for functions nobody has said anything specific about. Each function also carries its own quality line -- rounds, spread, and how many times the renderer had to re-measure that one -- since settling is judged per function now. Only a function that is still too shaky says so, in its own section, and "adopt anyway" appears when any of them is. Exceptions get a restore button, shown when that function's order differs from what the global order expands to on it. Page text cut down: the paragraph above the global order is one sentence now, and the drag hint and the exceptions blurb are gone -- the handle and the switch already say those. The handle keeps its content description for screen readers.
…driver The benchmark loads the renderer into this app's process, and ANGLE is not here -- it ships with the launcher. So on a device set to run ANGLE, the renderer could not open it, fell back to the system driver, and reported an order measured on a driver the game never touches. Nothing said so. ANGLE is GLES-on-Vulkan: which extensions exist, how baseVertex is done, what compute costs are all different there, so those orders were not merely imprecise, they were about something else. Launchers extract their native libraries to disk, an app's linker namespace permits paths under /data, and ANGLE needs nothing but system libraries -- so the two .so files can be loaded straight out of an installed launcher. The app finds the launchers that carry both of them and asks which one to trust, every time: this loads another application's native code into this process with this process's permissions, and a launcher can be updated into something else between one run and the next. The previous choice is only floated to the top of the list and labelled, never acted on by itself. GL info goes through the same renderer and had the same problem, so it borrows too -- but on a button, not on entry. Nobody opening a page to read it expects foreign native code to be loaded on their behalf; until then the page says plainly that it is describing the system driver. Results now carry which driver they came from, and one measured on the wrong one says so above the ranking and turns "adopt" into "adopt anyway".
…ning Two moments where the order on file stops describing this device, and neither said anything. Switching the ANGLE mode swaps the driver out from under an order that was measured on the other one. If the user has touched MultiDraw at all -- dragged it, added an exception, adopted a benchmark -- a snackbar now says so and offers to run it again. Only then: a default order was never measured, so nothing about it goes stale. It is a snackbar rather than a dialog because the user is in the middle of changing settings and should not be stopped. A device still on the factory order has never been measured at all, so the home page carries a quiet card saying as much, with one tap to measure. No dismiss button: adopting a result makes the order non-default and the card removes itself, which is the honest condition for it to disappear. The benchmark dialogs move up to the app level -- there are three ways in now (home card, settings button, snackbar action) and they were mounted inside two of the pages.
… query MobileGlues b34e0a0..9f0597f: the six EGL findings from the review -- probe teardown, context record ownership, create/destroy ordering, display holders, damage-swap through FSR, and the duplicate context table -- plus glGetActiveUniformName, whose stub is what left NeoForge's early loading window black.
MobileGlues 9f0597f..a6bfe0a. The eight sections of the framebuffer + pixel fix plan, the defects an adversarial review of that work found, host checks for the two pieces that had to be corrected twice, and one older bug it turned up on the way: an unpack buffer the driver would not let us map for reading had its whole upload dropped, which is why Xaero's world map rendered black.
…driver honestly Borrowing ANGLE did not work, and every symptom traced to one fact: the renderer reads MG_ANGLE_DIR and config.json only from its static constructor, and once libmobileglues is in a process it never leaves. /proc/PID/maps shows it still mapped after the dlclose that was meant to unload it -- the GL thread's thread_local destructors hold a reference the count never gets back. So the constructor runs once per process, and the driver chosen by the FIRST query was frozen for every query after it: a borrow made later had no effect, a system driver chosen later still reported ANGLE. The three-state MG_ANGLE_DIR contract was dead on arrival for the same reason -- nothing re-read it. Queries move into a :mgquery service process that kills itself on unbind, so 'every query starts fresh' stops being a hope and becomes structural. It also buys isolation, which the very first run demonstrated by crashing the renderer in there while the UI merely saw a dead binder. On top of that the app was asserting things it did not know. 'Read through ANGLE' came from having passed a directory, never from whether ANGLE loaded, so it claimed ANGLE on a device that had ignored the borrow; the affordance to retry disappeared at exactly that moment. It now reads the renderer's own answer and distinguishes borrowed, borrow-ineffective, and system. The benchmark's one wrong-driver line likewise became three: deliberately measured on the system driver, the borrow failed to load, and the device cannot use ANGLE at all -- the last of which is not a warning, since the game will use the system driver too and the ranking does hold. Also: a missing enableANGLE key decoded as EnableIfPossible here and DisableIfPossible in the renderer, so the UI could show a mode the game would never run. And the binder call had to stop throwing out of async, which propagates to the parent scope no matter what the await is wrapped in -- the first native crash in the query process took the whole app down with it.
…g built on it Also bumps both version numbers to Dev3 -- the renderer builds its own string from VERSION_DEV_NUMBER, so the two have to move together.
…he context The renderer discovers how large a scene the device can take by growing it while the readings are shaky, so it needs somewhere to come back to when the growing goes too far. It cannot be the renderer itself: what the driver threw away is not only the GL context but the VkDevice under it, and building another one in the same process is a gamble. Binding MgQuery again is not -- every bind is a fresh query process, so a clean start is something the structure provides rather than something the driver is asked for. Coming back is a restart, not a continuation. Microseconds scale with the scene, so numbers from two sizes in one ranking are two rulers measuring one thing; the run that died is discarded whole. The ceiling lives only for the duration of one tap. Whether the device was busy or warm moves that line, and recording one bad moment as a property of the phone would make every later run carry it. runBench therefore takes the opening size and the ceiling, and the report carries the size back -- including on the error path, where it is the only thing the caller can act on.
The benchmark's copy had drifted into explaining mechanism. The context-lost message spent ninety characters on dead contexts and draws that never happened, when what the reader needs is that the run was abandoned and the numbers after that point are not valid. Several others were written in the register of a changelog rather than a product: strategies "挂了", the default order described as what "一般来说" is fast, a benchmark you "跑". Trimmed to the fact and its consequence for the decision in front of the user, in both languages, with the placeholders and CDATA untouched. The progress line also gains the case the backoff introduced, ahead of the attempt counter: after a lost context the previous pass is void, and calling the next one "attempt 2" would suggest the first still counts.
…ct upload Brings in the two renderer-side changes the app half of this work depends on: mg_multidraw_bench_run now takes an opening scene size and a ceiling, and the MultiDraw indirect commands are staged on the CPU instead of written through a buffer mapping.
…g left The cells were a fixed 72dp and FlowRow packs from the start, so everything the width did not divide evenly into piled up on the right. At 440dpi that is 970px of room, four columns of 198px, and 178px of empty space against one edge and none against the other -- close enough to a whole column to read as a mistake rather than as spacing. How much lands there depends on the density, so it looks different on every device and wrong on some. Fitting as many cells of at least that width as the room allows and then sharing the room among them puts both edges flush at any density, and since every row uses the same cell the columns still line up when the last row is short. The sharing is done in whole pixels. A dp cell width is a float, every child rounds it to pixels on its own, and the columns can add up to a few pixels more than the row they were meant to fill -- at which point the last one wraps and leaves exactly the column-wide hole this was meant to remove. Integer division cannot exceed the row, and the leftover is under one pixel per column. Checked at 320, 440 and 560 dpi: six, four and three columns, each filling the width. Wider cells also buy the names a few more characters before the ellipsis.
The renderer now uses tsl::robin_map throughout, in place of the four map implementations it had accumulated. No plugin-side change is needed: nothing here names those types.
Renderer side only; nothing here names the type. The third-party list gains flat_hash_map, pointing at MobileGL-Dev's fork rather than upstream, because that is the code actually compiled in -- upstream's header cannot be included on a 32-bit target. Attribution stays with the original author.
The renderer fixed all fifteen findings from the performance audit: binding queries answered from tracked state instead of driver round-trips, redundancy filters on the hottest state setters, the per-draw allocations in the baseVertex emulation and the DSA binding stacks gone, the shader cache's O(n^2) save amortised. The version follows to 2.0.0.Dev4; nothing on the plugin side changes behaviour.
…enderer fixes
A bench run took a tap on a button and then a tap on a dialog, which meant
reproducing one from a script involved guessing screen coordinates and failing
whenever the phone was on a different page. MainActivity now consumes an intent
extra and hands it to a headless entry point on the controller:
adb shell am start -n com.fcl.plugin.mobileglues/.MainActivity \
--es mg_bench all --es mg_angle borrow
No UI leads to it. It differs from the normal path in one respect -- it does
not raise the ANGLE source dialog, because a script cannot answer one -- and
resolves the borrow to the remembered source, falling back to the system driver
exactly as choosing "use the system driver" does. The extras are cleared off
the Intent once consumed, or a rotation would start a second run.
The renderer side carries the fix for the benchmark it was measuring wrong, and
the linker change that stops this library's internal frontend calls from
resolving into the system driver.
…ult a ranking Two places the UI said more than the run could back. The progress copy promised "about 8 seconds". That is the base budget; a run whose readings will not settle grows the scene and re-measures, and the total is capped at seventy-five, not eight. It now says the base budget and the worst case, so a minute-long run is what was described rather than a hang. A function with exactly one measurable strategy was still presented as a sorted list with ×1.00 at the top. There is nothing to compare it against and the multiplier means nothing; both skins now say so under the row. Also carries the renderer's per-frame pass boundary and output gate.
Three things broke when the window got short, and all three were the same mistake: a layout written as if height were free. Dialogs capped their scrolling body at a hardcoded 420dp. In portrait that is under the available height and nothing shows; in landscape the window is barely more than that, so the title and the buttons were pushed off screen -- the text was readable and "confirm" was not reachable. The cap now comes from the actual window height minus what the title, the buttons and the system bars need, with a floor so a very long body still gets a screenful. Six sites in Miuix had the literal; four in Material had no cap at all and relied on AlertDialog's own sizing. The home page was a centred Column at fillMaxSize with no scrolling. Its content height is fixed, portrait fits it, landscape does not -- so both ends were clipped and unreachable. It scrolls now, and keeps centring only while it fits (heightIn(min = maxHeight)), so tall screens look exactly as before. The bottom navigation ate the scarcest axis. Below 480dp of height it moves to a side rail, vertically centred, in both skins. The test is on height rather than orientation on purpose: an unfolded foldable is wide but not short, and it should keep the bottom bar. Responsive.kt holds that decision so the two skins cannot drift apart on it.
"Benchmark" and "restore default" were TextButtons -- MD3's lowest emphasis, which renders as bare text with no container and reads as a caption in a list of captions. They are the only two things on that page you can do, so they get containers: filled-tonal for the benchmark, outlined for the reset. Side by side the pair now also carries its own hierarchy, the action above the undo.
On Xiaomi's own system the Miuix skin is what the rest of the phone looks like, so it is the better first impression. Only the default moves: the stored choice still wins, including a HyperOS user who picked Material on purpose. That needed telling "never chose" apart from "chose Material", which the old read could not do -- ofKey answered Material for both. Detection is of the system, not the vendor. Build.MANUFACTURER == "Xiaomi" is the obvious test and it is wrong: a Xiaomi phone flashed with a GSI is still Xiaomi hardware running stock Android, and Miuix there would look out of place. The two development phones are exactly that pair -- same vendor, and only the one running HyperOS reports ro.miui.ui.version.name=V816 and ro.mi.os.version.name=OS3.0, while the GSI reports neither. So it goes by what the system itself leaves behind: the miui.os.Build class on the boot classpath, or either of those two properties. The property read is reflection into a non-public API, so the whole thing is wrapped -- unreadable means "no", and the worst outcome is that the default stays Material.
…n take A benchmark can run for minutes with no touch input at all, which is precisely what the system reads as "nobody is here" -- and once the display sleeps the run is no longer measuring the GPU the game will use: composition stops, the frequency policy changes, and the user comes back to a ranking built under conditions that never occur in play. The view keeps the screen on while a run is in flight, released when it ends. View.keepScreenOn rather than the window flag: no walking up contexts to find the Activity, and it lapses on its own when the view stops being visible, which covers leaving the app mid-run. It lives in AppRoot because the run belongs to the controller and both skins share it -- one implementation, so neither can forget it. The copy has to follow. It told the user to keep the screen on, which is now the app's job, and it promised "about a minute" -- measured budget alone caps at 75 seconds, and scene rebuilds, probes and the output-gate frames sit on top of that. A run that grew to 1024 sections took two minutes on a Mali-G77. It now states the base budget and admits an unsteady device can take a couple of minutes, so a long run reads as expected rather than as a hang.
versionName drops the qualifier to match the renderer, which now reports plain 2.0.0 in GL_VERSION, and versionCode goes to 2000 so the number reads as the version rather than as one short of it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.