Android: Implement proper DPI-aware scaling and fullscreen mode#3
Open
automaton82 wants to merge 17 commits into
Open
Android: Implement proper DPI-aware scaling and fullscreen mode#3automaton82 wants to merge 17 commits into
automaton82 wants to merge 17 commits into
Conversation
This commit addresses critical UX issues on high-DPI Android devices: DPI-Aware Rendering: - Implement dynamic DPI scaling using AConfiguration_getDensity() API - Calculate logical screen dimensions for game rendering (physical pixels / DPI scale) - Game now renders to logical dimensions while OpenGL scales to physical resolution - All UI elements (radar, chat, HUD, text) now scale proportionally across devices - Separate scaling for ImGui menus (1.33x) vs in-game rendering (2.4x on 560 DPI) - Font sizes now scale with device DPI for readability (1.5x multiplier on menus) Immersive Fullscreen Mode: - Add hideSystemUI() method to MainActivity for edge-to-edge rendering - Hide status bar and navigation bar using WindowInsetsController (API 30+) - Fall back to SYSTEM_UI_FLAG_IMMERSIVE_STICKY for older Android versions - Restore immersive mode when window regains focus Build System Updates: - Update CMake version from 3.10.2 to 3.22.1 to match installed SDK - Update local server IP addresses (192.168.7.168) Documentation: - Add comprehensive CLAUDE.md with full development workflow - Document build process, emulator setup, programmatic control via adb - Include coordinate transformation formulas and automation examples - Update README.md with Android build instructions Testing: Verified on Samsung Galaxy S23 Ultra (1440x3120 @ 560 DPI) and Android emulator (2400x1080 @ 420 DPI)
Repositioned right-side icons (spectator indicator, ship items, weapon/bomb indicators) from y=57% to y=50 pixels to prevent overlap with radar minimap on DPI-scaled screens. Changes: - Move SpectateView icon from calculated position to fixed y=50 - Move ShipController indicators from calculated position to fixed y=50 - Position icons just below FPS counter with proper clearance - Make gradlew executable for convenience The dynamic DPI scaling introduced in previous commit changed logical screen dimensions, causing the percentage-based positioning (57% of screen height) to overlap with the bottom-right radar. Fixed positioning at y=50 ensures icons are visible below FPS while keeping clear space for the radar minimap. Tested on emulator (2400x1080 @ 420 DPI) and Samsung S23 Ultra (1440x3120 @ 560 DPI).
Contributor
Author
|
There are some issues I found I will fix first. |
- Multi-touch support: simultaneous d-pad flying and weapon buttons - Virtual d-pad (140px) in bottom-left with green directional arrows - GUN/BOMB round buttons (72px) in bottom-right with grey/blue styling - Ship controls: virtual d-pad angle calculation for directional flying - Ability buttons fire once per tap (trigger flag prevents repeat) - Menu redesigned: two-column button layout (420x240px), tap top UI to open - Icons bottom-aligned: item icons left, status icons right - Timed indicators moved to left side with text offset right of icon - FPS counter moved to top-left, notifications positioned below it - Radar moved to top-right corner - Energy bar shortened to 160px, energy value shown as text - Fix shader precision mediump->highp in TileRenderer to fix blurry textures on high-DPI displays (Samsung Galaxy S25 Ultra)
The root cause: APP_CMD_TERM_WINDOW/INIT_WINDOW are not guaranteed symmetric on modern Android (Samsung One UI / Android 14+). The old architecture destroyed the entire EGL stack on TERM_WINDOW and relied on INIT_WINDOW to recreate it, which never fired on resume from the app switcher. Fix: - Keep EGLDisplay + EGLContext + GL resources alive across pause/resume - Only destroy EGLSurface on APP_CMD_TERM_WINDOW (destroySurfaceOnly) - Add ensureSurface() which lazily recreates the surface whenever the render loop finds g_EglSurface == EGL_NO_SURFACE and app->window != nullptr - ensureSurface() also called in APP_CMD_INIT_WINDOW and APP_CMD_GAINED_FOCUS as opportunistic hints (not relied upon exclusively) - Check eglSwapBuffers result; on EGL_BAD_SURFACE/EGL_CONTEXT_LOST call destroySurfaceOnly() so the next tick recreates it - Store g_EglConfig globally so surface can be recreated without reinit
… recreate When ensureSurface() recreates the EGL surface after resume, it must also: - Call ANativeWindow_setBuffersGeometry with the original scaled dimensions (width * 0.6, height * 0.6) so the surface comes back at the same size - Call glViewport with the stored physical dimensions to restore the GL viewport Store egl_format, buffer dimensions, and physical dimensions globally so they are available to ensureSurface() without a full reinit.
SpriteRenderer was called 4+ times per frame, each time uploading sprite data with glBufferSubData. On mobile unified-memory GPUs (Mali, Adreno), glBufferSubData stalls the CPU until the GPU finishes reading the previous buffer. Measured 33-50ms per call even with zero sprites — pure sync overhead. Fix in three parts: 1. SpriteRenderer::Render() now uploads all sprites at once with glBufferData(GL_STREAM_DRAW). Buffer orphaning causes the driver to allocate fresh storage, so the CPU never waits on in-flight GPU reads. Draw calls then use vertex offsets into the already- uploaded VBO — no re-upload per texture batch. 2. WeaponManager::Render() no longer calls renderer.Render() internally. Weapon sprites are pushed to the buffer and flushed by the game loop, same as every other subsystem. 3. Game::RenderGame() reduced from 4+ sprite flush points to exactly two: one world-space flush (weapons, players, tiles) and one UI-space flush (HUD, radar, chat, ship controller). Result: 20 FPS -> 93 FPS on Samsung SM-X400 (Mali-G68).
…te file Platform-specific UI layout changes are now guarded so the shared src/null/ code compiles correctly on Windows/Linux without mobile assumptions baked in: - Notification.cpp: position guarded (mobile: top-left, desktop: proportional) - Radar.cpp: radar position guarded (mobile: top-right, desktop: bottom-right) - SpectateView.cpp: icon y-position guarded - ShipController.cpp: - Energy digit display restored on desktop, guarded off on Android - RenderTimedIndicator position/alignment guarded (left vs right side) - Item indicator y-positions guarded (bottom-anchored vs proportional) - kBarWidth constant (160 Android / 240 desktop) - Energy text label guarded to Android-only - GUN/BOMB buttons and D-pad wrapped in #ifdef __ANDROID__ - Game.cpp: FPS counter position guarded (top-left Android / top-right desktop) - Game.h + src/null/android/GameMenuAndroid.cpp: Android menu moved to its own file via RenderMenuAndroid(). Desktop RenderMenu() in Game.cpp restored to original. Menu layout will diverge freely without polluting shared code.
Contributor
Author
|
@plushmonkey this is a large change as I had to change quite a lot to make the experience better on Android. There was also a few rendering issues. Let me know if you have any questions. I used Claude to assist as my C++ is pretty awful. I did manually test on the emulator and my phone as noted in the PR, plus a Samsung Tab S10 Lite (which had severe rendering issues, it was stuck at 20fps until b4f1496 was done). I did not test on Windows or Linux. The |
Major Features: - Redesigned ESC menu: 3-button sidebar (Quit, Stat Box, Help) + 3x3 ship grid with rendered sprites - Portal/Warp toggle: Tap portal icon when active to warp back - Multifire toggle: Tap gun icon to toggle multifire mode (with sound) - Mine mode toggle: Tap bomb icon to switch between bombs and mines * Button text changes from 'BOMB' (white) to 'MINE' (yellow) * Sound feedback on mode switch - Toggleable abilities: Tap right-side icons to toggle stealth, cloak, xradar, antiwarp - Afterburner: Drag beyond d-pad radius to activate, with 'AB' indicator and green ring - Random player names: 'null space XXX' with 3-digit random suffix to prevent collisions UI Improvements: - Enlarged menu buttons (35px height) for better touch targeting - Ship selection grid with visual ship sprites (110x70 cells) - Mine mode visual feedback (yellow text in button) - Afterburner visual indicator (text + ring around d-pad) - Stat Box accessible from menu without closing menu Quality of Life: - Spectator mode now spawns at map center (512, 512) instead of origin - All Android-specific UI properly isolated with #ifdef __ANDROID__ - Clean separation of Android menu code in GameMenuAndroid.cpp Technical: - Added g_AndroidMineMode global for cross-file mine state tracking - Portal/warp toggle checks ship_controller.ship.portal_time - Mine mode integrates with existing Mine InputAction - All new features use action_callback for proper toggle behavior
Maps with an embedded BMP tileset (e.g. apocalypse.lvl) replace the default tile graphics. Custom tilesets typically leave the tile 171 (safe zone) slot empty, so the shader sampled empty/black pixels and the safe zone area appeared invisible in the main view (the radar worked because it uses a hardcoded green color for id 171). Tiles >172 are protected by a black-pixel discard fallback in the shader, and tiles 170/172 are drawn by other systems, so tile 171 was uniquely affected. After uploading the map's tileset to the 3D texture array, override layer 170 with the safe zone graphic from graphics/tiles.bm2 so the safe zone always renders correctly regardless of the map's tileset.
Wormholes (220) now render as dark purple, asteroids (216-218) as brown/ orange, and space stations (219) as medium gray on the minimap. Previously these tiles rendered as generic dark gray, making them indistinguishable from walls.
Join and leave messages now use the notification system on Android (5-second timeout) instead of the persistent chat log. This prevents them from staying on screen indefinitely over the d-pad. Desktop retains the original chat log behavior for these messages.
UI / rendering: - Energy number: render beside the centered bar on Android (between bar and radar) instead of top-right; remove the separate small Blue text underneath the bar (now uses the same big-sprite digits as desktop). - FPS counter: hide on Android (it was overlapping the statbox area). - Timed indicators (portal / super / shield / flag): right-anchor on both platforms; on Android stack them above the right-side icon column so they no longer collide with the energy bar. - Notifications: anchor below the statbox on Android instead of using a fixed y=55; pass the statbox bottom from Game::RenderGame. Statbox (Android): - Dynamically size sliding_view.size to roughly match the radar height (derived from the same surface_dim.x / 6 formula Radar uses), so the list fits the visible area on any device. - Default to StatViewType::Points (more useful than the Names view on a small screen). - Show a '+N more' overflow indicator in Names, Points, TeamSort, and Full views when not all players fit. - Widen name column (12 -> 18 chars, kNamesWidth 108 -> 140) so longer player names aren't truncated. Score / flag sync: - Register and parse ProtocolS2C::ScoreUpdate (0x09): updates kill_points / flag_points / wins / losses on the target player so scoreboard numbers stay in sync with the server. - On PlayerDeath, provisionally credit kill bounty to the killer (skipping self-kills and team-kills) so points update immediately; the next ScoreUpdate from the server corrects any drift. - Radar: show dropped flags for both own team (RadarTeamFlag) and enemy team (RadarEnemyFlag); skip flags with id 0xFFFF or not in the Dropped state. - Handle ProtocolS2C::FlagVictory: push a green VICTORY notification with the points awarded (or a yellow info line for other freqs). Android server config: - Update 'local' and 'subgame' zone IPs from 192.168.7.168 to 192.168.7.161 (host LAN address changed).
Do not skip the flag proximity loop when CarryFlags=0. Turf flag games (fg_turf) run with CarryFlags=0 but still require the client to send the 0x13 touch packet for server-side ownership tracking. The inner can_carry / GameFlag_Turf check already handles both modes. Also fix carry_count <= 0 guard (was == 0, missed negative values).
- Show notification when any player picks up or drops the flag - Color-coded: green for teammates, red for enemies - Improves situational awareness in flag games
Implement automatic expiration of chat messages on Android to prevent screen clutter. Messages now disappear after 15 seconds instead of persisting indefinitely.
Exclude consumables and Multiprize from InitialBounty spawn prize loop in ResetShip(): - Repel, Burst, Decoy, Thor, Rocket, Portal (consumables) - Multiprize (can grant consumables via sub-loop) Without this fix, InitialRepel/InitialBurst/etc. config settings were meaningless because the InitialBounty prize loop would randomly grant these items regardless, overriding the configured Initial values. This appears to be an SVS bug where consumables were incorrectly included in spawn rewards. Now Initial* config values work as intended: ships spawn with configured amounts (typically 0), and players collect consumables from green prize boxes during gameplay.
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.
DPI-Aware Rendering:
AConfiguration_getDensity()APIImmersive Fullscreen Mode:
hideSystemUI()method toMainActivityfor edge-to-edge renderingWindowInsetsController(API 30+)SYSTEM_UI_FLAG_IMMERSIVE_STICKYfor older Android versionsTouch Controls:
Mobile UI Layout:
Bug Fixes:
mediumptohighp—mediumpfloat (16-bit) was insufficient for UV coordinate calculations at world coordinates >512on INIT_WINDOW to recreate it, which never fired on resume from the app switcher.
Build System Updates:
Documentation:
render: fix CPU-GPU sync stalls causing 20 FPS on mobile
SpriteRenderer was called 4+ times per frame, each time uploading
sprite data with glBufferSubData. On mobile unified-memory GPUs
(Mali, Adreno), glBufferSubData stalls the CPU until the GPU
finishes reading the previous buffer. Measured 33-50ms per call
even with zero sprites — pure sync overhead.
Fix in three parts:
SpriteRenderer::Render() now uploads all sprites at once with
glBufferData(GL_STREAM_DRAW). Buffer orphaning causes the driver
to allocate fresh storage, so the CPU never waits on in-flight
GPU reads. Draw calls then use vertex offsets into the already-
uploaded VBO — no re-upload per texture batch.
WeaponManager::Render() no longer calls renderer.Render()
internally. Weapon sprites are pushed to the buffer and flushed
by the game loop, same as every other subsystem.
Game::RenderGame() reduced from 4+ sprite flush points to exactly
two: one world-space flush (weapons, players, tiles) and one
UI-space flush (HUD, radar, chat, ship controller).
Result: 20 FPS -> 93 FPS on Samsung SM-X400 (Mali-G68).
Testing: Verified on Samsung Galaxy S25 Ultra (1440×3120 @ 560 DPI) and Android emulator (2400×1080 @ 420 DPI)
Example screenshot: