diff --git a/.efrocachemap b/.efrocachemap
index 95f67d38..4f28e4fe 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -421,7 +421,7 @@
"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": "7a5f49ae1738b012a6d7c16740af80a0",
+ "build/assets/ba_data/data/langdata.json": "1a960da2db069ca3926b8ee6b8f82df7",
"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": "b0d4e874ba8d22c8fd0d7a0eaaf96ac9",
@@ -433,30 +433,30 @@
"build/assets/ba_data/data/languages/english.json": "e70277fc6325126d3d893524c8df03c9",
"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/french.json": "4e218dcd488fa63e7db5b4da2261b9e1",
"build/assets/ba_data/data/languages/german.json": "450fa41ae264f29a5d1af22143d0d0ad",
- "build/assets/ba_data/data/languages/gibberish.json": "7863ceeedb1e87eef46f7769bae5f842",
- "build/assets/ba_data/data/languages/greek.json": "a65d78f912e9a89f98de004405167a6a",
- "build/assets/ba_data/data/languages/hindi.json": "88ee0cda537bab9ac827def5e236fe1a",
+ "build/assets/ba_data/data/languages/gibberish.json": "b8dfd407fb7fd9b268129c364b70ca54",
+ "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/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/persian.json": "a391d80ff58ea22926499e4b19d2c0d0",
"build/assets/ba_data/data/languages/polish.json": "e1a1a801851924748ad38fa68216439a",
"build/assets/ba_data/data/languages/portuguese.json": "9fcd6b4da9e5d0dc0e337ab00b5debe2",
"build/assets/ba_data/data/languages/romanian.json": "aeebdd54f65939c2facc6ac50c117826",
"build/assets/ba_data/data/languages/russian.json": "70f79c606ccc5ec7bd6ce0303fdece70",
"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": "6ccd728df4766be1969434d6f04c36d2",
+ "build/assets/ba_data/data/languages/spanish.json": "e72e394f94b99d3e838c1b81a9d17615",
"build/assets/ba_data/data/languages/swedish.json": "77d671f10613291ebf9c71da66f18a18",
"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": "42318070b817663f671d78a9c8f3019c",
"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,50 +4056,50 @@
"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": "ece14cc6d7a449f581c810a2d6d3449d",
- "build/prefab/full/linux_arm64_gui/release/ballisticakit": "b0b75bde134af8c73aa1f7e239bd84dc",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "f0742e77993c006a5f2df3e9bee6732e",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "b803f154b4bf2aeb908a603fa7888301",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "2be90b3e6fc6908448a7677dd3cfb594",
- "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "4d12d1887901f7c77b5df965bb0b4622",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "987f4e024c7ed08e58223369b40aa309",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "7dd6ce5ab63d9d255029fb907cf6fb63",
- "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "ad505c3ad979b2cf52c664ee79798575",
- "build/prefab/full/mac_arm64_gui/release/ballisticakit": "3773aa5c6d396b4c38883321067f5523",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "748f3877c0ac40f48ebc5d8aab442173",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "2d059f03286603ac416718eb262241ab",
- "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "3bd988564ed41c15b4d0f493eced88ef",
- "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "f01539e046d72d86d63da0b4b6fc28df",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "d554e6d3ef9709ad7d7c848633901089",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "967375f76d43831afd7e10208502dcc1",
- "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "7dba8e8a0b8ffbe7f8d73b33b0c41ed5",
- "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "11ccabb65197c9f2e3059ac434888e11",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "8d79aece6620eb017896a7e816a78f0d",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "a3331c3d60962e7f0c2b62728bf7f43e",
- "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "1636f9569ee8b8a6c0abed5c9e31e3f7",
- "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "3fc153ee973090358916b90938429931",
- "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "1636f9569ee8b8a6c0abed5c9e31e3f7",
- "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "3fc153ee973090358916b90938429931",
- "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "1c7ed5b60c2961cf7d1a918157f90bce",
- "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "0cbfd345b7e6a02d2a6bdfe7966d03d1",
- "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "1c7ed5b60c2961cf7d1a918157f90bce",
- "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "0cbfd345b7e6a02d2a6bdfe7966d03d1",
- "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "1360496e008c0d0fb631b2fde94e0d54",
- "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "f64f8060f46a1f7088c7aadef33220dd",
- "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "1360496e008c0d0fb631b2fde94e0d54",
- "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "f64f8060f46a1f7088c7aadef33220dd",
- "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "dc45874c7796f4fc740c224243efac28",
- "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "c7e7528347b1ec5bc37b13ed8ae88df1",
- "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "abeca8c975a6cd5766fc90df99e8dcd1",
- "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "c7e7528347b1ec5bc37b13ed8ae88df1",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "7aa3fa305f66461ec5e5bbc550aa742d",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "89c02f2300860fded6b44855f9b8407f",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "69f97da125d43fc396eeaea8013cb133",
- "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "9e56ac32e0cc2785811a162de68c69da",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "a62570a46fed2002590be0bafe5055e8",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "b2a10b1eb917197da8f981d5a5daed44",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "c3af2f896ddb7a0b5f2ee2f35bac0318",
- "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "30628de8aa6a7d9cfccf09f102ff9953",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "48b5a58b401a2b22b88491f7bcd0e22a",
+ "build/prefab/full/linux_arm64_gui/release/ballisticakit": "6d83849db3e1398503e2bb682eb4323e",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "b054c284b778dc77edc9c9b046303f46",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "b16789743a603fac02763c09bbca446b",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "ab0a78d42cc511b4041478205e892897",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "4bbb41936ffe72a7fe9bdc803761b937",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "a1427251545496f84c4d4e2d90e6e25a",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "729bec30bafe25cac07f920c0cc30ab8",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "f8b086fdf6bca929ebc75b117b80f522",
+ "build/prefab/full/mac_arm64_gui/release/ballisticakit": "7d8f3ffe791e5a665ecbb2c517483814",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "5f4207138b152a110593c6c5ea8a9b32",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "4211f250197995e7df6942d32cffd202",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "40be1e38cbac3baf88dee161eeb912e1",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "d4aacb95a1855e969d1cc8db33732c40",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "570f49dc1de66e3fe75d76ee5f9306e0",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "ad319e6fb3cd7043a597f0780de42a98",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "4c15b26f43b4cb81f433beeb927c8aa6",
+ "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "a06aaa7a95abb56d49ba7925cc503a28",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "82619b88184faf3ef7ae4bf41ea282ce",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "61251e6fe58347224750fdf30d4bf8bc",
+ "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "bbd5b31cd9b4d30e48ce46e2cf968fcf",
+ "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "4e11b895cbf2e1339cf34bc06c54a4ea",
+ "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "bbd5b31cd9b4d30e48ce46e2cf968fcf",
+ "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "4e11b895cbf2e1339cf34bc06c54a4ea",
+ "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "b483573e1ef7e6be1090c67187e9d1d8",
+ "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "ae5f87286947575463c386cfe1c443e4",
+ "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "b483573e1ef7e6be1090c67187e9d1d8",
+ "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "ae5f87286947575463c386cfe1c443e4",
+ "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "cf4e9ef8006953c365b0928c68f5a21b",
+ "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "2692dc69f7cb2501f0aaa8675f559987",
+ "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "cf4e9ef8006953c365b0928c68f5a21b",
+ "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "2692dc69f7cb2501f0aaa8675f559987",
+ "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "840e96e79a56393c353184475cf28fbc",
+ "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "48c4873dae2344c1d4092a1d85dab424",
+ "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "d0940a2237c33b922cf3747cbf3910ef",
+ "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "48c4873dae2344c1d4092a1d85dab424",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "52ccfcbba95dcf3d06620748690446be",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "8369a217dc8cd95db308851de9f35d86",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "6bc8e9e67a0cbe50ab2c6891d454570f",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "2ce9d1659647bca4725f404d192c3554",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "8fd2fd1ec12170942823e809332e8cb9",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "f40bd1a61620168791b88901975ea8ee",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "4e3f244ac43cd400ffdbd2ac2e887399",
+ "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "a5e5e62c259e23429eca4af7054cc7cb",
"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": "bb96031e3f844704fcc9a0549a6d2c41",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 37afc519..eda3bcdd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-### 1.7.28 (build 21479, api 8, 2023-10-17)
+### 1.7.28 (build 21486, api 8, 2023-10-20)
- 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
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/_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/baenv.py b/src/assets/ba_data/python/baenv.py
index 9c68772d..b34bebc9 100644
--- a/src/assets/ba_data/python/baenv.py
+++ b/src/assets/ba_data/python/baenv.py
@@ -52,7 +52,7 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be
# using.
-TARGET_BALLISTICA_BUILD = 21479
+TARGET_BALLISTICA_BUILD = 21486
TARGET_BALLISTICA_VERSION = '1.7.28'
diff --git a/src/ballistica/base/app_adapter/app_adapter.cc b/src/ballistica/base/app_adapter/app_adapter.cc
index 566383ac..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"
@@ -305,4 +305,14 @@ 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 67879ca8..606b5113 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
@@ -181,6 +191,17 @@ class AppAdapter {
/// 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.
+ ///
+ /// Whenever graphics settings will result in a change to the graphics
+ /// context (ie: something visible to rendering code in the logic thread)
+ /// the adapter should call g_base->graphics_server->set_context() with
+ /// the updated context.
+ 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 53c4f771..69b36034 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()) {
@@ -109,11 +78,11 @@ void AppAdapterApple::ReloadRenderer_(
// 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);
+ // 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 +92,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 +109,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 {
diff --git a/src/ballistica/base/app_adapter/app_adapter_apple.h b/src/ballistica/base/app_adapter/app_adapter_apple.h
index 133966b1..d800759d 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 {
@@ -32,7 +33,7 @@ class AppAdapterApple : public AppAdapter {
auto TryRender() -> bool;
/// Called by FromSwift.
- void SetScreenResolution(float pixel_width, float pixel_height);
+ // void SetScreenResolution(float pixel_width, float pixel_height);
auto FullscreenControlAvailable() const -> bool override;
auto FullscreenControlGet() const -> bool override;
@@ -42,6 +43,8 @@ class AppAdapterApple : public AppAdapter {
auto HasDirectKeyboardInput() -> bool override;
+ void EnableResizeFriendlyMode(int width, int height);
+
protected:
void DoPushMainThreadRunnable(Runnable* runnable) override;
void DoPushGraphicsContextRunnable(Runnable* runnable) override;
@@ -50,16 +53,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 51552e14..a77ee149 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,7 +442,7 @@ 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 {
@@ -380,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;
}
@@ -546,114 +629,96 @@ 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());
+// void AppAdapterSDL::ApplyGraphicsSettings_(const GraphicsSettings_* settings)
+// {
+// assert(g_core->InMainThread());
+// assert(!g_core->HeadlessMode());
- // In strict mode, allow graphics stuff in here.
- auto allow = ScopedAllowGraphics_(this);
+// // In strict mode, allow graphics stuff while 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{};
- bool do_toggle_fs{};
- bool do_set_existing_fullscreen{};
+// auto* graphics_server = g_base->graphics_server;
- 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 = ((sdl_window_ == nullptr
+// || 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 =
- ((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_(settings->fullscreen, settings->graphics_quality,
+// settings->texture_quality);
+// } else if (settings->fullscreen != fullscreen_) {
+// SDL_SetWindowFullscreen(
+// sdl_window_, settings->fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP :
+// 0);
+// fullscreen_ = settings->fullscreen;
+// }
- 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 (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;
+// }
- // 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_ = settings->max_fps;
- // 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_);
+// }
+// }
- // 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;
@@ -664,7 +729,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);
@@ -672,7 +737,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;
}
@@ -729,15 +794,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.
@@ -749,8 +815,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 {
diff --git a/src/ballistica/base/app_adapter/app_adapter_sdl.h b/src/ballistica/base/app_adapter/app_adapter_sdl.h
index 9f57b127..585c29f1 100644
--- a/src/ballistica/base/app_adapter/app_adapter_sdl.h
+++ b/src/ballistica/base/app_adapter/app_adapter_sdl.h
@@ -41,6 +41,9 @@ class AppAdapterSDL : public AppAdapter {
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;
@@ -52,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.
@@ -70,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 {};
@@ -85,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..e00b84f3 100644
--- a/src/ballistica/base/assets/assets.cc
+++ b/src/ballistica/base/assets/assets.cc
@@ -82,8 +82,9 @@ 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(g_base->graphics_server->texture_compression_types_are_set());
+ // assert(g_base->graphics_server->texture_quality_set());
assert(!asset_loads_allowed_); // We should only be called once.
asset_loads_allowed_ = true;
@@ -1102,10 +1103,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..cbfa7ba1 100644
--- a/src/ballistica/base/assets/assets.h
+++ b/src/ballistica/base/assets/assets.h
@@ -136,20 +136,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 +178,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..c834d38a 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();
diff --git a/src/ballistica/base/base.h b/src/ballistica/base/base.h
index c2bd3a43..fa413c36 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;
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..773e85dd 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
@@ -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..022e58d8 100644
--- a/src/ballistica/base/graphics/graphics.h
+++ b/src/ballistica/base/graphics/graphics.h
@@ -6,13 +6,15 @@
#include
#include