diff --git a/.efrocachemap b/.efrocachemap
index 665bb33f..b7a571d2 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -421,42 +421,42 @@
"build/assets/ba_data/audio/zoeOw.ogg": "74befe45a8417e95b6a2233c51992a26",
"build/assets/ba_data/audio/zoePickup01.ogg": "48ab8cddfcde36a750856f3f81dd20c8",
"build/assets/ba_data/audio/zoeScream01.ogg": "2b468aedfa8741090247f04eb9e6df55",
- "build/assets/ba_data/data/langdata.json": "992c5c5ce292132c4f011f39e0d13de8",
- "build/assets/ba_data/data/languages/arabic.json": "d1f900ab5aa2433d402bd46ed1149cc7",
+ "build/assets/ba_data/data/langdata.json": "2a2c4783fddc4b24d07b4ce0d8a74393",
+ "build/assets/ba_data/data/languages/arabic.json": "295c559911fa251f401f8cdcad91c226",
"build/assets/ba_data/data/languages/belarussian.json": "e151808b6b4f6dc159cf55ee62adad3c",
- "build/assets/ba_data/data/languages/chinese.json": "8d889accdd49334591209bdaf6eaf02f",
+ "build/assets/ba_data/data/languages/chinese.json": "0a9d9534e7329d1e886adae6fdc007c4",
"build/assets/ba_data/data/languages/chinesetraditional.json": "f858da49be0a5374157c627857751078",
"build/assets/ba_data/data/languages/croatian.json": "766532c67af5bd0144c2d63cab0516fa",
"build/assets/ba_data/data/languages/czech.json": "93c5fe0d884d95435da6c675f64e30e0",
"build/assets/ba_data/data/languages/danish.json": "3fd69080783d5c9dcc0af737f02b6f1e",
"build/assets/ba_data/data/languages/dutch.json": "22b44a33bf81142ba2befad14eb5746e",
- "build/assets/ba_data/data/languages/english.json": "b38d54aecf3ac47b8d8ca97d8bab3006",
+ "build/assets/ba_data/data/languages/english.json": "6fb6ec37e79064edb4b8864eabdd024d",
"build/assets/ba_data/data/languages/esperanto.json": "0e397cfa5f3fb8cef5f4a64f21cda880",
- "build/assets/ba_data/data/languages/filipino.json": "347f38524816691170d266708fe25894",
- "build/assets/ba_data/data/languages/french.json": "d8527da977a563185de25ef02bacf826",
- "build/assets/ba_data/data/languages/german.json": "549754d2a530d825200c6126be56df5c",
- "build/assets/ba_data/data/languages/gibberish.json": "837423db378b3e7679683805826aa26e",
- "build/assets/ba_data/data/languages/greek.json": "a65d78f912e9a89f98de004405167a6a",
- "build/assets/ba_data/data/languages/hindi.json": "88ee0cda537bab9ac827def5e236fe1a",
+ "build/assets/ba_data/data/languages/filipino.json": "2efdfb879135b196a272dd47fc2039a2",
+ "build/assets/ba_data/data/languages/french.json": "4e218dcd488fa63e7db5b4da2261b9e1",
+ "build/assets/ba_data/data/languages/german.json": "450fa41ae264f29a5d1af22143d0d0ad",
+ "build/assets/ba_data/data/languages/gibberish.json": "63c6212c774622346f3ad0d87ff31e80",
+ "build/assets/ba_data/data/languages/greek.json": "287c0ec437b38772284ef9d3e4fb2fc3",
+ "build/assets/ba_data/data/languages/hindi.json": "8ea0c58a44a24edb131d0e53b074d1f6",
"build/assets/ba_data/data/languages/hungarian.json": "796a290a8c44a1e7635208c2ff5fdc6e",
- "build/assets/ba_data/data/languages/indonesian.json": "bff88ce57744a639810b93a1d1dd79f4",
- "build/assets/ba_data/data/languages/italian.json": "338e7a03dff47f4eefc0ca3a995cd4f4",
+ "build/assets/ba_data/data/languages/indonesian.json": "53961b1484a1831f32bec2cc2941e672",
+ "build/assets/ba_data/data/languages/italian.json": "58ecf53a963dbeca1bbf3605e5ab6a2f",
"build/assets/ba_data/data/languages/korean.json": "ca1122a9ee551da3f75ae632012bd0e2",
"build/assets/ba_data/data/languages/malay.json": "832562ce997fc70704b9234c95fb2e38",
- "build/assets/ba_data/data/languages/persian.json": "71cc5b33abda0f285b970b8cc4a014a8",
- "build/assets/ba_data/data/languages/polish.json": "e1a1a801851924748ad38fa68216439a",
- "build/assets/ba_data/data/languages/portuguese.json": "9fcd6b4da9e5d0dc0e337ab00b5debe2",
+ "build/assets/ba_data/data/languages/persian.json": "a391d80ff58ea22926499e4b19d2c0d0",
+ "build/assets/ba_data/data/languages/polish.json": "7a4a6cb882cf90dad32e6607215525bf",
+ "build/assets/ba_data/data/languages/portuguese.json": "51e362956f89da3eec980f587c092253",
"build/assets/ba_data/data/languages/romanian.json": "aeebdd54f65939c2facc6ac50c117826",
- "build/assets/ba_data/data/languages/russian.json": "910cf653497654a16d5c4f067d6def22",
+ "build/assets/ba_data/data/languages/russian.json": "561504cca28eb3204ac194950029e565",
"build/assets/ba_data/data/languages/serbian.json": "d7452dd72ac0e51680cb39b5ebaa1c69",
"build/assets/ba_data/data/languages/slovak.json": "27962d53dc3f7dd4e877cd40faafeeef",
- "build/assets/ba_data/data/languages/spanish.json": "0122b0b24aa111ab259af02bbae9b7b6",
+ "build/assets/ba_data/data/languages/spanish.json": "5a4dbd505060dd02d634bd4de4d5faab",
"build/assets/ba_data/data/languages/swedish.json": "77d671f10613291ebf9c71da66f18a18",
- "build/assets/ba_data/data/languages/tamil.json": "b9d4b4e107456ea6420ee0f9d9d7a03e",
+ "build/assets/ba_data/data/languages/tamil.json": "65ab7798d637fa62a703750179eeb723",
"build/assets/ba_data/data/languages/thai.json": "33f63753c9af9a5b238d229a0bf23fbc",
- "build/assets/ba_data/data/languages/turkish.json": "9d7e58c9062dc517c3779c255a9b3142",
+ "build/assets/ba_data/data/languages/turkish.json": "776d1a0c9ef2333a9110d93558ab19e2",
"build/assets/ba_data/data/languages/ukrainian.json": "f72eb51abfbbb56e27866895d7e947d2",
- "build/assets/ba_data/data/languages/venetian.json": "88595b7ee696b4094d7874c3c4188852",
+ "build/assets/ba_data/data/languages/venetian.json": "9fe1a58d9e5dfb00f31ce3b2eb9993f4",
"build/assets/ba_data/data/languages/vietnamese.json": "921cd1e50f60fe3e101f246e172750ba",
"build/assets/ba_data/data/maps/big_g.json": "1dd301d490643088a435ce75df971054",
"build/assets/ba_data/data/maps/bridgit.json": "6aea74805f4880cc11237c5734a24422",
@@ -4056,53 +4056,53 @@
"build/assets/windows/Win32/ucrtbased.dll": "2def5335207d41b21b9823f6805997f1",
"build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae",
"build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599",
- "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "8ea626f6dd70d998ee77c58fffc51545",
- "build/prefab/full/linux_arm64_gui/release/ballisticakit": "d7ad10904fe7c4d4555366ccb1feedcb",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "a9eea92d521e97b1772b5e44b402ce8a",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "57043f40971d800d27ee6d646aae8d61",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "3e4009d7fa4b90abc526f56361ecab05",
- "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "334cdc0688e66a1bc75cd05bae1729c7",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "95278d80378be5b61026253492cbfa70",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "0a3da8e5264a7b733960e83a0e8c4bba",
- "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "a089d4aaa39e18553cf0a70a77b4cfcd",
- "build/prefab/full/mac_arm64_gui/release/ballisticakit": "e66c4eceb79710b8fda2bfea781e241a",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "6b3021b9a7584da86bbb95324e81e851",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "6fe90d50f905a0da9fa52c39a458d1e3",
- "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "afc7ed826486aec82613832865177570",
- "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "ae8b6dad770793188aad8e2966189bb3",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "7622dde1a021152cac42e7db3e803392",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "8a4b1e521bf668cc6ec6a65519defb12",
- "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "9a69c0d4c9ae319595843b16f14795fc",
- "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "89c02260fb4781f5e293658cecbb363f",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "3165d4230069c22300abfff8abf1d714",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "41e46dfdbb542e3e823f6aee87e93ac9",
- "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "20cc128ff9d44f9d74e4301c6d49f48f",
- "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "f93bc8f98ee31f39b54ab46264eccb22",
- "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "20cc128ff9d44f9d74e4301c6d49f48f",
- "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "f93bc8f98ee31f39b54ab46264eccb22",
- "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "1565f3b227843827d692cb3ef65847b6",
- "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "d8d74c6c40db43054ccc7d27920cfbfe",
- "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "1565f3b227843827d692cb3ef65847b6",
- "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "d8d74c6c40db43054ccc7d27920cfbfe",
- "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "43bdfa8acd84e9cf2e443ce8e923c229",
- "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "d935dc21becdfd65bec51c5f5b2fd770",
- "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "43bdfa8acd84e9cf2e443ce8e923c229",
- "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "d935dc21becdfd65bec51c5f5b2fd770",
- "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "6d49ad39f194480da458b431405f5a2b",
- "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "c8715c85010ea431d7346f40f5421819",
- "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "49775819d4ba9af15061080d17377a18",
- "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "c8715c85010ea431d7346f40f5421819",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "750c2964308cd3f3e5986fcda9a25706",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "772af7da6a115f53b0b3f6a4afd3baec",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "178c1a53a7ad50297aed68d0ca3a1476",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "b011bc2b6437995f2d33f5215b4ffa36",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "a758a4f98336208381b093aacb735878",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "6c86545fab2327105114676a20ca5e68",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "f75525fdc9f7db4a81ca9bae6a79add5",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "2e297069baec404d43ccdb18abeef658",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "ad13d636bcb25150044a7644846b8a09",
+ "build/prefab/full/linux_arm64_gui/release/ballisticakit": "7c5df955611590ef491bf614fbd60179",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "ae38bd212ae64b51482a2ccb9c1cbfd3",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "d0bcee2dd5567719aa35667c5206dffc",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "eb3cd4f86175afcf8ffa2749afa32fa3",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "6b86ba36c3719773008feaa6cdc0d0f8",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "877c9ae4532fef809a3dcbd8ffea343c",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "7df313c48c87460f56fa837502965088",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "bc40bb549d26437fb8679c1e9d088272",
+ "build/prefab/full/mac_arm64_gui/release/ballisticakit": "47aa08cb9f5e660023f0f3c0e4ffd65e",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "bbb0a8383d6ce1ca887190ea49223f4f",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "7dd91e3407d49981c1c975d4f01ac205",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "c87883aa2f832e792e945fd9208d712a",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "fea8fd84d8c060f2f82f402902b8c54e",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "0a454a8be47f37231655761d15e3f7e5",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "4c79db3a882eb0b8b225a8df0339b1cc",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "43b9ef321f8e80da29ddb19a760dbd77",
+ "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "6f891004f2f07c452dea29bd53f29d30",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "bf7a1ce0e7a2015d538406c6f6df761c",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "cf213dce81901a67c9970b3befdaa320",
+ "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "473e7e6c0cf90b9e6ac653552b18f68d",
+ "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "4e11b895cbf2e1339cf34bc06c54a4ea",
+ "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "473e7e6c0cf90b9e6ac653552b18f68d",
+ "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "4e11b895cbf2e1339cf34bc06c54a4ea",
+ "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "d9af1a429cff9346e0cad6fcea017e5b",
+ "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "ae5f87286947575463c386cfe1c443e4",
+ "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "d9af1a429cff9346e0cad6fcea017e5b",
+ "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "ae5f87286947575463c386cfe1c443e4",
+ "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "110eef3dc285a35a1899510e368c73b1",
+ "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "2692dc69f7cb2501f0aaa8675f559987",
+ "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "110eef3dc285a35a1899510e368c73b1",
+ "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "2692dc69f7cb2501f0aaa8675f559987",
+ "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "344954c4f788d7d9b4d7035ebb6131d8",
+ "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "48c4873dae2344c1d4092a1d85dab424",
+ "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "abcede4e60fa8877f18e66e086fb7387",
+ "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "48c4873dae2344c1d4092a1d85dab424",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "6149911c660a9864b651cc1a8e50eec1",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "57cef68ab703ba819bd0fbe9e4b1c331",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "a47bab28b86c7cefce891b8e5c8b687a",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "d68ebb1139363d711b044de65e17b204",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "ea1349137f64f3d662b9a95278ca4c02",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "c8731ff226716cee3d1e46027ead1cfe",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "4ee6b633a99c5bcbea4f5dee5bda186e",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "2c3bd4952b30d88247229ad309f73092",
"src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
"src/assets/ba_data/python/babase/_mgen/enums.py": "28323912b56ec07701eda3d41a6a4101",
- "src/ballistica/base/mgen/pyembed/binding_base.inc": "ba8ce3ca3858b4c2d20db68f99b788b2",
+ "src/ballistica/base/mgen/pyembed/binding_base.inc": "6df0f34207346d89a72924249ddd4706",
"src/ballistica/base/mgen/pyembed/binding_base_app.inc": "00f81f9bd92386ec12a6e60170678a98",
"src/ballistica/classic/mgen/pyembed/binding_classic.inc": "3ceb412513963f0818ab39c58bf292e3",
"src/ballistica/core/mgen/pyembed/binding_core.inc": "9d0a3c9636138e35284923e0c8311c69",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4cabf797..f785e42f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-### 1.7.28 (build 21465, api 8, 2023-10-14)
+### 1.7.28 (build 21491, api 8, 2023-10-22)
- Massively cleaned up code related to rendering and window systems (OpenGL,
SDL, etc). This code had been growing into a nasty tangle for 15 years
@@ -146,7 +146,14 @@
rates are high.
- Added a proper graceful shutdown process for the audio server. This should
result in fewer ugly pops and warning messages when the app is quit.
-
+- Tidied up some keyboard shortcuts to be more platform-appropriate. For
+ example, toggling fullscreen on Windows is now Alt+Enter or F11.
+- Fancy rebuilt Mac build should now automatically sync its frame rate to the
+ display its running on (using CVDisplayLinks, not VSync).
+- Mac build is now relying solely on Apple's Game Controller Framework, which
+ seems pretty awesome these days. It should support most stuff SDL does and
+ with less configuring involved. Please holler if you come across something
+ that doesn't work.
### 1.7.27 (build 21282, api 8, 2023-08-30)
diff --git a/ballisticakit-cmake/CMakeLists.txt b/ballisticakit-cmake/CMakeLists.txt
index b9050756..297cfe0b 100644
--- a/ballisticakit-cmake/CMakeLists.txt
+++ b/ballisticakit-cmake/CMakeLists.txt
@@ -353,6 +353,10 @@ set(BALLISTICA_SOURCES
${BA_SRC_ROOT}/ballistica/base/graphics/support/camera.h
${BA_SRC_ROOT}/ballistica/base/graphics/support/frame_def.cc
${BA_SRC_ROOT}/ballistica/base/graphics/support/frame_def.h
+ ${BA_SRC_ROOT}/ballistica/base/graphics/support/graphics_client_context.cc
+ ${BA_SRC_ROOT}/ballistica/base/graphics/support/graphics_client_context.h
+ ${BA_SRC_ROOT}/ballistica/base/graphics/support/graphics_settings.cc
+ ${BA_SRC_ROOT}/ballistica/base/graphics/support/graphics_settings.h
${BA_SRC_ROOT}/ballistica/base/graphics/support/net_graph.cc
${BA_SRC_ROOT}/ballistica/base/graphics/support/net_graph.h
${BA_SRC_ROOT}/ballistica/base/graphics/support/render_command_buffer.h
@@ -688,6 +692,7 @@ set(BALLISTICA_SOURCES
${BA_SRC_ROOT}/ballistica/shared/generic/lambda_runnable.h
${BA_SRC_ROOT}/ballistica/shared/generic/runnable.cc
${BA_SRC_ROOT}/ballistica/shared/generic/runnable.h
+ ${BA_SRC_ROOT}/ballistica/shared/generic/snapshot.h
${BA_SRC_ROOT}/ballistica/shared/generic/timer_list.cc
${BA_SRC_ROOT}/ballistica/shared/generic/timer_list.h
${BA_SRC_ROOT}/ballistica/shared/generic/utf8.cc
diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj
index 3ec34dcc..affcd239 100644
--- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj
+++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj
@@ -345,6 +345,10 @@
+
+
+
+
@@ -680,6 +684,7 @@
+
diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters
index f77535ec..b3e5ebdf 100644
--- a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters
+++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters
@@ -469,6 +469,18 @@
ballistica\base\graphics\support
+
+ ballistica\base\graphics\support
+
+
+ ballistica\base\graphics\support
+
+
+ ballistica\base\graphics\support
+
+
+ ballistica\base\graphics\support
+
ballistica\base\graphics\support
@@ -1474,6 +1486,9 @@
ballistica\shared\generic
+
+ ballistica\shared\generic
+
ballistica\shared\generic
diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj
index 505b71c1..82d010b3 100644
--- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj
+++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj
@@ -340,6 +340,10 @@
+
+
+
+
@@ -675,6 +679,7 @@
+
diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters
index f77535ec..b3e5ebdf 100644
--- a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters
+++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters
@@ -469,6 +469,18 @@
ballistica\base\graphics\support
+
+ ballistica\base\graphics\support
+
+
+ ballistica\base\graphics\support
+
+
+ ballistica\base\graphics\support
+
+
+ ballistica\base\graphics\support
+
ballistica\base\graphics\support
@@ -1474,6 +1486,9 @@
ballistica\shared\generic
+
+ ballistica\shared\generic
+
ballistica\shared\generic
diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py
index fa542c74..9eb09af0 100644
--- a/src/assets/ba_data/python/babase/__init__.py
+++ b/src/assets/ba_data/python/babase/__init__.py
@@ -27,7 +27,10 @@ from _babase import (
apptime,
apptimer,
AppTimer,
- can_toggle_fullscreen,
+ fullscreen_control_available,
+ fullscreen_control_get,
+ fullscreen_control_key_shortcut,
+ fullscreen_control_set,
charstr,
clipboard_get_text,
clipboard_has_text,
@@ -200,7 +203,10 @@ __all__ = [
'apptimer',
'AppTimer',
'Call',
- 'can_toggle_fullscreen',
+ 'fullscreen_control_available',
+ 'fullscreen_control_get',
+ 'fullscreen_control_key_shortcut',
+ 'fullscreen_control_set',
'charstr',
'clipboard_get_text',
'clipboard_has_text',
diff --git a/src/assets/ba_data/python/babase/_app.py b/src/assets/ba_data/python/babase/_app.py
index 84147e5d..ec0e0d8b 100644
--- a/src/assets/ba_data/python/babase/_app.py
+++ b/src/assets/ba_data/python/babase/_app.py
@@ -895,10 +895,18 @@ class App:
import asyncio
# Spin and wait for anything blocking shutdown to complete.
+ starttime = _babase.apptime()
_babase.lifecyclelog('shutdown-suppress wait begin')
while _babase.shutdown_suppress_count() > 0:
await asyncio.sleep(0.001)
_babase.lifecyclelog('shutdown-suppress wait end')
+ duration = _babase.apptime() - starttime
+ if duration > 1.0:
+ logging.warning(
+ 'Shutdown-suppressions lasted longer than ideal '
+ '(%.2f seconds).',
+ duration,
+ )
async def _fade_and_shutdown_graphics(self) -> None:
import asyncio
diff --git a/src/assets/ba_data/python/babase/_env.py b/src/assets/ba_data/python/babase/_env.py
index 1e386689..2af28c23 100644
--- a/src/assets/ba_data/python/babase/_env.py
+++ b/src/assets/ba_data/python/babase/_env.py
@@ -185,10 +185,8 @@ def _feed_logs_to_babase(log_handler: LogHandler) -> None:
def _on_log(entry: LogEntry) -> None:
# Forward this along to the engine to display in the in-app
# console, in the Android log, etc.
- _babase.display_log(
- name=entry.name,
- level=entry.level.name,
- message=entry.message,
+ _babase.emit_log(
+ name=entry.name, level=entry.level.name, message=entry.message
)
# We also want to feed some logs to the old v1-cloud-log system.
diff --git a/src/assets/ba_data/python/babase/_hooks.py b/src/assets/ba_data/python/babase/_hooks.py
index 90dde9be..8ea73a5e 100644
--- a/src/assets/ba_data/python/babase/_hooks.py
+++ b/src/assets/ba_data/python/babase/_hooks.py
@@ -33,18 +33,30 @@ def reset_to_main_menu() -> None:
logging.warning('reset_to_main_menu: no-op due to classic not present.')
-def set_config_fullscreen_on() -> None:
+def store_config_fullscreen_on() -> None:
"""The OS has changed our fullscreen state and we should take note."""
_babase.app.config['Fullscreen'] = True
_babase.app.config.commit()
-def set_config_fullscreen_off() -> None:
+def store_config_fullscreen_off() -> None:
"""The OS has changed our fullscreen state and we should take note."""
_babase.app.config['Fullscreen'] = False
_babase.app.config.commit()
+def set_config_fullscreen_on() -> None:
+ """Set and store fullscreen state"""
+ _babase.app.config['Fullscreen'] = True
+ _babase.app.config.apply_and_commit()
+
+
+def set_config_fullscreen_off() -> None:
+ """The OS has changed our fullscreen state and we should take note."""
+ _babase.app.config['Fullscreen'] = False
+ _babase.app.config.apply_and_commit()
+
+
def not_signed_in_screen_message() -> None:
from babase._language import Lstr
@@ -377,3 +389,17 @@ def string_edit_adapter_can_be_replaced(adapter: StringEditAdapter) -> bool:
def get_dev_console_tab_names() -> list[str]:
"""Return the current set of dev-console tab names."""
return [t.name for t in _babase.app.devconsole.tabs]
+
+
+def unsupported_controller_message(name: str) -> None:
+ """Print a message when an unsupported controller is connected."""
+ from babase._language import Lstr
+
+ # Ick; this can get called early in the bootstrapping process
+ # before we're allowed to load assets. Guard against that.
+ if _babase.asset_loads_allowed():
+ _babase.getsimplesound('error').play()
+ _babase.screenmessage(
+ Lstr(resource='unsupportedControllerText', subs=[('${NAME}', name)]),
+ color=(1, 0, 0),
+ )
diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py
index b832f68b..0d19c817 100644
--- a/src/assets/ba_data/python/baenv.py
+++ b/src/assets/ba_data/python/baenv.py
@@ -40,7 +40,7 @@ if TYPE_CHECKING:
# the last load. Either way, however, multiple execs will happen in some
# form.
#
-# So we need to do a few things to handle that situation gracefully.
+# To handle that situation gracefully, we need to do a few things:
#
# - First, we need to store any mutable global state in the __main__
# module; not in ourself. This way, alternate versions of ourself will
@@ -48,11 +48,11 @@ if TYPE_CHECKING:
#
# - Second, we should avoid the use of isinstance and similar calls for
# our types. An EnvConfig we create would technically be a different
-# type than that created by an alternate baenv.
+# type than an EnvConfig created by an alternate baenv.
# Build number and version of the ballistica binary we expect to be
# using.
-TARGET_BALLISTICA_BUILD = 21465
+TARGET_BALLISTICA_BUILD = 21491
TARGET_BALLISTICA_VERSION = '1.7.28'
diff --git a/src/assets/ba_data/python/bauiv1/__init__.py b/src/assets/ba_data/python/bauiv1/__init__.py
index 3d3a7b42..535d6719 100644
--- a/src/assets/ba_data/python/bauiv1/__init__.py
+++ b/src/assets/ba_data/python/bauiv1/__init__.py
@@ -31,7 +31,10 @@ from babase import (
apptimer,
AppTimer,
Call,
- can_toggle_fullscreen,
+ fullscreen_control_available,
+ fullscreen_control_get,
+ fullscreen_control_key_shortcut,
+ fullscreen_control_set,
charstr,
clipboard_is_supported,
clipboard_set_text,
@@ -139,7 +142,10 @@ __all__ = [
'buttonwidget',
'Call',
'can_show_ad',
- 'can_toggle_fullscreen',
+ 'fullscreen_control_available',
+ 'fullscreen_control_get',
+ 'fullscreen_control_key_shortcut',
+ 'fullscreen_control_set',
'charstr',
'checkboxwidget',
'clipboard_is_supported',
diff --git a/src/assets/ba_data/python/bauiv1lib/account/settings.py b/src/assets/ba_data/python/bauiv1lib/account/settings.py
index 50959d01..4cea92da 100644
--- a/src/assets/ba_data/python/bauiv1lib/account/settings.py
+++ b/src/assets/ba_data/python/bauiv1lib/account/settings.py
@@ -704,7 +704,7 @@ class AccountSettingsWindow(bui.Window):
position=((self._sub_width - button_width) * 0.5, v + 30),
autoselect=True,
size=(button_width, 60),
- label=bui.Lstr(resource=self._r + '.manageAccountText'),
+ label=bui.Lstr(resource=f'{self._r}.manageAccountText'),
color=(0.55, 0.5, 0.6),
icon=bui.gettexture('settingsIcon'),
textcolor=(0.75, 0.7, 0.8),
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/controls.py b/src/assets/ba_data/python/bauiv1lib/settings/controls.py
index 0b9abd39..168df15c 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/controls.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/controls.py
@@ -98,9 +98,11 @@ class ControlsSettingsWindow(bui.Window):
# made-for-iOS/Mac systems
# (we can run into problems where devices register as one of each
# type otherwise)..
+ # UPDATE: We always use the apple system these days (which should
+ # support older controllers). So no need for a switch.
show_mac_controller_subsystem = False
- if platform == 'mac' and bui.is_xcode_build():
- show_mac_controller_subsystem = True
+ # if platform == 'mac' and bui.is_xcode_build():
+ # show_mac_controller_subsystem = True
if show_mac_controller_subsystem:
height += spacing * 1.5
@@ -311,6 +313,7 @@ class ControlsSettingsWindow(bui.Window):
maxwidth=width * 0.8,
)
v -= spacing
+
if show_mac_controller_subsystem:
PopupMenu(
parent=self._root_widget,
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py b/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py
index 9534ec8d..b576d422 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/gamepadselect.py
@@ -29,16 +29,16 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None:
logging.exception('Error transitioning out main_menu_window.')
bui.getsound('activateBeep').play()
bui.getsound('swish').play()
- inputdevice = event['input_device']
- assert isinstance(inputdevice, bs.InputDevice)
- if inputdevice.allows_configuring:
+ device = event['input_device']
+ assert isinstance(device, bs.InputDevice)
+ if device.allows_configuring:
bui.app.ui_v1.set_main_menu_window(
- gamepad.GamepadSettingsWindow(inputdevice).get_root_widget()
+ gamepad.GamepadSettingsWindow(device).get_root_widget()
)
else:
width = 700
height = 200
- button_width = 100
+ button_width = 80
uiscale = bui.app.ui_v1.uiscale
dlg = bui.containerwidget(
scale=(
@@ -52,8 +52,13 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None:
transition='in_right',
)
bui.app.ui_v1.set_main_menu_window(dlg)
- device_name = inputdevice.name
- if device_name == 'iDevice':
+
+ if device.allows_configuring_in_system_settings:
+ msg = bui.Lstr(
+ resource='configureDeviceInSystemSettingsText',
+ subs=[('${DEVICE}', device.name)],
+ )
+ elif device.is_controller_app:
msg = bui.Lstr(
resource='bsRemoteConfigureInAppText',
subs=[('${REMOTE_APP_NAME}', bui.get_remote_app_name())],
@@ -61,7 +66,7 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None:
else:
msg = bui.Lstr(
resource='cantConfigureDeviceText',
- subs=[('${DEVICE}', device_name)],
+ subs=[('${DEVICE}', device.name)],
)
bui.textwidget(
parent=dlg,
diff --git a/src/assets/ba_data/python/bauiv1lib/settings/graphics.py b/src/assets/ba_data/python/bauiv1lib/settings/graphics.py
index ec6879b4..1c235095 100644
--- a/src/assets/ba_data/python/bauiv1lib/settings/graphics.py
+++ b/src/assets/ba_data/python/bauiv1lib/settings/graphics.py
@@ -52,7 +52,7 @@ class GraphicsSettingsWindow(bui.Window):
self._show_fullscreen = False
fullscreen_spacing_top = spacing * 0.2
fullscreen_spacing = spacing * 1.2
- if bui.can_toggle_fullscreen():
+ if bui.fullscreen_control_available():
self._show_fullscreen = True
height += fullscreen_spacing + fullscreen_spacing_top
@@ -122,21 +122,29 @@ class GraphicsSettingsWindow(bui.Window):
self._fullscreen_checkbox: bui.Widget | None = None
if self._show_fullscreen:
v -= fullscreen_spacing_top
- self._fullscreen_checkbox = ConfigCheckBox(
+ # Fullscreen control does not necessarily talk to the
+ # app config so we have to wrangle it manually instead of
+ # using a config-checkbox.
+ label = bui.Lstr(resource=f'{self._r}.fullScreenText')
+
+ # Show keyboard shortcut alongside the control if they
+ # provide one.
+ shortcut = bui.fullscreen_control_key_shortcut()
+ if shortcut is not None:
+ label = bui.Lstr(
+ value='$(NAME) [$(SHORTCUT)]',
+ subs=[('$(NAME)', label), ('$(SHORTCUT)', shortcut)],
+ )
+ self._fullscreen_checkbox = bui.checkboxwidget(
parent=self._root_widget,
position=(100, v),
- maxwidth=200,
+ value=bui.fullscreen_control_get(),
+ on_value_change_call=bui.fullscreen_control_set,
+ maxwidth=250,
size=(300, 30),
- configkey='Fullscreen',
- displayname=bui.Lstr(
- resource=self._r
- + (
- '.fullScreenCmdText'
- if app.classic.platform == 'mac'
- else '.fullScreenCtrlText'
- )
- ),
- ).widget
+ text=label,
+ )
+
if not self._have_selected_child:
bui.containerwidget(
edit=self._root_widget,
@@ -528,8 +536,10 @@ class GraphicsSettingsWindow(bui.Window):
and bui.apptime() - self._last_max_fps_set_time > 1.0
):
self._apply_max_fps()
+
if self._show_fullscreen:
+ # Keep the fullscreen checkbox up to date with the current value.
bui.checkboxwidget(
edit=self._fullscreen_checkbox,
- value=bui.app.config.resolve('Fullscreen'),
+ value=bui.fullscreen_control_get(),
)
diff --git a/src/ballistica/base/app_adapter/app_adapter.cc b/src/ballistica/base/app_adapter/app_adapter.cc
index f7fb480c..a7f1cf89 100644
--- a/src/ballistica/base/app_adapter/app_adapter.cc
+++ b/src/ballistica/base/app_adapter/app_adapter.cc
@@ -2,7 +2,7 @@
#include "ballistica/base/app_adapter/app_adapter.h"
-#if BA_OSTYPE_ANDROID
+#if BA_OSTYPE_ANDROID // Remove conditional once android sources are public.
#include "ballistica/base/app_adapter/app_adapter_android.h"
#endif
#include "ballistica/base/app_adapter/app_adapter_apple.h"
@@ -15,6 +15,8 @@
#include "ballistica/base/networking/network_reader.h"
#include "ballistica/base/networking/networking.h"
#include "ballistica/base/platform/base_platform.h"
+#include "ballistica/base/python/base_python.h"
+#include "ballistica/base/support/app_config.h"
#include "ballistica/base/support/stress_test.h"
#include "ballistica/base/ui/ui.h"
#include "ballistica/shared/foundation/event_loop.h"
@@ -239,7 +241,7 @@ void AppAdapter::DoExitMainThreadEventLoop() {
FatalError("DoExitMainThreadEventLoop is not implemented here.");
}
-auto AppAdapter::CanToggleFullscreen() -> bool const { return false; }
+auto AppAdapter::FullscreenControlAvailable() const -> bool { return false; }
auto AppAdapter::SupportsVSync() -> bool const { return false; }
@@ -253,6 +255,29 @@ void AppAdapter::DoPushGraphicsContextRunnable(Runnable* runnable) {
DoPushMainThreadRunnable(runnable);
}
+auto AppAdapter::FullscreenControlGet() const -> bool {
+ assert(g_base->InLogicThread());
+
+ // By default, just go through config (assume we have full control over
+ // the fullscreen state ourself).
+ return g_base->app_config->Resolve(AppConfig::BoolID::kFullscreen);
+}
+
+void AppAdapter::FullscreenControlSet(bool fullscreen) {
+ assert(g_base->InLogicThread());
+ // By default, just set these in the config and apply it (assumes config
+ // changes get plugged into actual fullscreen state).
+ g_base->python->objs()
+ .Get(fullscreen ? BasePython::ObjID::kSetConfigFullscreenOnCall
+ : BasePython::ObjID::kSetConfigFullscreenOffCall)
+ .Call();
+}
+
+auto AppAdapter::FullscreenControlKeyShortcut() const
+ -> std::optional {
+ return {};
+}
+
void AppAdapter::CursorPositionForDraw(float* x, float* y) {
assert(x && y);
@@ -271,14 +296,23 @@ auto AppAdapter::ShouldUseCursor() -> bool { return true; }
auto AppAdapter::HasHardwareCursor() -> bool { return false; }
-void AppAdapter::SetHardwareCursorVisible(bool visible) {
- printf("SHOULD SET VIS %d\n", static_cast(visible));
-}
+void AppAdapter::SetHardwareCursorVisible(bool visible) {}
auto AppAdapter::CanSoftQuit() -> bool { return false; }
auto AppAdapter::CanBackQuit() -> bool { return false; }
void AppAdapter::DoBackQuit() { FatalError("Fixme unimplemented."); }
void AppAdapter::DoSoftQuit() { FatalError("Fixme unimplemented."); }
void AppAdapter::TerminateApp() { FatalError("Fixme unimplemented."); }
+auto AppAdapter::HasDirectKeyboardInput() -> bool { return false; }
+
+void AppAdapter::ApplyGraphicsSettings(const GraphicsSettings* settings) {}
+
+auto AppAdapter::GetGraphicsSettings() -> GraphicsSettings* {
+ return new GraphicsSettings();
+}
+
+auto AppAdapter::GetGraphicsClientContext() -> GraphicsClientContext* {
+ return new GraphicsClientContext();
+}
} // namespace ballistica::base
diff --git a/src/ballistica/base/app_adapter/app_adapter.h b/src/ballistica/base/app_adapter/app_adapter.h
index 427f536c..25fb83de 100644
--- a/src/ballistica/base/app_adapter/app_adapter.h
+++ b/src/ballistica/base/app_adapter/app_adapter.h
@@ -30,6 +30,16 @@ class AppAdapter {
virtual void OnScreenSizeChange();
virtual void DoApplyAppConfig();
+ /// When called, should allocate an instance of a GraphicsSettings
+ /// subclass using 'new', fill it out, and return it. Runs in the logic
+ /// thread.
+ virtual auto GetGraphicsSettings() -> GraphicsSettings*;
+
+ /// When called, should allocate an instance of a GraphicsClientContext
+ /// subclass using 'new', fill it out, and return it. Runs in the graphics
+ /// context.
+ virtual auto GetGraphicsClientContext() -> GraphicsClientContext*;
+
/// Return whether this class manages the main thread event loop itself.
/// Default is true. If this is true, RunMainThreadEventLoopToCompletion()
/// will be called to run the app. This should return false on builds
@@ -113,9 +123,27 @@ class AppAdapter {
auto app_suspended() const { return app_suspended_; }
/// Return whether this AppAdapter supports a 'fullscreen' toggle for its
- /// display. This currently will simply affect whether that option is
- /// available in display settings or via a hotkey.
- virtual auto CanToggleFullscreen() -> bool const;
+ /// display. This will affect whether that option is available in display
+ /// settings or via a hotkey. Must be called from the logic thread.
+ virtual auto FullscreenControlAvailable() const -> bool;
+
+ /// AppAdapters supporting a 'fullscreen' control should return the
+ /// current fullscreen state here. By default this simply returns the
+ /// app-config fullscreen value (so assumes the actual state is synced to
+ /// that). Must be called from the logic thread.
+ virtual auto FullscreenControlGet() const -> bool;
+
+ /// AppAdapters supporting a 'fullscreen' control should set the
+ /// current fullscreen state here. By default this simply sets the
+ /// app-config fullscreen value (so assumes the actual state is synced to
+ /// that). Must be called from the logic thread.
+ virtual void FullscreenControlSet(bool fullscreen);
+
+ /// AppAdapters supporting a 'fullscreen' control can return a key name
+ /// here to display if they support toggling via key ('ctrl-F', etc.).
+ /// Must be called from the logic thread.
+ virtual auto FullscreenControlKeyShortcut() const
+ -> std::optional;
/// Return whether this AppAdapter supports vsync controls for its display.
virtual auto SupportsVSync() -> bool const;
@@ -153,6 +181,22 @@ class AppAdapter {
/// this point.
virtual void TerminateApp();
+ /// Should return whether there is a keyboard attached that will deliver
+ /// direct text-editing related events to the app. When this is false,
+ /// alternate entry methods such as keyboard-entry-dialogs and on-screen
+ /// keyboards will be used. This value can change based on conditions such
+ /// as a hardware keyboard getting attached or detached or the language
+ /// changing (it may be preferable to rely on dialogs for non-english
+ /// languages/etc.). Default implementation returns false. This function
+ /// should be callable from any thread.
+ virtual auto HasDirectKeyboardInput() -> bool;
+
+ /// Called in the graphics context to apply new settings coming in from
+ /// the logic subsystem. This will be called initially to jump-start the
+ /// graphics system as well as before frame draws to update any new
+ /// settings coming in.
+ virtual void ApplyGraphicsSettings(const GraphicsSettings* settings);
+
protected:
AppAdapter();
virtual ~AppAdapter();
diff --git a/src/ballistica/base/app_adapter/app_adapter_apple.cc b/src/ballistica/base/app_adapter/app_adapter_apple.cc
index 13ebe5a8..aaefa65e 100644
--- a/src/ballistica/base/app_adapter/app_adapter_apple.cc
+++ b/src/ballistica/base/app_adapter/app_adapter_apple.cc
@@ -44,58 +44,27 @@ void AppAdapterApple::DoPushMainThreadRunnable(Runnable* runnable) {
BallisticaKit::FromCppPushRawRunnableToMain(runnable);
}
-void AppAdapterApple::DoApplyAppConfig() {
- assert(g_base->InLogicThread());
+void AppAdapterApple::DoApplyAppConfig() { assert(g_base->InLogicThread()); }
- g_base->graphics_server->PushSetScreenPixelScaleCall(
- g_base->app_config->Resolve(AppConfig::FloatID::kScreenPixelScale));
+void AppAdapterApple::ApplyGraphicsSettings(const GraphicsSettings* settings) {
+ auto* graphics_server = g_base->graphics_server;
- auto graphics_quality_requested =
- g_base->graphics->GraphicsQualityFromAppConfig();
-
- auto texture_quality_requested =
- g_base->graphics->TextureQualityFromAppConfig();
-
- g_base->app_adapter->PushGraphicsContextCall([=] {
- SetScreen_(texture_quality_requested, graphics_quality_requested);
- });
-}
-
-void AppAdapterApple::SetScreen_(
- TextureQualityRequest texture_quality_requested,
- GraphicsQualityRequest graphics_quality_requested) {
- // If we know what we support, filter our request types to what is
- // supported. This will keep us from rebuilding contexts if request type
- // is flipping between different types that we don't support.
- if (g_base->graphics->has_supports_high_quality_graphics_value()) {
- if (!g_base->graphics->supports_high_quality_graphics()
- && graphics_quality_requested > GraphicsQualityRequest::kMedium) {
- graphics_quality_requested = GraphicsQualityRequest::kMedium;
- }
- }
-
- auto* gs = g_base->graphics_server;
+ // We need a full renderer reload if quality values have changed
+ // or if we don't have a renderer yet.
+ bool need_full_reload = ((graphics_server->texture_quality_requested()
+ != settings->texture_quality)
+ || (graphics_server->graphics_quality_requested()
+ != settings->graphics_quality));
// We need a full renderer reload if quality values have changed or if we
- // don't have one yet.
- bool need_full_reload =
- ((gs->texture_quality_requested() != texture_quality_requested)
- || (gs->graphics_quality_requested() != graphics_quality_requested)
- || !gs->texture_quality_set() || !gs->graphics_quality_set());
+ // don't yet have a renderer.
if (need_full_reload) {
- ReloadRenderer_(graphics_quality_requested, texture_quality_requested);
+ ReloadRenderer_(settings);
}
-
- // Let the logic thread know we've got a graphics system up and running.
- // It may use this cue to kick off asset loads and other bootstrapping.
- g_base->logic->event_loop()->PushCall(
- [] { g_base->logic->OnGraphicsReady(); });
}
-void AppAdapterApple::ReloadRenderer_(
- GraphicsQualityRequest graphics_quality_requested,
- TextureQualityRequest texture_quality_requested) {
+void AppAdapterApple::ReloadRenderer_(const GraphicsSettings* settings) {
auto* gs = g_base->graphics_server;
if (gs->renderer() && gs->renderer_loaded()) {
@@ -105,15 +74,9 @@ void AppAdapterApple::ReloadRenderer_(
gs->set_renderer(new RendererGL());
}
- // Set a dummy screen resolution to start with. The main thread will kick
- // along the latest real resolution just before each frame draw, but we
- // need *something* here or else we'll get errors due to framebuffers
- // getting made at size 0/etc.
- g_base->graphics_server->SetScreenResolution(320.0, 240.0);
-
// Update graphics quality based on request.
- gs->set_graphics_quality_requested(graphics_quality_requested);
- gs->set_texture_quality_requested(texture_quality_requested);
+ gs->set_graphics_quality_requested(settings->graphics_quality);
+ gs->set_texture_quality_requested(settings->texture_quality);
// (Re)load stuff with these latest quality settings.
gs->LoadRenderer();
@@ -123,12 +86,6 @@ void AppAdapterApple::UpdateScreenSizes_() {
assert(g_base->app_adapter->InGraphicsContext());
}
-void AppAdapterApple::SetScreenResolution(float pixel_width,
- float pixel_height) {
- auto allow = ScopedAllowGraphics_(this);
- g_base->graphics_server->SetScreenResolution(pixel_width, pixel_height);
-}
-
auto AppAdapterApple::TryRender() -> bool {
auto allow = ScopedAllowGraphics_(this);
@@ -146,10 +103,45 @@ auto AppAdapterApple::TryRender() -> bool {
call->RunAndLogErrors();
delete call;
}
- // Lastly render.
- return g_base->graphics_server->TryRender();
- return true;
+ // Lastly, render.
+ auto result = g_base->graphics_server->TryRender();
+
+ // A little trick to make mac resizing look a lot smoother. Because we
+ // render in a background thread, we often don't render at the most up to
+ // date window size during a window resize. Normally this makes our image
+ // jerk around in an ugly way, but if we just re-render once or twice in
+ // those cases we mostly always get the most up to date window size.
+ if (result && resize_friendly_frames_ > 0) {
+ // Leave this enabled for just a few frames every time it is set.
+ // (so just in case it breaks we won't draw each frame serveral times for
+ // eternity).
+ resize_friendly_frames_ -= 1;
+
+ // Keep on drawing until the drawn window size
+ // matches what we have (or until we try for too long or fail at drawing).
+ seconds_t start_time = g_core->GetAppTimeSeconds();
+ for (int i = 0; i < 5; ++i) {
+ if (((std::abs(resize_target_resolution_.x
+ - g_base->graphics_server->screen_pixel_width())
+ > 0.01f)
+ || (std::abs(resize_target_resolution_.y
+ - g_base->graphics_server->screen_pixel_height())
+ > 0.01f))
+ && g_core->GetAppTimeSeconds() - start_time < 0.1 && result) {
+ result = g_base->graphics_server->TryRender();
+ } else {
+ break;
+ }
+ }
+ }
+
+ return result;
+}
+
+void AppAdapterApple::EnableResizeFriendlyMode(int width, int height) {
+ resize_friendly_frames_ = 5;
+ resize_target_resolution_ = Vector2f(width, height);
}
auto AppAdapterApple::InGraphicsContext() -> bool {
@@ -202,6 +194,36 @@ void AppAdapterApple::TerminateApp() {
#endif
}
+auto AppAdapterApple::FullscreenControlAvailable() const -> bool {
+ // Currently Mac only. Any window-management stuff elsewhere such as
+ // iPadOS is out of our hands.
+ if (g_buildconfig.ostype_macos()) {
+ return true;
+ }
+ return false;
+}
+
+auto AppAdapterApple::FullscreenControlGet() const -> bool {
+#if BA_OSTYPE_MACOS
+ return BallisticaKit::CocoaFromCppGetMainWindowIsFullscreen();
+#else
+ return false;
+#endif
+}
+
+void AppAdapterApple::FullscreenControlSet(bool fullscreen) {
+#if BA_OSTYPE_MACOS
+ return BallisticaKit::CocoaFromCppSetMainWindowFullscreen(fullscreen);
+#endif
+}
+
+auto AppAdapterApple::FullscreenControlKeyShortcut() const
+ -> std::optional {
+ return "fn+F";
+}
+
+auto AppAdapterApple::HasDirectKeyboardInput() -> bool { return true; };
+
} // namespace ballistica::base
#endif // BA_XCODE_BUILD
diff --git a/src/ballistica/base/app_adapter/app_adapter_apple.h b/src/ballistica/base/app_adapter/app_adapter_apple.h
index 09ccdbc2..9217299b 100644
--- a/src/ballistica/base/app_adapter/app_adapter_apple.h
+++ b/src/ballistica/base/app_adapter/app_adapter_apple.h
@@ -11,6 +11,7 @@
#include "ballistica/base/app_adapter/app_adapter.h"
#include "ballistica/shared/generic/runnable.h"
+#include "ballistica/shared/math/vector2f.h"
namespace ballistica::base {
@@ -31,8 +32,14 @@ class AppAdapterApple : public AppAdapter {
/// Called by FromSwift.
auto TryRender() -> bool;
- /// Called by FromSwift.
- void SetScreenResolution(float pixel_width, float pixel_height);
+ auto FullscreenControlAvailable() const -> bool override;
+ auto FullscreenControlGet() const -> bool override;
+ void FullscreenControlSet(bool fullscreen) override;
+ auto FullscreenControlKeyShortcut() const
+ -> std::optional override;
+
+ auto HasDirectKeyboardInput() -> bool override;
+ void EnableResizeFriendlyMode(int width, int height);
protected:
void DoPushMainThreadRunnable(Runnable* runnable) override;
@@ -42,16 +49,18 @@ class AppAdapterApple : public AppAdapter {
auto HasHardwareCursor() -> bool override;
void SetHardwareCursorVisible(bool visible) override;
void TerminateApp() override;
+ void ApplyGraphicsSettings(const GraphicsSettings* settings) override;
private:
- void UpdateScreenSizes_();
class ScopedAllowGraphics_;
- void SetScreen_(TextureQualityRequest texture_quality_requested,
- GraphicsQualityRequest graphics_quality_requested);
- void ReloadRenderer_(GraphicsQualityRequest graphics_quality_requested,
- TextureQualityRequest texture_quality_requested);
+
+ void UpdateScreenSizes_();
+ void ReloadRenderer_(const GraphicsSettings* settings);
+
std::thread::id graphics_thread_{};
- bool graphics_allowed_;
+ bool graphics_allowed_ : 1 {};
+ uint8_t resize_friendly_frames_{};
+ Vector2f resize_target_resolution_{-1.0f, -1.0f};
std::mutex graphics_calls_mutex_;
std::vector graphics_calls_;
};
diff --git a/src/ballistica/base/app_adapter/app_adapter_headless.cc b/src/ballistica/base/app_adapter/app_adapter_headless.cc
index 2b8339bb..bc313f33 100644
--- a/src/ballistica/base/app_adapter/app_adapter_headless.cc
+++ b/src/ballistica/base/app_adapter/app_adapter_headless.cc
@@ -4,6 +4,7 @@
#include "ballistica/base/app_adapter/app_adapter_headless.h"
#include "ballistica/base/graphics/graphics_server.h"
+#include "ballistica/base/graphics/support/graphics_client_context.h"
#include "ballistica/shared/ballistica.h"
namespace ballistica::base {
@@ -19,12 +20,7 @@ void AppAdapterHeadless::OnMainThreadStartApp() {
new EventLoop(EventLoopID::kMain, ThreadSource::kWrapCurrent);
}
-void AppAdapterHeadless::DoApplyAppConfig() {
- // Normal graphical app-adapters kick off screen creation here
- // which then leads to remaining app bootstrapping. We create
- // a 'null' screen purely for the same effect.
- PushMainThreadCall([] { g_base->graphics_server->SetNullGraphics(); });
-}
+void AppAdapterHeadless::DoApplyAppConfig() {}
void AppAdapterHeadless::RunMainThreadEventLoopToCompletion() {
assert(g_core->InMainThread());
@@ -40,6 +36,11 @@ void AppAdapterHeadless::DoExitMainThreadEventLoop() {
main_event_loop_->Exit();
}
+auto AppAdapterHeadless::GetGraphicsClientContext() -> GraphicsClientContext* {
+ // Special dummy form.
+ return new GraphicsClientContext(0);
+}
+
} // namespace ballistica::base
#endif // BA_HEADLESS_BUILD
diff --git a/src/ballistica/base/app_adapter/app_adapter_headless.h b/src/ballistica/base/app_adapter/app_adapter_headless.h
index 6d687bb2..2df675ea 100644
--- a/src/ballistica/base/app_adapter/app_adapter_headless.h
+++ b/src/ballistica/base/app_adapter/app_adapter_headless.h
@@ -17,6 +17,8 @@ class AppAdapterHeadless : public AppAdapter {
void DoApplyAppConfig() override;
+ auto GetGraphicsClientContext() -> GraphicsClientContext* override;
+
protected:
void DoPushMainThreadRunnable(Runnable* runnable) override;
void RunMainThreadEventLoopToCompletion() override;
diff --git a/src/ballistica/base/app_adapter/app_adapter_sdl.cc b/src/ballistica/base/app_adapter/app_adapter_sdl.cc
index 75443228..b43f8397 100644
--- a/src/ballistica/base/app_adapter/app_adapter_sdl.cc
+++ b/src/ballistica/base/app_adapter/app_adapter_sdl.cc
@@ -104,32 +104,115 @@ void AppAdapterSDL::OnMainThreadStartApp() {
SDL_ShowCursor(SDL_DISABLE);
}
+/// Our particular flavor of graphics settings.
+struct AppAdapterSDL::GraphicsSettings_ : public GraphicsSettings {
+ bool fullscreen = g_base->app_config->Resolve(AppConfig::BoolID::kFullscreen);
+ VSyncRequest vsync = g_base->graphics->VSyncFromAppConfig();
+ int max_fps = g_base->app_config->Resolve(AppConfig::IntID::kMaxFPS);
+};
+
void AppAdapterSDL::DoApplyAppConfig() {
assert(g_base->InLogicThread());
- g_base->graphics_server->PushSetScreenPixelScaleCall(
- g_base->app_config->Resolve(AppConfig::FloatID::kScreenPixelScale));
-
- auto graphics_quality_requested =
- g_base->graphics->GraphicsQualityFromAppConfig();
-
- auto texture_quality_requested =
- g_base->graphics->TextureQualityFromAppConfig();
-
// Android res string.
// std::string android_res =
// g_base->app_config->Resolve(AppConfig::StringID::kResolutionAndroid);
+}
- bool fullscreen = g_base->app_config->Resolve(AppConfig::BoolID::kFullscreen);
+auto AppAdapterSDL::GetGraphicsSettings() -> GraphicsSettings* {
+ assert(g_base->InLogicThread());
+ return new GraphicsSettings_();
+}
- auto vsync = g_base->graphics->VSyncFromAppConfig();
- int max_fps = g_base->app_config->Resolve(AppConfig::IntID::kMaxFPS);
+void AppAdapterSDL::ApplyGraphicsSettings(
+ const GraphicsSettings* settings_base) {
+ assert(g_core->InMainThread());
+ assert(!g_core->HeadlessMode());
- // Tell the main thread to set up the screen with these settings.
- g_base->app_adapter->PushMainThreadCall([=] {
- SetScreen_(fullscreen, max_fps, vsync, texture_quality_requested,
- graphics_quality_requested);
- });
+ // In strict mode, allow graphics stuff while in here.
+ auto allow = ScopedAllowGraphics_(this);
+
+ // Settings will always be our subclass (since we created it).
+ auto* settings = static_cast(settings_base);
+
+ // Apply any changes.
+ bool do_toggle_fs{};
+ bool do_set_existing_fullscreen{};
+
+ auto* graphics_server = g_base->graphics_server;
+
+ // We need a full renderer reload if quality values have changed
+ // or if we don't have a renderer yet.
+ bool need_full_reload = ((sdl_window_ == nullptr
+ || graphics_server->texture_quality_requested()
+ != settings->texture_quality)
+ || (graphics_server->graphics_quality_requested()
+ != settings->graphics_quality));
+
+ if (need_full_reload) {
+ ReloadRenderer_(settings);
+ } else if (settings->fullscreen != fullscreen_) {
+ SDL_SetWindowFullscreen(
+ sdl_window_, settings->fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
+ fullscreen_ = settings->fullscreen;
+ }
+
+ // VSync always gets set independent of the screen (though we set it down
+ // here to make sure we have a screen when its set).
+ VSync vsync;
+ switch (settings->vsync) {
+ case VSyncRequest::kNever:
+ vsync = VSync::kNever;
+ break;
+ case VSyncRequest::kAlways:
+ vsync = VSync::kAlways;
+ break;
+ case VSyncRequest::kAuto:
+ vsync = VSync::kAdaptive;
+ break;
+ default:
+ vsync = VSync::kNever;
+ break;
+ }
+ if (vsync != vsync_) {
+ switch (vsync) {
+ case VSync::kUnset:
+ case VSync::kNever: {
+ SDL_GL_SetSwapInterval(0);
+ vsync_actually_enabled_ = false;
+ break;
+ }
+ case VSync::kAlways: {
+ SDL_GL_SetSwapInterval(1);
+ vsync_actually_enabled_ = true;
+ break;
+ }
+ case VSync::kAdaptive: {
+ // In this case, let's try setting to 'adaptive' and turn it off if
+ // that is unsupported.
+ auto result = SDL_GL_SetSwapInterval(-1);
+ if (result == 0) {
+ vsync_actually_enabled_ = true;
+ } else {
+ SDL_GL_SetSwapInterval(0);
+ vsync_actually_enabled_ = false;
+ }
+ break;
+ }
+ }
+ vsync_ = vsync;
+ }
+
+ // This we can set anytime. Probably could have just set it from the logic
+ // thread where we read it, but let's be pedantic and keep everything to
+ // the main thread.
+ max_fps_ = settings->max_fps;
+
+ // Take -1 to mean no max. Otherwise clamp to a reasonable range.
+ if (max_fps_ != -1) {
+ max_fps_ = std::max(10, max_fps_);
+ max_fps_ = std::min(99999, max_fps_);
+ }
}
void AppAdapterSDL::RunMainThreadEventLoopToCompletion() {
@@ -158,7 +241,7 @@ void AppAdapterSDL::RunMainThreadEventLoopToCompletion() {
auto AppAdapterSDL::TryRender() -> bool {
if (strict_graphics_context_) {
- // In strict mode, allow graphics stuff in here. Normally we allow it
+ // In strict mode, allow graphics stuff in here. Otherwise we allow it
// anywhere in the main thread.
auto allow = ScopedAllowGraphics_(this);
@@ -179,7 +262,7 @@ auto AppAdapterSDL::TryRender() -> bool {
// Lastly render.
return g_base->graphics_server->TryRender();
} else {
- // Simple path; just render.
+ // Simpler path; just render.
return g_base->graphics_server->TryRender();
}
}
@@ -188,7 +271,7 @@ void AppAdapterSDL::SleepUntilNextEventCycle_(microsecs_t cycle_start_time) {
// Special case: if we're hidden, we simply sleep for a long bit; no fancy
// timing.
if (hidden_) {
- g_core->platform->SleepMillisecs(100);
+ g_core->platform->SleepSeconds(0.1);
return;
}
@@ -359,12 +442,14 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) {
break;
case SDL_QUIT:
- if (g_core->GetAppTimeMillisecs() - last_windowevent_close_time_ < 100) {
+ if (g_core->GetAppTimeSeconds() - last_windowevent_close_time_ < 0.1) {
// If they hit the window close button, skip the confirm.
g_base->QuitApp(false);
} else {
- // By default, confirm before quitting.
- g_base->QuitApp(true);
+ // For all other quits we might want to default to a confirm dialog.
+ // Update: going to try without confirm for a bit and see how that
+ // feels.
+ g_base->QuitApp(false);
}
break;
@@ -378,7 +463,7 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) {
case SDL_WINDOWEVENT_CLOSE: {
// Simply note that this happened. We use this to adjust our
// SDL_QUIT behavior (quit is called right after this).
- last_windowevent_close_time_ = g_core->GetAppTimeMillisecs();
+ last_windowevent_close_time_ = g_core->GetAppTimeSeconds();
break;
}
@@ -394,7 +479,7 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) {
fullscreen_ = true;
g_base->logic->event_loop()->PushCall([] {
g_base->python->objs()
- .Get(BasePython::ObjID::kSetConfigFullscreenOnCall)
+ .Get(BasePython::ObjID::kStoreConfigFullscreenOnCall)
.Call();
});
}
@@ -407,7 +492,7 @@ void AppAdapterSDL::HandleSDLEvent_(const SDL_Event& event) {
fullscreen_ = false;
g_base->logic->event_loop()->PushCall([] {
g_base->python->objs()
- .Get(BasePython::ObjID::kSetConfigFullscreenOffCall)
+ .Get(BasePython::ObjID::kStoreConfigFullscreenOffCall)
.Call();
});
}
@@ -544,114 +629,7 @@ auto AppAdapterSDL::GetSDLJoystickInput_(int sdl_joystick_id) const
return nullptr; // Epic fail.
}
-void AppAdapterSDL::SetScreen_(
- bool fullscreen, int max_fps, VSyncRequest vsync_requested,
- TextureQualityRequest texture_quality_requested,
- GraphicsQualityRequest graphics_quality_requested) {
- assert(g_core->InMainThread());
- assert(!g_core->HeadlessMode());
-
- // In strict mode, allow graphics stuff in here.
- auto allow = ScopedAllowGraphics_(this);
-
- // If we know what we support, filter our request types to what is
- // supported. This will keep us from rebuilding contexts if request type
- // is flipping between different types that we don't support.
- if (g_base->graphics->has_supports_high_quality_graphics_value()) {
- if (!g_base->graphics->supports_high_quality_graphics()
- && graphics_quality_requested > GraphicsQualityRequest::kMedium) {
- graphics_quality_requested = GraphicsQualityRequest::kMedium;
- }
- }
-
- bool do_toggle_fs{};
- bool do_set_existing_fullscreen{};
-
- auto* gs = g_base->graphics_server;
-
- // We need a full renderer reload if quality values have changed
- // or if we don't have one yet.
- bool need_full_reload =
- ((sdl_window_ == nullptr
- || gs->texture_quality_requested() != texture_quality_requested)
- || (gs->graphics_quality_requested() != graphics_quality_requested)
- || !gs->texture_quality_set() || !gs->graphics_quality_set());
-
- if (need_full_reload) {
- ReloadRenderer_(fullscreen, graphics_quality_requested,
- texture_quality_requested);
- } else if (fullscreen != fullscreen_) {
- SDL_SetWindowFullscreen(sdl_window_,
- fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
- fullscreen_ = fullscreen;
- }
-
- // VSync always gets set independent of the screen (though we set it down
- // here to make sure we have a screen when its set).
- VSync vsync;
- switch (vsync_requested) {
- case VSyncRequest::kNever:
- vsync = VSync::kNever;
- break;
- case VSyncRequest::kAlways:
- vsync = VSync::kAlways;
- break;
- case VSyncRequest::kAuto:
- vsync = VSync::kAdaptive;
- break;
- default:
- vsync = VSync::kNever;
- break;
- }
- if (vsync != vsync_) {
- switch (vsync) {
- case VSync::kUnset:
- case VSync::kNever: {
- SDL_GL_SetSwapInterval(0);
- vsync_actually_enabled_ = false;
- break;
- }
- case VSync::kAlways: {
- SDL_GL_SetSwapInterval(1);
- vsync_actually_enabled_ = true;
- break;
- }
- case VSync::kAdaptive: {
- // In this case, let's try setting to 'adaptive' and turn it off if
- // that is unsupported.
- auto result = SDL_GL_SetSwapInterval(-1);
- if (result == 0) {
- vsync_actually_enabled_ = true;
- } else {
- SDL_GL_SetSwapInterval(0);
- vsync_actually_enabled_ = false;
- }
- break;
- }
- }
- vsync_ = vsync;
- }
-
- // This we can set anytime. Probably could have just set it from the logic
- // thread where we read it, but let's be pedantic and keep everything to
- // the main thread.
- max_fps_ = max_fps;
-
- // Take -1 to mean no max. Otherwise clamp to a reasonable range.
- if (max_fps_ != -1) {
- max_fps_ = std::max(10, max_fps_);
- max_fps_ = std::min(99999, max_fps_);
- }
-
- // Let the logic thread know we've got a graphics system up and running.
- // It may use this cue to kick off asset loads and other bootstrapping.
- g_base->logic->event_loop()->PushCall(
- [] { g_base->logic->OnGraphicsReady(); });
-}
-
-void AppAdapterSDL::ReloadRenderer_(
- bool fullscreen, GraphicsQualityRequest graphics_quality_requested,
- TextureQualityRequest texture_quality_requested) {
+void AppAdapterSDL::ReloadRenderer_(const GraphicsSettings_* settings) {
assert(g_base->app_adapter->InGraphicsContext());
auto* gs = g_base->graphics_server;
@@ -662,7 +640,7 @@ void AppAdapterSDL::ReloadRenderer_(
// If we don't haven't yet, create our window and renderer.
if (!sdl_window_) {
- fullscreen_ = fullscreen;
+ fullscreen_ = settings->fullscreen;
// A reasonable default window size.
auto width = static_cast(kBaseVirtualResX * 0.8f);
@@ -670,7 +648,7 @@ void AppAdapterSDL::ReloadRenderer_(
uint32_t flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN
| SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE;
- if (fullscreen) {
+ if (settings->fullscreen) {
flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
}
@@ -727,15 +705,16 @@ void AppAdapterSDL::ReloadRenderer_(
}
}
- // Update graphics quality based on request.
- gs->set_graphics_quality_requested(graphics_quality_requested);
- gs->set_texture_quality_requested(texture_quality_requested);
+ // Update graphics-server's qualities based on request.
+ gs->set_graphics_quality_requested(settings->graphics_quality);
+ gs->set_texture_quality_requested(settings->texture_quality);
gs->LoadRenderer();
}
void AppAdapterSDL::UpdateScreenSizes_() {
- assert(g_base->app_adapter->InGraphicsContext());
+ // This runs in the main thread in response to SDL events.
+ assert(g_core->InMainThread());
// Grab logical window dimensions (points?). This is the coordinate space
// SDL's events deal in.
@@ -747,8 +726,13 @@ void AppAdapterSDL::UpdateScreenSizes_() {
// dimensions.
int pixels_x, pixels_y;
SDL_GL_GetDrawableSize(sdl_window_, &pixels_x, &pixels_y);
- g_base->graphics_server->SetScreenResolution(static_cast(pixels_x),
- static_cast(pixels_y));
+
+ // Push this over to the logic thread which owns the canonical value
+ // for this.
+ g_base->logic->event_loop()->PushCall([pixels_x, pixels_y] {
+ g_base->graphics->SetScreenResolution(static_cast(pixels_x),
+ static_cast(pixels_y));
+ });
}
auto AppAdapterSDL::InGraphicsContext() -> bool {
@@ -818,10 +802,36 @@ void AppAdapterSDL::CursorPositionForDraw(float* x, float* y) {
*y = immediate_y;
}
-auto AppAdapterSDL::CanToggleFullscreen() -> bool const { return true; }
+auto AppAdapterSDL::FullscreenControlAvailable() const -> bool { return true; }
+auto AppAdapterSDL::FullscreenControlKeyShortcut() const
+ -> std::optional {
+ if (g_buildconfig.ostype_windows()) {
+ // On Windows we support F11 and Alt+Enter to toggle fullscreen. Let's
+ // mention Alt+Enter which seems like it might be more commonly used
+ return "Alt+Enter";
+ }
+ if (g_buildconfig.ostype_macos()) {
+ // The Mac+SDL situation is a bit of a mess. By default, there is 'Enter
+ // Full Screen' in the window menu which is mapped to fn-F, but that
+ // will only work if a window was created in SDL as windowed. If we
+ // fullscreen that window and restart the app, we'll then have a *real*
+ // fullscreen sdl window and that shortcut won't work anymore. So to
+ // keep things consistent we advertise ctrl-f which we always handle
+ // ourselves. Maybe this situation will be cleaned up in SDL 3, but its
+ // not a huge deal anyway since our Cocoa Mac version behaves cleanly.
+ return "Ctrl+F";
+ }
+ return {};
+};
+
auto AppAdapterSDL::SupportsVSync() -> bool const { return true; }
auto AppAdapterSDL::SupportsMaxFPS() -> bool const { return true; }
+auto AppAdapterSDL::HasDirectKeyboardInput() -> bool {
+ // We always provide direct keyboard events.
+ return true;
+}
+
} // namespace ballistica::base
#endif // BA_SDL_BUILD
diff --git a/src/ballistica/base/app_adapter/app_adapter_sdl.h b/src/ballistica/base/app_adapter/app_adapter_sdl.h
index cc0f0405..585c29f1 100644
--- a/src/ballistica/base/app_adapter/app_adapter_sdl.h
+++ b/src/ballistica/base/app_adapter/app_adapter_sdl.h
@@ -34,10 +34,17 @@ class AppAdapterSDL : public AppAdapter {
auto TryRender() -> bool;
- auto CanToggleFullscreen() -> bool const override;
+ auto FullscreenControlAvailable() const -> bool override;
+ auto FullscreenControlKeyShortcut() const
+ -> std::optional override;
auto SupportsVSync() -> bool const override;
auto SupportsMaxFPS() -> bool const override;
+ auto HasDirectKeyboardInput() -> bool override;
+ void ApplyGraphicsSettings(const GraphicsSettings* settings) override;
+
+ auto GetGraphicsSettings() -> GraphicsSettings* override;
+
protected:
void DoPushMainThreadRunnable(Runnable* runnable) override;
void RunMainThreadEventLoopToCompletion() override;
@@ -48,14 +55,11 @@ class AppAdapterSDL : public AppAdapter {
private:
class ScopedAllowGraphics_;
- void SetScreen_(bool fullscreen, int max_fps, VSyncRequest vsync_requested,
- TextureQualityRequest texture_quality_requested,
- GraphicsQualityRequest graphics_quality_requested);
+ struct GraphicsSettings_;
+
void HandleSDLEvent_(const SDL_Event& event);
void UpdateScreenSizes_();
- void ReloadRenderer_(bool fullscreen,
- GraphicsQualityRequest graphics_quality_requested,
- TextureQualityRequest texture_quality_requested);
+ void ReloadRenderer_(const GraphicsSettings_* settings);
void OnSDLJoystickAdded_(int index);
void OnSDLJoystickRemoved_(int index);
// Given an SDL joystick ID, returns our Ballistica input for it.
@@ -66,6 +70,7 @@ class AppAdapterSDL : public AppAdapter {
void RemoveSDLInputDevice_(int index);
void SleepUntilNextEventCycle_(microsecs_t cycle_start_time);
+ int max_fps_{60};
bool done_ : 1 {};
bool fullscreen_ : 1 {};
bool vsync_actually_enabled_ : 1 {};
@@ -81,17 +86,16 @@ class AppAdapterSDL : public AppAdapter {
/// that require such a setup.
bool strict_graphics_context_ : 1 {};
bool strict_graphics_allowed_ : 1 {};
- std::mutex strict_graphics_calls_mutex_;
- std::vector strict_graphics_calls_;
VSync vsync_{VSync::kUnset};
uint32_t sdl_runnable_event_id_{};
- int max_fps_{60};
+ std::mutex strict_graphics_calls_mutex_;
+ std::vector strict_graphics_calls_;
microsecs_t oversleep_{};
std::vector sdl_joysticks_;
Vector2f window_size_{1.0f, 1.0f};
SDL_Window* sdl_window_{};
void* sdl_gl_context_{};
- millisecs_t last_windowevent_close_time_{};
+ seconds_t last_windowevent_close_time_{};
};
} // namespace ballistica::base
diff --git a/src/ballistica/base/app_adapter/app_adapter_vr.cc b/src/ballistica/base/app_adapter/app_adapter_vr.cc
index bfa614cd..703dbca2 100644
--- a/src/ballistica/base/app_adapter/app_adapter_vr.cc
+++ b/src/ballistica/base/app_adapter/app_adapter_vr.cc
@@ -40,7 +40,8 @@ void AppAdapterVR::PushVRSimpleRemoteStateCall(
}
void AppAdapterVR::VRSetDrawDimensions(int w, int h) {
- g_base->graphics_server->SetScreenResolution(w, h);
+ FatalError("FIXME UPDATE SET-SCREEN-RESOLUTION");
+ // g_base->graphics_server->SetScreenResolution(w, h);
}
void AppAdapterVR::VRPreDraw() {
diff --git a/src/ballistica/base/app_mode/app_mode.cc b/src/ballistica/base/app_mode/app_mode.cc
index 5953f720..e87ab5a6 100644
--- a/src/ballistica/base/app_mode/app_mode.cc
+++ b/src/ballistica/base/app_mode/app_mode.cc
@@ -36,8 +36,6 @@ void AppMode::HandleGameQuery(const char* buffer, size_t size,
auto AppMode::DoesWorldFillScreen() -> bool { return false; }
-void AppMode::GraphicsQualityChanged(GraphicsQuality quality) {}
-
void AppMode::DrawWorld(FrameDef* frame_def) {}
void AppMode::ChangeGameSpeed(int offs) {}
diff --git a/src/ballistica/base/app_mode/app_mode.h b/src/ballistica/base/app_mode/app_mode.h
index 119f7659..5a12fa0d 100644
--- a/src/ballistica/base/app_mode/app_mode.h
+++ b/src/ballistica/base/app_mode/app_mode.h
@@ -62,8 +62,6 @@ class AppMode {
virtual void DrawWorld(FrameDef* frame_def);
- virtual void GraphicsQualityChanged(GraphicsQuality quality);
-
/// Called whenever screen size changes.
virtual void OnScreenSizeChange();
diff --git a/src/ballistica/base/assets/assets.cc b/src/ballistica/base/assets/assets.cc
index a8da1408..1fe097d1 100644
--- a/src/ballistica/base/assets/assets.cc
+++ b/src/ballistica/base/assets/assets.cc
@@ -82,10 +82,10 @@ void Assets::StartLoading() {
assert(g_base);
assert(g_base->audio_server && g_base->assets_server
&& g_base->graphics_server);
- assert(g_base->graphics_server->texture_compression_types_are_set());
- assert(g_base->graphics_server->texture_quality_set());
+ assert(g_base->graphics->has_client_context());
- assert(!asset_loads_allowed_); // We should only be called once.
+ // We should only be called once.
+ assert(!asset_loads_allowed_);
asset_loads_allowed_ = true;
// Just grab the lock once for all this stuff for efficiency.
@@ -1102,10 +1102,13 @@ auto Assets::FindAssetFile(FileType type, const std::string& name)
}
}
- assert(g_base->graphics_server
- && g_base->graphics_server->texture_compression_types_are_set());
- assert(g_base->graphics_server
- && g_base->graphics_server->texture_quality_set());
+ // Make sure we know what compression/quality to use.
+ assert(g_base->graphics->has_client_context());
+ // assert(g_base->graphics_server
+ // &&
+ // g_base->graphics_server->texture_compression_types_are_set());
+ // assert(g_base->graphics_server
+ // && g_base->graphics_server->texture_quality_set());
prefix = "textures/";
#if BA_OSTYPE_ANDROID && !BA_ANDROID_DDS_BUILD
diff --git a/src/ballistica/base/assets/assets.h b/src/ballistica/base/assets/assets.h
index 6db00287..0dc756ec 100644
--- a/src/ballistica/base/assets/assets.h
+++ b/src/ballistica/base/assets/assets.h
@@ -117,6 +117,8 @@ class Assets {
auto language_state() const { return language_state_; }
+ auto asset_loads_allowed() const { return asset_loads_allowed_; }
+
private:
static void MarkAssetForLoad(Asset* c);
void LoadSystemTexture(SysTextureID id, const char* name);
@@ -136,20 +138,21 @@ class Assets {
std::unordered_map >* c_list)
-> Object::Ref;
- std::vector asset_paths_;
+ int language_state_{};
bool have_pending_loads_[static_cast(AssetType::kLast)]{};
+
+ // Will be true while a AssetListLock exists. Good to debug-verify this
+ // during any asset list access.
+ bool asset_lists_locked_ : 1 {};
+ bool asset_loads_allowed_ : 1 {};
+ bool sys_assets_loaded_ : 1 {};
+
+ std::vector asset_paths_;
std::unordered_map packages_;
// For use by AssetListLock; don't manually acquire.
std::mutex asset_lists_mutex_;
- // Will be true while a AssetListLock exists. Good to debug-verify this
- // during any asset list access.
- bool asset_lists_locked_{};
-
- // 'hard-wired' internal assets
- bool asset_loads_allowed_{};
- bool sys_assets_loaded_{};
std::vector > system_textures_;
std::vector > system_cube_map_textures_;
std::vector > system_sounds_;
@@ -177,7 +180,6 @@ class Assets {
// Text & Language (need to mold this into more asset-like concepts).
std::mutex language_mutex_;
std::unordered_map language_;
- int language_state_{};
std::mutex special_char_mutex_;
std::unordered_map special_char_strings_;
};
diff --git a/src/ballistica/base/assets/assets_server.cc b/src/ballistica/base/assets/assets_server.cc
index e5352ddb..0fddc71f 100644
--- a/src/ballistica/base/assets/assets_server.cc
+++ b/src/ballistica/base/assets/assets_server.cc
@@ -4,6 +4,7 @@
#include "ballistica/base/assets/asset.h"
#include "ballistica/base/assets/assets.h"
+#include "ballistica/base/graphics/graphics.h"
#include "ballistica/base/graphics/graphics_server.h"
#include "ballistica/base/support/huffman.h"
#include "ballistica/shared/foundation/event_loop.h"
@@ -221,12 +222,18 @@ void AssetsServer::WriteReplayMessages() {
void AssetsServer::Process() {
// Make sure we don't do any loading until we know what kind/quality of
// textures we'll be loading.
- if (!g_base->assets || !g_base->graphics_server
- || !g_base->graphics_server
- ->texture_compression_types_are_set() // NOLINT
- || !g_base->graphics_server->texture_quality_set()) {
+
+ // FIXME - we'll need to revisit this when adding support for
+ // renderer switches, since this is not especially thread-safe.
+
+ if (!g_base->graphics->has_client_context()) {
return;
}
+ // if (!g_base->assets ||
+ // || !g_base->graphics->texture_compression_types_are_set() // NOLINT
+ // || !g_base->graphics_server->texture_quality_set()) {
+ // return;
+ // }
// Process exactly 1 preload item. Empty out our non-audio list first
// (audio is less likely to cause noticeable hitches if it needs to be loaded
diff --git a/src/ballistica/base/assets/texture_asset.cc b/src/ballistica/base/assets/texture_asset.cc
index b43c13bb..aa757cb4 100644
--- a/src/ballistica/base/assets/texture_asset.cc
+++ b/src/ballistica/base/assets/texture_asset.cc
@@ -93,11 +93,14 @@ auto TextureAsset::GetNameFull() const -> std::string {
void TextureAsset::DoPreload() {
assert(valid_);
- assert(g_base->graphics_server
- && g_base->graphics_server->texture_compression_types_are_set());
+ // Make sure we're not loading without knowing what texture types we
+ // support.
+ // assert(g_base->graphics->has_client_context());
+ // assert(g_base->graphics_server
+ // && g_base->graphics_server->texture_compression_types_are_set());
- // We figure out which LOD should be our base level based on quality.
- TextureQuality texture_quality = g_base->graphics_server->texture_quality();
+ // Figure out which LOD should be our base level based on texture quality.
+ auto texture_quality = g_base->graphics->placeholder_texture_quality();
// If we're a text-texture.
if (packer_.Exists()) {
@@ -218,12 +221,14 @@ void TextureAsset::DoPreload() {
&preload_datas_[0].base_level);
// We should only be loading this if we support etc1 in hardware.
- assert(g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kETC1));
+ assert(g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kETC1));
// Decompress dxt1/dxt5 ones if we don't natively support S3TC.
- if (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kS3TC)) {
+ if (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kS3TC)) {
if ((preload_datas_[0].formats[preload_datas_[0].base_level]
== TextureFormat::kDXT5)
|| (preload_datas_[0].formats[preload_datas_[0].base_level]
@@ -241,8 +246,9 @@ void TextureAsset::DoPreload() {
&preload_datas_[0].base_level);
// Decompress dxt1/dxt5 if we don't natively support it.
- if (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kS3TC)) {
+ if (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kS3TC)) {
preload_datas_[0].ConvertToUncompressed(this);
}
} else if (!strcmp(file_name_full_.c_str() + file_name_size - 4,
@@ -264,16 +270,18 @@ void TextureAsset::DoPreload() {
== TextureFormat::kETC2_RGB)
|| (preload_datas_[0].formats[preload_datas_[0].base_level]
== TextureFormat::kETC2_RGBA))
- && (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kETC2))) {
+ && (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kETC2))) {
preload_datas_[0].ConvertToUncompressed(this);
}
// Decompress etc1 if we don't natively support it.
if ((preload_datas_[0].formats[preload_datas_[0].base_level]
== TextureFormat::kETC1)
- && (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kETC1))) {
+ && (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kETC1))) {
preload_datas_[0].ConvertToUncompressed(this);
}
@@ -287,8 +295,9 @@ void TextureAsset::DoPreload() {
&preload_datas_[0].base_level);
// We should only be loading this if we support pvr in hardware.
- assert(g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kPVR));
+ assert(
+ g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(TextureCompressionType::kPVR));
} else if (!strcmp(file_name_full_.c_str() + file_name_size - 4,
".nop")) {
// Dummy path for headless; nothing to do here.
@@ -342,12 +351,14 @@ void TextureAsset::DoPreload() {
}
// We should only be loading this if we support etc1 in hardware.
- assert(g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kETC1));
+ assert(g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kETC1));
// Decompress dxt1/dxt5 ones if we don't natively support S3TC.
- if (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kS3TC)) {
+ if (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kS3TC)) {
if ((preload_datas_[d].formats[preload_datas_[d].base_level]
== TextureFormat::kDXT5)
|| (preload_datas_[d].formats[preload_datas_[d].base_level]
@@ -365,8 +376,9 @@ void TextureAsset::DoPreload() {
&preload_datas_[d].base_level);
// Decompress dxt1/dxt5 if we don't natively support it.
- if (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kS3TC)) {
+ if (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kS3TC)) {
preload_datas_[d].ConvertToUncompressed(this);
}
} else if (!strcmp(file_name_full_.c_str() + file_name_size - 4,
@@ -383,16 +395,18 @@ void TextureAsset::DoPreload() {
== TextureFormat::kETC2_RGB)
|| (preload_datas_[d].formats[preload_datas_[d].base_level]
== TextureFormat::kETC2_RGBA))
- && (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kETC2))) {
+ && (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kETC2))) {
preload_datas_[d].ConvertToUncompressed(this);
}
// Decompress etc1 if we don't natively support it.
if ((preload_datas_[d].formats[preload_datas_[d].base_level]
== TextureFormat::kETC1)
- && (!g_base->graphics_server->SupportsTextureCompressionType(
- TextureCompressionType::kETC1))) {
+ && (!g_base->graphics->placeholder_client_context()
+ ->SupportsTextureCompressionType(
+ TextureCompressionType::kETC1))) {
preload_datas_[d].ConvertToUncompressed(this);
}
diff --git a/src/ballistica/base/audio/audio.cc b/src/ballistica/base/audio/audio.cc
index f7daba43..f1ddb876 100644
--- a/src/ballistica/base/audio/audio.cc
+++ b/src/ballistica/base/audio/audio.cc
@@ -5,6 +5,7 @@
#include "ballistica/base/assets/sound_asset.h"
#include "ballistica/base/audio/audio_server.h"
#include "ballistica/base/audio/audio_source.h"
+#include "ballistica/base/graphics/graphics.h"
#include "ballistica/base/support/app_config.h"
#include "ballistica/shared/foundation/event_loop.h"
@@ -12,6 +13,19 @@ namespace ballistica::base {
Audio::Audio() = default;
+auto Audio::UseLowQualityAudio() -> bool {
+ assert(g_base->InLogicThread());
+ // Currently just piggybacking off graphics quality here.
+ if (g_core->HeadlessMode() || g_base->graphics->has_client_context()) {
+ return true;
+ }
+ // We don't have a frame-def to look at so need to calc this ourself; ugh.
+ auto quality = Graphics::GraphicsQualityFromRequest(
+ g_base->graphics->settings()->graphics_quality,
+ g_base->graphics->client_context()->auto_graphics_quality);
+ return quality < GraphicsQuality::kMedium;
+}
+
void Audio::Reset() {
assert(g_base->InLogicThread());
g_base->audio_server->PushResetCall();
diff --git a/src/ballistica/base/audio/audio.h b/src/ballistica/base/audio/audio.h
index d3126565..9c87daec 100644
--- a/src/ballistica/base/audio/audio.h
+++ b/src/ballistica/base/audio/audio.h
@@ -29,36 +29,41 @@ class Audio {
virtual void OnScreenSizeChange();
virtual void StepDisplayTime();
+ /// Can be keyed off of to cut corners in audio (leaving sounds out, etc.)
+ /// Currently just piggybacks off graphics quality settings but this logic
+ /// may get fancier in the future.
+ auto UseLowQualityAudio() -> bool;
+
void SetVolumes(float music_volume, float sound_volume);
void SetListenerPosition(const Vector3f& p);
void SetListenerOrientation(const Vector3f& forward, const Vector3f& up);
void SetSoundPitch(float pitch);
- // Return a pointer to a locked sound source, or nullptr if they're all busy.
- // The sound source will be reset to standard settings (no loop, fade 1, pos
- // 0,0,0, etc.).
- // Send the source any immediate commands and then unlock it.
- // For later modifications, re-retrieve the sound with GetPlayingSound()
+ /// Return a pointer to a locked sound source, or nullptr if they're all busy.
+ /// The sound source will be reset to standard settings (no loop, fade 1, pos
+ /// 0,0,0, etc.).
+ /// Send the source any immediate commands and then unlock it.
+ /// For later modifications, re-retrieve the sound with GetPlayingSound()
auto SourceBeginNew() -> AudioSource*;
- // If a sound play id is playing, locks and returns its sound source.
- // on success, you must unlock the source once done with it.
+ /// If a sound play id is playing, locks and returns its sound source.
+ /// on success, you must unlock the source once done with it.
auto SourceBeginExisting(uint32_t play_id, int debug_id) -> AudioSource*;
- // Return true if the sound id is currently valid. This is not guaranteed
- // to be super accurate, but can be used to determine if a sound is still
- // playing.
+ /// Return true if the sound id is currently valid. This is not guaranteed
+ /// to be super accurate, but can be used to determine if a sound is still
+ /// playing.
auto IsSoundPlaying(uint32_t play_id) -> bool;
- // Simple one-shot play functions.
+ /// Simple one-shot play functions.
auto PlaySound(SoundAsset* s, float volume = 1.0f) -> std::optional;
auto PlaySoundAtPosition(SoundAsset* sound, float volume, float x, float y,
float z) -> std::optional;
- // Call this if you want to prevent repeated plays of the same sound. It'll
- // tell you if the sound has been played recently. The one-shot sound-play
- // functions use this under the hood. (PlaySound, PlaySoundAtPosition).
+ /// Call this if you want to prevent repeated plays of the same sound. It'll
+ /// tell you if the sound has been played recently. The one-shot sound-play
+ /// functions use this under the hood. (PlaySound, PlaySoundAtPosition).
auto ShouldPlay(SoundAsset* s) -> bool;
// Hmm; shouldn't these be accessed through the Source class?
@@ -73,15 +78,15 @@ class Audio {
}
private:
- // Flat list of client sources indexed by id.
+ /// Flat list of client sources indexed by id.
std::vector client_sources_;
- // List of sources that are ready to use.
- // This is kept filled by the audio thread
- // and used by the client.
+ /// List of sources that are ready to use.
+ /// This is kept filled by the audio thread
+ /// and used by the client.
std::vector available_sources_;
- // This must be locked whenever accessing the availableSources list.
+ /// This must be locked whenever accessing the availableSources list.
std::mutex available_sources_mutex_;
};
diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc
index 822b02fd..b7e4abb3 100644
--- a/src/ballistica/base/base.cc
+++ b/src/ballistica/base/base.cc
@@ -420,6 +420,10 @@ auto BaseFeatureSet::IsUnmodifiedBlessedBuild() -> bool {
return false;
}
+auto BaseFeatureSet::InMainThread() const -> bool {
+ return g_core->InMainThread();
+}
+
auto BaseFeatureSet::InAssetsThread() const -> bool {
if (auto* loop = assets_server->event_loop()) {
return loop->ThreadIsCurrent();
@@ -658,6 +662,10 @@ void BaseFeatureSet::DoPushObjCall(const PythonObjectSetBase* objset, int id,
auto BaseFeatureSet::IsAppStarted() const -> bool { return app_started_; }
+auto BaseFeatureSet::IsAppBootstrapped() const -> bool {
+ return logic->app_bootstrapping_complete();
+}
+
auto BaseFeatureSet::ShutdownSuppressBegin() -> bool {
std::scoped_lock lock(shutdown_suppress_lock_);
diff --git a/src/ballistica/base/base.h b/src/ballistica/base/base.h
index c2bd3a43..473e5edf 100644
--- a/src/ballistica/base/base.h
+++ b/src/ballistica/base/base.h
@@ -59,6 +59,8 @@ class DataAsset;
class FrameDef;
class Graphics;
class GraphicsServer;
+struct GraphicsSettings;
+struct GraphicsClientContext;
class Huffman;
class ImageMesh;
class Input;
@@ -662,6 +664,7 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
/// allowing certain functionality before this time.
auto IsBaseCompletelyImported() -> bool;
+ auto InMainThread() const -> bool;
auto InAssetsThread() const -> bool override;
auto InLogicThread() const -> bool override;
auto InAudioThread() const -> bool override;
@@ -672,11 +675,18 @@ class BaseFeatureSet : public FeatureSetNativeComponent,
/// High level screen-message call usable from any thread.
void ScreenMessage(const std::string& s, const Vector3f& color) override;
- /// Has StartApp been called (and completely finished its work)?
- /// Code that sends calls/messages to other threads or otherwise uses
- /// app functionality may want to check this to avoid crashes.
+ /// Has StartApp been called (and completely finished its work)? Code that
+ /// sends calls/messages to other threads or otherwise uses app
+ /// functionality may want to check this to avoid crashes. Note that some
+ /// app functionality such as loading assets is not available until
+ /// IsAppBootstrapped returns true. This call is thread safe.
auto IsAppStarted() const -> bool override;
+ /// Has the app bootstrapping phase completed? The bootstrapping phase
+ /// involves initial screen/graphics setup. Asset loading is not allowed
+ /// until it is complete.
+ auto IsAppBootstrapped() const -> bool override;
+
void PlusDirectSendV1CloudLogs(const std::string& prefix,
const std::string& suffix, bool instant,
int* result) override;
diff --git a/src/ballistica/base/dynamics/bg/bg_dynamics.cc b/src/ballistica/base/dynamics/bg/bg_dynamics.cc
index a1b82815..231a964b 100644
--- a/src/ballistica/base/dynamics/bg/bg_dynamics.cc
+++ b/src/ballistica/base/dynamics/bg/bg_dynamics.cc
@@ -39,10 +39,15 @@ void BGDynamics::Emit(const BGDynamicsEmission& e) {
void BGDynamics::Step(const Vector3f& cam_pos, int step_millisecs) {
assert(g_base->InLogicThread());
+ // Don't actually start doing anything until there's a
+ // client-graphics-context. We need this to calculate qualities/etc.
+ if (!g_base->graphics->has_client_context()) {
+ return;
+ }
+
// The BG dynamics thread just processes steps as fast as it can;
// we need to throttle what we send or tell it to cut back if its behind
int step_count = g_base->bg_dynamics_server->step_count();
- // printf("STEP COUNT %d\n", step_count);
// If we're really getting behind, start pruning stuff.
if (step_count > 3) {
@@ -62,6 +67,9 @@ void BGDynamics::Step(const Vector3f& cam_pos, int step_millisecs) {
// Pass a newly allocated raw pointer to the bg-dynamics thread; it takes care
// of disposing it when done.
auto d = Object::NewDeferred();
+ d->graphics_quality = Graphics::GraphicsQualityFromRequest(
+ g_base->graphics->settings()->graphics_quality,
+ g_base->graphics->client_context()->auto_graphics_quality);
d->step_millisecs = step_millisecs;
d->cam_pos = cam_pos;
@@ -174,7 +182,7 @@ void BGDynamics::Draw(FrameDef* frame_def) {
// In high-quality, we draw in the overlay pass so that we don't get wiped
// out by depth-of-field.
- bool draw_in_overlay = (frame_def->quality() >= GraphicsQuality::kHigh);
+ bool draw_in_overlay = frame_def->quality() >= GraphicsQuality::kHigh;
SpriteComponent c(draw_in_overlay ? frame_def->overlay_3d_pass()
: frame_def->beauty_pass());
c.SetCameraAligned(true);
@@ -232,7 +240,7 @@ void BGDynamics::Draw(FrameDef* frame_def) {
tendrils_mesh_->SetIndexData(ds->tendril_indices);
tendrils_mesh_->SetData(
Object::Ref>(ds->tendril_vertices));
- bool draw_in_overlay = (frame_def->quality() >= GraphicsQuality::kHigh);
+ bool draw_in_overlay = frame_def->quality() >= GraphicsQuality::kHigh;
SmokeComponent c(draw_in_overlay ? frame_def->overlay_3d_pass()
: frame_def->beauty_pass());
c.SetOverlay(draw_in_overlay);
diff --git a/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc b/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc
index 424e46c5..d0ff57bf 100644
--- a/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc
+++ b/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc
@@ -2282,7 +2282,8 @@ void BGDynamicsServer::Step(StepData* step_data) {
auto ref(Object::CompleteDeferred(step_data));
// Keep our quality in sync with the graphics thread's.
- graphics_quality_ = g_base->graphics_server->graphics_quality();
+ graphics_quality_ = step_data->graphics_quality;
+ assert(graphics_quality_ != GraphicsQuality::kUnset);
cam_pos_ = step_data->cam_pos;
diff --git a/src/ballistica/base/dynamics/bg/bg_dynamics_server.h b/src/ballistica/base/dynamics/bg/bg_dynamics_server.h
index 46918f45..f52694f0 100644
--- a/src/ballistica/base/dynamics/bg/bg_dynamics_server.h
+++ b/src/ballistica/base/dynamics/bg/bg_dynamics_server.h
@@ -73,6 +73,7 @@ class BGDynamicsServer {
auto GetDefaultOwnerThread() const -> EventLoopID override {
return EventLoopID::kBGDynamics;
}
+ GraphicsQuality graphics_quality{};
int step_millisecs{};
Vector3f cam_pos{0.0f, 0.0f, 0.0f};
diff --git a/src/ballistica/base/graphics/gl/program/program_gl.h b/src/ballistica/base/graphics/gl/program/program_gl.h
index 437d2723..84f1f57b 100644
--- a/src/ballistica/base/graphics/gl/program/program_gl.h
+++ b/src/ballistica/base/graphics/gl/program/program_gl.h
@@ -238,10 +238,10 @@ class RendererGL::ProgramGL {
// Update matrices as necessary.
- uint32_t mvpState =
+ int mvp_state =
g_base->graphics_server->GetModelViewProjectionMatrixState();
- if (mvpState != mvp_state_) {
- mvp_state_ = mvpState;
+ if (mvp_state != mvp_state_) {
+ mvp_state_ = mvp_state;
glUniformMatrix4fv(
mvp_uniform_, 1, 0,
g_base->graphics_server->GetModelViewProjectionMatrix().m);
@@ -251,7 +251,7 @@ class RendererGL::ProgramGL {
if (pflags_ & PFLAG_USES_MODEL_WORLD_MATRIX) {
// With world space points this would be identity; don't waste time.
assert(!(pflags_ & PFLAG_WORLD_SPACE_PTS));
- uint32_t state = g_base->graphics_server->GetModelWorldMatrixState();
+ int state = g_base->graphics_server->GetModelWorldMatrixState();
if (state != model_world_matrix_state_) {
model_world_matrix_state_ = state;
glUniformMatrix4fv(model_world_matrix_uniform_, 1, 0,
@@ -264,8 +264,7 @@ class RendererGL::ProgramGL {
// With world space points this would be identity; don't waste time.
assert(!(pflags_ & PFLAG_WORLD_SPACE_PTS));
// There's no state for just modelview but this works.
- uint32_t state =
- g_base->graphics_server->GetModelViewProjectionMatrixState();
+ int state = g_base->graphics_server->GetModelViewProjectionMatrixState();
if (state != model_view_matrix_state_) {
model_view_matrix_state_ = state;
glUniformMatrix4fv(model_view_matrix_uniform_, 1, 0,
@@ -275,7 +274,7 @@ class RendererGL::ProgramGL {
BA_DEBUG_CHECK_GL_ERROR;
if (pflags_ & PFLAG_USES_CAM_POS) {
- uint32_t state = g_base->graphics_server->cam_pos_state();
+ int state = g_base->graphics_server->cam_pos_state();
if (state != cam_pos_state_) {
cam_pos_state_ = state;
const Vector3f& p(g_base->graphics_server->cam_pos());
@@ -285,7 +284,7 @@ class RendererGL::ProgramGL {
BA_DEBUG_CHECK_GL_ERROR;
if (pflags_ & PFLAG_USES_CAM_ORIENT_MATRIX) {
- uint32_t state = g_base->graphics_server->GetCamOrientMatrixState();
+ int state = g_base->graphics_server->GetCamOrientMatrixState();
if (state != cam_orient_matrix_state_) {
cam_orient_matrix_state_ = state;
glUniformMatrix4fv(cam_orient_matrix_uniform_, 1, 0,
@@ -295,7 +294,7 @@ class RendererGL::ProgramGL {
BA_DEBUG_CHECK_GL_ERROR;
if (pflags_ & PFLAG_USES_SHADOW_PROJECTION_MATRIX) {
- uint32_t state =
+ int state =
g_base->graphics_server->light_shadow_projection_matrix_state();
if (state != light_shadow_projection_matrix_state_) {
light_shadow_projection_matrix_state_ = state;
@@ -336,19 +335,19 @@ class RendererGL::ProgramGL {
Object::Ref vertex_shader_;
std::string name_;
GLuint program_{};
- int pflags_{};
- uint32_t mvp_state_{};
GLint mvp_uniform_{};
GLint model_world_matrix_uniform_{};
GLint model_view_matrix_uniform_{};
GLint light_shadow_projection_matrix_uniform_{};
- uint32_t light_shadow_projection_matrix_state_{};
- uint32_t model_world_matrix_state_{};
- uint32_t model_view_matrix_state_{};
GLint cam_pos_uniform_{};
- uint32_t cam_pos_state_{};
GLint cam_orient_matrix_uniform_{};
- GLuint cam_orient_matrix_state_{};
+ int cam_orient_matrix_state_{};
+ int light_shadow_projection_matrix_state_{};
+ int pflags_{};
+ int mvp_state_{};
+ int cam_pos_state_{};
+ int model_world_matrix_state_{};
+ int model_view_matrix_state_{};
BA_DISALLOW_CLASS_COPIES(ProgramGL);
};
diff --git a/src/ballistica/base/graphics/gl/renderer_gl.cc b/src/ballistica/base/graphics/gl/renderer_gl.cc
index ffa42e0f..adb60eae 100644
--- a/src/ballistica/base/graphics/gl/renderer_gl.cc
+++ b/src/ballistica/base/graphics/gl/renderer_gl.cc
@@ -366,7 +366,7 @@ void RendererGL::CheckGLCapabilities_() {
// Both GL 3 and GL ES 3.0 support depth textures (and thus our high
// quality mode) as a core feature.
- g_base->graphics->SetSupportsHighQualityGraphics(true);
+ // g_base->graphics->SetSupportsHighQualityGraphics(true);
// Store the tex-compression type we support.
BA_DEBUG_CHECK_GL_ERROR;
@@ -2598,7 +2598,8 @@ void RendererGL::RetainShader_(ProgramGL* p) { shaders_.emplace_back(p); }
void RendererGL::Load() {
assert(g_base->app_adapter->InGraphicsContext());
assert(!data_loaded_);
- assert(g_base->graphics_server->graphics_quality_set());
+ assert(g_base->graphics_server->graphics_quality()
+ != GraphicsQuality::kUnset);
BA_DEBUG_CHECK_GL_ERROR;
if (!got_screen_framebuffer_) {
got_screen_framebuffer_ = true;
diff --git a/src/ballistica/base/graphics/graphics.cc b/src/ballistica/base/graphics/graphics.cc
index 5582a189..cc01e49f 100644
--- a/src/ballistica/base/graphics/graphics.cc
+++ b/src/ballistica/base/graphics/graphics.cc
@@ -115,9 +115,14 @@ void Graphics::OnAppShutdownComplete() { assert(g_base->InLogicThread()); }
void Graphics::DoApplyAppConfig() {
assert(g_base->InLogicThread());
+ // Any time we load the config we ship a new graphics-settings to
+ // the graphics server since something likely changed.
+ graphics_settings_dirty_ = true;
+
show_fps_ = g_base->app_config->Resolve(AppConfig::BoolID::kShowFPS);
show_ping_ = g_base->app_config->Resolve(AppConfig::BoolID::kShowPing);
- tv_border_ = g_base->app_config->Resolve(AppConfig::BoolID::kEnableTVBorder);
+ // tv_border_ =
+ // g_base->app_config->Resolve(AppConfig::BoolID::kEnableTVBorder);
bool disable_camera_shake =
g_base->app_config->Resolve(AppConfig::BoolID::kDisableCameraShake);
@@ -126,6 +131,52 @@ void Graphics::DoApplyAppConfig() {
bool disable_camera_gyro =
g_base->app_config->Resolve(AppConfig::BoolID::kDisableCameraGyro);
set_camera_gyro_explicitly_disabled(disable_camera_gyro);
+
+ applied_app_config_ = true;
+
+ // At this point we may want to send initial graphics settings to the
+ // graphics server if we haven't.
+ UpdateInitialGraphicsSettingsSend_();
+}
+
+void Graphics::UpdateInitialGraphicsSettingsSend_() {
+ assert(g_base->InLogicThread());
+ if (sent_initial_graphics_settings_) {
+ return;
+ }
+
+ // We need to send an initial graphics-settings to the server to kick
+ // things off, but we need a few things to be in place first.
+ auto app_config_ready = applied_app_config_;
+ // At some point we may want to wait to know our actual screen res before
+ // sending. This won't apply everywhere though since on some platforms the
+ // screen doesn't exist until we send this.
+ auto screen_resolution_ready = true;
+
+ if (app_config_ready && screen_resolution_ready) {
+ // Update/grab the current settings snapshot.
+ auto* settings = GetGraphicsSettingsSnapshot();
+
+ // We need to explicitly push settings to the graphics server to kick
+ // things off. We need to keep this settings instance alive until
+ // handled by the graphics context (which might be in another thread
+ // where we're not allowed to muck with settings' refs from). So let's
+ // explicitly increment its refcount here in the logic thread now and
+ // then push a call back here to decrement it when we're done.
+ settings->ObjectIncrementStrongRefCount();
+ // auto* s = settings_.Get();
+ g_base->app_adapter->PushGraphicsContextCall([settings] {
+ assert(g_base->app_adapter->InGraphicsContext());
+ g_base->graphics_server->ApplySettings(settings->Get());
+ g_base->logic->event_loop()->PushCall([settings] {
+ // Release our strong ref back here in the logic thread.
+ assert(g_base->InLogicThread());
+ settings->ObjectDecrementStrongRefCount();
+ });
+ });
+
+ sent_initial_graphics_settings_ = true;
+ }
}
void Graphics::StepDisplayTime() { assert(g_base->InLogicThread()); }
@@ -976,6 +1027,20 @@ auto Graphics::GetEmptyFrameDef() -> FrameDef* {
return frame_def;
}
+auto Graphics::GetGraphicsSettingsSnapshot() -> Snapshot* {
+ assert(g_base->InLogicThread());
+
+ // If need be, ask the app-adapter to build us a new settings instance.
+ if (graphics_settings_dirty_) {
+ auto* new_settings = g_base->app_adapter->GetGraphicsSettings();
+ new_settings->index = next_settings_index_++;
+ settings_snapshot_ = Object::New>(new_settings);
+ graphics_settings_dirty_ = false;
+ }
+ assert(settings_snapshot_.Exists());
+ return settings_snapshot_.Get();
+}
+
void Graphics::ClearFrameDefDeleteList() {
assert(g_base->InLogicThread());
std::scoped_lock lock(frame_def_delete_list_mutex_);
@@ -1120,6 +1185,8 @@ void Graphics::DrawDevUI(FrameDef* frame_def) {
void Graphics::BuildAndPushFrameDef() {
assert(g_base->InLogicThread());
+
+ BA_PRECONDITION_FATAL(g_base->logic->app_bootstrapping_complete());
assert(camera_.Exists());
assert(!g_core->HeadlessMode());
@@ -1128,10 +1195,6 @@ void Graphics::BuildAndPushFrameDef() {
assert(!building_frame_def_);
building_frame_def_ = true;
- // We should not be building/pushing any frames until the native
- // layer is fully bootstrapped.
- BA_PRECONDITION_FATAL(g_base->logic->app_bootstrapping_complete());
-
microsecs_t app_time_microsecs = g_core->GetAppTimeMicrosecs();
// Store how much time this frame_def represents.
@@ -1187,13 +1250,6 @@ void Graphics::BuildAndPushFrameDef() {
internal_components_inited_ = true;
}
- // If graphics quality has changed since our last draw, inform anyone who
- // wants to know.
- if (last_frame_def_graphics_quality_ != frame_def->quality()) {
- last_frame_def_graphics_quality_ = frame_def->quality();
- g_base->app_mode()->GraphicsQualityChanged(frame_def->quality());
- }
-
ApplyCamera(frame_def);
if (progress_bar_) {
@@ -1254,7 +1310,7 @@ void Graphics::BuildAndPushFrameDef() {
RunCleanFrameCommands();
}
- frame_def->Finalize();
+ frame_def->Complete();
// Include all mesh-data loads and unloads that have accumulated up to
// this point the graphics thread will have to handle these before
@@ -1465,10 +1521,10 @@ void Graphics::DoDrawFade(FrameDef* frame_def, float amt) {
void Graphics::DrawCursor(FrameDef* frame_def) {
assert(g_base->InLogicThread());
- millisecs_t app_time_millisecs = frame_def->app_time_millisecs();
+ auto app_time = frame_def->app_time();
- bool can_show_cursor = g_base->app_adapter->ShouldUseCursor();
- bool should_show_cursor =
+ auto can_show_cursor = g_base->app_adapter->ShouldUseCursor();
+ auto should_show_cursor =
camera_->manual() || g_base->input->IsCursorVisible();
if (g_base->app_adapter->HasHardwareCursor()) {
@@ -1482,9 +1538,9 @@ void Graphics::DrawCursor(FrameDef* frame_def) {
// Ship this state when it changes and also every now and then just in
// case things go wonky.
if (new_cursor_visibility != hardware_cursor_visible_
- || app_time_millisecs - last_cursor_visibility_event_time_ > 2000) {
+ || app_time - last_cursor_visibility_event_time_ > 2.137) {
hardware_cursor_visible_ = new_cursor_visibility;
- last_cursor_visibility_event_time_ = app_time_millisecs;
+ last_cursor_visibility_event_time_ = app_time;
g_base->app_adapter->PushMainThreadCall([this] {
assert(g_core && g_core->InMainThread());
g_base->app_adapter->SetHardwareCursorVisible(hardware_cursor_visible_);
@@ -1555,11 +1611,6 @@ void Graphics::DrawBlotches(FrameDef* frame_def) {
}
}
-void Graphics::SetSupportsHighQualityGraphics(bool s) {
- supports_high_quality_graphics_ = s;
- has_supports_high_quality_graphics_value_ = true;
-}
-
void Graphics::ClearScreenMessageTranslations() {
assert(g_base && g_base->InLogicThread());
for (auto&& i : screen_messages_) {
@@ -1922,20 +1973,55 @@ auto Graphics::ScreenMessageEntry::GetText() -> TextGroup& {
void Graphics::OnScreenSizeChange() {}
-void Graphics::SetScreenSize(float virtual_width, float virtual_height,
- float pixel_width, float pixel_height) {
+void Graphics::CalcVirtualRes_(float* x, float* y) {
+ float x_in = *x;
+ float y_in = *y;
+ if (*x / *y > static_cast(kBaseVirtualResX)
+ / static_cast(kBaseVirtualResY)) {
+ *y = kBaseVirtualResY;
+ *x = *y * (x_in / y_in);
+ } else {
+ *x = kBaseVirtualResX;
+ *y = *x * (y_in / x_in);
+ }
+}
+
+void Graphics::SetScreenResolution(float x, float y) {
assert(g_base->InLogicThread());
- res_x_virtual_ = virtual_width;
- res_y_virtual_ = virtual_height;
- res_x_ = pixel_width;
- res_y_ = pixel_height;
+
+ // Ignore redundant sets.
+ if (res_x_ == x && res_y_ == y) {
+ return;
+ }
+
+ // We'll need to ship a new settings to the server with this change.
+ graphics_settings_dirty_ = true;
+
+ res_x_ = x;
+ res_y_ = y;
+
+ // Calc virtual res. In vr mode our virtual res is independent of our
+ // screen size (since it gets drawn to an overlay).
+ if (g_core->IsVRMode()) {
+ res_x_virtual_ = kBaseVirtualResX;
+ res_y_virtual_ = kBaseVirtualResY;
+ } else {
+ res_x_virtual_ = res_x_;
+ res_y_virtual_ = res_y_;
+ CalcVirtualRes_(&res_x_virtual_, &res_y_virtual_);
+ }
// Need to rebuild internal components (some are sized to the screen).
internal_components_inited_ = false;
- // This will inform all applicable logic thread subsystems.
- g_base->logic->OnScreenSizeChange(virtual_width, virtual_height, pixel_width,
- pixel_height);
+ // Inform all our logic thread buddies of this change.
+ g_base->logic->OnScreenSizeChange(res_x_virtual_, res_y_virtual_, res_x_,
+ res_y_);
+
+ // This may trigger us sending initial graphics settings to the
+ // graphics-server to kick off drawing.
+ got_screen_resolution_ = true;
+ UpdateInitialGraphicsSettingsSend_();
}
void Graphics::ScreenMessageEntry::UpdateTranslation() {
@@ -2030,4 +2116,61 @@ void Graphics::LanguageChanged() {
ClearScreenMessageTranslations();
}
+auto Graphics::GraphicsQualityFromRequest(GraphicsQualityRequest request,
+ GraphicsQuality auto_val)
+ -> GraphicsQuality {
+ switch (request) {
+ case GraphicsQualityRequest::kLow:
+ return GraphicsQuality::kLow;
+ case GraphicsQualityRequest::kMedium:
+ return GraphicsQuality::kMedium;
+ case GraphicsQualityRequest::kHigh:
+ return GraphicsQuality::kHigh;
+ case GraphicsQualityRequest::kHigher:
+ return GraphicsQuality::kHigher;
+ case GraphicsQualityRequest::kAuto:
+ return auto_val;
+ default:
+ Log(LogLevel::kError, "Unhandled GraphicsQualityRequest value: "
+ + std::to_string(static_cast(request)));
+ return GraphicsQuality::kLow;
+ }
+}
+auto Graphics::TextureQualityFromRequest(TextureQualityRequest request,
+ TextureQuality auto_val)
+ -> TextureQuality {
+ switch (request) {
+ case TextureQualityRequest::kLow:
+ return TextureQuality::kLow;
+ case TextureQualityRequest::kMedium:
+ return TextureQuality::kMedium;
+ case TextureQualityRequest::kHigh:
+ return TextureQuality::kHigh;
+ case TextureQualityRequest::kAuto:
+ return auto_val;
+ default:
+ Log(LogLevel::kError, "Unhandled TextureQualityRequest value: "
+ + std::to_string(static_cast(request)));
+ return TextureQuality::kLow;
+ }
+}
+
+void Graphics::set_client_context(Snapshot* context) {
+ assert(g_base->InLogicThread());
+
+ // Currently we only expect this to be set once. That will change
+ // once we support renderer swapping/etc.
+ assert(!g_base->logic->graphics_ready());
+ assert(!client_context_snapshot_.Exists());
+ client_context_snapshot_ = context;
+
+ // Update our static placeholder value (we don't want to calc it dynamically
+ // since it can be accessed from other threads).
+ texture_quality_placeholder_ = TextureQualityFromRequest(
+ settings()->texture_quality, client_context()->auto_texture_quality);
+
+ // Let the logic system know its free to proceed beyond bootstrapping.
+ g_base->logic->OnGraphicsReady();
+}
+
} // namespace ballistica::base
diff --git a/src/ballistica/base/graphics/graphics.h b/src/ballistica/base/graphics/graphics.h
index 92e6c18f..f04ff79c 100644
--- a/src/ballistica/base/graphics/graphics.h
+++ b/src/ballistica/base/graphics/graphics.h
@@ -6,13 +6,15 @@
#include
#include