Skip to content

Commit 2050564

Browse files
committed
Move OS text measurement off the logic thread, rework the credits window to avoid hitches, optimize bg-dynamics sprite mesh indexing, and fade audio out before iOS/tvOS suspend.
1 parent 14f33f1 commit 2050564

65 files changed

Lines changed: 1950 additions & 614 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.efrocachemap

Lines changed: 28 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
### 1.8.0 (build 22997, api 9, 2026-08-22)
1+
### 1.8.0 (build 22998, api 9, 2026-08-24)
22
- Fully implemented asset packages (more on this soon)
33
- App-config committing (dirty-tracking, debounced disk writes, and
44
suspend/shutdown flushes) now lives fully in `babase` instead of routing
@@ -113,6 +113,10 @@
113113
divisible by 4).
114114
- Android builds should now properly respect camera cutouts and round corners.
115115
Please holler if you see any UI elements being cut off at this point.
116+
- OS Text rendering (Asian languages, emoji, etc.) is now more efficient and
117+
renders more cleanly in the background, meaning it should never cause frame
118+
hitches. The credits window used to be a major offender here and should now
119+
come up smoothly.
116120

117121
### 1.7.63 (build 22870, api 9, 2026-06-08)
118122
- Fixed mouse-wheel zooming in manual camera mode.

docs/design/ios-tvos-app-platform.md

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ calls three `from_swift` entry points — `SetAppActive(bool)`, `SuspendApp()`,
4949
- **running** (`sceneWillEnterForeground` / `sceneDidEnterBackground`, the
5050
analogue of Android onStart/onStop) → `UnsuspendApp()` / `SuspendApp()`.
5151
`SuspendApp` parks all event-loop threads; the audio thread's suspend
52-
callback calls `alcDevicePauseSOFT`, which is what actually stops audio.
52+
callback fades master volume out and then calls `alcDevicePauseSOFT`,
53+
which is what actually stops audio (see "Audio around suspend" below).
5354
The `CADisplayLink` is also paused
5455
(`UIKitGLViewController.setRenderingPaused`) so no GPU work happens in the
5556
background.
@@ -71,6 +72,34 @@ boot; the iOS shell avoids that.)
7172

7273
Files: `app_platform/apple/{from_swift.h,from_swift.cc,UIKitSupport.swift,UIKitSceneDelegate.swift,UIKitGLViewController.swift}`.
7374

75+
## Audio around suspend
76+
77+
`alcDevicePauseSOFT` is a hard stop: it stops the CoreAudio unit
78+
immediately, so before 2026-08-22 backgrounding cut whatever was playing
79+
off mid-waveform at full amplitude. On iOS that lands right as the OS is
80+
tearing down our audio session, and the result sounded like a moment of
81+
corrupted audio rather than a clean stop.
82+
83+
`AudioServer::FadeMasterGainForSuspend_` therefore ramps OpenAL's master
84+
(listener) gain to silence over 120ms and lets the mixer run on that
85+
silence for another 60ms *before* pausing the device, then ramps back up
86+
after unsuspending. It early-outs when nothing audible is playing, so it
87+
only costs time when it buys something. Budget-wise this is fine: a
88+
measured suspend with music playing completes in ~211ms against
89+
`SuspendApp`'s 4s cap (and Apple's ~5s background deadline).
90+
91+
Two related facts worth knowing before touching this:
92+
93+
- **Nobody in the process owns the `AVAudioSession`** — not our code, and
94+
not the OpenAL Soft xcframework (it links AudioToolbox/CoreAudio, no
95+
AVFAudio). So we run on the implicit default category, never explicitly
96+
activated, with no interruption handler, and the system's session
97+
teardown races our own `AudioOutputUnitStop`. The fade hides the
98+
symptom; owning the session would remove the race. See `followups.md`.
99+
- **Listener gain is otherwise unused** by the engine (only
100+
position/velocity/orientation are set), which is why it's available as
101+
a master knob here.
102+
74103
## Input
75104

76105
- **Touch:** `UIKitGLViewController` overrides
@@ -181,6 +210,56 @@ platform-specific pieces worth knowing:
181210
--predicate 'subsystem == "net.froemling.ballistica"'` (or `log stream`).
182211
Note `simctl launch --console[-pty]` does *not* capture the sim app's
183212
stderr — os_log is the reliable channel.
213+
- **`LowLevelDebugLog` goes nowhere on Apple.** `HandleLowLevelDebugLog`
214+
is only overridden on Android (crash-log breadcrumbs); the base impl in
215+
`platform.cc` is empty. So calls like the `"Calling alcDevicePauseSOFT
216+
at ..."` breadcrumbs in `audio_server.cc` are invisible here — don't
217+
plan an iOS investigation around grepping for one. Use a real logger.
218+
- **Exercising background/foreground in the Simulator**: background the
219+
app by launching another one (`xcrun simctl launch booted
220+
com.apple.Preferences`), then foreground it with `xcrun simctl launch
221+
booted <our-bundle-id>` — that resumes the existing process rather than
222+
relaunching (same pid back), so it exercises the real
223+
suspend/unsuspend path. Pair with `--log 'ba=DEBUG'` to get
224+
`SuspendApp() completed in Nms.` / `UnsuspendApp() completed in Nms.`,
225+
which are logged on `LogName::kBa` at DEBUG in debug builds only.
226+
- **`make ios` / `make tvos` device pick** (impl: lifecycle in
227+
`tools/batools/iossim.py`; pcommands `ios_sim_run` / `ios_sim_log` in
228+
`tools/batools/pcommands4.py`). Order, no config needed:
229+
`IOS_SIM_DEVICE` override (name or udid) → reuse an already-booted
230+
device → the Simulator app's `CurrentDeviceUDID` (`defaults read
231+
com.apple.iphonesimulator`) → newest available, booted on demand. The
232+
`CurrentDeviceUDID` step matters: `open -a Simulator` auto-boots the
233+
app's last-used device, so a disagreeing pick yields TWO sim windows.
234+
Booting is async — always `simctl bootstatus -b` before install, and
235+
boot BEFORE opening Simulator.app (opening first races the auto-boot
236+
→ "Unable to boot device in current state: Booted").
237+
238+
### Testing the software keyboard in the Simulator
239+
240+
Verifying anything keyboard-related needs setup the sim fights you on
241+
(cost ~20 min on 2026-08-19):
242+
243+
- The sim **suppresses the software keyboard by default** (hardware
244+
keyboard connected), so keyboard-layout bugs simply cannot appear.
245+
`defaults write com.apple.iphonesimulator ConnectHardwareKeyboard
246+
-bool false` is **blocked by the unsandboxed-Bash hook**, and `simctl
247+
ui` has no equivalent — so ask Eric to hit ⌘⇧K (I/O ▸ Keyboard ▸
248+
Connect Hardware Keyboard) once the sim is up.
249+
- iOS then shows a one-time **swipe-typing intro** covering the keyboard;
250+
`xcrun simctl spawn booted defaults write
251+
com.apple.keyboard.ContinuousPath DidShowContinuousPathIntroduction
252+
-bool true` clears it (works sandboxed — it writes inside the sim, not
253+
host prefs).
254+
- `automation_drive --screenshot` captures **only the GL framebuffer**,
255+
so UIKit overlays (the string editor, the keyboard) are invisible in
256+
it. Use `xcrun simctl io booted screenshot <path>` instead — note it
257+
captures portrait-rotated for a landscape app.
258+
- Android counterpart: the emulator reports `keysexposed-qwerty`
259+
(`adb shell am get-config`), so any code branching on "is a hardware
260+
keyboard attached" always takes the hardware path there and cannot be
261+
tested; Gboard's **stylus-handwriting onboarding** blocks the keyboard
262+
until `adb shell settings put secure stylus_handwriting_enabled 0`.
184263

185264
### Simulator build/launch recipe (verified 2026-06-18)
186265

pconfig/projectconfig.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"bauiv1lib": "buil"
2424
},
2525
"efrocache_repository_url": "https://files.ballistica.net/cache/ba1",
26-
"engine_build_number": 22997,
26+
"engine_build_number": 22998,
2727
"name": "BallisticaKit",
2828
"public": true,
2929
"python_paths": [
@@ -43,5 +43,5 @@
4343
"tests",
4444
"config"
4545
],
46-
"version": "1.8.0a100"
46+
"version": "1.8.0a101"
4747
}

pconfig/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
cpplint==2.0.2
22
cryptography==50.0.0
33
dmgbuild==1.6.7
4-
filelock==3.32.3
4+
filelock==3.32.4
55
furo==2025.12.19
66
libcst==1.9.0
77
mypy==2.3.1

pconfig/requirements_lock.txt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -479,9 +479,9 @@ ds-store==1.3.3 \
479479
--hash=sha256:a7380896ea1e880f7ba07c5cdc24bd1530fb86836cad0be531f3e80e2d721ac5 \
480480
--hash=sha256:b92a371efbf1b4ccce2a04d1ed13fceacc4736c81ba09cf5aefb74c088160a35
481481
# via dmgbuild
482-
filelock==3.32.3 \
483-
--hash=sha256:0ffa185a3540854c95caa7fa76b76cb219d907415e2c5dc9af25fd970563487f \
484-
--hash=sha256:7f0ca4bcc0e181c60dbbd8aa9ab5b120ebb99e4e064e83636340056f833a1f09
482+
filelock==3.32.4 \
483+
--hash=sha256:22e58ca3b1ae3b98993b762d7338367ae64fe50252bf78d59da3bfebcdf1cedd \
484+
--hash=sha256:2bde2e4cf732e0153406d8a7bc80620ecf5e621fe0d25e41143c4e3b4733ff30
485485
# via -r pconfig/requirements.txt
486486
furo==2025.12.19 \
487487
--hash=sha256:188d1f942037d8b37cd3985b955839fea62baa1730087dc29d157677c857e2a7 \
@@ -491,9 +491,9 @@ idna==3.19 \
491491
--hash=sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15 \
492492
--hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4
493493
# via requests
494-
imagesize==2.0.0 \
495-
--hash=sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96 \
496-
--hash=sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3
494+
imagesize==2.0.1 \
495+
--hash=sha256:b2ba6a4dea487a7ebcd53248d3476aca449d30db12a2dde5e0c5ca9624fd77e5 \
496+
--hash=sha256:ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02
497497
# via sphinx
498498
importlib-metadata==9.0.0 \
499499
--hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \
@@ -912,9 +912,9 @@ pbxproj==4.3.3 \
912912
--hash=sha256:13fc8d4d66d38627040eb66fdaee425df551321417b2943dd1bb1106a48ea521 \
913913
--hash=sha256:76deb4e622d0abb99d01fbeff9e5a33126cdd04cb3e82864ebf7589a5e144eec
914914
# via -r pconfig/requirements.txt
915-
platformdirs==4.11.3 \
916-
--hash=sha256:5ed065d443751de711da036041a7a214122efc4a4de393b3f4137ba5576540e7 \
917-
--hash=sha256:66a73d38a849810252df809a3d8bcbda8e26f6c189920e7535ad608a48dbb5ab
915+
platformdirs==4.11.4 \
916+
--hash=sha256:e34ff91a24bcddc6d939b878bdf3f5c437c9c46fe9e212b1bf455fdf1ee57586 \
917+
--hash=sha256:f3373be828247211d0febabea97e238c3dfde8a60b3c90c32756fb52cb21556d
918918
# via
919919
# black
920920
# pylint

src/assets/ba_data/python/baenv.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@
5555

5656
# Build number and version of the ballistica binary we expect to be
5757
# using.
58-
TARGET_BALLISTICA_BUILD = 22997
59-
TARGET_BALLISTICA_VERSION = '1.8.0a100'
58+
TARGET_BALLISTICA_BUILD = 22998
59+
TARGET_BALLISTICA_VERSION = '1.8.0a101'
6060

6161

6262
@dataclass

src/assets/ba_data/python/bascenev1lib/activity/coopscore.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,6 +1394,7 @@ def _got_score_results(self, results: dict[str, Any] | None) -> None:
13941394
).evaluate()
13951395
),
13961396
suppress_warning=True,
1397+
suppress_logic_thread_warning=True,
13971398
),
13981399
)
13991400

src/assets/ba_data/python/bauiv1lib/account/settings.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,12 @@ def _refresh(self) -> None:
543543
v -= via_space * 0.1
544544
sscale = 0.7
545545
swidth = (
546-
bui.get_string_width(via, suppress_warning=True) * sscale
546+
bui.get_string_width(
547+
via,
548+
suppress_warning=True,
549+
suppress_logic_thread_warning=True,
550+
)
551+
* sscale
547552
)
548553
bui.textwidget(
549554
parent=self._subcontainer,

src/assets/ba_data/python/bauiv1lib/account/viewer.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,9 @@ def _on_query_response(self, data: dict[str, Any] | None) -> None:
278278
account_name_spacing = 15
279279
tscale = 0.65
280280
ts_height = bui.get_string_height(
281-
trophystr, suppress_warning=True
281+
trophystr,
282+
suppress_warning=True,
283+
suppress_logic_thread_warning=True,
282284
)
283285
sub_width = self._width - 80
284286
sub_height = (
@@ -428,7 +430,11 @@ def _on_query_response(self, data: dict[str, Any] | None) -> None:
428430
).evaluate()
429431
rank_str_width = min(
430432
sub_width * maxwidth_scale,
431-
bui.get_string_width(rank_str, suppress_warning=True)
433+
bui.get_string_width(
434+
rank_str,
435+
suppress_warning=True,
436+
suppress_logic_thread_warning=True,
437+
)
432438
* 0.55,
433439
)
434440

@@ -488,7 +494,11 @@ def _on_query_response(self, data: dict[str, Any] | None) -> None:
488494
rank_str = f'{season_str}: {rank_part}'
489495
rank_str_width = min(
490496
sub_width * maxwidth_scale,
491-
bui.get_string_width(rank_str, suppress_warning=True)
497+
bui.get_string_width(
498+
rank_str,
499+
suppress_warning=True,
500+
suppress_logic_thread_warning=True,
501+
)
492502
* 0.3,
493503
)
494504

0 commit comments

Comments
 (0)