From 8c446dd0d63aa91c63800d36847b6d6a1f0e43d3 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Fri, 2 Oct 2020 13:48:18 -0500 Subject: [PATCH] More C++ work --- .efrocachemap | 20 +- docs/ba_module.md | 2 +- src/ballistica/app/app_config.cc | 245 - src/ballistica/app/app_globals.cc | 12 - src/ballistica/app/app_globals.h | 3 - src/ballistica/app/headless_app.cc | 21 - src/ballistica/app/stress_test.cc | 101 - src/ballistica/app/vr_app.cc | 110 - src/ballistica/ballistica.cc | 2 +- src/ballistica/game/account.cc | 199 + src/ballistica/game/account.h | 64 + .../game/client_controller_interface.h | 22 + src/ballistica/game/friend_score_set.h | 30 + src/ballistica/game/game.cc | 3165 ++++++++ src/ballistica/game/game.h | 464 ++ src/ballistica/game/game_stream.cc | 1242 ++++ src/ballistica/game/game_stream.h | 159 + src/ballistica/game/host_activity.cc | 528 ++ src/ballistica/game/host_activity.h | 120 + src/ballistica/game/player.cc | 418 ++ src/ballistica/game/player.h | 167 + src/ballistica/game/player_spec.cc | 109 + src/ballistica/game/player_spec.h | 56 + src/ballistica/game/score_to_beat.h | 28 + src/ballistica/game/session/client_session.cc | 1070 +++ src/ballistica/game/session/client_session.h | 99 + src/ballistica/game/session/host_session.cc | 765 ++ src/ballistica/game/session/host_session.h | 131 + .../game/session/net_client_session.cc | 147 + .../game/session/net_client_session.h | 35 + .../game/session/replay_client_session.cc | 321 + .../game/session/replay_client_session.h | 43 + src/ballistica/game/session/session.cc | 36 + src/ballistica/game/session/session.h | 44 + src/ballistica/graphics/area_of_interest.cc | 19 + src/ballistica/graphics/area_of_interest.h | 33 + src/ballistica/graphics/camera.cc | 1016 +++ src/ballistica/graphics/camera.h | 152 + .../graphics/component/empty_component.h | 30 + .../graphics/component/object_component.cc | 195 + .../graphics/component/object_component.h | 166 + .../component/post_process_component.cc | 21 + .../component/post_process_component.h | 31 + .../graphics/component/render_component.cc | 83 + .../graphics/component/render_component.h | 262 + .../graphics/component/shield_component.cc | 9 + .../graphics/component/shield_component.h | 21 + .../graphics/component/simple_component.cc | 228 + .../graphics/component/simple_component.h | 185 + .../graphics/component/smoke_component.cc | 19 + .../graphics/component/smoke_component.h | 39 + .../graphics/component/special_component.cc | 12 + .../graphics/component/special_component.h | 26 + .../graphics/component/sprite_component.cc | 25 + .../graphics/component/sprite_component.h | 61 + src/ballistica/graphics/frame_def.cc | 173 + src/ballistica/graphics/frame_def.h | 228 + src/ballistica/graphics/framebuffer.h | 19 + src/ballistica/graphics/gl/gl_sys.cc | 368 + src/ballistica/graphics/gl/gl_sys.h | 201 + src/ballistica/graphics/gl/renderer_gl.cc | 6617 +++++++++++++++++ src/ballistica/graphics/gl/renderer_gl.h | 263 + src/ballistica/graphics/graphics.cc | 1868 +++++ src/ballistica/graphics/graphics.h | 424 ++ src/ballistica/graphics/graphics_server.cc | 765 ++ src/ballistica/graphics/graphics_server.h | 334 + src/ballistica/graphics/mesh/image_mesh.cc | 17 + src/ballistica/graphics/mesh/image_mesh.h | 27 + src/ballistica/graphics/mesh/mesh.h | 46 + src/ballistica/graphics/mesh/mesh_buffer.h | 28 + .../graphics/mesh/mesh_buffer_base.h | 21 + .../mesh/mesh_buffer_vertex_simple_full.h | 18 + .../mesh/mesh_buffer_vertex_smoke_full.h | 18 + .../graphics/mesh/mesh_buffer_vertex_sprite.h | 18 + src/ballistica/graphics/mesh/mesh_data.cc | 24 + src/ballistica/graphics/mesh/mesh_data.h | 42 + .../graphics/mesh/mesh_data_client_handle.cc | 17 + .../graphics/mesh/mesh_data_client_handle.h | 22 + .../graphics/mesh/mesh_index_buffer_16.h | 17 + .../graphics/mesh/mesh_index_buffer_32.h | 17 + src/ballistica/graphics/mesh/mesh_indexed.h | 41 + .../graphics/mesh/mesh_indexed_base.h | 106 + .../mesh/mesh_indexed_dual_texture_full.h | 18 + .../graphics/mesh/mesh_indexed_object_split.h | 20 + .../graphics/mesh/mesh_indexed_simple_full.h | 18 + .../graphics/mesh/mesh_indexed_simple_split.h | 20 + .../graphics/mesh/mesh_indexed_smoke_full.h | 18 + .../mesh/mesh_indexed_static_dynamic.h | 58 + .../graphics/mesh/mesh_non_indexed.h | 42 + .../graphics/mesh/mesh_renderer_data.h | 15 + src/ballistica/graphics/mesh/sprite_mesh.h | 17 + src/ballistica/graphics/mesh/text_mesh.cc | 574 ++ src/ballistica/graphics/mesh/text_mesh.h | 32 + src/ballistica/graphics/net_graph.cc | 153 + src/ballistica/graphics/net_graph.h | 27 + .../graphics/render_command_buffer.h | 576 ++ src/ballistica/graphics/render_pass.cc | 507 ++ src/ballistica/graphics/render_pass.h | 167 + src/ballistica/graphics/render_target.cc | 84 + src/ballistica/graphics/render_target.h | 47 + src/ballistica/graphics/renderer.cc | 850 +++ src/ballistica/graphics/renderer.h | 298 + .../graphics/text/font_page_map_data.h | 89 + src/ballistica/graphics/text/text_graphics.cc | 1176 +++ src/ballistica/graphics/text/text_graphics.h | 111 + src/ballistica/graphics/text/text_group.cc | 324 + src/ballistica/graphics/text/text_group.h | 85 + src/ballistica/graphics/text/text_packer.cc | 205 + src/ballistica/graphics/text/text_packer.h | 85 + src/ballistica/graphics/texture/dds.cc | 148 + src/ballistica/graphics/texture/dds.h | 157 + src/ballistica/graphics/texture/ktx.cc | 2291 ++++++ src/ballistica/graphics/texture/ktx.h | 29 + src/ballistica/graphics/texture/pvr.cc | 248 + src/ballistica/graphics/texture/pvr.h | 20 + src/ballistica/graphics/vr_graphics.cc | 336 + src/ballistica/graphics/vr_graphics.h | 86 + .../input/device/client_input_device.cc | 102 + .../input/device/client_input_device.h | 44 + src/ballistica/input/device/input_device.cc | 320 + src/ballistica/input/device/input_device.h | 182 + src/ballistica/input/device/joystick.cc | 1568 ++++ src/ballistica/input/device/joystick.h | 201 + src/ballistica/input/device/keyboard_input.cc | 471 ++ src/ballistica/input/device/keyboard_input.h | 60 + src/ballistica/input/device/test_input.cc | 148 + src/ballistica/input/device/test_input.h | 37 + src/ballistica/input/device/touch_input.cc | 1077 +++ src/ballistica/input/device/touch_input.h | 95 + src/ballistica/input/input.cc | 1842 +++++ src/ballistica/input/input.h | 195 + src/ballistica/input/remote_app.cc | 547 ++ src/ballistica/input/remote_app.h | 67 + src/ballistica/input/std_input_module.cc | 82 + src/ballistica/input/std_input_module.h | 19 + src/ballistica/math/matrix44f.cc | 340 + src/ballistica/math/matrix44f.h | 201 + src/ballistica/math/point2d.h | 17 + src/ballistica/math/random.cc | 544 ++ src/ballistica/math/random.h | 17 + src/ballistica/math/rect.h | 21 + src/ballistica/math/vector2f.h | 27 + src/ballistica/math/vector3f.cc | 51 + src/ballistica/math/vector3f.h | 200 + src/ballistica/math/vector4f.h | 33 + .../media/component/collide_model.cc | 46 + .../media/component/collide_model.h | 41 + .../media/component/cube_map_texture.cc | 21 + .../media/component/cube_map_texture.h | 32 + src/ballistica/media/component/data.cc | 45 + src/ballistica/media/component/data.h | 43 + .../media/component/media_component.cc | 37 + .../media/component/media_component.h | 63 + src/ballistica/media/component/model.cc | 45 + src/ballistica/media/component/model.h | 44 + src/ballistica/media/component/sound.cc | 44 + src/ballistica/media/component/sound.h | 37 + src/ballistica/media/component/texture.cc | 59 + src/ballistica/media/component/texture.h | 39 + .../media/data/collide_model_data.cc | 134 + .../media/data/collide_model_data.h | 47 + src/ballistica/media/data/data_data.cc | 50 + src/ballistica/media/data/data_data.h | 47 + .../media/data/media_component_data.cc | 108 + .../media/data/media_component_data.h | 136 + src/ballistica/media/data/model_data.cc | 128 + src/ballistica/media/data/model_data.h | 67 + .../media/data/model_renderer_data.h | 21 + src/ballistica/media/data/sound_data.cc | 322 + src/ballistica/media/data/sound_data.h | 58 + src/ballistica/media/data/texture_data.cc | 450 ++ src/ballistica/media/data/texture_data.h | 62 + .../media/data/texture_preload_data.cc | 577 ++ .../media/data/texture_preload_data.h | 40 + .../media/data/texture_renderer_data.h | 27 + src/ballistica/media/media.cc | 1251 ++++ src/ballistica/media/media.h | 215 + src/ballistica/media/media_server.cc | 240 + src/ballistica/media/media_server.h | 39 + src/ballistica/networking/network_reader.h | 60 + .../networking/network_write_module.h | 21 + src/ballistica/networking/networking.h | 168 + src/ballistica/networking/networking_sys.h | 30 + src/ballistica/networking/sockaddr.h | 55 + src/ballistica/networking/telnet_server.cc | 241 + src/ballistica/networking/telnet_server.h | 49 + .../platform/linux/platform_linux.cc | 72 + .../platform/linux/platform_linux.h | 29 + src/ballistica/platform/min_sdl.h | 792 ++ src/ballistica/platform/platform.cc | 1364 ++++ src/ballistica/platform/platform.h | 515 ++ src/ballistica/platform/sdl/sdl_app.cc | 614 ++ src/ballistica/platform/sdl/sdl_app.h | 65 + 193 files changed, 49975 insertions(+), 504 deletions(-) delete mode 100644 src/ballistica/app/app_config.cc delete mode 100644 src/ballistica/app/app_globals.cc delete mode 100644 src/ballistica/app/headless_app.cc delete mode 100644 src/ballistica/app/stress_test.cc delete mode 100644 src/ballistica/app/vr_app.cc create mode 100644 src/ballistica/game/account.cc create mode 100644 src/ballistica/game/account.h create mode 100644 src/ballistica/game/client_controller_interface.h create mode 100644 src/ballistica/game/friend_score_set.h create mode 100644 src/ballistica/game/game.cc create mode 100644 src/ballistica/game/game.h create mode 100644 src/ballistica/game/game_stream.cc create mode 100644 src/ballistica/game/game_stream.h create mode 100644 src/ballistica/game/host_activity.cc create mode 100644 src/ballistica/game/host_activity.h create mode 100644 src/ballistica/game/player.cc create mode 100644 src/ballistica/game/player.h create mode 100644 src/ballistica/game/player_spec.cc create mode 100644 src/ballistica/game/player_spec.h create mode 100644 src/ballistica/game/score_to_beat.h create mode 100644 src/ballistica/game/session/client_session.cc create mode 100644 src/ballistica/game/session/client_session.h create mode 100644 src/ballistica/game/session/host_session.cc create mode 100644 src/ballistica/game/session/host_session.h create mode 100644 src/ballistica/game/session/net_client_session.cc create mode 100644 src/ballistica/game/session/net_client_session.h create mode 100644 src/ballistica/game/session/replay_client_session.cc create mode 100644 src/ballistica/game/session/replay_client_session.h create mode 100644 src/ballistica/game/session/session.cc create mode 100644 src/ballistica/game/session/session.h create mode 100644 src/ballistica/graphics/area_of_interest.cc create mode 100644 src/ballistica/graphics/area_of_interest.h create mode 100644 src/ballistica/graphics/camera.cc create mode 100644 src/ballistica/graphics/camera.h create mode 100644 src/ballistica/graphics/component/empty_component.h create mode 100644 src/ballistica/graphics/component/object_component.cc create mode 100644 src/ballistica/graphics/component/object_component.h create mode 100644 src/ballistica/graphics/component/post_process_component.cc create mode 100644 src/ballistica/graphics/component/post_process_component.h create mode 100644 src/ballistica/graphics/component/render_component.cc create mode 100644 src/ballistica/graphics/component/render_component.h create mode 100644 src/ballistica/graphics/component/shield_component.cc create mode 100644 src/ballistica/graphics/component/shield_component.h create mode 100644 src/ballistica/graphics/component/simple_component.cc create mode 100644 src/ballistica/graphics/component/simple_component.h create mode 100644 src/ballistica/graphics/component/smoke_component.cc create mode 100644 src/ballistica/graphics/component/smoke_component.h create mode 100644 src/ballistica/graphics/component/special_component.cc create mode 100644 src/ballistica/graphics/component/special_component.h create mode 100644 src/ballistica/graphics/component/sprite_component.cc create mode 100644 src/ballistica/graphics/component/sprite_component.h create mode 100644 src/ballistica/graphics/frame_def.cc create mode 100644 src/ballistica/graphics/frame_def.h create mode 100644 src/ballistica/graphics/framebuffer.h create mode 100644 src/ballistica/graphics/gl/gl_sys.cc create mode 100644 src/ballistica/graphics/gl/gl_sys.h create mode 100644 src/ballistica/graphics/gl/renderer_gl.cc create mode 100644 src/ballistica/graphics/gl/renderer_gl.h create mode 100644 src/ballistica/graphics/graphics.cc create mode 100644 src/ballistica/graphics/graphics.h create mode 100644 src/ballistica/graphics/graphics_server.cc create mode 100644 src/ballistica/graphics/graphics_server.h create mode 100644 src/ballistica/graphics/mesh/image_mesh.cc create mode 100644 src/ballistica/graphics/mesh/image_mesh.h create mode 100644 src/ballistica/graphics/mesh/mesh.h create mode 100644 src/ballistica/graphics/mesh/mesh_buffer.h create mode 100644 src/ballistica/graphics/mesh/mesh_buffer_base.h create mode 100644 src/ballistica/graphics/mesh/mesh_buffer_vertex_simple_full.h create mode 100644 src/ballistica/graphics/mesh/mesh_buffer_vertex_smoke_full.h create mode 100644 src/ballistica/graphics/mesh/mesh_buffer_vertex_sprite.h create mode 100644 src/ballistica/graphics/mesh/mesh_data.cc create mode 100644 src/ballistica/graphics/mesh/mesh_data.h create mode 100644 src/ballistica/graphics/mesh/mesh_data_client_handle.cc create mode 100644 src/ballistica/graphics/mesh/mesh_data_client_handle.h create mode 100644 src/ballistica/graphics/mesh/mesh_index_buffer_16.h create mode 100644 src/ballistica/graphics/mesh/mesh_index_buffer_32.h create mode 100644 src/ballistica/graphics/mesh/mesh_indexed.h create mode 100644 src/ballistica/graphics/mesh/mesh_indexed_base.h create mode 100644 src/ballistica/graphics/mesh/mesh_indexed_dual_texture_full.h create mode 100644 src/ballistica/graphics/mesh/mesh_indexed_object_split.h create mode 100644 src/ballistica/graphics/mesh/mesh_indexed_simple_full.h create mode 100644 src/ballistica/graphics/mesh/mesh_indexed_simple_split.h create mode 100644 src/ballistica/graphics/mesh/mesh_indexed_smoke_full.h create mode 100644 src/ballistica/graphics/mesh/mesh_indexed_static_dynamic.h create mode 100644 src/ballistica/graphics/mesh/mesh_non_indexed.h create mode 100644 src/ballistica/graphics/mesh/mesh_renderer_data.h create mode 100644 src/ballistica/graphics/mesh/sprite_mesh.h create mode 100644 src/ballistica/graphics/mesh/text_mesh.cc create mode 100644 src/ballistica/graphics/mesh/text_mesh.h create mode 100644 src/ballistica/graphics/net_graph.cc create mode 100644 src/ballistica/graphics/net_graph.h create mode 100644 src/ballistica/graphics/render_command_buffer.h create mode 100644 src/ballistica/graphics/render_pass.cc create mode 100644 src/ballistica/graphics/render_pass.h create mode 100644 src/ballistica/graphics/render_target.cc create mode 100644 src/ballistica/graphics/render_target.h create mode 100644 src/ballistica/graphics/renderer.cc create mode 100644 src/ballistica/graphics/renderer.h create mode 100644 src/ballistica/graphics/text/font_page_map_data.h create mode 100644 src/ballistica/graphics/text/text_graphics.cc create mode 100644 src/ballistica/graphics/text/text_graphics.h create mode 100644 src/ballistica/graphics/text/text_group.cc create mode 100644 src/ballistica/graphics/text/text_group.h create mode 100644 src/ballistica/graphics/text/text_packer.cc create mode 100644 src/ballistica/graphics/text/text_packer.h create mode 100644 src/ballistica/graphics/texture/dds.cc create mode 100644 src/ballistica/graphics/texture/dds.h create mode 100644 src/ballistica/graphics/texture/ktx.cc create mode 100644 src/ballistica/graphics/texture/ktx.h create mode 100644 src/ballistica/graphics/texture/pvr.cc create mode 100644 src/ballistica/graphics/texture/pvr.h create mode 100644 src/ballistica/graphics/vr_graphics.cc create mode 100644 src/ballistica/graphics/vr_graphics.h create mode 100644 src/ballistica/input/device/client_input_device.cc create mode 100644 src/ballistica/input/device/client_input_device.h create mode 100644 src/ballistica/input/device/input_device.cc create mode 100644 src/ballistica/input/device/input_device.h create mode 100644 src/ballistica/input/device/joystick.cc create mode 100644 src/ballistica/input/device/joystick.h create mode 100644 src/ballistica/input/device/keyboard_input.cc create mode 100644 src/ballistica/input/device/keyboard_input.h create mode 100644 src/ballistica/input/device/test_input.cc create mode 100644 src/ballistica/input/device/test_input.h create mode 100644 src/ballistica/input/device/touch_input.cc create mode 100644 src/ballistica/input/device/touch_input.h create mode 100644 src/ballistica/input/input.cc create mode 100644 src/ballistica/input/input.h create mode 100644 src/ballistica/input/remote_app.cc create mode 100644 src/ballistica/input/remote_app.h create mode 100644 src/ballistica/input/std_input_module.cc create mode 100644 src/ballistica/input/std_input_module.h create mode 100644 src/ballistica/math/matrix44f.cc create mode 100644 src/ballistica/math/matrix44f.h create mode 100644 src/ballistica/math/point2d.h create mode 100644 src/ballistica/math/random.cc create mode 100644 src/ballistica/math/random.h create mode 100644 src/ballistica/math/rect.h create mode 100644 src/ballistica/math/vector2f.h create mode 100644 src/ballistica/math/vector3f.cc create mode 100644 src/ballistica/math/vector3f.h create mode 100644 src/ballistica/math/vector4f.h create mode 100644 src/ballistica/media/component/collide_model.cc create mode 100644 src/ballistica/media/component/collide_model.h create mode 100644 src/ballistica/media/component/cube_map_texture.cc create mode 100644 src/ballistica/media/component/cube_map_texture.h create mode 100644 src/ballistica/media/component/data.cc create mode 100644 src/ballistica/media/component/data.h create mode 100644 src/ballistica/media/component/media_component.cc create mode 100644 src/ballistica/media/component/media_component.h create mode 100644 src/ballistica/media/component/model.cc create mode 100644 src/ballistica/media/component/model.h create mode 100644 src/ballistica/media/component/sound.cc create mode 100644 src/ballistica/media/component/sound.h create mode 100644 src/ballistica/media/component/texture.cc create mode 100644 src/ballistica/media/component/texture.h create mode 100644 src/ballistica/media/data/collide_model_data.cc create mode 100644 src/ballistica/media/data/collide_model_data.h create mode 100644 src/ballistica/media/data/data_data.cc create mode 100644 src/ballistica/media/data/data_data.h create mode 100644 src/ballistica/media/data/media_component_data.cc create mode 100644 src/ballistica/media/data/media_component_data.h create mode 100644 src/ballistica/media/data/model_data.cc create mode 100644 src/ballistica/media/data/model_data.h create mode 100644 src/ballistica/media/data/model_renderer_data.h create mode 100644 src/ballistica/media/data/sound_data.cc create mode 100644 src/ballistica/media/data/sound_data.h create mode 100644 src/ballistica/media/data/texture_data.cc create mode 100644 src/ballistica/media/data/texture_data.h create mode 100644 src/ballistica/media/data/texture_preload_data.cc create mode 100644 src/ballistica/media/data/texture_preload_data.h create mode 100644 src/ballistica/media/data/texture_renderer_data.h create mode 100644 src/ballistica/media/media.cc create mode 100644 src/ballistica/media/media.h create mode 100644 src/ballistica/media/media_server.cc create mode 100644 src/ballistica/media/media_server.h create mode 100644 src/ballistica/networking/network_reader.h create mode 100644 src/ballistica/networking/network_write_module.h create mode 100644 src/ballistica/networking/networking.h create mode 100644 src/ballistica/networking/networking_sys.h create mode 100644 src/ballistica/networking/sockaddr.h create mode 100644 src/ballistica/networking/telnet_server.cc create mode 100644 src/ballistica/networking/telnet_server.h create mode 100644 src/ballistica/platform/linux/platform_linux.cc create mode 100644 src/ballistica/platform/linux/platform_linux.h create mode 100644 src/ballistica/platform/min_sdl.h create mode 100644 src/ballistica/platform/platform.cc create mode 100644 src/ballistica/platform/platform.h create mode 100644 src/ballistica/platform/sdl/sdl_app.cc create mode 100644 src/ballistica/platform/sdl/sdl_app.h diff --git a/.efrocachemap b/.efrocachemap index 3c3b59f8..9987ce92 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -3934,14 +3934,14 @@ "assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f", "build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ac/96/c3b9934061393fe09cc90ff24b8d", "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/38/2b/5641b3b40846f74f232771ac0457", - "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/55/e7/7493c35661e347a164ccc9a6e150", - "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/f4/a8/5f874f2c8ee0de54649b3142c2c5", - "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/87/cd/e155776004a096cd1981e8c45539", - "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/44/90/8453cc086294eaf8bd15f4df8332", - "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e8/98/2363d625af50c24c6ef3d2e9c58d", - "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/7a/fd/1b7ecd5d084ea0e52974e463b0be", - "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/e8/88/8fe7875aa34660db68e74f25051d", - "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/23/2d/8239623b2d8745b4feb4b380b12a", - "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/c3/2f/03140734daf10cd7e46a29c5c820", - "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/26/7a/d23e25fdda6f6b1d9ed3b03040b6" + "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/07/e7/d8f0add439e55e3cce5e5768c80f", + "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/fd/72/faa94ff6532a95c121fcb5a4f788", + "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d2/d1/99514fbe084fb0480d75f92ecb2c", + "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/dd/5d/f8c5b24579236bef5209d7089044", + "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/5f/ff/5d34815d90dd4cd36f2a6f587958", + "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/45/1e/cea9badaf52032adb40e6c3b5e21", + "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/5d/63/96c2bbbedc03bd23824d7354b07d", + "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/44/94/4fa92ae4a1e726fb0b37e626e107", + "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/66/fd/8bb36157e75f78caa5373d9def18", + "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/af/2d/7546ed3c987435a743442603c21b" } \ No newline at end of file diff --git a/docs/ba_module.md b/docs/ba_module.md index a98f543a..a4b30a75 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2020-09-30 for Ballistica version 1.5.26 build 20190

+

last updated on 2020-10-02 for Ballistica version 1.5.26 build 20194

This page documents the Python classes and functions in the 'ba' module, which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!


diff --git a/src/ballistica/app/app_config.cc b/src/ballistica/app/app_config.cc deleted file mode 100644 index 5a9bbcce..00000000 --- a/src/ballistica/app/app_config.cc +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling - -#include "ballistica/app/app_config.h" - -#include - -#include "ballistica/ballistica.h" -#include "ballistica/platform/platform.h" -#include "ballistica/python/python.h" - -namespace ballistica { - -void AppConfig::Init() { new AppConfig(); } - -auto AppConfig::Entry::FloatValue() const -> float { - throw Exception("not a float entry"); -} - -auto AppConfig::Entry::StringValue() const -> std::string { - throw Exception("not a string entry"); -} - -auto AppConfig::Entry::IntValue() const -> int { - throw Exception("not an int entry"); -} - -auto AppConfig::Entry::BoolValue() const -> bool { - throw Exception("not a bool entry"); -} - -auto AppConfig::Entry::DefaultFloatValue() const -> float { - throw Exception("not a float entry"); -} - -auto AppConfig::Entry::DefaultStringValue() const -> std::string { - throw Exception("not a string entry"); -} - -auto AppConfig::Entry::DefaultIntValue() const -> int { - throw Exception("not an int entry"); -} - -auto AppConfig::Entry::DefaultBoolValue() const -> bool { - throw Exception("not a bool entry"); -} - -class AppConfig::StringEntry : public AppConfig::Entry { - public: - StringEntry() = default; - StringEntry(const char* name, std::string default_value) - : Entry(name), default_value_(std::move(default_value)) {} - auto GetType() const -> Type override { return Type::kString; } - auto Resolve() const -> std::string { - return g_python->GetRawConfigValue(name().c_str(), default_value_.c_str()); - } - auto StringValue() const -> std::string override { return Resolve(); } - auto DefaultStringValue() const -> std::string override { - return default_value_; - } - - private: - std::string default_value_; -}; - -class AppConfig::FloatEntry : public AppConfig::Entry { - public: - FloatEntry() = default; - FloatEntry(const char* name, float default_value) - : Entry(name), default_value_(default_value) {} - auto GetType() const -> Type override { return Type::kFloat; } - auto Resolve() const -> float { - return g_python->GetRawConfigValue(name().c_str(), default_value_); - } - auto FloatValue() const -> float override { return Resolve(); } - auto DefaultFloatValue() const -> float override { return default_value_; } - - private: - float default_value_{}; -}; - -class AppConfig::IntEntry : public AppConfig::Entry { - public: - IntEntry() = default; - IntEntry(const char* name, int default_value) - : Entry(name), default_value_(default_value) {} - auto GetType() const -> Type override { return Type::kInt; } - auto Resolve() const -> int { - return g_python->GetRawConfigValue(name().c_str(), default_value_); - } - auto IntValue() const -> int override { return Resolve(); } - auto DefaultIntValue() const -> int override { return default_value_; } - - private: - int default_value_{}; -}; - -class AppConfig::BoolEntry : public AppConfig::Entry { - public: - BoolEntry() = default; - BoolEntry(const char* name, bool default_value) - : Entry(name), default_value_(default_value) {} - auto GetType() const -> Type override { return Type::kBool; } - auto Resolve() const -> bool { - return g_python->GetRawConfigValue(name().c_str(), default_value_); - } - auto BoolValue() const -> bool override { return Resolve(); } - auto DefaultBoolValue() const -> bool override { return default_value_; } - - private: - bool default_value_{}; -}; - -AppConfig::AppConfig() { - // (We're a singleton). - assert(g_app_config == nullptr); - g_app_config = this; - SetupEntries(); -} - -template -void AppConfig::CompleteMap(const T& entry_map) { - for (auto&& i : entry_map) { - assert(entries_by_name_.find(i.second.name()) == entries_by_name_.end()); - assert(i.first < decltype(i.first)::kLast); - entries_by_name_[i.second.name()] = &i.second; - } - - // Make sure all values have entries. -#if BA_DEBUG_BUILD - int last = static_cast(decltype(entry_map.begin()->first)::kLast); // ew - for (int j = 0; j < last; ++j) { - auto i2 = - entry_map.find(static_castfirst)>(j)); - if (i2 == entry_map.end()) { - throw Exception("Missing appconfig entry " + std::to_string(j)); - } - } -#endif -} - -void AppConfig::SetupEntries() { - // Register all our typed entries. - float_entries_[FloatID::kScreenGamma] = FloatEntry("Screen Gamma", 1.0F); - float_entries_[FloatID::kScreenPixelScale] = - FloatEntry("Screen Pixel Scale", 1.0F); - float_entries_[FloatID::kTouchControlsScale] = - FloatEntry("Touch Controls Scale", 1.0F); - float_entries_[FloatID::kTouchControlsScaleMovement] = - FloatEntry("Touch Controls Scale Movement", 1.0F); - float_entries_[FloatID::kTouchControlsScaleActions] = - FloatEntry("Touch Controls Scale Actions", 1.0F); - float_entries_[FloatID::kSoundVolume] = FloatEntry("Sound Volume", 1.0F); - float_entries_[FloatID::kMusicVolume] = FloatEntry("Music Volume", 1.0F); - - // Note: keep this synced with the defaults in MainActivity.java. - float gvrrts_default = g_platform->IsRunningOnDaydream() ? 1.0F : 0.5F; - float_entries_[FloatID::kGoogleVRRenderTargetScale] = - FloatEntry("GVR Render Target Scale", gvrrts_default); - - string_entries_[StringID::kResolutionAndroid] = - StringEntry("Resolution (Android)", "Auto"); - string_entries_[StringID::kTouchActionControlType] = - StringEntry("Touch Action Control Type", "buttons"); - string_entries_[StringID::kTouchMovementControlType] = - StringEntry("Touch Movement Control Type", "swipe"); - string_entries_[StringID::kGraphicsQuality] = - StringEntry("Graphics Quality", "Auto"); - string_entries_[StringID::kTextureQuality] = - StringEntry("Texture Quality", "Auto"); - string_entries_[StringID::kVerticalSync] = - StringEntry("Vertical Sync", "Auto"); - string_entries_[StringID::kVRHeadRelativeAudio] = - StringEntry("VR Head Relative Audio", "Auto"); - string_entries_[StringID::kMacControllerSubsystem] = - StringEntry("Mac Controller Subsystem", "Classic"); - string_entries_[StringID::kTelnetPassword] = - StringEntry("Telnet Password", "changeme"); - - int_entries_[IntID::kPort] = IntEntry("Port", kDefaultPort); - int_entries_[IntID::kTelnetPort] = - IntEntry("Telnet Port", kDefaultTelnetPort); - - bool_entries_[BoolID::kTouchControlsSwipeHidden] = - BoolEntry("Touch Controls Swipe Hidden", false); - bool_entries_[BoolID::kFullscreen] = BoolEntry("Fullscreen", false); - bool_entries_[BoolID::kKickIdlePlayers] = - BoolEntry("Kick Idle Players", false); - bool_entries_[BoolID::kAlwaysUseInternalKeyboard] = - BoolEntry("Always Use Internal Keyboard", false); - bool_entries_[BoolID::kShowFPS] = BoolEntry("Show FPS", false); - bool_entries_[BoolID::kTVBorder] = - BoolEntry("TV Border", g_platform->IsRunningOnTV()); - bool_entries_[BoolID::kKeyboardP2Enabled] = - BoolEntry("Keyboard P2 Enabled", false); - bool_entries_[BoolID::kEnablePackageMods] = - BoolEntry("Enable Package Mods", false); - bool_entries_[BoolID::kChatMuted] = BoolEntry("Chat Muted", false); - bool_entries_[BoolID::kEnableRemoteApp] = - BoolEntry("Enable Remote App", true); - bool_entries_[BoolID::kEnableTelnet] = BoolEntry("Enable Telnet", true); - bool_entries_[BoolID::kDisableCameraShake] = - BoolEntry("Disable Camera Shake", false); - bool_entries_[BoolID::kDisableCameraGyro] = - BoolEntry("Disable Camera Gyro", false); - - // Now add everything to our name map and make sure all is kosher. - CompleteMap(float_entries_); - CompleteMap(int_entries_); - CompleteMap(string_entries_); - CompleteMap(bool_entries_); -} - -auto AppConfig::Resolve(FloatID id) -> float { - auto i = float_entries_.find(id); - if (i == float_entries_.end()) { - throw Exception("Invalid config entry"); - } - return i->second.Resolve(); -} - -auto AppConfig::Resolve(StringID id) -> std::string { - auto i = string_entries_.find(id); - if (i == string_entries_.end()) { - throw Exception("Invalid config entry"); - } - return i->second.Resolve(); -} - -auto AppConfig::Resolve(BoolID id) -> bool { - auto i = bool_entries_.find(id); - if (i == bool_entries_.end()) { - throw Exception("Invalid config entry"); - } - return i->second.Resolve(); -} - -auto AppConfig::Resolve(IntID id) -> int { - auto i = int_entries_.find(id); - if (i == int_entries_.end()) { - throw Exception("Invalid config entry"); - } - return i->second.Resolve(); -} - -} // namespace ballistica diff --git a/src/ballistica/app/app_globals.cc b/src/ballistica/app/app_globals.cc deleted file mode 100644 index 3afbb3fe..00000000 --- a/src/ballistica/app/app_globals.cc +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling - -#include "ballistica/app/app_globals.h" - -namespace ballistica { - -AppGlobals::AppGlobals(int argc_in, char** argv_in) - : argc{argc_in}, - argv{argv_in}, - main_thread_id{std::this_thread::get_id()} {} - -} // namespace ballistica diff --git a/src/ballistica/app/app_globals.h b/src/ballistica/app/app_globals.h index 7af2803e..67b91272 100644 --- a/src/ballistica/app/app_globals.h +++ b/src/ballistica/app/app_globals.h @@ -10,7 +10,6 @@ #include #include "ballistica/ballistica.h" -#include "ballistica/networking/master_server_config.h" namespace ballistica { @@ -85,8 +84,6 @@ class AppGlobals { std::mutex real_time_mutex; std::mutex thread_name_map_mutex; std::map thread_name_map; - std::string master_server_addr{BA_MASTER_SERVER_DEFAULT_ADDR}; - std::string master_server_fallback_addr{BA_MASTER_SERVER_FALLBACK_ADDR}; #if BA_DEBUG_BUILD std::mutex object_list_mutex; Object* object_list_first{}; diff --git a/src/ballistica/app/headless_app.cc b/src/ballistica/app/headless_app.cc deleted file mode 100644 index 6d88748e..00000000 --- a/src/ballistica/app/headless_app.cc +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling -#if BA_HEADLESS_BUILD - -#include "ballistica/app/headless_app.h" - -#include "ballistica/ballistica.h" - -namespace ballistica { - -// We could technically use the vanilla App class here since we're not -// changing anything. -HeadlessApp::HeadlessApp(Thread* thread) : App(thread) { - // NewThreadTimer(10, true, NewLambdaRunnable([this] { - // assert(g_app); - // g_app->RunEvents(); - // })); -} - -} // namespace ballistica - -#endif // BA_HEADLESS_BUILD diff --git a/src/ballistica/app/stress_test.cc b/src/ballistica/app/stress_test.cc deleted file mode 100644 index 4846e9ac..00000000 --- a/src/ballistica/app/stress_test.cc +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling - -#include "ballistica/app/stress_test.h" - -#include "ballistica/ballistica.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/renderer.h" -#include "ballistica/input/input.h" -#include "ballistica/platform/platform.h" - -namespace ballistica { - -void StressTest::Update() { - assert(InMainThread()); - - // Handle a little misc stuff here. - // If we're currently running stress-tests, update that stuff. - if (stress_testing_ && g_input) { - // Update our fake inputs to make our dudes run around. - g_input->ProcessStressTesting(stress_test_player_count_); - - // Every 10 seconds update our stress-test stats. - millisecs_t t = GetRealTime(); - if (t - last_stress_test_update_time_ >= 10000) { - if (stress_test_stats_file_ == nullptr) { - assert(g_platform); - std::string f_name = - g_platform->GetUserPythonDirectory() + "/stress_test_stats.csv"; - stress_test_stats_file_ = g_platform->FOpen(f_name.c_str(), "wb"); - if (stress_test_stats_file_ != nullptr) { - fprintf(stress_test_stats_file_, - "time,averageFps,nodes,models,collide_models,textures,sounds," - "pssMem,sharedDirtyMem,privateDirtyMem\n"); - fflush(stress_test_stats_file_); - if (g_buildconfig.ostype_android()) { - // On android, let the OS know we've added or removed a file - // (limit to android or we'll get an unimplemented warning). - g_platform->AndroidRefreshFile(f_name); - } - } - } - if (stress_test_stats_file_ != nullptr) { - // See how many frames we've rendered this past interval. - int total_frames_rendered; - if (g_graphics_server && g_graphics_server->renderer()) { - total_frames_rendered = - g_graphics_server->renderer()->total_frames_rendered(); - } else { - total_frames_rendered = last_total_frames_rendered_; - } - float avg = - static_cast(total_frames_rendered - - last_total_frames_rendered_) - / (static_cast(t - last_stress_test_update_time_) / 1000.0f); - last_total_frames_rendered_ = total_frames_rendered; - uint32_t model_count = 0; - uint32_t collide_model_count = 0; - uint32_t texture_count = 0; - uint32_t sound_count = 0; - uint32_t node_count = 0; - if (g_media) { - model_count = g_media->total_model_count(); - collide_model_count = g_media->total_collide_model_count(); - texture_count = g_media->total_texture_count(); - sound_count = g_media->total_sound_count(); - } - assert(g_game); - std::string mem_usage = g_platform->GetMemUsageInfo(); - fprintf(stress_test_stats_file_, "%d,%.1f,%d,%d,%d,%d,%d,%s\n", - static_cast_check_fit(GetRealTime()), avg, node_count, - model_count, collide_model_count, texture_count, sound_count, - mem_usage.c_str()); - fflush(stress_test_stats_file_); - } - last_stress_test_update_time_ = t; - } - } -} - -void StressTest::Set(bool enable, int player_count) { - assert(InMainThread()); - bool was_stress_testing = stress_testing_; - stress_testing_ = enable; - stress_test_player_count_ = player_count; - - // If we're turning on, reset our intervals and things. - if (!was_stress_testing && stress_testing_) { - // So our first sample is 1 interval from now. - last_stress_test_update_time_ = GetRealTime(); - - // Reset our frames-rendered tally. - if (g_graphics_server && g_graphics_server->renderer()) { - last_total_frames_rendered_ = - g_graphics_server->renderer()->total_frames_rendered(); - } else { - // Assume zero if there's no graphics yet. - last_total_frames_rendered_ = 0; - } - } -} -} // namespace ballistica diff --git a/src/ballistica/app/vr_app.cc b/src/ballistica/app/vr_app.cc deleted file mode 100644 index e7d8909e..00000000 --- a/src/ballistica/app/vr_app.cc +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling -#if BA_VR_BUILD - -#include "ballistica/app/vr_app.h" - -#include "ballistica/game/game.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/renderer.h" - -namespace ballistica { - -VRApp::VRApp(Thread* thread) : App(thread) {} - -void VRApp::PushVRSimpleRemoteStateCall(const VRSimpleRemoteState& state) { - PushCall([this, state] { - // Convert this to a full hands state, adding in some simple elbow - // positioning of our own and left/right. - VRHandsState s; - s.l.tx = -0.2f; - s.l.ty = -0.2f; - s.l.tz = -0.3f; - - // Hmm; for now lets always assign this as right hand even when its in - // left-handed mode to keep things simple on the back-end. Can change later - // if there's a downside to that. - s.r.type = VRHandType::kDaydreamRemote; - s.r.tx = 0.2f; - s.r.ty = -0.2f; - s.r.tz = -0.3f; - s.r.yaw = state.r0; - s.r.pitch = state.r1; - s.r.roll = state.r2; - VRSetHands(s); - }); -} - -void VRApp::VRSetDrawDimensions(int w, int h) { - g_graphics_server->VideoResize(w, h); -} - -void VRApp::VRPreDraw() { - if (!g_graphics_server || !g_graphics_server->renderer()) { - return; - } - assert(InMainThread()); - if (FrameDef* frame_def = g_graphics_server->GetRenderFrameDef()) { - // Note: this could be part of PreprocessRenderFrameDef but - // the non-vr path needs it to be separate since preprocess doesn't - // happen sometimes. Should probably clean that up. - g_graphics_server->RunFrameDefMeshUpdates(frame_def); - - // store this for the duration of this frame - vr_render_frame_def_ = frame_def; - g_graphics_server->PreprocessRenderFrameDef(frame_def); - } -} - -void VRApp::VRPostDraw() { - assert(InMainThread()); - if (!g_graphics_server || !g_graphics_server->renderer()) { - return; - } - if (vr_render_frame_def_) { - g_graphics_server->FinishRenderFrameDef(vr_render_frame_def_); - vr_render_frame_def_ = nullptr; - } - RunRenderUpkeepCycle(); -} - -void VRApp::VRSetHead(float tx, float ty, float tz, float yaw, float pitch, - float roll) { - assert(InMainThread()); - Renderer* renderer = g_graphics_server->renderer(); - if (renderer == nullptr) return; - renderer->VRSetHead(tx, ty, tz, yaw, pitch, roll); -} - -void VRApp::VRSetHands(const VRHandsState& state) { - assert(InMainThread()); - - // Pass this along to the renderer (in this same thread) for drawing - // (so hands can be drawn at their absolute most up-to-date positions, etc). - Renderer* renderer = g_graphics_server->renderer(); - if (renderer == nullptr) return; - renderer->VRSetHands(state); - - // ALSO ship it off to the game/ui thread to actually handle input from it. - g_game->PushVRHandsState(state); -} - -void VRApp::VRDrawEye(int eye, float yaw, float pitch, float roll, float tan_l, - float tan_r, float tan_b, float tan_t, float eye_x, - float eye_y, float eye_z, int viewport_x, - int viewport_y) { - if (!g_graphics_server || !g_graphics_server->renderer()) { - return; - } - assert(InMainThread()); - if (vr_render_frame_def_) { - // set up VR eye stuff... - Renderer* renderer = g_graphics_server->renderer(); - renderer->VRSetEye(eye, yaw, pitch, roll, tan_l, tan_r, tan_b, tan_t, eye_x, - eye_y, eye_z, viewport_x, viewport_y); - g_graphics_server->DrawRenderFrameDef(vr_render_frame_def_); - } -} - -} // namespace ballistica - -#endif // BA_VR_BUILD diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc index 8c0228e7..3bbb99db 100644 --- a/src/ballistica/ballistica.cc +++ b/src/ballistica/ballistica.cc @@ -29,7 +29,7 @@ namespace ballistica { // These are set automatically via script; don't change here. -const int kAppBuildNumber = 20193; +const int kAppBuildNumber = 20194; const char* kAppVersion = "1.5.26"; const char* kBlessingHash = nullptr; diff --git a/src/ballistica/game/account.cc b/src/ballistica/game/account.cc new file mode 100644 index 00000000..b432f315 --- /dev/null +++ b/src/ballistica/game/account.cc @@ -0,0 +1,199 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/game/account.h" + +#include "ballistica/app/app_globals.h" +#include "ballistica/game/game.h" +#include "ballistica/generic/utils.h" +#include "ballistica/python/python.h" + +namespace ballistica { + +auto Account::AccountTypeFromString(const std::string& val) -> AccountType { + if (val == "Game Center") { + return AccountType::kGameCenter; + } else if (val == "Game Circle") { + return AccountType::kGameCircle; + } else if (val == "Google Play") { + return AccountType::kGooglePlay; + } else if (val == "Steam") { + return AccountType::kSteam; + } else if (val == "Oculus") { + return AccountType::kOculus; + } else if (val == "NVIDIA China") { + return AccountType::kNvidiaChina; + } else if (val == "Test") { + return AccountType::kTest; + } else if (val == "Local") { + return AccountType::kDevice; + } else if (val == "Server") { + return AccountType::kServer; + } else { + return AccountType::kInvalid; + } +} + +auto Account::AccountTypeToString(AccountType type) -> std::string { + switch (type) { + case AccountType::kGameCenter: + return "Game Center"; + case AccountType::kGameCircle: + return "Game Circle"; + case AccountType::kGooglePlay: + return "Google Play"; + case AccountType::kSteam: + return "Steam"; + case AccountType::kOculus: + return "Oculus"; + case AccountType::kTest: + return "Test"; + case AccountType::kDevice: + return "Local"; + case AccountType::kServer: + return "Server"; + case AccountType::kNvidiaChina: + return "NVIDIA China"; + default: + return ""; + } +} + +auto Account::AccountTypeToIconString(AccountType type) -> std::string { + switch (type) { + case AccountType::kTest: + return g_game->CharStr(SpecialChar::kTestAccount); + case AccountType::kNvidiaChina: + return g_game->CharStr(SpecialChar::kNvidiaLogo); + case AccountType::kGooglePlay: + return g_game->CharStr(SpecialChar::kGooglePlayGamesLogo); + case AccountType::kSteam: + return g_game->CharStr(SpecialChar::kSteamLogo); + case AccountType::kOculus: + return g_game->CharStr(SpecialChar::kOculusLogo); + case AccountType::kGameCenter: + return g_game->CharStr(SpecialChar::kGameCenterLogo); + case AccountType::kGameCircle: + return g_game->CharStr(SpecialChar::kGameCircleLogo); + case AccountType::kDevice: + case AccountType::kServer: + return g_game->CharStr(SpecialChar::kLocalAccount); + default: + return ""; + } +} + +Account::Account() = default; + +auto Account::GetAccountName() -> std::string { + std::lock_guard lock(mutex_); + return account_name_; +} + +auto Account::GetAccountID() -> std::string { + std::lock_guard lock(mutex_); + return account_id_; +} + +auto Account::GetAccountToken() -> std::string { + std::lock_guard lock(mutex_); + return account_token_; +} + +auto Account::GetAccountExtra() -> std::string { + std::lock_guard lock(mutex_); + return account_extra_; +} + +auto Account::GetAccountExtra2() -> std::string { + std::lock_guard lock(mutex_); + return account_extra_2_; +} + +auto Account::GetAccountState(int* state_num) -> AccountState { + std::lock_guard lock(mutex_); + if (state_num) { + *state_num = account_state_num_; + } + return account_state_; +} + +void Account::SetAccountExtra(const std::string& extra) { + std::lock_guard lock(mutex_); + account_extra_ = extra; +} + +void Account::SetAccountExtra2(const std::string& extra) { + std::lock_guard lock(mutex_); + account_extra_2_ = extra; +} + +void Account::SetAccountToken(const std::string& account_id, + const std::string& token) { + std::lock_guard lock(mutex_); + // Hmm does this compare logic belong in here? + if (account_id_ == account_id) { + account_token_ = token; + } +} + +void Account::SetAccount(AccountType account_type, AccountState account_state, + const std::string& account_name, + const std::string& account_id) { + bool call_account_changed = false; + { + std::lock_guard lock(mutex_); + + // We call out to python so need to be in game thread. + assert(InGameThread()); + if (account_state_ != account_state + || g_app_globals->account_type != account_type + || account_id_ != account_id || account_name_ != account_name) { + // Special case: if they sent a sign-out for an account type that is. + // currently not signed in, ignore it. + if (account_state == AccountState::kSignedOut + && (account_type != g_app_globals->account_type)) { + // No-op. + } else { + account_state_ = account_state; + g_app_globals->account_type = account_type; + account_id_ = account_id; + account_name_ = Utils::GetValidUTF8(account_name.c_str(), "gthm"); + + // If they signed out of an account, account type switches to invalid. + if (account_state == AccountState::kSignedOut) { + g_app_globals->account_type = AccountType::kInvalid; + } + account_state_num_ += 1; + call_account_changed = true; + } + } + } + if (call_account_changed) { + // Inform python layer this has changed. + g_python->AccountChanged(); + } +} + +void Account::SetProductsPurchased(const std::vector& products) { + std::lock_guard lock(mutex_); + std::map purchases_old = product_purchases_; + product_purchases_.clear(); + for (auto&& i : products) { + product_purchases_[i] = true; + } + if (product_purchases_ != purchases_old) { + product_purchases_state_++; + } +} + +auto Account::GetProductPurchased(const std::string& product) -> bool { + std::lock_guard lock(mutex_); + auto i = product_purchases_.find(product); + if (i == product_purchases_.end()) { + return false; + } else { + return i->second; + } +} + +} // namespace ballistica diff --git a/src/ballistica/game/account.h b/src/ballistica/game/account.h new file mode 100644 index 00000000..79dc2f8b --- /dev/null +++ b/src/ballistica/game/account.h @@ -0,0 +1,64 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_ACCOUNT_H_ +#define BALLISTICA_GAME_ACCOUNT_H_ + +#include +#include +#include +#include + +#include "ballistica/ballistica.h" + +namespace ballistica { + +// Global account functionality. +class Account { + public: + Account(); + static auto AccountTypeFromString(const std::string& val) -> AccountType; + static auto AccountTypeToString(AccountType type) -> std::string; + static auto AccountTypeToIconString(AccountType type) -> std::string; + + auto GetAccountName() -> std::string; + auto GetAccountID() -> std::string; + auto GetAccountToken() -> std::string; + auto GetAccountExtra() -> std::string; + auto GetAccountExtra2() -> std::string; + + // Return the current account state. + // If an int pointer is passed, state-num will also be returned. + auto GetAccountState(int* state_num = nullptr) -> AccountState; + + // An extra value included when passing our account info to the server + // ..(can be used for platform-specific install-signature stuff, etc). + void SetAccountExtra(const std::string& extra); + void SetAccountExtra2(const std::string& extra); + void SetAccountToken(const std::string& account_id, const std::string& token); + + void SetAccount(AccountType account_type, AccountState account_state, + const std::string& name, const std::string& id); + + void SetProductsPurchased(const std::vector& products); + auto GetProductPurchased(const std::string& product) -> bool; + auto product_purchases_state() const -> int { + return product_purchases_state_; + } + + private: + // Protects all access to this account (we're thread-safe). + std::mutex mutex_; + std::map product_purchases_; + int product_purchases_state_{}; + std::string account_name_; + std::string account_id_; + std::string account_token_; + std::string account_extra_; + std::string account_extra_2_; + AccountState account_state_{AccountState::kSignedOut}; + int account_state_num_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_ACCOUNT_H_ diff --git a/src/ballistica/game/client_controller_interface.h b/src/ballistica/game/client_controller_interface.h new file mode 100644 index 00000000..19bc0d7d --- /dev/null +++ b/src/ballistica/game/client_controller_interface.h @@ -0,0 +1,22 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_CLIENT_CONTROLLER_INTERFACE_H_ +#define BALLISTICA_GAME_CLIENT_CONTROLLER_INTERFACE_H_ + +#include "ballistica/ballistica.h" + +namespace ballistica { + +// An interface for something that can control client-connections. +// (such as an output-stream or a replay-client-session) +// objects can register themselves as the current client-connection-controller +// and then they will get control of all existing (and forthcoming) clients +class ClientControllerInterface { + public: + virtual void OnClientConnected(ConnectionToClient* c) = 0; + virtual void OnClientDisconnected(ConnectionToClient* c) = 0; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_CLIENT_CONTROLLER_INTERFACE_H_ diff --git a/src/ballistica/game/friend_score_set.h b/src/ballistica/game/friend_score_set.h new file mode 100644 index 00000000..a4108c8d --- /dev/null +++ b/src/ballistica/game/friend_score_set.h @@ -0,0 +1,30 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_FRIEND_SCORE_SET_H_ +#define BALLISTICA_GAME_FRIEND_SCORE_SET_H_ + +#include +#include +#include + +namespace ballistica { + +// Used by game-center/etc when reporting friend scores to the game. +struct FriendScoreSet { + FriendScoreSet(bool success, void* user_data) + : success(success), user_data(user_data) {} + struct Entry { + Entry(int score, std::string name, bool is_me) + : score(score), name(std::move(name)), is_me(is_me) {} + int score; + std::string name; + bool is_me; + }; + std::list entries; + bool success; + void* user_data; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_FRIEND_SCORE_SET_H_ diff --git a/src/ballistica/game/game.cc b/src/ballistica/game/game.cc new file mode 100644 index 00000000..77c563f3 --- /dev/null +++ b/src/ballistica/game/game.cc @@ -0,0 +1,3165 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/game/game.h" + +#include +#include +#include + +#include "ballistica/app/app.h" +#include "ballistica/app/app_config.h" +#include "ballistica/audio/audio.h" +#include "ballistica/core/thread.h" +#include "ballistica/dynamics/bg/bg_dynamics.h" +#include "ballistica/game/account.h" +#include "ballistica/game/connection/connection_to_client_udp.h" +#include "ballistica/game/connection/connection_to_host_udp.h" +#include "ballistica/game/friend_score_set.h" +#include "ballistica/game/host_activity.h" +#include "ballistica/game/player.h" +#include "ballistica/game/score_to_beat.h" +#include "ballistica/game/session/client_session.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/game/session/net_client_session.h" +#include "ballistica/game/session/replay_client_session.h" +#include "ballistica/generic/json.h" +#include "ballistica/generic/timer.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/graphics/text/text_graphics.h" +#include "ballistica/input/device/client_input_device.h" +#include "ballistica/input/device/keyboard_input.h" +#include "ballistica/input/device/touch_input.h" +#include "ballistica/math/vector3f.h" +#include "ballistica/networking/network_write_module.h" +#include "ballistica/networking/networking.h" +#include "ballistica/networking/sockaddr.h" +#include "ballistica/networking/telnet_server.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_command.h" +#include "ballistica/python/python_context_call.h" +#include "ballistica/python/python_sys.h" +#include "ballistica/scene/node/globals_node.h" +#include "ballistica/ui/console.h" +#include "ballistica/ui/root_ui.h" +#include "ballistica/ui/ui.h" +#include "ballistica/ui/widget/root_widget.h" +#include "ballistica/ui/widget/text_widget.h" + +namespace ballistica { + +/// How long a kick vote lasts. +const int kKickVoteDuration = 30000; + +/// How long everyone has to wait to start a new kick vote after a failed one. +const int kKickVoteFailRetryDelay = 60000; + +/// Extra delay for the initiator of a failed vote. +const int kKickVoteFailRetryDelayInitiatorExtra = 120000; + +// Minimum clients that must be present for a kick vote to count. +// (for non-headless builds we require more votes since the host doesn't count +// but may be playing (in a 2on2 with 3 clients, don't want 2 clients able to +// kick). +// NOLINTNEXTLINE(cert-err58-cpp) +const int kKickVoteMinimumClients = (g_buildconfig.headless_build() ? 3 : 4); + +const int kMaxChatMessages = 40; + +// Go with 5 minute ban. +const int kKickBanSeconds = 5 * 60; + +Game::Game(Thread* thread) + : Module("game", thread), + game_roster_(cJSON_CreateArray()), + realtimers_(new TimerList()) { + assert(g_game == nullptr); + g_game = this; + + try { + // Spin up some other game-thread-based stuff. + AppConfig::Init(); + assert(g_graphics == nullptr); + g_graphics = g_platform->CreateGraphics(); + TextGraphics::Init(); + Media::Init(); + Audio::Init(); + if (!HeadlessMode()) { + BGDynamics::Init(); + } + + InitSpecialChars(); + + Context::Init(); + + // Waaah does UI need to be a bs::Object? + // Update: yes it does in order to be a context target. + // (need to be able to create weak-refs to it). + assert(g_ui == nullptr); + g_ui = Object::NewUnmanaged(); + + assert(g_networking == nullptr); + g_networking = new Networking(); + + assert(g_input == nullptr); + g_input = new Input(); + + // Init python and apply our settings immediately. + // This way we can get started loading stuff in the background + // and it'll come in with the correct texture quality etc. + assert(g_python == nullptr); + g_python = new Python(); + g_python->Reset(true); + + // We're the thread that 'owns' python so we need to wrangle the GIL. + thread->SetOwnsPython(); + } catch (const std::exception& e) { + // If anything went wrong, trigger a deferred error. + // This way it is more likely we can show a fatal error dialog + // since the main thread won't be blocking waiting for us to init. + std::string what = e.what(); + PushCall([what] { + // Just throw a standard exception since our what already + // contains a stack trace; if we throw an Exception we wind + // up with a useless second one. + throw std::logic_error(what.c_str()); + }); + } +} + +void Game::InitSpecialChars() { + std::lock_guard lock(special_char_mutex_); + + special_char_strings_[SpecialChar::kDownArrow] = "\xee\x80\x84"; + special_char_strings_[SpecialChar::kUpArrow] = "\xee\x80\x83"; + special_char_strings_[SpecialChar::kLeftArrow] = "\xee\x80\x81"; + special_char_strings_[SpecialChar::kRightArrow] = "\xee\x80\x82"; + special_char_strings_[SpecialChar::kTopButton] = "\xee\x80\x86"; + special_char_strings_[SpecialChar::kLeftButton] = "\xee\x80\x85"; + special_char_strings_[SpecialChar::kRightButton] = "\xee\x80\x87"; + special_char_strings_[SpecialChar::kBottomButton] = "\xee\x80\x88"; + special_char_strings_[SpecialChar::kDelete] = "\xee\x80\x89"; + special_char_strings_[SpecialChar::kShift] = "\xee\x80\x8A"; + special_char_strings_[SpecialChar::kBack] = "\xee\x80\x8B"; + special_char_strings_[SpecialChar::kLogoFlat] = "\xee\x80\x8C"; + special_char_strings_[SpecialChar::kRewindButton] = "\xee\x80\x8D"; + special_char_strings_[SpecialChar::kPlayPauseButton] = "\xee\x80\x8E"; + special_char_strings_[SpecialChar::kFastForwardButton] = "\xee\x80\x8F"; + special_char_strings_[SpecialChar::kDpadCenterButton] = "\xee\x80\x90"; + + special_char_strings_[SpecialChar::kOuyaButtonO] = "\xee\x80\x99"; + special_char_strings_[SpecialChar::kOuyaButtonU] = "\xee\x80\x9A"; + special_char_strings_[SpecialChar::kOuyaButtonY] = "\xee\x80\x9B"; + special_char_strings_[SpecialChar::kOuyaButtonA] = "\xee\x80\x9C"; + special_char_strings_[SpecialChar::kOuyaLogo] = "\xee\x80\x9D"; + special_char_strings_[SpecialChar::kLogo] = "\xee\x80\x9E"; + special_char_strings_[SpecialChar::kTicket] = "\xee\x80\x9F"; + special_char_strings_[SpecialChar::kGooglePlayGamesLogo] = "\xee\x80\xA0"; + special_char_strings_[SpecialChar::kGameCenterLogo] = "\xee\x80\xA1"; + special_char_strings_[SpecialChar::kDiceButton1] = "\xee\x80\xA2"; + special_char_strings_[SpecialChar::kDiceButton2] = "\xee\x80\xA3"; + special_char_strings_[SpecialChar::kDiceButton3] = "\xee\x80\xA4"; + special_char_strings_[SpecialChar::kDiceButton4] = "\xee\x80\xA5"; + special_char_strings_[SpecialChar::kGameCircleLogo] = "\xee\x80\xA6"; + special_char_strings_[SpecialChar::kPartyIcon] = "\xee\x80\xA7"; + special_char_strings_[SpecialChar::kTestAccount] = "\xee\x80\xA8"; + special_char_strings_[SpecialChar::kTicketBacking] = "\xee\x80\xA9"; + special_char_strings_[SpecialChar::kTrophy1] = "\xee\x80\xAA"; + special_char_strings_[SpecialChar::kTrophy2] = "\xee\x80\xAB"; + special_char_strings_[SpecialChar::kTrophy3] = "\xee\x80\xAC"; + special_char_strings_[SpecialChar::kTrophy0a] = "\xee\x80\xAD"; + special_char_strings_[SpecialChar::kTrophy0b] = "\xee\x80\xAE"; + special_char_strings_[SpecialChar::kTrophy4] = "\xee\x80\xAF"; + special_char_strings_[SpecialChar::kLocalAccount] = "\xee\x80\xB0"; + special_char_strings_[SpecialChar::kAlibabaLogo] = "\xee\x80\xB1"; + + special_char_strings_[SpecialChar::kFlagUnitedStates] = "\xee\x80\xB2"; + special_char_strings_[SpecialChar::kFlagMexico] = "\xee\x80\xB3"; + special_char_strings_[SpecialChar::kFlagGermany] = "\xee\x80\xB4"; + special_char_strings_[SpecialChar::kFlagBrazil] = "\xee\x80\xB5"; + special_char_strings_[SpecialChar::kFlagRussia] = "\xee\x80\xB6"; + special_char_strings_[SpecialChar::kFlagChina] = "\xee\x80\xB7"; + special_char_strings_[SpecialChar::kFlagUnitedKingdom] = "\xee\x80\xB8"; + special_char_strings_[SpecialChar::kFlagCanada] = "\xee\x80\xB9"; + special_char_strings_[SpecialChar::kFlagIndia] = "\xee\x80\xBA"; + special_char_strings_[SpecialChar::kFlagJapan] = "\xee\x80\xBB"; + special_char_strings_[SpecialChar::kFlagFrance] = "\xee\x80\xBC"; + special_char_strings_[SpecialChar::kFlagIndonesia] = "\xee\x80\xBD"; + special_char_strings_[SpecialChar::kFlagItaly] = "\xee\x80\xBE"; + special_char_strings_[SpecialChar::kFlagSouthKorea] = "\xee\x80\xBF"; + special_char_strings_[SpecialChar::kFlagNetherlands] = "\xee\x81\x80"; + + special_char_strings_[SpecialChar::kFedora] = "\xee\x81\x81"; + special_char_strings_[SpecialChar::kHal] = "\xee\x81\x82"; + special_char_strings_[SpecialChar::kCrown] = "\xee\x81\x83"; + special_char_strings_[SpecialChar::kYinYang] = "\xee\x81\x84"; + special_char_strings_[SpecialChar::kEyeBall] = "\xee\x81\x85"; + special_char_strings_[SpecialChar::kSkull] = "\xee\x81\x86"; + special_char_strings_[SpecialChar::kHeart] = "\xee\x81\x87"; + special_char_strings_[SpecialChar::kDragon] = "\xee\x81\x88"; + special_char_strings_[SpecialChar::kHelmet] = "\xee\x81\x89"; + special_char_strings_[SpecialChar::kMushroom] = "\xee\x81\x8A"; + + special_char_strings_[SpecialChar::kNinjaStar] = "\xee\x81\x8B"; + special_char_strings_[SpecialChar::kVikingHelmet] = "\xee\x81\x8C"; + special_char_strings_[SpecialChar::kMoon] = "\xee\x81\x8D"; + special_char_strings_[SpecialChar::kSpider] = "\xee\x81\x8E"; + special_char_strings_[SpecialChar::kFireball] = "\xee\x81\x8F"; + + special_char_strings_[SpecialChar::kFlagUnitedArabEmirates] = "\xee\x81\x90"; + special_char_strings_[SpecialChar::kFlagQatar] = "\xee\x81\x91"; + special_char_strings_[SpecialChar::kFlagEgypt] = "\xee\x81\x92"; + special_char_strings_[SpecialChar::kFlagKuwait] = "\xee\x81\x93"; + special_char_strings_[SpecialChar::kFlagAlgeria] = "\xee\x81\x94"; + special_char_strings_[SpecialChar::kFlagSaudiArabia] = "\xee\x81\x95"; + special_char_strings_[SpecialChar::kFlagMalaysia] = "\xee\x81\x96"; + special_char_strings_[SpecialChar::kFlagCzechRepublic] = "\xee\x81\x97"; + special_char_strings_[SpecialChar::kFlagAustralia] = "\xee\x81\x98"; + special_char_strings_[SpecialChar::kFlagSingapore] = "\xee\x81\x99"; + + special_char_strings_[SpecialChar::kOculusLogo] = "\xee\x81\x9A"; + special_char_strings_[SpecialChar::kSteamLogo] = "\xee\x81\x9B"; + special_char_strings_[SpecialChar::kNvidiaLogo] = "\xee\x81\x9C"; + + special_char_strings_[SpecialChar::kFlagIran] = "\xee\x81\x9D"; + special_char_strings_[SpecialChar::kFlagPoland] = "\xee\x81\x9E"; + special_char_strings_[SpecialChar::kFlagArgentina] = "\xee\x81\x9F"; + special_char_strings_[SpecialChar::kFlagPhilippines] = "\xee\x81\xA0"; + special_char_strings_[SpecialChar::kFlagChile] = "\xee\x81\xA1"; + + special_char_strings_[SpecialChar::kMikirog] = "\xee\x81\xA2"; +} + +void Game::SetGameRoster(cJSON* r) { + if (game_roster_ != nullptr) { + cJSON_Delete(game_roster_); + } + game_roster_ = r; +} + +void Game::ResetActivityTracking() { + largest_draw_time_increment_since_last_reset_ = 0; + first_draw_real_time_ = last_draw_real_time_ = g_platform->GetTicks(); +} + +void Game::RegisterClientController(ClientControllerInterface* c) { + // This shouldn't happen, but if there's already a controller registered, + // detach all clients from it. + if (client_controller_) { + Log("RegisterClientController() called " + "but already have a controller; bad."); + for (auto&& i : connections_to_clients_) { + assert(i.second.exists()); + i.second->SetController(nullptr); + } + } + + // Ok, now assign the new and attach all currently-connected clients to it. + client_controller_ = c; + if (client_controller_) { + for (auto&& i : connections_to_clients_) { + assert(i.second.exists()); + if (i.second->can_communicate()) { + i.second->SetController(client_controller_); + } + } + } +} + +void Game::UnregisterClientController(ClientControllerInterface* c) { + assert(c); + + // This shouldn't happen. + if (client_controller_ != c) { + Log("UnregisterClientController() called with a non-registered " + "controller"); + return; + } + + // Ok, detach all our controllers from this guy. + if (client_controller_) { + for (auto&& i : connections_to_clients_) { + i.second->SetController(nullptr); + } + } + client_controller_ = nullptr; +} + +#if BA_VR_BUILD + +void Game::PushVRHandsState(const VRHandsState& state) { + PushCall([this, state] { vr_hands_state_ = state; }); +} + +#endif // BA_VR_BUILD + +void Game::PushMediaPruneCall(int level) { + PushCall([level] { + assert(InGameThread()); + g_media->Prune(level); + }); +} + +void Game::PushSetAccountTokenCall(const std::string& account_id, + const std::string& token) { + PushCall( + [account_id, token] { g_account->SetAccountToken(account_id, token); }); +} + +void Game::PushSetAccountCall(AccountType account_type, + AccountState account_state, + const std::string& account_name, + const std::string& account_id) { + PushCall([this, account_type, account_state, account_name, account_id] { + g_account->SetAccount(account_type, account_state, account_name, + account_id); + }); +} + +void Game::PushInitialScreenCreatedCall() { + PushCall([this] { InitialScreenCreated(); }); +} + +void Game::InitialScreenCreated() { + assert(InGameThread()); + + // Ok; graphics-server is telling us we've got a screen. + + // We can now let the media thread go to town pre-loading system media + // while we wait. + g_media->LoadSystemMedia(); + + // FIXME: ideally we should create this as part of bootstrapping, but + // we need it to be possible to load textures/etc. before the renderer + // exists. + if (!HeadlessMode()) { + assert(!g_app_globals->console); + g_app_globals->console = new Console(); + } + + // Set up our timers. + process_timer_ = + NewThreadTimer(0, true, NewLambdaRunnable([this] { Process(); })); + media_prune_timer_ = + NewThreadTimer(2345, true, NewLambdaRunnable([this] { Prune(); })); + + // Normally we schedule updates when we're asked to draw a frame. + // In headless mode, however, we're not drawing, so we need a dedicated + // timer to take its place. + if (HeadlessMode()) { + headless_update_timer_ = + NewThreadTimer(8, true, NewLambdaRunnable([this] { Update(); })); + } + + RunAppLaunchCommands(); +} + +void Game::PushPurchaseTransactionCall(const std::string& item, + const std::string& receipt, + const std::string& signature, + const std::string& order_id, + bool user_initiated) { + PushCall([this, item, receipt, signature, order_id, user_initiated] { + PurchaseTransaction(item, receipt, signature, order_id, user_initiated); + }); +} + +void Game::PurchaseTransaction(const std::string& item, + const std::string& receipt, + const std::string& signature, + const std::string& order_id, + bool user_initiated) { + assert(InGameThread()); + g_python->AddPurchaseTransaction(item, receipt, signature, order_id, + user_initiated); +} + +void Game::Prune() { g_media->Prune(); } + +void Game::PushAdViewCompleteCall(const std::string& purpose, + bool actually_showed) { + PushCall([this, purpose, actually_showed] { + AdViewComplete(purpose, actually_showed); + }); +} + +void Game::AdViewComplete(const std::string& purpose, bool actually_showed) { + assert(InGameThread()); + CallAdCompletionCall(actually_showed); + + // If they *actually* viewed an ad, and it was a between-game ad, inform the + // user that they can disable them. + if (purpose == "between_game" && actually_showed) { + g_python->obj(Python::ObjID::kRemoveInGameAdsMessageCall).Call(); + } + RunGeneralAdComplete(actually_showed); +} + +void Game::RunGeneralAdComplete(bool actually_showed) { + assert(InGameThread()); + PythonRef ad_complete_call = + g_python->obj(Python::ObjID::kAccountClient).GetAttr("ad_complete"); + if (ad_complete_call.exists()) { + PythonRef args(Py_BuildValue("(Oi)", actually_showed ? Py_True : Py_False, + static_cast(g_platform->GetTicks() + - last_ad_start_time_)), + PythonRef::kSteal); + ad_complete_call.Call(args); + } else { + Log("Error on ad-complete call"); + } +} + +void Game::PushAnalyticsCall(const std::string& type, int increment) { + PushCall([this, type, increment] { Analytics(type, increment); }); +} + +void Game::Analytics(const std::string& type, int increment) { + assert(InGameThread()); + g_python->HandleAnalytics(type, increment); +} + +void Game::PushAwardAdTicketsCall() { + PushCall([this] { AwardAdTickets(); }); +} + +void Game::AwardAdTickets() { + try { + PythonRef add_transaction = + g_python->obj(Python::ObjID::kAccountClient).GetAttr("add_transaction"); + PythonRef args(Py_BuildValue("({ss})", "type", "AWARD_AD_TICKETS"), + PythonRef::kSteal); + if (add_transaction.exists()) add_transaction.Call(args); + g_python->RunTransactions(); + } catch (const std::exception& e) { + Log("Error in AwardAdTicketsMessage: " + std::string(e.what())); + } +} + +void Game::PushAwardAdTournamentEntryCall() { + PushCall([this] { AwardAdTournamentEntry(); }); +} + +void Game::AwardAdTournamentEntry() { + try { + PythonRef add_transaction = + g_python->obj(Python::ObjID::kAccountClient).GetAttr("add_transaction"); + PythonRef args(Py_BuildValue("({ss})", "type", "AWARD_AD_TOURNAMENT_ENTRY"), + PythonRef::kSteal); + if (add_transaction.exists()) add_transaction.Call(args); + g_python->RunTransactions(); + } catch (const std::exception& e) { + Log("Error in AwardAdTournamentEntryMessage: " + std::string(e.what())); + } +} + +// Launch into main menu or whatever else. +void Game::RunAppLaunchCommands() { + assert(!ran_app_launch_commands_); + + // First off, run our python app-launch call. + { + // Run this in the UI context. + ScopedSetContext cp(GetUIContext()); + g_python->obj(Python::ObjID::kOnAppLaunchCall).Call(); + } + + // If we were passed launch command args, run them. + if (!g_app_globals->game_commands.empty()) { + bool success = PythonCommand(g_app_globals->game_commands, BA_BCFN).Run(); + if (!success) { + exit(1); + } + } + + // If the stuff we just ran didn't result in a session, create a default one. + if (!foreground_session_.exists()) { + RunMainMenu(); + } + + UpdateProcessTimer(); + + ran_app_launch_commands_ = true; +} + +Game::~Game() = default; + +// Set up our sleeping based on what we're doing. +void Game::UpdateProcessTimer() { + assert(InGameThread()); + + // This might get called before we set up our timer in some cases. (such as + // very early) should be safe to ignore since we update the interval + // explicitly after creating the timers. + if (!process_timer_) return; + + // If there's loading to do, keep at it rather vigorously. + if (have_pending_loads_) { + assert(process_timer_); + process_timer_->SetLength(1); + } else { + // Otherwise we've got nothing to do; go to sleep until something changes. + assert(process_timer_); + process_timer_->SetLength(-1); + } +} + +void Game::PruneSessions() { + bool have_dead_session = false; + for (auto&& i : sessions_) { + if (i.exists()) { + // If this session is no longer foreground and is ready to die, kill it. + if (i.exists() && i.get() != foreground_session_.get()) { + try { + i.Clear(); + } catch (const std::exception& e) { + Log("Exception killing Session: " + std::string(e.what())); + } + have_dead_session = true; + } + } else { + have_dead_session = true; + } + } + if (have_dead_session) { + std::vector > live_list; + for (auto&& i : sessions_) { + if (i.exists()) { + live_list.push_back(i); + } + } + sessions_.swap(live_list); + } +} + +void Game::SetClientInfoFromMasterServer(const std::string& client_token, + PyObject* info_obj) { + // NOLINTNEXTLINE (python doing bitwise math on signed int) + if (!PyDict_Check(info_obj)) { + Log("got non-dict for master-server client info for token " + client_token + + ": " + Python::ObjToString(info_obj)); + return; + } + for (ConnectionToClient* client : GetConnectionsToClients()) { + if (client->token() == client_token) { + client->HandleMasterServerClientInfo(info_obj); + + // Roster will now include account-id... + game_roster_dirty_ = true; + break; + } + } +} + +void Game::UpdateKickVote() { + if (!kick_vote_in_progress_) { + return; + } + ConnectionToClient* kick_vote_starter = kick_vote_starter_.get(); + ConnectionToClient* kick_vote_target = kick_vote_target_.get(); + + // If the target is no longer with us, silently end. + if (kick_vote_target == nullptr) { + kick_vote_in_progress_ = false; + return; + } + millisecs_t current_time = GetRealTime(); + int total_client_count = 0; + int yes_votes = 0; + int no_votes = 0; + + // Tally current votes for connected clients; if anything has changed, print + // the update and possibly perform the kick. + for (ConnectionToClient* client : GetConnectionsToClients()) { + ++total_client_count; + if (client->kick_voted_) { + if (client->kick_vote_choice_) { + ++yes_votes; + } else { + ++no_votes; + } + } + } + bool vote_failed = false; + + // If we've fallen below the minimum necessary voters or time has run out, + // fail. + if (total_client_count < kKickVoteMinimumClients) { + vote_failed = true; + } + if (current_time > kick_vote_end_time_) { + vote_failed = true; + } + + if (vote_failed) { + SendScreenMessageToClients(R"({"r":"kickVoteFailedText"})", 1, 1, 0); + kick_vote_in_progress_ = false; + + // Disallow kicking for a while for everyone.. but ESPECIALLY so for the guy + // who launched the failed vote. + for (ConnectionToClient* client : GetConnectionsToClients()) { + millisecs_t delay = kKickVoteFailRetryDelay; + if (client == kick_vote_starter) { + delay += kKickVoteFailRetryDelayInitiatorExtra; + } + client->next_kick_vote_allow_time_ = + std::max(client->next_kick_vote_allow_time_, current_time + delay); + } + } else { + int votes_required; + switch (total_client_count) { + case 1: + case 2: + votes_required = 2; // Shouldn't actually be possible. + break; + case 3: + votes_required = HeadlessMode() ? 2 : 3; + break; + case 4: + votes_required = 3; + break; + case 5: + votes_required = HeadlessMode() ? 3 : 4; + break; + case 6: + votes_required = 4; + break; + case 7: + votes_required = HeadlessMode() ? 4 : 5; + break; + default: + votes_required = total_client_count - 3; + break; + } + int votes_needed = votes_required - yes_votes; + if (votes_needed <= 0) { + // ZOMG the vote passed; perform the kick. + SendScreenMessageToClients( + R"({"r":"kickOccurredText","s":[["${NAME}",)" + + Utils::GetJSONString(kick_vote_target->GetCombinedSpec() + .GetDisplayString() + .c_str()) + + "]]}", + 1, 1, 0); + kick_vote_in_progress_ = false; + DisconnectClient(kick_vote_target->id(), kKickBanSeconds); + + } else if (votes_needed != last_kick_votes_needed_) { + last_kick_votes_needed_ = votes_needed; + SendScreenMessageToClients(R"({"r":"votesNeededText","s":[["${NUMBER}",")" + + std::to_string(votes_needed) + "\"]]}", + 1, 1, 0); + } + } +} + +// Bring our scenes, real-time timers, etc up to date. +void Game::Update() { + assert(InGameThread()); + millisecs_t real_time = GetRealTime(); + g_platform->SetDebugKey("LastUpdateTime", + std::to_string(Platform::GetCurrentMilliseconds())); + if (first_update_) { + master_time_offset_ = master_time_ - real_time; + first_update_ = false; + } + in_update_ = true; + g_input->Update(); + UpdateKickVote(); + + // Send the game roster to our clients if it's changed recently. + if (game_roster_dirty_) { + if (real_time > last_game_roster_send_time_ + 2500) { + // Now send it to all connected clients. + std::vector msg = GetGameRosterMessage(); + for (auto&& c : GetConnectionsToClients()) { + c->SendReliableMessage(msg); + } + game_roster_dirty_ = false; + last_game_roster_send_time_ = real_time; + } + } + + // First do housekeeping on our client/host connections. + for (auto&& i : connections_to_clients_) { + BA_IFDEBUG(Object::WeakRef test_ref(i.second)); + i.second->Update(); + + // Make sure the connection didn't kill itself in the update. + assert(test_ref.exists()); + } + + if (connection_to_host_.exists()) { + connection_to_host_->Update(); + } + + // Ok, here's the deal: + // This is where we regulate the speed of everything that's running under us + // (sessions, activities, frame_def-creation, etc) + // we have a master_time which we try to have match real-time as closely + // as possible (unless we physically aren't fast enough to get everything + // done, in which case it'll be slower). We also increment our underlying + // machinery in 8ms increments (1/120 of a second) and try to do 2 updates + // each time we're called, since we're usually being called in a 60hz refresh + // cycle and that'll line our draws up perfectly with our sim steps. + + // TODO(ericf): On modern systems (VR and otherwise) we'll see 80hz, 90hz, + // 120hz, 240hz, etc. It would be great to generalize this to gravitate + // towards clean step patterns in all cases, not just the 60hz and 90hz cases + // we handle now. In general we want stuff like 1,1,2,1,1,2,1,1,2, not + // 1,1,1,2,1,2,2,1,1. + + // Figure out where our net-time *should* be getting to to match real-time. + millisecs_t target_master_time = real_time + master_time_offset_; + millisecs_t amount_behind = target_master_time - master_time_; + + // Normally we assume 60hz so we gravitate towards 2 steps per update to line + // up with our 120hz update timing. + int target_steps = 2; + +#if BA_RIFT_BUILD + // On Rift VR mode we're running 90hz, so lets aim for 1/2/1/2 steps to hit + // our 120hz target. + if (IsVRMode()) { + target_steps = rift_step_index_ + 1; + rift_step_index_ = !rift_step_index_; + } +#endif // BA_RIFT_BUILD + + // Ideally we should be behind by 16 (or 8 for single steps); if its + // *slightly* more than that, let our timing slip a tiny bit to maintain sync. + // This lets us match framerates that are a tiny bit slower than 60hz, such as + // seems to be the case with the Gear VR. + if (amount_behind > 16) { + master_time_offset_ -= 1; + + //.. and recalc these.. + target_master_time = real_time + master_time_offset_; + amount_behind = target_master_time - master_time_; + } + + // if we've fallen behind by a lot, just cut our losses + if (amount_behind > 50) { + master_time_offset_ -= (amount_behind - 50); + target_master_time = real_time + master_time_offset_; + } + + // min/max net-time targets we can aim for; gives us about a steps worth of + // wiggle room to try and keep our exact target cadence + millisecs_t min_target_master_time = + target_master_time >= 8 ? (target_master_time - 8) : 0; + millisecs_t max_target_master_time = target_master_time + 8; + + // run up our real-time timers + realtimers_->Run(real_time); + + // Run session updates until we catch up with projected base time (or run out + // of time). + int step = 1; + + while (true) { + // Try to stick to our target step count whenever possible, but if we get + // too far off target we may need to bail earlier/later. + if (step > target_steps) { + // As long as we're within a step of where we should be, bail now. + if (master_time_ >= min_target_master_time) break; + } else { + // If we've gone too far already, bail. + if (master_time_ >= max_target_master_time) { + // Log("BAILING EARLY"); + // On rift if this is a 2-step and we bailed after 1, aim for 2 again + // next time (otherwise we'll always get 3 singles in a row when this + // happens). +#if BA_RIFT_BUILD + if (IsVRMode() && target_steps == 2 && step == 2) { + rift_step_index_ = !rift_step_index_; + } +#endif // BA_RIFT_BUILD + break; + } + } + + // Update our UI scene/etc. + g_ui->Update(8); + + // Update all of our sessions. + for (auto&& i : sessions_) { + assert(i.exists()); + i->Update(8); + } + + last_session_update_master_time_ = master_time_; + + // Go ahead and prune dead ones. + PruneSessions(); + + // Advance master time.. + master_time_ += 8; + + // Bail if we spend too much time in here. + millisecs_t new_real_time = GetRealTime(); + if (new_real_time - real_time > 30) { + break; + } + step++; + } + in_update_ = false; +} + +// Reset the game to a blank slate. +void Game::Reset() { + assert(InGameThread()); + + // Tear down any existing setup. + // This should allow high-level objects to die gracefully. + assert(g_python->inited()); + + // Tear down our existing session. + foreground_session_.Clear(); + PruneSessions(); + + // If all is well our sessions should all be dead. + if (g_app_globals->session_count != 0) { + Log("Error: session-count is non-zero (" + + std::to_string(g_app_globals->session_count) + ") on Game::Reset."); + } + + // Note: we don't clear real-time timers anymore. Should we?.. + g_ui->Reset(); + g_input->Reset(); + g_graphics->Reset(); + g_python->Reset(); + g_audio->Reset(); + + if (!HeadlessMode()) { + // If we haven't, send a first frame_def to the graphics thread to kick + // things off (it'll start sending us requests for more after it gets the + // first). + if (!have_sent_initial_frame_def_) { + g_graphics->BuildAndPushFrameDef(); + have_sent_initial_frame_def_ = true; + } + } +} + +auto Game::IsInUIContext() const -> bool { + return (g_ui && Context::current().target.get() == g_ui); +} + +void Game::PushShowURLCall(const std::string& url) { + PushCall([url] { + assert(InGameThread()); + assert(g_python); + g_python->ShowURL(url); + }); +} + +auto Game::GetForegroundContext() -> Context { + Session* s = GetForegroundSession(); + if (s) { + return s->GetForegroundContext(); + } else { + return Context(); + } +} + +void Game::PushBackButtonCall(InputDevice* input_device) { + PushCall([this, input_device] { + assert(InGameThread()); + + // Ignore if UI isn't up yet. + if (!g_ui || !g_ui->overlay_root_widget() || !g_ui->screen_root_widget()) { + return; + } + + // If there's a UI up, send along a cancel message. + if (g_ui->overlay_root_widget()->GetChildCount() != 0 + || g_ui->screen_root_widget()->GetChildCount() != 0) { + g_ui->root_widget()->HandleMessage( + WidgetMessage(WidgetMessage::Type::kCancel)); + } else { + // If there's no main screen or overlay windows, ask for a menu owned by + // this device. + MainMenuPress(input_device); + } + }); +} + +void Game::PushStringEditSetCall(const std::string& value) { + PushCall([value] { + if (!g_ui) { + Log("Error: No ui on StringEditSetEvent."); + return; + } +#if BA_OSTYPE_ANDROID + TextWidget* w = TextWidget::GetAndroidStringEditWidget(); + if (w) { + w->SetText(value); + } +#else + throw Exception(); // Shouldn't get here. +#endif + }); +} + +void Game::PushStringEditCancelCall() { + PushCall([] { + if (!g_ui) { + Log("Error: No ui in PushStringEditCancelCall."); + return; + } + }); +} + +// Called by a newly made Session instance to set itself as the current +// session. +void Game::SetForegroundSession(Session* s) { + assert(InGameThread()); + foreground_session_ = s; +} + +void Game::SetForegroundScene(Scene* sg) { + assert(InGameThread()); + if (foreground_scene_.get() != sg) { + foreground_scene_ = sg; + + // If this scene has a globals-node, put it in charge of stuff. + if (GlobalsNode* g = sg->globals_node()) { + g->SetAsForeground(); + } + } +} + +void Game::LaunchClientSession() { + if (in_update_) { + throw Exception( + "can't launch a session from within a session update; use " + "ba.pushcall()"); + } + assert(InGameThread()); + + // Don't want to pick up any old stuff in here. + ScopedSetContext cp(nullptr); + + // This should kill any current session and get us back to a blank slate. + Reset(); + + // Create the new session. + Object::WeakRef old_foreground_session(foreground_session_); + try { + auto s(Object::New()); + sessions_.push_back(s); + + // It should have set itself as FG. + assert(foreground_session_ == s); + } catch (const std::exception& e) { + // If it failed, restore the previous current session and re-throw. + SetForegroundSession(old_foreground_session.get()); + throw Exception(std::string("HostSession failed: ") + e.what()); + } +} + +void Game::LaunchReplaySession(const std::string& file_name) { + if (in_update_) + throw Exception( + "can't launch a session from within a session update; use " + "ba.pushcall()"); + + assert(InGameThread()); + + // Don't want to pick up any old stuff in here. + ScopedSetContext cp(nullptr); + + // This should kill any current session and get us back to a blank slate. + Reset(); + + // Create the new session. + Object::WeakRef old_foreground_session(foreground_session_); + try { + auto s(Object::New(file_name)); + sessions_.push_back(s); + + // It should have set itself as FG. + assert(foreground_session_ == s); + } catch (const std::exception& e) { + // If it failed, restore the previous current session and re-throw the + // exception. + SetForegroundSession(old_foreground_session.get()); + throw Exception(std::string("HostSession failed: ") + e.what()); + } +} + +void Game::LaunchHostSession(PyObject* session_type_obj, + BenchmarkType benchmark_type) { + if (in_update_) { + throw Exception( + "can't call host_session() from within session update; use " + "ba.pushcall()"); + } + + assert(InGameThread()); + + // If for some reason we're still attached to a host, kill the connection. + if (connection_to_host_.exists()) { + Log("Had host-connection during LaunchHostSession(); shouldn't happen."); + connection_to_host_->RequestDisconnect(); + connection_to_host_.Clear(); + has_connection_to_host_ = false; + UpdateGameRoster(); + } + + // Don't want to pick up any old stuff in here. + ScopedSetContext cp(nullptr); + + // This should kill any current session and get us back to a blank slate. + Reset(); + + Object::WeakRef old_foreground_session(foreground_session_); + try { + // Create the new session. + auto s(Object::New(session_type_obj)); + s->set_benchmark_type(benchmark_type); + sessions_.emplace_back(s); + + // It should have set itself as FG. + assert(foreground_session_ == s); + } catch (const std::exception& e) { + // If it failed, restore the previous session context and re-throw the + // exception. + SetForegroundSession(old_foreground_session.get()); + throw Exception(std::string("HostSession failed: ") + e.what()); + } +} + +void Game::RunMainMenu() { + PushCall([this] { + if (g_app_globals->shutting_down) { + return; + } + assert(g_python); + assert(InGameThread()); + PythonRef result = + g_python->obj(Python::ObjID::kLaunchMainMenuSessionCall).Call(); + if (!result.exists()) { + throw Exception("error running main menu"); + } + }); +} + +// Commands run via the in-game console. These are a bit more 'casual' and run +// in the current visible context. + +void Game::PushInGameConsoleScriptCommand(const std::string& command) { + PushCall([this, command] { + // These are always run in whichever context is 'visible'. + ScopedSetContext cp(GetForegroundContext()); + PythonCommand cmd(command, ""); + if (!g_app_globals->user_ran_commands) { + g_app_globals->user_ran_commands = true; + } + if (cmd.CanEval()) { + PyObject* obj = cmd.RunReturnObj(true); + if (obj && obj != Py_None) { + PyObject* s = PyObject_Repr(obj); + if (s) { + const char* c = PyUnicode_AsUTF8(s); + if (g_app_globals->console) { + g_app_globals->console->Print(std::string(c) + "\n"); + } + Py_DECREF(s); + } + Py_DECREF(obj); + } + } else { + // Not eval-able; just run it. + cmd.Run(); + } + }); +} + +// Commands run via stdin. +void Game::PushStdinScriptCommand(const std::string& command) { + PushCall([this, command] { + // These are always run in whichever context is 'visible'. + ScopedSetContext cp(GetForegroundContext()); + PythonCommand cmd(command, ""); + if (!g_app_globals->user_ran_commands) { + g_app_globals->user_ran_commands = true; + } + + // Eval this if possible (so we can possibly print return value). + if (cmd.CanEval()) { + if (PyObject* obj = cmd.RunReturnObj(true)) { + // Print the value if we're running directly from a terminal + // (or being run under the server-manager) + if ((IsStdinATerminal() || g_app->server_wrapper_managed()) + && obj != Py_None) { + PyObject* s = PyObject_Repr(obj); + if (s) { + const char* c = PyUnicode_AsUTF8(s); + printf("%s\n", c); + fflush(stdout); + Py_DECREF(s); + } + } + Py_DECREF(obj); + } + } else { + // Can't eval it; just run it. + cmd.Run(); + } + }); +} + +void Game::PushInterruptSignalCall() { + PushCall([this] { + assert(InGameThread()); + + // Special case; when running under the server-wrapper, we completely + // ignore interrupt signals (the wrapper acts on them). + if (g_app->server_wrapper_managed()) { + return; + } + + // Just go through _ba.quit() + // FIXME: Shouldn't need to go out to the python layer here... + g_python->obj(Python::ObjID::kQuitCall).Call(); + }); +} + +void Game::PushAskUserForTelnetAccessCall() { + PushCall([this] { + assert(InGameThread()); + ScopedSetContext cp(GetUIContext()); + g_python->obj(Python::ObjID::kTelnetAccessRequestCall).Call(); + }); +} + +void Game::HandleThreadPause() { + // Give userspace python stuff a chance to pause. + ScopedSetContext cp(GetUIContextTarget()); + g_python->obj(Python::ObjID::kOnAppPauseCall).Call(); + + // Tell our account client to commit any outstanding changes to disk. + g_python->CommitLocalData(); +} + +// void Game::PushTelnetScriptCommand(const std::string& command) { +// PushCall([this, command] { +// // These are always run in whichever context is 'visible'. +// ScopedSetContext cp(GetForegroundContext()); +// if (!g_app_globals->user_ran_commands) { +// g_app_globals->user_ran_commands = true; +// } +// PythonCommand cmd(command, ""); +// if (cmd.CanEval()) { +// PyObject* obj = cmd.RunReturnObj(true); +// if (obj && obj != Py_None) { +// PyObject* s = PyObject_Repr(obj); +// if (s) { +// const char* c = PyUnicode_AsUTF8(s); +// PushTelnetPrintCall(std::string(c) + "\n"); +// Py_DECREF(s); +// } +// Py_DECREF(obj); +// } +// } else { +// // Not eval-able; just run it. +// cmd.Run(); +// } +// PushTelnetPrintCall("ballisticacore> "); +// }); +// } + +// void Game::PushTelnetPrintCall(const std::string& message) { +// PushCall([message] { +// if (g_app_globals->telnet_server) { +// g_app_globals->telnet_server->Print(message); +// } +// }); +// } + +void Game::PushPythonCall(const Object::Ref& call) { + // Since we're mucking with refs, need to limit to game thread. + BA_PRECONDITION(InGameThread()); + BA_PRECONDITION(call->object_strong_ref_count() > 0); + PushCall([call] { + assert(call.exists()); + call->Run(); + }); +} + +void Game::PushPythonCallArgs(const Object::Ref& call, + const PythonRef& args) { + // Since we're mucking with refs, need to limit to game thread. + BA_PRECONDITION(InGameThread()); + BA_PRECONDITION(call->object_strong_ref_count() > 0); + PushCall([call, args] { + assert(call.exists()); + call->Run(args.get()); + }); +} + +void Game::PushPythonWeakCall(const Object::WeakRef& call) { + // Since we're mucking with refs, need to limit to game thread. + BA_PRECONDITION(InGameThread()); + + // Even though we only hold a weak ref, we expect a valid strong-reffed + // object to be passed in. + assert(call.exists() && call->object_strong_ref_count() > 0); + + PushCall([call] { + if (call.exists()) { + Python::ScopedCallLabel label("PythonWeakCallMessage"); + call->Run(); + } + }); +} + +void Game::PushPythonWeakCallArgs( + const Object::WeakRef& call, const PythonRef& args) { + // Since we're mucking with refs, need to limit to game thread. + BA_PRECONDITION(InGameThread()); + + // Even though we only hold a weak ref, we expect a valid strong-reffed + // object to be passed in. + assert(call.exists() && call->object_strong_ref_count() > 0); + + PushCall([call, args] { + if (call.exists()) call->Run(args.get()); + }); +} + +void Game::PushPythonRawCallable(PyObject* callable) { + PushCall([this, callable] { + assert(InGameThread()); + + // Lets run this in the UI context. + // (can add other options if we need later) + ScopedSetContext cp(GetUIContext()); + + // This event contains a raw python obj with an incremented ref-count. + auto call(Object::New(callable)); + Py_DECREF(callable); // now just held by call + + call->Run(); + }); +} + +void Game::PushScreenMessage(const std::string& message, + const Vector3f& color) { + PushCall([message, color] { g_graphics->AddScreenMessage(message, color); }); +} + +void Game::SetReplaySpeedExponent(int val) { + replay_speed_exponent_ = std::min(3, std::max(-3, val)); + replay_speed_mult_ = powf(2.0f, static_cast(replay_speed_exponent_)); +} + +void Game::SetDebugSpeedExponent(int val) { + debug_speed_exponent_ = val; + debug_speed_mult_ = powf(2.0f, static_cast(debug_speed_exponent_)); + + Session* s = GetForegroundSession(); + if (s) s->DebugSpeedMultChanged(); +} + +void Game::ChangeGameSpeed(int offs) { + assert(InGameThread()); + + // if we're in a replay session, adjust playback speed there + if (dynamic_cast(GetForegroundSession())) { + int old_speed = replay_speed_exponent(); + SetReplaySpeedExponent(replay_speed_exponent() + offs); + if (old_speed != replay_speed_exponent()) { + ScreenMessage( + "{\"r\":\"watchWindow.playbackSpeedText\"," + "\"s\":[[\"${SPEED}\",\"" + + std::to_string(replay_speed_mult()) + "\"]]}"); + } + return; + } + // Otherwise, in debug build, we allow speeding/slowing anything. +#if BA_DEBUG_BUILD + debug_speed_exponent_ += offs; + debug_speed_mult_ = powf(2.0f, static_cast(debug_speed_exponent_)); + ScreenMessage("DEBUG GAME SPEED TO " + std::to_string(debug_speed_mult_)); + Session* s = GetForegroundSession(); + if (s) { + s->DebugSpeedMultChanged(); + } +#endif // BA_DEBUG_BUILD +} + +auto Game::GetUIContext() const -> Context { + return Context(GetUIContextTarget()); +} + +void Game::PushToggleManualCameraCall() { + PushCall([] { g_graphics->ToggleManualCamera(); }); +} + +void Game::PushToggleDebugInfoDisplayCall() { + PushCall([] { g_graphics->ToggleDebugInfoDisplay(); }); +} + +void Game::PushToggleCollisionGeometryDisplayCall() { + PushCall([] { g_graphics->ToggleDebugDraw(); }); +} + +void Game::PushMainMenuPressCall(InputDevice* device) { + PushCall([this, device] { MainMenuPress(device); }); +} + +void Game::MainMenuPress(InputDevice* device) { + assert(InGameThread()); + g_python->HandleDeviceMenuPress(device); +} + +void Game::PushScreenResizeCall(float virtual_width, float virtual_height, + float pixel_width, float pixel_height) { + PushCall([=] { + ScreenResize(virtual_width, virtual_height, pixel_width, pixel_height); + }); +} + +void Game::ScreenResize(float virtual_width, float virtual_height, + float pixel_width, float pixel_height) { + assert(InGameThread()); + assert(g_graphics != nullptr); + if (g_graphics) { + g_graphics->ScreenResize(virtual_width, virtual_height, pixel_width, + pixel_height); + } + if (g_ui) { + g_ui->ScreenSizeChanged(); + } + if (Session* session = GetForegroundSession()) { + session->ScreenSizeChanged(); + } +} + +void Game::PushGameServiceAchievementListCall( + const std::set& achievements) { + PushCall([this, achievements] { GameServiceAchievementList(achievements); }); +} + +void Game::GameServiceAchievementList( + const std::set& achievements) { + assert(g_python); + assert(InGameThread()); + g_python->DispatchRemoteAchievementList(achievements); +} + +void Game::PushScoresToBeatResponseCall(bool success, + const std::list& scores, + void* py_callback) { + PushCall([this, success, scores, py_callback] { + ScoresToBeatResponse(success, scores, py_callback); + }); +} + +void Game::ScoresToBeatResponse(bool success, + const std::list& scores, + void* py_callback) { + assert(g_python); + assert(InGameThread()); + g_python->DispatchScoresToBeatResponse(success, scores, py_callback); +} + +void Game::PushPlaySoundCall(SystemSoundID sound) { + PushCall([sound] { g_audio->PlaySound(g_media->GetSound(sound)); }); +} + +void Game::PushFriendScoreSetCall(const FriendScoreSet& score_set) { + PushCall([score_set] { g_python->HandleFriendScoresCB(score_set); }); +} + +void Game::PushConfirmQuitCall() { + PushCall([this] { + assert(InGameThread()); + if (HeadlessMode()) { + Log("PushConfirmQuitCall() unhandled on headless."); + } else { + // If input is locked, just quit immediately.. a confirm screen wouldn't + // work anyway + if (g_input->IsInputLocked() + || (g_app_globals->console != nullptr + && g_app_globals->console->active())) { + // Just go through _ba.quit() + // FIXME: Shouldn't need to go out to the python layer here... + g_python->obj(Python::ObjID::kQuitCall).Call(); + return; + } else { + // this needs to be run in the UI context + ScopedSetContext cp(GetUIContextTarget()); + + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kSwish)); + g_python->obj(Python::ObjID::kQuitWindowCall).Call(); + + // if we have a keyboard, give it UI ownership + InputDevice* keyboard = g_input->keyboard_input(); + if (keyboard) { + g_ui->SetUIInputDevice(keyboard); + } + } + } + }); +} + +void Game::Draw() { + g_graphics->BuildAndPushFrameDef(); + + // Now bring the game up to date. + // By doing this *after* shipping a new frame_def we're reducing the + // chance of frame drops at the expense of adding a bit of visual latency. + // Could maybe try to be smart about which to do first, but not sure + // if its worth it. + Update(); + + // Update our cheat tests. + millisecs_t now = g_platform->GetTicks(); + millisecs_t elapsed = now - last_draw_real_time_; + if (elapsed > largest_draw_time_increment_since_last_reset_) { + largest_draw_time_increment_since_last_reset_ = elapsed; + } + last_draw_real_time_ = now; + + // Sanity test: can make sure our scene is taking exactly 2 steps + // per frame here.. (should generally be the case on 60hz devices). + if (explicit_bool(false)) { + static int64_t last_step = 0; + HostActivity* ha = GetForegroundContext().GetHostActivity(); + if (ha) { + int64_t step = ha->scene()->stepnum(); + Log(std::to_string(step - last_step)); + last_step = step; + } + } +} + +void Game::PushFrameDefRequest() { + PushCall([this] { Draw(); }); +} + +void Game::PushOnAppResumeCall() { + PushCall([] { + // Wipe out whatever input device was in control of the UI. + assert(g_ui); + g_ui->SetUIInputDevice(nullptr); + }); +} + +// Look through everything in our config dict and act on it. +void Game::ApplyConfig() { + assert(InGameThread()); + + // Not relevant for fullscreen anymore + // since we're fullscreen windows everywhere. + int width = 800; + int height = 600; + + // Texture quality. + TextureQuality texture_quality_requested; + std::string texqualstr = + g_app_config->Resolve(AppConfig::StringID::kTextureQuality); + + if (texqualstr == "Auto") { + texture_quality_requested = TextureQuality::kAuto; + } else if (texqualstr == "High") { + texture_quality_requested = TextureQuality::kHigh; + } else if (texqualstr == "Medium") { + texture_quality_requested = TextureQuality::kMedium; + } else if (texqualstr == "Low") { + texture_quality_requested = TextureQuality::kLow; + } else { + Log("Invalid texture quality: '" + texqualstr + "'; defaulting to low."); + texture_quality_requested = TextureQuality::kLow; + } + + // Graphics quality. + std::string gqualstr = + g_app_config->Resolve(AppConfig::StringID::kGraphicsQuality); + GraphicsQuality graphics_quality_requested; + + if (gqualstr == "Auto") { + graphics_quality_requested = GraphicsQuality::kAuto; + } else if (gqualstr == "Higher") { + graphics_quality_requested = GraphicsQuality::kHigher; + } else if (gqualstr == "High") { + graphics_quality_requested = GraphicsQuality::kHigh; + } else if (gqualstr == "Medium") { + graphics_quality_requested = GraphicsQuality::kMedium; + } else if (gqualstr == "Low") { + graphics_quality_requested = GraphicsQuality::kLow; + } else { + Log("Error: Invalid graphics quality: '" + gqualstr + + "'; defaulting to auto."); + graphics_quality_requested = GraphicsQuality::kAuto; + } + + // Android res string. + std::string android_res = + g_app_config->Resolve(AppConfig::StringID::kResolutionAndroid); + + bool fullscreen = g_app_config->Resolve(AppConfig::BoolID::kFullscreen); + + // Note: when the graphics-thread applies the first set-screen event it will + // trigger the remainder of startup such as media-loading; make sure nothing + // below this will affect that. + g_graphics_server->PushSetScreenCall(fullscreen, width, height, + texture_quality_requested, + graphics_quality_requested, android_res); + + // FIXME: The graphics server should kick this off *AFTER* it sets the actual + // quality values; here we're just sending along our requested values which + // is wrong. If there's a session up, inform it of the (potential) change. + Session* session = GetForegroundSession(); + if (session) { + session->GraphicsQualityChanged(graphics_quality_requested); + } + + if (!HeadlessMode()) { + g_app_globals->remote_server_accepting_connections = + g_app_config->Resolve(AppConfig::BoolID::kEnableRemoteApp); + } + + chat_muted_ = g_app_config->Resolve(AppConfig::BoolID::kChatMuted); + g_graphics->set_show_fps(g_app_config->Resolve(AppConfig::BoolID::kShowFPS)); + + // Set tv border (for both client and server). + bool tv_border = g_app_config->Resolve(AppConfig::BoolID::kTVBorder); + g_graphics_server->PushCall( + [tv_border] { g_graphics_server->set_tv_border(tv_border); }); + + // FIXME: this should exist either on the client or the server; not both. + // (and should be communicated via frameldefs/etc.) + g_graphics->set_tv_border(tv_border); + + g_graphics_server->PushSetScreenGammaCall( + g_app_config->Resolve(AppConfig::FloatID::kScreenGamma)); + g_graphics_server->PushSetScreenPixelScaleCall( + g_app_config->Resolve(AppConfig::FloatID::kScreenPixelScale)); + + TextWidget::set_always_use_internal_keyboard( + g_app_config->Resolve(AppConfig::BoolID::kAlwaysUseInternalKeyboard)); + + // V-sync setting. + std::string v_sync = + g_app_config->Resolve(AppConfig::StringID::kVerticalSync); + bool do_v_sync{}; + bool auto_v_sync{}; + if (v_sync == "Auto") { + do_v_sync = true; + auto_v_sync = true; + } else if (v_sync == "Always") { + do_v_sync = true; + auto_v_sync = false; + } else if (v_sync == "Never") { + do_v_sync = false; + auto_v_sync = false; + } else { + do_v_sync = false; + auto_v_sync = false; + Log("Error: Invalid 'Vertical Sync' value: '" + v_sync + "'"); + } + g_graphics_server->PushSetVSyncCall(do_v_sync, auto_v_sync); + + g_audio->SetVolumes(g_app_config->Resolve(AppConfig::FloatID::kMusicVolume), + g_app_config->Resolve(AppConfig::FloatID::kSoundVolume)); + + // Kick-idle-players setting (hmm is this still relevant?). + auto* host_session = dynamic_cast(foreground_session_.get()); + kick_idle_players_ = + g_app_config->Resolve(AppConfig::BoolID::kKickIdlePlayers); + if (host_session) { + host_session->SetKickIdlePlayers(kick_idle_players_); + } + + // Input doesn't yet exist when we first run; it updates itself initially when + // it comes up (but we do thereafter). + // if (input_) { + assert(g_input); + g_input->ApplyAppConfig(); + // } + + // Set up network ports/states. + int port = g_app_config->Resolve(AppConfig::IntID::kPort); + int telnet_port = g_app_config->Resolve(AppConfig::IntID::kTelnetPort); + + // NOTE: Hard disabling telnet for now in headless builds; + // it was being exploited to own servers. + bool enable_telnet = + g_buildconfig.headless_build() + ? false + : g_app_config->Resolve(AppConfig::BoolID::kEnableTelnet); + std::string telnet_password = + g_app_config->Resolve(AppConfig::StringID::kTelnetPassword); + + g_app->PushNetworkSetupCall(port, telnet_port, enable_telnet, + telnet_password); + + bool disable_camera_shake = + g_app_config->Resolve(AppConfig::BoolID::kDisableCameraShake); + g_graphics->set_camera_shake_disabled(disable_camera_shake); + + bool disable_camera_gyro = + g_app_config->Resolve(AppConfig::BoolID::kDisableCameraGyro); + g_graphics->set_camera_gyro_explicitly_disabled(disable_camera_gyro); + + // Any platform-specific settings. + g_platform->ApplyConfig(); +} + +void Game::PushApplyConfigCall() { + PushCall([this] { ApplyConfig(); }); +} + +void Game::PushRemoveGraphicsServerRenderHoldCall() { + PushCall([] { + // This call acts as a flush of sorts; when it goes through, + // we push a call to the graphics server saying its ok for it + // to start rendering again. Thus any already-queued-up + // frame_defs or whatnot will be ignored. + g_graphics_server->PushRemoveRenderHoldCall(); + }); +} + +void Game::PushSetFriendListCall(const std::vector& friends) { + PushCall([friends] { g_python->DispatchFriendList(friends); }); +} + +void Game::PushFreeMediaComponentRefsCall( + const std::vector*>& components) { + PushCall([components] { + for (auto&& i : components) { + delete i; + } + }); +} + +void Game::PushHavePendingLoadsDoneCall() { + PushCall([] { g_media->ClearPendingLoadsDoneList(); }); +} + +void Game::ToggleConsole() { + assert(InGameThread()); + if (auto console = g_app_globals->console) { + console->ToggleState(); + } +} + +void Game::PushConsolePrintCall(const std::string& msg) { + PushCall([msg] { + // Send them to the console if its been created or store them + // for when it is (unless we're headless in which case it never will). + if (auto console = g_app_globals->console) { + console->Print(msg); + } else if (!HeadlessMode()) { + g_app_globals->console_startup_messages += msg; + } + }); +} + +void Game::PushHavePendingLoadsCall() { + PushCall([this] { + have_pending_loads_ = true; + UpdateProcessTimer(); + }); +} + +void Game::PushShutdownCall(bool soft) { + PushCall([this, soft] { Shutdown(soft); }); +} + +void Game::Shutdown(bool soft) { + assert(InGameThread()); + + if (!g_app_globals->shutting_down) { + g_app_globals->shutting_down = true; + + // Nuke the app if we get stuck shutting down. + Utils::StartSuicideTimer("shutdown", 10000); + + // Call our shutdown callback. + g_python->obj(Python::ObjID::kShutdownCall).Call(); + + // If we have any client/host connections, give them + // a chance to shoot off disconnect packets or whatnot. + for (auto& connection : connections_to_clients_) { + connection.second->RequestDisconnect(); + } + if (connection_to_host_.exists()) { + connection_to_host_->RequestDisconnect(); + } + + // Let's do the same stuff we do when our thread is pausing. (committing + // account-client to disk, etc). + HandleThreadPause(); + + // Attempt to report/store outstanding log stuff. + Python::PutLog(false); + + // Ideally we'd want to give some of the above stuff + // a few seconds to complete, but just calling it done for now. + g_app->PushShutdownCompleteCall(); + } +} + +void Game::ResetInput() { + assert(InGameThread()); + g_input->ResetKeyboardHeldKeys(); + g_input->ResetJoyStickHeldButtons(); +} + +auto Game::RemovePlayer(Player* player) -> void { + assert(InGameThread()); + if (HostSession* host_session = player->GetHostSession()) { + host_session->RemovePlayer(player); + } else { + Log("Got RemovePlayer call but have no host_session"); + } +} + +auto Game::NewRealTimer(millisecs_t length, bool repeat, + const Object::Ref& runnable) -> int { + int offset = 0; + Timer* t = realtimers_->NewTimer(GetRealTime(), length, offset, + repeat ? -1 : 0, runnable); + return t->id(); +} + +void Game::DeleteRealTimer(int timer_id) { realtimers_->DeleteTimer(timer_id); } + +void Game::SetRealTimerLength(int timer_id, millisecs_t length) { + Timer* t = realtimers_->GetTimer(timer_id); + if (t) { + t->SetLength(length); + } else { + Log("Error: Game::SetRealTimerLength() called on nonexistent timer."); + } +} + +void Game::Process() { + have_pending_loads_ = g_media->RunPendingLoadsGameThread(); + UpdateProcessTimer(); +} + +void Game::SetLanguageKeys(const std::map& language) { + assert(InGameThread()); + { + std::lock_guard lock(language_mutex_); + language_ = language; + } + + // Let's also inform existing session stuff so it can update itself. + if (Session* session = GetForegroundSession()) { + session->LanguageChanged(); + } + + // As well as existing UI stuff. + if (Widget* root_widget = g_ui->root_widget()) { + root_widget->OnLanguageChange(); + } + + // Also clear translations on all screen-messages. + g_graphics->ClearScreenMessageTranslations(); +} + +auto DoCompileResourceString(cJSON* obj) -> std::string { + assert(InGameThread()); + assert(obj != nullptr); + + std::string result; + + // If its got a "r" key, look it up as a resource.. (with optional fallback). + cJSON* resource = cJSON_GetObjectItem(obj, "r"); + if (resource == nullptr) { + resource = cJSON_GetObjectItem(obj, "resource"); + // As of build 14318, complain if we find long key names; hope to remove + // them soon. + if (resource != nullptr) { + static bool printed = false; + if (!printed) { + printed = true; + char* c = cJSON_Print(obj); + BA_LOG_ONCE("found long key 'resource' in raw lstr json: " + + std::string(c)); + free(c); + } + } + } + if (resource != nullptr) { + // Look for fallback-resource. + cJSON* fallback_resource = cJSON_GetObjectItem(obj, "f"); + if (fallback_resource == nullptr) { + fallback_resource = cJSON_GetObjectItem(obj, "fallback"); + + // As of build 14318, complain if we find old long key names; hope to + // remove them soon. + if (fallback_resource != nullptr) { + static bool printed = false; + if (!printed) { + printed = true; + char* c = cJSON_Print(obj); + BA_LOG_ONCE("found long key 'fallback' in raw lstr json: " + + std::string(c)); + free(c); + } + } + } + cJSON* fallback_value = cJSON_GetObjectItem(obj, "fv"); + result = g_python->GetResource( + resource->valuestring, + fallback_resource ? fallback_resource->valuestring : nullptr, + fallback_value ? fallback_value->valuestring : nullptr); + } else { + // Apparently not a resource; lets try as a translation ("t" keys). + cJSON* translate = cJSON_GetObjectItem(obj, "t"); + if (translate == nullptr) { + translate = cJSON_GetObjectItem(obj, "translate"); + + // As of build 14318, complain if we find long key names; hope to remove + // them soon. + if (translate != nullptr) { + static bool printed = false; + if (!printed) { + printed = true; + char* c = cJSON_Print(obj); + BA_LOG_ONCE("found long key 'translate' in raw lstr json: " + + std::string(c)); + free(c); + } + } + } + if (translate != nullptr) { + if (translate->type != cJSON_Array + || cJSON_GetArraySize(translate) != 2) { + throw Exception("Expected a 2 member array for translate"); + } + cJSON* category = cJSON_GetArrayItem(translate, 0); + if (category->type != cJSON_String) { + throw Exception( + "First member of translate array (category) must be a string"); + } + cJSON* value = cJSON_GetArrayItem(translate, 1); + if (value->type != cJSON_String) { + throw Exception( + "Second member of translate array (value) must be a string"); + } + result = + g_python->GetTranslation(category->valuestring, value->valuestring); + } else { + // Lastly try it as a value ("value" or "v"). + // (can be useful for feeding explicit strings while still allowing + // translated subs + cJSON* value = cJSON_GetObjectItem(obj, "v"); + if (value == nullptr) { + value = cJSON_GetObjectItem(obj, "value"); + + // As of build 14318, complain if we find long key names; hope to remove + // them soon. + if (value != nullptr) { + static bool printed = false; + if (!printed) { + printed = true; + char* c = cJSON_Print(obj); + BA_LOG_ONCE("found long key 'value' in raw lstr json: " + + std::string(c)); + free(c); + } + } + } + if (value != nullptr) { + if (value->type != cJSON_String) { + throw Exception("Expected a string for value"); + } + result = value->valuestring; + } else { + throw Exception("no 'resource', 'translate', or 'value' keys found"); + } + } + } + // Ok; now no matter what it was, see if it contains any subs and replace + // them. + // ("subs" or "s") + cJSON* subs = cJSON_GetObjectItem(obj, "s"); + if (subs == nullptr) { + subs = cJSON_GetObjectItem(obj, "subs"); + + // As of build 14318, complain if we find long key names; hope to remove + // them soon. + if (subs != nullptr) { + static bool printed = false; + if (!printed) { + printed = true; + char* c = cJSON_Print(obj); + BA_LOG_ONCE("found long key 'subs' in raw lstr json: " + + std::string(c)); + free(c); + } + } + } + if (subs != nullptr) { + if (subs->type != cJSON_Array) { + throw Exception("expected an array for 'subs'"); + } + int subsCount = cJSON_GetArraySize(subs); + for (int i = 0; i < subsCount; i++) { + cJSON* sub = cJSON_GetArrayItem(subs, i); + if (sub->type != cJSON_Array || cJSON_GetArraySize(sub) != 2) { + throw Exception( + "Invalid subs entry; expected length 2 list of sub/replacement."); + } + + // First item should be a string. + cJSON* key = cJSON_GetArrayItem(sub, 0); + if (key->type != cJSON_String) { + throw Exception("Sub keys must be strings."); + } + std::string s_key = key->valuestring; + + // Second item can be a string or a dict; if its a dict, we go recursive. + cJSON* value = cJSON_GetArrayItem(sub, 1); + std::string s_val; + if (value->type == cJSON_String) { + s_val = value->valuestring; + } else if (value->type == cJSON_Object) { + s_val = DoCompileResourceString(value); + } else { + throw Exception("Sub values must be strings or dicts."); + } + + // Replace *ALL* occurrences. + // FIXME: Using this simple logic, If our replace value contains our + // search value we get an infinite loop. For now, just error in that case. + if (s_val.find(s_key) != std::string::npos) { + throw Exception("Subs replace string cannot contain search string."); + } + while (true) { + size_t pos = result.find(s_key); + if (pos == std::string::npos) { + break; + } + result.replace(pos, s_key.size(), s_val); + } + } + } + return result; +} + +auto Game::CompileResourceString(const std::string& s, const std::string& loc, + bool* valid) -> std::string { + assert(InGameThread()); + assert(g_python != nullptr); + + bool dummyvalid; + if (valid == nullptr) { + valid = &dummyvalid; + } + + // Quick out: if it doesn't start with a { and end with a }, treat it as a + // literal and just return it as-is. + if (s.size() < 2 || s[0] != '{' || s[s.size() - 1] != '}') { + *valid = true; + return s; + } + + cJSON* root = cJSON_Parse(s.c_str()); + if (root == nullptr) { + Log("CompileResourceString failed (loc " + loc + "); invalid json: '" + s + + "'"); + *valid = false; + return ""; + } + std::string result; + try { + result = DoCompileResourceString(root); + *valid = true; + } catch (const std::exception& e) { + Log("CompileResourceString failed (loc " + loc + + "): " + std::string(e.what()) + "; str='" + s + "'"); + result = ""; + *valid = false; + } + cJSON_Delete(root); + return result; +} + +auto Game::GetResourceString(const std::string& key) -> std::string { + std::string val; + { + std::lock_guard lock(language_mutex_); + auto i = language_.find(key); + if (i != language_.end()) { + val = i->second; + } + } + return val; +} + +auto Game::CharStr(SpecialChar id) -> std::string { + std::lock_guard lock(special_char_mutex_); + std::string val; + auto i = special_char_strings_.find(id); + if (i != special_char_strings_.end()) { + val = i->second; + } else { + BA_LOG_PYTHON_TRACE_ONCE("invalid key in CharStr(): '" + + std::to_string(static_cast(id)) + "'"); + val = "?"; + } + return val; +} + +void Game::HandleClientDisconnected(int id) { + auto i = connections_to_clients_.find(id); + if (i != connections_to_clients_.end()) { + bool was_connected = i->second->can_communicate(); + std::string leaver_spec = i->second->peer_spec().GetSpecString(); + std::vector leave_msg(leaver_spec.size() + 1); + leave_msg[0] = BA_MESSAGE_PARTY_MEMBER_LEFT; + memcpy(&(leave_msg[1]), leaver_spec.c_str(), leaver_spec.size()); + connections_to_clients_.erase(i); + + // If the client was connected, they were on the roster. + // We need to update it and send it to all remaining clients since they're + // gone. Also inform everyone who just left so they can announce it + // (technically could consolidate these messages but whatever...). + if (was_connected) { + UpdateGameRoster(); + for (auto&& connection : connections_to_clients_) { + if (ShouldAnnouncePartyJoinsAndLeaves()) { + connection.second->SendReliableMessage(leave_msg); + } + } + } + } +} + +void Game::PushClientDisconnectedCall(int id) { + PushCall([this, id] { HandleClientDisconnected(id); }); +} + +auto Game::ShouldAnnouncePartyJoinsAndLeaves() -> bool { + assert(InGameThread()); + + // At the moment we don't announce these for public internet parties.. (too + // much noise). + return !public_party_enabled(); +} + +void Game::CleanUpBeforeConnectingToHost() { + // We can't have connected clients and a host-connection at the same time. + // Make a minimal attempt to disconnect any client connections we have, but + // get them off the list immediately. + // FIXME: Should we have a 'purgatory' for dying client connections?.. + // (they may not get the single 'go away' packet we send here) + ForceDisconnectClients(); + + // Also make sure our public party state is off; this will inform the server + // that it should not be handing out our address to anyone. + assert(g_python); + SetPublicPartyEnabled(false); +} + +auto Game::DisconnectClient(int client_id, int ban_seconds) -> bool { + assert(InGameThread()); + + if (connection_to_host_.exists()) { + // Kick-votes first appeared in 14248 + if (connection_to_host_->build_number() < 14248) { + return false; + } + if (client_id > 255) { + Log("DisconnectClient got client_id > 255 (" + std::to_string(client_id) + + ")"); + } else { + std::vector msg_out(2); + msg_out[0] = BA_MESSAGE_KICK_VOTE; + msg_out[1] = static_cast_check_fit(client_id); + connection_to_host_->SendReliableMessage(msg_out); + return true; + } + } else { + // No host connection - look for clients. + auto i = connections_to_clients_.find(client_id); + + if (i != connections_to_clients_.end()) { + // If this is considered a kick, add an entry to our banned list so we + // know not to let them back in for a while. + if (ban_seconds > 0) { + BanPlayer(i->second->peer_spec(), 1000 * ban_seconds); + } + i->second->RequestDisconnect(); + + // Do the official local disconnect immediately with the sounds and all + // that. + PushClientDisconnectedCall(client_id); + + return true; + } + } + return false; +} + +void Game::ForceDisconnectClients() { + for (auto&& i : connections_to_clients_) { + if (ConnectionToClient* client = i.second.get()) { + client->RequestDisconnect(); + } + } + connections_to_clients_.clear(); +} + +void Game::PushHostConnectedUDPCall(const SockAddr& addr, + bool print_connect_progress) { + PushCall([this, addr, print_connect_progress] { + // Attempt to disconnect any clients we have, turn off public-party + // advertising, etc. + CleanUpBeforeConnectingToHost(); + print_udp_connect_progress_ = print_connect_progress; + connection_to_host_ = Object::New(addr); + has_connection_to_host_ = true; + printed_host_disconnect_ = false; + }); +} + +void Game::PushDisconnectFromHostCall() { + PushCall([this] { + if (connection_to_host_.exists()) { + connection_to_host_->RequestDisconnect(); + } + }); +} + +void Game::PushDisconnectedFromHostCall() { + PushCall([this] { + if (connection_to_host_.exists()) { + bool was_connected = connection_to_host_->can_communicate(); + connection_to_host_.Clear(); + has_connection_to_host_ = false; + + // Clear out our party roster. + UpdateGameRoster(); + + // Go back to main menu *if* the connection was fully connected. + // Otherwise we're still probably sitting at the main menu + // so no need to reset it. + if (was_connected) { + RunMainMenu(); + } + } + }); +} + +void Game::SendScreenMessageToAll(const std::string& s, float r, float g, + float b) { + SendScreenMessageToClients(s, r, g, b); + ScreenMessage(s, {r, g, b}); +} + +void Game::SendScreenMessageToClients(const std::string& s, float r, float g, + float b) { + for (auto&& i : connections_to_clients_) { + if (i.second.exists() && i.second->can_communicate()) { + i.second->SendScreenMessage(s, r, g, b); + } + } +} + +void Game::SendScreenMessageToSpecificClients(const std::string& s, float r, + float g, float b, + const std::vector& clients) { + for (auto&& i : connections_to_clients_) { + if (i.second.exists() && i.second->can_communicate()) { + // Only send if this client is in our list. + for (auto c : clients) { + if (c == i.second->id()) { + i.second->SendScreenMessage(s, r, g, b); + break; + } + } + } + } + + // Now print locally only if -1 is in our list. + for (auto c : clients) { + if (c == -1) { + ScreenMessage(s, {r, g, b}); + break; + } + } +} + +auto Game::GetConnectionToHostUDP() -> ConnectionToHostUDP* { + ConnectionToHost* h = connection_to_host_.get(); + return h ? h->GetAsUDP() : nullptr; +} + +void Game::PushPartyInviteCall(const std::string& name, + const std::string& invite_id) { + PushCall([this, name, invite_id] { PartyInvite(name, invite_id); }); +} + +void Game::PartyInvite(const std::string& name, const std::string& invite_id) { + assert(InGameThread()); + g_python->PartyInvite(name, invite_id); +} + +void Game::PushPartyInviteRevokeCall(const std::string& invite_id) { + PushCall([this, invite_id] { PartyInviteRevoke(invite_id); }); +} + +void Game::PartyInviteRevoke(const std::string& invite_id) { + assert(InGameThread()); + g_python->PartyInviteRevoke(invite_id); +} + +void Game::PushUDPConnectionPacketCall(const std::vector& data, + const SockAddr& addr) { + PushCall([this, data, addr] { UDPConnectionPacket(data, addr); }); +} + +// Called for low level packets coming in pertaining to udp +// host/client-connections. +void Game::UDPConnectionPacket(const std::vector& data_in, + const SockAddr& addr) { + assert(!data_in.empty()); + + const uint8_t* data = &(data_in[0]); + auto data_size = static_cast(data_in.size()); + + switch (data[0]) { + case BA_PACKET_CLIENT_ACCEPT: { + if (data_size == 3) { + uint8_t request_id = data[2]; + + // If we have a udp-host-connection and its request-id matches, we're + // accepted; hooray! + ConnectionToHostUDP* hc = GetConnectionToHostUDP(); + if (hc && hc->request_id() == request_id) { + hc->set_client_id(data[1]); + } + } + break; + } + case BA_PACKET_DISCONNECT_FROM_CLIENT_REQUEST: { + if (data_size == 2) { + // Client is telling us (host) that it wants to disconnect. + uint8_t client_id = data[1]; + + // Wipe that client out (if it still exists). + PushClientDisconnectedCall(client_id); + + // Now send an ack so they know it's been taken care of. + g_network_write_module->PushSendToCall( + {BA_PACKET_DISCONNECT_FROM_CLIENT_ACK, client_id}, addr); + } + break; + } + case BA_PACKET_DISCONNECT_FROM_CLIENT_ACK: { + if (data_size == 2) { + // Host is telling us (client) that we've been disconnected. + uint8_t client_id = data[1]; + ConnectionToHostUDP* hc = GetConnectionToHostUDP(); + if (hc && hc->client_id() == client_id) { + PushDisconnectedFromHostCall(); + } + } + break; + } + case BA_PACKET_DISCONNECT_FROM_HOST_REQUEST: { + if (data_size == 2) { + uint8_t client_id = data[1]; + + // Host is telling us (client) to disconnect. + ConnectionToHostUDP* hc = GetConnectionToHostUDP(); + if (hc && hc->client_id() == client_id) { + PushDisconnectedFromHostCall(); + } + + // Now send an ack so they know it's been taken care of. + g_network_write_module->PushSendToCall( + {BA_PACKET_DISCONNECT_FROM_HOST_ACK, client_id}, addr); + } + break; + } + case BA_PACKET_DISCONNECT_FROM_HOST_ACK: { + break; + } + case BA_PACKET_CLIENT_GAMEPACKET_COMPRESSED: { + if (data_size > 2) { + uint8_t client_id = data[1]; + auto i = connections_to_clients_.find(client_id); + if (i != connections_to_clients_.end()) { + // FIXME: could change HandleGamePacketCompressed to avoid this + // copy. + std::vector data2(data_size - 2); + memcpy(data2.data(), data + 2, data_size - 2); + i->second->HandleGamePacketCompressed(data2); + return; + } else { + // Send a disconnect request aimed at them. + g_network_write_module->PushSendToCall( + {BA_PACKET_DISCONNECT_FROM_HOST_REQUEST, client_id}, addr); + } + } + break; + } + + case BA_PACKET_HOST_GAMEPACKET_COMPRESSED: { + if (data_size > 2) { + uint8_t request_id = data[1]; + + ConnectionToHostUDP* hc = GetConnectionToHostUDP(); + if (hc && hc->request_id() == request_id) { + // FIXME: Should change HandleGamePacketCompressed to avoid this copy. + std::vector data2(data_size - 2); + memcpy(data2.data(), data + 2, data_size - 2); + hc->HandleGamePacketCompressed(data2); + } + } + break; + } + + case BA_PACKET_CLIENT_DENY: + case BA_PACKET_CLIENT_DENY_PARTY_FULL: + case BA_PACKET_CLIENT_DENY_ALREADY_IN_PARTY: + case BA_PACKET_CLIENT_DENY_VERSION_MISMATCH: { + if (data_size == 2) { + uint8_t request_id = data[1]; + ConnectionToHostUDP* hc = GetConnectionToHostUDP(); + + // If they're for-sure rejecting *this* connection, kill it. + if (hc && hc->request_id() == request_id) { + bool keep_trying = false; + + // OBSOLETE BUT HERE FOR BACKWARDS COMPAT WITH 1.4.98 servers. + // Newer servers never deny us in this way and simply include + // their protocol version in the handshake they send us, allowing us + // to decide whether we support talking to them or not. + if (data[0] == BA_PACKET_CLIENT_DENY_VERSION_MISMATCH) { + // If we've got more protocols we can try, keep trying to connect + // with our other protocols until one works or we run out. + // FIXME: We should move this logic to the gamepacket or message + // level so it works for all connection types. + keep_trying = hc->SwitchProtocol(); + if (!keep_trying) { + if (!printed_host_disconnect_) { + ScreenMessage( + GetResourceString("connectionFailedVersionMismatchText"), + {1, 0, 0}); + printed_host_disconnect_ = true; + } + } + } else if (data[0] == BA_PACKET_CLIENT_DENY_PARTY_FULL) { + if (!printed_host_disconnect_) { + if (print_udp_connect_progress_) { + ScreenMessage( + GetResourceString("connectionFailedPartyFullText"), + {1, 0, 0}); + } + printed_host_disconnect_ = true; + } + } else if (data[0] == BA_PACKET_CLIENT_DENY_ALREADY_IN_PARTY) { + if (!printed_host_disconnect_) { + ScreenMessage( + GetResourceString("connectionFailedHostAlreadyInPartyText"), + {1, 0, 0}); + printed_host_disconnect_ = true; + } + } else { + if (!printed_host_disconnect_) { + ScreenMessage(GetResourceString("connectionRejectedText"), + {1, 0, 0}); + printed_host_disconnect_ = true; + } + } + if (!keep_trying) { + PushDisconnectedFromHostCall(); + } + } + } + break; + } + case BA_PACKET_CLIENT_REQUEST: { + if (data_size > 4) { + // Bytes 2 and 3 are their protocol ID, byte 4 is request ID, the rest + // is session-id. + uint16_t protocol_id; + memcpy(&protocol_id, data + 1, 2); + uint8_t request_id = data[3]; + + // They also send us their session-ID which should + // be completely unique to them; we can use this to lump client + // requests together and such. + std::vector client_name_buffer(data_size - 4 + 1); + memcpy(&(client_name_buffer[0]), data + 4, data_size - 4); + client_name_buffer[data_size - 4] = 0; // terminate string + std::string client_name = &(client_name_buffer[0]); + + if (static_cast(connections_to_clients_.size() + 1) + >= public_party_max_size()) { + // If we've reached our party size limit (including ourself in that + // count), reject. + + // Newer version have a specific party-full message; send that first + // but also follow up with a generic deny message for older clients. + g_network_write_module->PushSendToCall( + {BA_PACKET_CLIENT_DENY_PARTY_FULL, request_id}, addr); + + g_network_write_module->PushSendToCall( + {BA_PACKET_CLIENT_DENY, request_id}, addr); + + } else if (connection_to_host_.exists()) { + // If we're connected to someone else, we can't have clients. + g_network_write_module->PushSendToCall( + {BA_PACKET_CLIENT_DENY_ALREADY_IN_PARTY, request_id}, addr); + } else { + // Otherwise go ahead and make them a new client connection. + Object::Ref connection_to_client; + + // Go through and see if we already have a client-connection for + // this request-id. + for (auto&& i : connections_to_clients_) { + if (ConnectionToClientUDP* cc_udp = i.second->GetAsUDP()) { + if (cc_udp->client_name() == client_name) { + connection_to_client = cc_udp; + break; + } + } + } + if (!connection_to_client.exists()) { + // Create them a client object. + // Try to find an unused client-id in the range 0-255. + int client_id = 0; + bool found = false; + for (int i = 0; i < 256; i++) { + int test_id = (next_connection_to_client_id_ + i) % 255; + if (connections_to_clients_.find(test_id) + == connections_to_clients_.end()) { + client_id = test_id; + found = true; + break; + } + } + next_connection_to_client_id_++; + + // If all 255 slots are taken (whaaaaaaa?), reject them. + if (!found) { + std::vector msg_out(2); + msg_out[0] = BA_PACKET_CLIENT_DENY; + msg_out[1] = request_id; + g_network_write_module->PushSendToCall(msg_out, addr); + Log("All client slots full; really?.."); + break; + } + connection_to_client = Object::New( + addr, client_name, request_id, client_id); + connections_to_clients_[client_id] = connection_to_client; + } + + // If we got to this point, regardless of whether + // we already had a connection or not, tell them + // they're accepted. + std::vector msg_out(3); + msg_out[0] = BA_PACKET_CLIENT_ACCEPT; + assert(connection_to_client->id() < 256); + msg_out[1] = + static_cast_check_fit(connection_to_client->id()); + msg_out[2] = request_id; + g_network_write_module->PushSendToCall(msg_out, addr); + } + } + break; + } + default: + // Assuming we can get random other noise in here; + // should just silently ignore. + break; + } +} + +// Can probably kill this. +auto Game::GetConnectionsToClients() -> std::vector { + std::vector connections; + connections.reserve(connections_to_clients_.size()); + for (auto& connections_to_client : connections_to_clients_) { + if (connections_to_client.second.exists()) { + connections.push_back(connections_to_client.second.get()); + } else { + Log("HAVE NONEXISTENT CONNECTION_TO_CLIENT IN LIST; UNEXPECTED"); + } + } + return connections; +} + +#if BA_GOOGLE_BUILD + +void Game::PushClientDisconnectedGooglePlayCall(int google_id) { + PushCall([this, google_id] { + int id = ClientIDFromGooglePlayClientID(google_id); + HandleClientDisconnected(id); + }); +} + +int Game::ClientIDFromGooglePlayClientID(int google_id) { + auto i = google_play_id_to_client_id_map_.find(google_id); + if (i != google_play_id_to_client_id_map_.end()) { + return i->second; + } else { + BA_LOG_ONCE("ClientIDFromGooglePlayClientID failed for id " + + std::to_string(google_id)); + return -1; + } +} + +int Game::GooglePlayClientIDFromClientID(int client_id) { + auto i = client_id_to_google_play_id_map_.find(client_id); + if (i != client_id_to_google_play_id_map_.end()) { + return i->second; + } else { + BA_LOG_ONCE("client_id_to_google_play_id_map_ failed for id " + + std::to_string(client_id)); + return -1; + } +} + +// Called for Google Play connections. +void Game::PushCompressedGamePacketFromHostGooglePlayCall( + const std::vector& data) { + PushCall([this, data] { + if (!connection_to_host_.exists()) { + Log("Error: Got host game-packet message but have no host."); + return; + } + connection_to_host_->HandleGamePacketCompressed(data); + }); +} + +// Called for Google Play connections. +void Game::PushCompressedGamePacketFromClientGooglePlayCall( + int google_client_id, const std::vector& data) { + PushCall([this, google_client_id, data] { + int client_id = ClientIDFromGooglePlayClientID(google_client_id); + auto i = connections_to_clients_.find(client_id); + if (i == connections_to_clients_.end()) { + Log("Error: Got data-from-client msg for nonexistent client."); + return; + } + i->second->HandleGamePacketCompressed(data); + }); +} + +void Game::PushClientConnectedGooglePlayCall(int id) { + PushCall([this, id] { + // Find a free ballistica client_id. + int client_id = 0; + bool found = false; + for (int i = 0; i < 256; i++) { + int test_id = (next_connection_to_client_id_ + i) % 255; + if (connections_to_clients_.find(test_id) + == connections_to_clients_.end()) { + client_id = test_id; + found = true; + break; + } + } + next_connection_to_client_id_++; + + if (found) { + google_play_id_to_client_id_map_[id] = client_id; + client_id_to_google_play_id_map_[client_id] = id; + if (connections_to_clients_.find(client_id) + != connections_to_clients_.end()) { + Log("Error: Got client-connected message" + " for already existing client-id."); + } + connections_to_clients_[client_id] = + g_platform->AndroidGPGSNewConnectionToClient(client_id); + } else { + Log("no client_id available in ClientConnectedGooglePlayCall"); + } + }); +} + +void Game::PushHostConnectedGooglePlayCall() { + PushCall([this] { + // Attempt to disconnect any existing clients we have, turn off public-party + // advertising, etc. + CleanUpBeforeConnectingToHost(); + connection_to_host_ = g_platform->AndroidGPGSNewConnectionToHost(); + has_connection_to_host_ = true; + printed_host_disconnect_ = false; + }); +} + +int Game::GetGooglePlayClientCount() const { + assert(InGameThread()); + int count = 0; + for (auto&& i : connections_to_clients_) { + if (i.second.exists() && i.second->can_communicate() + && g_platform->AndroidIsGPGSConnectionToClient(i.second.get())) { + count++; + } + } + return count; +} +#endif // BA_GOOGLE_BUILD + +auto Game::GetConnectedClientCount() const -> int { + assert(InGameThread()); + int count = 0; + for (auto&& i : connections_to_clients_) { + if (i.second.exists() && i.second->can_communicate()) { + count++; + } + } + return count; +} + +auto Game::GetPartySize() const -> int { + assert(InGameThread()); + assert(game_roster_ != nullptr); + return cJSON_GetArraySize(game_roster_); +} + +void Game::LocalDisplayChatMessage(const std::vector& buffer) { + // 1 type byte, 1 spec-len byte, 1 or more spec chars, 0 or more msg chars. + if (buffer.size() > 3) { + size_t spec_len = buffer[1]; + if (spec_len > 0 && spec_len + 2 <= buffer.size()) { + size_t msg_len = buffer.size() - spec_len - 2; + std::vector b1(spec_len + 1); + memcpy(&(b1[0]), &(buffer[2]), spec_len); + b1[spec_len] = 0; + std::vector b2(msg_len + 1); + if (msg_len > 0) { + memcpy(&(b2[0]), &(buffer[2 + spec_len]), msg_len); + } + b2[msg_len] = 0; + + std::string final_message = + PlayerSpec(b1.data()).GetDisplayString() + ": " + b2.data(); + + // Store it locally. + chat_messages_.push_back(final_message); + while (chat_messages_.size() > kMaxChatMessages) { + chat_messages_.pop_front(); + } + + // Show it on the screen if they don't have their chat window open + // (and don't have chat muted). + if (!g_ui->root_ui()->party_window_open()) { + if (!chat_muted_) { + ScreenMessage(final_message, {0.7f, 1.0f, 0.7f}); + } + } else { + // Party window is open - notify it that there's a new message. + g_python->HandleLocalChatMessage(final_message); + } + if (!chat_muted_) { + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kTap)); + } + } + } +} + +void Game::SendChatMessage(const std::string& message, + const std::vector* clients, + const std::string* sender_override) { + // Sending to particular clients is only applicable while hosting. + if (clients != nullptr && connection_to_host() != nullptr) { + throw Exception("Can't send chat message to specific clients as a client."); + } + + // Same with overriding sender name + if (sender_override != nullptr && connection_to_host() != nullptr) { + throw Exception( + "Can't send chat message with sender_override as a client."); + } + + std::string our_spec_string; + + if (sender_override != nullptr) { + std::string override_final = *sender_override; + if (override_final.size() > kMaxPartyNameCombinedSize) { + override_final.resize(kMaxPartyNameCombinedSize); + override_final += "..."; + } + our_spec_string = + PlayerSpec::GetDummyPlayerSpec(override_final).GetSpecString(); + } else { + if (connection_to_host() != nullptr) { + // NOTE - we send our own spec string with the chat message whether we're + // a client or server.. however on protocol version 30+ this is ignored + // by the server and replaced with a spec string it generates for us. + // so once we know we're connected to a 30+ server we can start sending + // blank strings as a client. + // (not that it really matters; chat messages are tiny overall) + our_spec_string = PlayerSpec::GetAccountPlayerSpec().GetSpecString(); + } else { + // As a host we want to do the equivalent of + // ConnectionToClient::GetCombinedSpec() except for local connections (so + // send our name as the combination of local players if possible). Look + // for players coming from this client-connection; if we find any, make a + // spec out of their name(s). + std::string p_name_combined; + if (auto* hs = dynamic_cast(GetForegroundSession())) { + for (auto&& p : hs->players()) { + InputDevice* input_device = p->GetInputDevice(); + if (p->accepted() && p->name_is_real() && input_device != nullptr + && !input_device->IsRemoteClient()) { + if (!p_name_combined.empty()) { + p_name_combined += "/"; + } + p_name_combined += p->GetName(); + } + } + } + if (p_name_combined.size() > kMaxPartyNameCombinedSize) { + p_name_combined.resize(kMaxPartyNameCombinedSize); + p_name_combined += "..."; + } + if (!p_name_combined.empty()) { + our_spec_string = + PlayerSpec::GetDummyPlayerSpec(p_name_combined).GetSpecString(); + } else { + our_spec_string = PlayerSpec::GetAccountPlayerSpec().GetSpecString(); + } + } + } + + // If we find a newline, only take the first line (prevent people from + // covering the screen with obnoxious chat messages). + std::string message2 = message; + size_t nlpos = message2.find('\n'); + if (nlpos != std::string::npos) { + message2 = message2.substr(0, nlpos); + } + + // If we're the host, run filters before we send the message out. + // If the filter kills the message, don't send. + bool allow_message = g_python->FilterChatMessage(&message2, -1); + if (!allow_message) { + return; + } + + // 1 byte type + 1 byte spec-string-length + message. + std::vector msg_out(1 + 1 + our_spec_string.size() + + message2.size()); + msg_out[0] = BA_MESSAGE_CHAT; + size_t spec_size = our_spec_string.size(); + assert(spec_size < 256); + msg_out[1] = static_cast(spec_size); + memcpy(&(msg_out[2]), our_spec_string.c_str(), spec_size); + memcpy(&(msg_out[2 + spec_size]), message2.c_str(), message2.size()); + + // If we're a client, send this to the host (it will make its way back to us + // when they send to clients). + if (ConnectionToHost* hc = connection_to_host()) { + hc->SendReliableMessage(msg_out); + } else { + // Ok we're the host. + + // Send to all (or at least some) connected clients. + for (auto&& i : connections_to_clients_) { + // Skip if its going to specific ones and this one doesn't match. + if (clients != nullptr) { + auto found = false; + for (auto&& c : *clients) { + if (c == i.second->id()) { + found = true; + } + } + if (!found) { + continue; + } + } + + if (i.second->can_communicate()) { + i.second->SendReliableMessage(msg_out); + } + } + + // And display locally if the message is addressed to all. + if (clients == nullptr) { + LocalDisplayChatMessage(msg_out); + } + } +} + +auto Game::GetGameRosterMessage() -> std::vector { + // This message is simply a flattened json string of our roster (including + // terminating char). + char* s = cJSON_PrintUnformatted(game_roster_); + // printf("ROSTER MESSAGE %s\n", s); + auto s_len = strlen(s); + std::vector msg(1 + s_len + 1); + msg[0] = BA_MESSAGE_PARTY_ROSTER; + memcpy(&(msg[1]), s, s_len + 1); + free(s); + + return msg; +} + +auto Game::IsPlayerBanned(const PlayerSpec& spec) -> bool { + millisecs_t current_time = GetRealTime(); + + // Now is a good time to prune no-longer-banned specs. + while (!banned_players_.empty() + && banned_players_.front().first < current_time) { + banned_players_.pop_front(); + } + for (auto&& test_spec : banned_players_) { + if (test_spec.second == spec) { + return true; + } + } + return false; +} + +void Game::StartKickVote(ConnectionToClient* starter, + ConnectionToClient* target) { + // Restrict votes per client. + millisecs_t current_time = GetRealTime(); + + if (starter == target) { + // Don't let anyone kick themselves. + starter->SendScreenMessage(R"({"r":"kickVoteCantKickSelfText",)" + R"("f":"kickVoteFailedText"})", + 1, 0, 0); + } else if (target->IsAdmin()) { + // Admins are immune to kicking + starter->SendScreenMessage(R"({"r":"kickVoteCantKickAdminText",)" + R"("f":"kickVoteFailedText"})", + 1, 0, 0); + } else if (starter->IsAdmin()) { + // Admin doing the kicking succeeds instantly. + SendScreenMessageToClients( + R"({"r":"kickOccurredText","s":[["${NAME}",)" + + Utils::GetJSONString( + target->GetCombinedSpec().GetDisplayString().c_str()) + + "]]}", + 1, 1, 0); + DisconnectClient(target->id(), kKickBanSeconds); + starter->SendScreenMessage(R"({"r":"kickVoteCantKickAdminText",)" + R"("f":"kickVoteFailedText"})", + 1, 0, 0); + } else if (!kick_voting_enabled_) { + // No kicking otherwise if its disabled. + starter->SendScreenMessage(R"({"r":"kickVotingDisabledText",)" + R"("f":"kickVoteFailedText"})", + 1, 0, 0); + } else if (kick_vote_in_progress_) { + // Vote in progress error. + starter->SendScreenMessage(R"({"r":"voteInProgressText"})", 1, 0, 0); + } else if (GetConnectedClientCount() < kKickVoteMinimumClients) { + // There's too few clients to effectively vote. + starter->SendScreenMessage(R"({"r":"kickVoteFailedNotEnoughVotersText",)" + R"("f":"kickVoteFailedText"})", + 1, 0, 0); + } else if (current_time < starter->next_kick_vote_allow_time_) { + // Not yet allowed error. + starter->SendScreenMessage( + R"({"r":"voteDelayText","s":[["${NUMBER}",")" + + std::to_string(std::max( + millisecs_t{1}, + (starter->next_kick_vote_allow_time_ - current_time) / 1000)) + + "\"]]}", + 1, 0, 0); + } else { + std::vector connected_clients = + GetConnectionsToClients(); + + // Ok, kick off a vote.. (send the question and instructions to everyone + // except the starter and the target). + for (auto&& client : connected_clients) { + if (client != starter && client != target) { + client->SendScreenMessage( + R"({"r":"kickQuestionText","s":[["${NAME}",)" + + Utils::GetJSONString( + target->GetCombinedSpec().GetDisplayString().c_str()) + + "]]}", + 1, 1, 0); + client->SendScreenMessage(R"({"r":"kickWithChatText","s":)" + R"([["${YES}","'1'"],["${NO}","'0'"]]})", + 1, 1, 0); + } else { + // For the kicker/kickee, simply print that a kick vote has been + // started. + client->SendScreenMessage( + R"({"r":"kickVoteStartedText","s":[["${NAME}",)" + + Utils::GetJSONString( + target->GetCombinedSpec().GetDisplayString().c_str()) + + "]]}", + 1, 1, 0); + } + } + kick_vote_end_time_ = current_time + kKickVoteDuration; + kick_vote_in_progress_ = true; + last_kick_votes_needed_ = -1; // make sure we print starting num + + // Keep track of who started the vote. + kick_vote_starter_ = starter; + kick_vote_target_ = target; + + // Reset votes for all connected clients. + for (ConnectionToClient* client : GetConnectionsToClients()) { + if (client == starter) { + client->kick_voted_ = true; + client->kick_vote_choice_ = true; + } else { + client->kick_voted_ = false; + } + } + } +} + +void Game::BanPlayer(const PlayerSpec& spec, millisecs_t duration) { + banned_players_.emplace_back(GetRealTime() + duration, spec); +} + +void Game::UpdateGameRoster() { + assert(InGameThread()); + + assert(game_roster_ != nullptr); + if (game_roster_ != nullptr) { + cJSON_Delete(game_roster_); + } + + // Our party-roster is just a json array of dicts containing player-specs. + game_roster_ = cJSON_CreateArray(); + + int total_party_size = 1; // include ourself here.. + + // Add ourself first (that's currently how they know we're the party leader) + // ..but only if we have a connected client (otherwise our party is + // considered 'empty'). + + // UPDATE: starting with our big ui revision we'll always include ourself + // here + bool include_self = (GetConnectedClientCount() > 0); +#if BA_TOOLBAR_TEST + include_self = true; +#endif // BA_TOOLBAR_TEST + + if (auto* hs = dynamic_cast(GetForegroundSession())) { + // Add our host-y self. + if (include_self) { + cJSON* client_dict = cJSON_CreateObject(); + cJSON_AddItemToObject( + client_dict, "spec", + cJSON_CreateString( + PlayerSpec::GetAccountPlayerSpec().GetSpecString().c_str())); + + // Add our list of local players. + cJSON* player_array = cJSON_CreateArray(); + for (auto&& p : hs->players()) { + InputDevice* input_device = p->GetInputDevice(); + + // Add some basic info for each local player (only ones with real + // names though; don't wanna send , etc). + if (p->accepted() && p->name_is_real() && input_device != nullptr + && !input_device->IsRemoteClient()) { + cJSON* player_dict = cJSON_CreateObject(); + cJSON_AddItemToObject(player_dict, "n", + cJSON_CreateString(p->GetName().c_str())); + cJSON_AddItemToObject(player_dict, "nf", + cJSON_CreateString(p->GetName(true).c_str())); + cJSON_AddItemToObject(player_dict, "i", cJSON_CreateNumber(p->id())); + cJSON_AddItemToArray(player_array, player_dict); + } + } + cJSON_AddItemToObject(client_dict, "p", player_array); + cJSON_AddItemToObject( + client_dict, "i", + cJSON_CreateNumber(-1)); // -1 client_id means we're the host. + cJSON_AddItemToArray(game_roster_, client_dict); + } + + // Add all connected clients. + for (auto&& i : connections_to_clients_) { + if (i.second->can_communicate()) { + cJSON* client_dict = cJSON_CreateObject(); + cJSON_AddItemToObject( + client_dict, "spec", + cJSON_CreateString(i.second->peer_spec().GetSpecString().c_str())); + + // Add their public account id (or None if we don't have it) + // cJSON* player_accountid{}; + // if (i.second->peer_public_account_id() == "") { + // player_accountid = cJSON_CreateNull(); + // } else { + // player_accountid = + // cJSON_CreateString(i.second->peer_public_account_id().c_str()); + // } + // cJSON_AddItemToObject(client_dict, "a", player_accountid); + + // Also add their list of players. + cJSON* player_array = cJSON_CreateArray(); + + // Include all players that are remote and coming from this same + // client connection. + for (auto&& p : hs->players()) { + InputDevice* input_device = p->GetInputDevice(); + if (p->accepted() && p->name_is_real() && input_device != nullptr + && input_device->IsRemoteClient()) { + auto* cid = static_cast(input_device); + ConnectionToClient* ctc = cid->connection_to_client(); + + // Add some basic info for each remote player. + if (ctc != nullptr && ctc == i.second.get()) { + cJSON* player_dict = cJSON_CreateObject(); + cJSON_AddItemToObject(player_dict, "n", + cJSON_CreateString(p->GetName().c_str())); + cJSON_AddItemToObject( + player_dict, "nf", + cJSON_CreateString(p->GetName(true).c_str())); + cJSON_AddItemToObject(player_dict, "i", + cJSON_CreateNumber(p->id())); + cJSON_AddItemToArray(player_array, player_dict); + } + } + } + cJSON_AddItemToObject(client_dict, "p", player_array); + cJSON_AddItemToObject(client_dict, "i", + cJSON_CreateNumber(i.second->id())); + cJSON_AddItemToArray(game_roster_, client_dict); + total_party_size += 1; + } + } + } + + // Keep the python layer informed on our number of connections; it may want + // to pass the info along to the master server if we're hosting a public + // party. + SetPublicPartySize(total_party_size); + + // Mark the roster as dirty so we know we need to send it to everyone soon. + game_roster_dirty_ = true; +} + +void Game::SetAdCompletionCall(PyObject* obj, bool pass_actually_showed) { + if (obj == Py_None) { + ad_completion_callback_.Clear(); + } else { + ad_completion_callback_ = Object::New(obj); + } + ad_completion_callback_pass_actually_showed_ = pass_actually_showed; + last_ad_start_time_ = g_platform->GetTicks(); +} + +void Game::CallAdCompletionCall(bool actually_showed) { + if (ad_completion_callback_.exists()) { + if (ad_completion_callback_pass_actually_showed_) { + PythonRef args(Py_BuildValue("(O)", actually_showed ? Py_True : Py_False), + PythonRef::kSteal); + ad_completion_callback_->Run(args); + } else { + ad_completion_callback_->Run(); + } + ad_completion_callback_.Clear(); // These are single-fire callbacks. + } +} + +void Game::SetPublicPartyEnabled(bool val) { + assert(InGameThread()); + if (val == public_party_enabled_) { + return; + } + public_party_enabled_ = val; + PushPublicPartyState(); +} + +void Game::SetPublicPartySize(int count) { + assert(InGameThread()); + if (count == public_party_size_) { + return; + } + public_party_size_ = count; + + // Push our new state to the server *ONLY* if public-party is turned on + // (wasteful otherwise). + if (public_party_enabled_) { + PushPublicPartyState(); + } +} + +void Game::SetPublicPartyMaxSize(int count) { + assert(InGameThread()); + if (count == public_party_max_size_) { + return; + } + public_party_max_size_ = count; + + // Push our new state to the server *ONLY* if public-party is turned on + // (wasteful otherwise). + if (public_party_enabled_) { + PushPublicPartyState(); + } +} + +void Game::SetPublicPartyName(const std::string& name) { + assert(InGameThread()); + if (name == public_party_name_) { + return; + } + public_party_name_ = name; + + // Push our new state to the server *ONLY* if public-party is turned on + // (wasteful otherwise). + if (public_party_enabled_) { + PushPublicPartyState(); + } +} + +void Game::SetPublicPartyStatsURL(const std::string& url) { + assert(InGameThread()); + if (url == public_party_stats_url_) { + return; + } + public_party_stats_url_ = url; + + // Push our new state to the server *ONLY* if public-party is turned on + // (wasteful otherwise). + if (public_party_enabled_) { + PushPublicPartyState(); + } +} + +void Game::SetPublicPartyPlayerCount(int count) { + assert(InGameThread()); + if (count == public_party_player_count_) { + return; + } + public_party_player_count_ = count; + + // Push our new state to the server *ONLY* if public-party is turned on + // (wasteful otherwise). + if (public_party_enabled_) { + PushPublicPartyState(); + } +} + +void Game::PushPublicPartyState() { + assert(InGameThread()); + PythonRef call = g_python->obj(Python::ObjID::kAccountClient) + .GetAttr("set_public_party_state"); + if (call.exists()) { + PythonRef args( + Py_BuildValue( + "(iiiiisss)", static_cast(public_party_enabled_), + public_party_size_, public_party_max_size_, + public_party_player_count_, public_party_max_player_count_, + public_party_name_.c_str(), public_party_min_league_.c_str(), + public_party_stats_url_.c_str()), + PythonRef::kSteal); + call.Call(args); + } else { + Log("Error on pushPublicPartyState call"); + } +} + +} // namespace ballistica diff --git a/src/ballistica/game/game.h b/src/ballistica/game/game.h new file mode 100644 index 00000000..c913b37b --- /dev/null +++ b/src/ballistica/game/game.h @@ -0,0 +1,464 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_GAME_H_ +#define BALLISTICA_GAME_GAME_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ballistica/core/module.h" + +namespace ballistica { + +const int kMaxPartyNameCombinedSize = 25; + +/// The Game Module generally runs on a dedicated thread; it manages +/// all game logic, builds frame_defs to send to the graphics-server for +/// rendering, etc. +class Game : public Module { + public: + explicit Game(Thread* thread); + ~Game() override; + auto LaunchHostSession(PyObject* session_type_obj, + BenchmarkType benchmark_type = BenchmarkType::kNone) + -> void; + auto LaunchClientSession() -> void; + auto LaunchReplaySession(const std::string& file_name) -> void; + + auto PushSetAccountCall(AccountType account_type, AccountState account_state, + const std::string& account_name, + const std::string& account_id) -> void; + auto PushSetAccountTokenCall(const std::string& account_id, + const std::string& token) -> void; + auto PushAdViewCompleteCall(const std::string& purpose, bool actually_showed) + -> void; + auto PushAnalyticsCall(const std::string& type, int increment) -> void; + auto PushAwardAdTicketsCall() -> void; + auto PushAwardAdTournamentEntryCall() -> void; + auto PushPurchaseTransactionCall(const std::string& item, + const std::string& receipt, + const std::string& signature, + const std::string& order_id, + bool user_initiated) -> void; + auto PushUDPConnectionPacketCall(const std::vector& data, + const SockAddr& addr) -> void; + auto PushPartyInviteCall(const std::string& name, + const std::string& invite_id) -> void; + auto PushPartyInviteRevokeCall(const std::string& invite_id) -> void; + auto PushInitialScreenCreatedCall() -> void; + auto PushApplyConfigCall() -> void; + auto PushRemoveGraphicsServerRenderHoldCall() -> void; + auto PushInterruptSignalCall() -> void; + + /// Push a generic 'menu press' event, optionally associated with an + /// input device (nullptr to specify none). Note: caller must ensure + /// a RemoveInputDevice() call does not arrive at the game thread + /// before this one. + auto PushMainMenuPressCall(InputDevice* device) -> void; + + /// Notify the game of a screen-size change (used by the graphics server). + auto PushScreenResizeCall(float virtual_width, float virtual_height, + float physical_width, float physical_height) + -> void; + + auto PushGameServiceAchievementListCall( + const std::set& achievements) -> void; + auto PushScoresToBeatResponseCall(bool success, + const std::list& scores, + void* py_callback) -> void; + auto PushToggleCollisionGeometryDisplayCall() -> void; + auto PushToggleDebugInfoDisplayCall() -> void; + auto PushToggleManualCameraCall() -> void; + auto PushHavePendingLoadsDoneCall() -> void; + auto PushFreeMediaComponentRefsCall( + const std::vector*>& components) -> void; + auto PushSetFriendListCall(const std::vector& friends) -> void; + auto PushHavePendingLoadsCall() -> void; + auto PushShutdownCall(bool soft) -> void; + + auto PushInGameConsoleScriptCommand(const std::string& command) -> void; + auto ToggleConsole() -> void; + auto PushConsolePrintCall(const std::string& msg) -> void; + auto PushStdinScriptCommand(const std::string& command) -> void; + auto PushMediaPruneCall(int level) -> void; + auto PushAskUserForTelnetAccessCall() -> void; + + // Push Python call and keep it alive; must be called from game thread. + auto PushPythonCall(const Object::Ref& call) -> void; + auto PushPythonCallArgs(const Object::Ref& call, + const PythonRef& args) -> void; + + // Push Python call without keeping it alive; must be called from game thread. + auto PushPythonWeakCall(const Object::WeakRef& call) + -> void; + auto PushPythonWeakCallArgs(const Object::WeakRef& call, + const PythonRef& args) -> void; + + // Push a raw Python call, decrements its refcount after running. + // Can be pushed from any thread. + auto PushPythonRawCallable(PyObject* callable) -> void; + auto PushScreenMessage(const std::string& message, const Vector3f& color) + -> void; + auto RemovePlayer(Player* player) -> void; + auto PushPlaySoundCall(SystemSoundID sound) -> void; + auto PushConfirmQuitCall() -> void; + auto PushStringEditSetCall(const std::string& value) -> void; + auto PushStringEditCancelCall() -> void; + auto PushFriendScoreSetCall(const FriendScoreSet& score_set) -> void; + auto PushShowURLCall(const std::string& url) -> void; + auto PushBackButtonCall(InputDevice* input_device) -> void; + auto PushOnAppResumeCall() -> void; + auto PushFrameDefRequest() -> void; + auto PushDisconnectFromHostCall() -> void; + auto PushClientDisconnectedCall(int id) -> void; + auto PushHostConnectedUDPCall(const SockAddr& addr, + bool print_connect_progress) -> void; + auto PushDisconnectedFromHostCall() -> void; + auto ChangeGameSpeed(int offs) -> void; + auto ResetInput() -> void; + auto RunMainMenu() -> void; + auto HandleThreadPause() -> void override; + +#if BA_GOOGLE_BUILD + auto PushClientDisconnectedGooglePlayCall(int id) -> void; + int GetGooglePlayClientCount() const; + auto PushHostConnectedGooglePlayCall() -> void; + auto PushClientConnectedGooglePlayCall(int id) -> void; + auto PushCompressedGamePacketFromHostGooglePlayCall( + const std::vector& data) -> void; + auto PushCompressedGamePacketFromClientGooglePlayCall( + int google_client_id, const std::vector& data) -> void; +#endif + +#if BA_VR_BUILD + auto PushVRHandsState(const VRHandsState& state) -> void; + const VRHandsState& vr_hands_state() const { return vr_hands_state_; } +#endif + + // Resets tracking used to detect cheating and tampering in local tournaments. + auto ResetActivityTracking() -> void; + + // Return whichever context is front and center. + auto GetForegroundContext() -> Context; + + // Return whichever session is front and center. + auto GetForegroundSession() const -> Session* { + return foreground_session_.get(); + } + + auto NewRealTimer(millisecs_t length, bool repeat, + const Object::Ref& runnable) -> int; + auto DeleteRealTimer(int timer_id) -> void; + auto SetRealTimerLength(int timer_id, millisecs_t length) -> void; + auto SetLanguageKeys(const std::map& language) + -> void; + auto GetResourceString(const std::string& key) -> std::string; + auto CharStr(SpecialChar id) -> std::string; + auto CompileResourceString(const std::string& s, const std::string& loc, + bool* valid = nullptr) -> std::string; + auto kick_idle_players() const -> bool { return kick_idle_players_; } + auto IsInUIContext() const -> bool; + + // Return the actual UI context (hmm couldn't we just use g_ui?). + auto GetUIContextTarget() const -> UI* { + assert(g_ui); + return g_ui; + } + + // Simply return a context-state pointing to the ui-context (so you don't have + // to include the ui header). + auto GetUIContext() const -> Context; + + // Returns the base time used to drive local sims/etc. This generally tries + // to match real-time but has a bit of leeway to sync up with frame drawing or + // slow down if things are behind (it tries to progress by exactly 1000/60 ms + // each frame, provided we're rendering 60hz). + auto master_time() const -> millisecs_t { return master_time_; } + + auto debug_speed_mult() const -> float { return debug_speed_mult_; } + auto SetDebugSpeedExponent(int val) -> void; + + auto SetReplaySpeedExponent(int val) -> void; + auto replay_speed_exponent() const -> int { return replay_speed_exponent_; } + auto replay_speed_mult() const -> float { return replay_speed_mult_; } + + // Returns our host-connection or nullptr if there is none. + auto connection_to_host() -> ConnectionToHost* { + return connection_to_host_.get(); + } + auto GetConnectionToHostUDP() -> ConnectionToHostUDP*; + + // Send a screen message to all connected clients AND print it on the host. + auto SendScreenMessageToAll(const std::string& s, float r, float g, float b) + -> void; + + // send a screen message to all connected clients + auto SendScreenMessageToClients(const std::string& s, float r, float g, + float b) -> void; + + // Send a screen message to specific connected clients (those matching the IDs + // specified) the id -1 can be used to specify the host. + auto SendScreenMessageToSpecificClients(const std::string& s, float r, + float g, float b, + const std::vector& clients) + -> void; + + // Return our client connections (if any). + // FIXME: this prunes invalid connections, but it is necessary? + // Can we just use connections_to_clients() for direct access? + auto GetConnectionsToClients() -> std::vector; + + // Return the number of connections-to-client with "connected" status true. + auto GetConnectedClientCount() const -> int; + + auto GetPartySize() const -> int; + auto last_connection_to_client_join_time() const -> millisecs_t { + return last_connection_to_client_join_time_; + } + auto set_last_connection_to_client_join_time(millisecs_t val) -> void { + last_connection_to_client_join_time_ = val; + } + + // Simple thread safe query. + auto has_connection_to_host() const -> bool { + return has_connection_to_host_; + } + + auto game_roster() const -> cJSON* { return game_roster_; } + + auto SendChatMessage(const std::string& message, + const std::vector* clients = nullptr, + const std::string* sender_override = nullptr) -> void; + + // Quick test as to whether there are clients. Does not check if they are + // fully connected. + auto has_connection_to_clients() const -> bool { + assert(InGameThread()); + return (!connections_to_clients_.empty()); + } + + auto chat_messages() const -> const std::list& { + return chat_messages_; + } + + // Whoever wants to wrangle current client connections should call this + // to register itself. Note that it must explicitly call unregister when + // unregistering itself. + auto RegisterClientController(ClientControllerInterface* c) -> void; + auto UnregisterClientController(ClientControllerInterface* c) -> void; + + // Used to know which globals is in control currently/etc. + auto GetForegroundScene() const -> Scene* { + assert(InGameThread()); + return foreground_scene_.get(); + } + auto SetForegroundScene(Scene* sg) -> void; + + // Returns true if disconnect attempts are supported. + auto DisconnectClient(int client_id, int ban_seconds) -> bool; + auto UpdateGameRoster() -> void; + auto IsPlayerBanned(const PlayerSpec& spec) -> bool; + auto BanPlayer(const PlayerSpec& spec, millisecs_t duration) -> void; + + // For applying player-profiles data from the master-server. + auto SetClientInfoFromMasterServer(const std::string& client_token, + PyObject* info) -> void; + auto GetPrintUDPConnectProgress() const -> bool { + return print_udp_connect_progress_; + } + + // For cheat detection. Returns the largest amount of time that has passed + // between frames since our last reset (for detecting memory modification + // UIs/etc). + auto largest_draw_time_increment() const -> millisecs_t { + return largest_draw_time_increment_since_last_reset_; + } + + // Anti-hacker stuff. + auto GetTotalTimeSinceReset() const -> millisecs_t { + return last_draw_real_time_ - first_draw_real_time_; + } + auto SetForegroundSession(Session* s) -> void; + auto SetGameRoster(cJSON* r) -> void; + auto LocalDisplayChatMessage(const std::vector& buffer) -> void; + auto ShouldAnnouncePartyJoinsAndLeaves() -> bool; + + auto SetAdCompletionCall(PyObject* obj, bool pass_actually_showed) -> void; + auto CallAdCompletionCall(bool actually_showed) -> void; + auto RunGeneralAdComplete(bool actually_watched) -> void; + + auto StartKickVote(ConnectionToClient* starter, ConnectionToClient* target) + -> void; + auto require_client_authentication() const { + return require_client_authentication_; + } + auto set_require_client_authentication(bool enable) -> void { + require_client_authentication_ = enable; + } + auto set_kick_voting_enabled(bool enable) -> void { + kick_voting_enabled_ = enable; + } + auto set_admin_public_ids(const std::set& ids) -> void { + admin_public_ids_ = ids; + } + const std::set& admin_public_ids() const { + return admin_public_ids_; + } + + auto connections_to_clients() + -> const std::map >& { + return connections_to_clients_; + } + auto client_controller() -> ClientControllerInterface* { + return client_controller_; + } + auto kick_vote_in_progress() const -> bool { return kick_vote_in_progress_; } + +#if BA_GOOGLE_BUILD + auto ClientIDFromGooglePlayClientID(int google_id) -> int; + auto GooglePlayClientIDFromClientID(int client_id) -> int; +#endif + + auto SetPublicPartyEnabled(bool val) -> void; + auto public_party_enabled() const { return public_party_enabled_; } + auto public_party_size() const { return public_party_size_; } + auto SetPublicPartySize(int count) -> void; + auto public_party_max_size() const { return public_party_max_size_; } + auto SetPublicPartyMaxSize(int count) -> void; + auto SetPublicPartyName(const std::string& name) -> void; + auto SetPublicPartyStatsURL(const std::string& name) -> void; + auto public_party_name() const { return public_party_name_; } + auto public_party_player_count() const { return public_party_player_count_; } + auto SetPublicPartyPlayerCount(int count) -> void; + auto ran_app_launch_commands() const { return ran_app_launch_commands_; } + + private: + auto InitSpecialChars() -> void; + auto AdViewComplete(const std::string& purpose, bool actually_showed) -> void; + auto Analytics(const std::string& type, int increment) -> void; + auto AwardAdTickets() -> void; + auto AwardAdTournamentEntry() -> void; + auto Draw() -> void; + auto PurchaseTransaction(const std::string& item, const std::string& receipt, + const std::string& signature, + const std::string& order_id, bool user_initiated) + -> void; + auto UDPConnectionPacket(const std::vector& data, + const SockAddr& addr) -> void; + auto PartyInvite(const std::string& name, const std::string& invite_id) + -> void; + auto PartyInviteRevoke(const std::string& invite_id) -> void; + auto InitialScreenCreated() -> void; + auto MainMenuPress(InputDevice* device) -> void; + auto ScreenResize(float virtual_width, float virtual_height, + float pixel_width, float pixel_height) -> void; + auto GameServiceAchievementList(const std::set& achievements) + -> void; + auto ScoresToBeatResponse(bool success, const std::list& scores, + void* py_callback) -> void; + +#if BA_VR_BUILD + VRHandsState vr_hands_state_; +#endif +#if BA_RIFT_BUILD + int rift_step_index_{}; +#endif + + auto HandleClientDisconnected(int id) -> void; + auto ForceDisconnectClients() -> void; + auto Prune() -> void; // Periodic pruning of dead stuff. + auto Update() -> void; + auto Process() -> void; + auto UpdateKickVote() -> void; + auto RunAppLaunchCommands() -> void; + auto PruneSessions() -> void; + auto ApplyConfig() -> void; + auto UpdateProcessTimer() -> void; + auto Reset() -> void; + auto GetGameRosterMessage() -> std::vector; + auto CleanUpBeforeConnectingToHost() -> void; + auto Shutdown(bool soft) -> void; + auto PushPublicPartyState() -> void; + + std::map google_play_id_to_client_id_map_; + std::map client_id_to_google_play_id_map_; + bool print_udp_connect_progress_{true}; + std::list > banned_players_; + ClientControllerInterface* client_controller_{}; + std::list chat_messages_; + + // Simple flag for thread-safe access. + bool has_connection_to_host_{}; + + // Prevents us from printing multiple 'you got disconnected' messages. + bool printed_host_disconnect_{}; + bool chat_muted_{}; + bool first_update_{true}; + bool game_roster_dirty_{}; + millisecs_t last_connection_to_client_join_time_{}; + int debug_speed_exponent_{}; + float debug_speed_mult_{1.0f}; + int replay_speed_exponent_{}; + float replay_speed_mult_{1.0f}; + bool have_sent_initial_frame_def_{}; + millisecs_t master_time_{}; + millisecs_t master_time_offset_{}; + millisecs_t last_session_update_master_time_{}; + millisecs_t last_game_roster_send_time_{}; + millisecs_t largest_draw_time_increment_since_last_reset_{}; + millisecs_t last_draw_real_time_{}; + millisecs_t first_draw_real_time_{}; + + // *All* existing sessions (including old ones waiting to shut down). + std::vector > sessions_; + Object::WeakRef foreground_scene_; + Object::WeakRef foreground_session_; + std::mutex language_mutex_; + std::map language_; + std::mutex special_char_mutex_; + std::map special_char_strings_; + bool ran_app_launch_commands_{}; + bool kick_idle_players_{}; + std::unique_ptr realtimers_; + Timer* process_timer_{}; + Timer* headless_update_timer_{}; + Timer* media_prune_timer_{}; + Timer* debug_timer_{}; + bool have_pending_loads_{}; + bool in_update_{}; + bool require_client_authentication_{}; + bool kick_voting_enabled_{true}; + std::set admin_public_ids_; + + // Try to minimize the chance a garbage packet will have this id. + int next_connection_to_client_id_{113}; + std::map > connections_to_clients_; + Object::Ref connection_to_host_; + cJSON* game_roster_{}; + millisecs_t kick_vote_end_time_{}; + bool kick_vote_in_progress_{}; + int last_kick_votes_needed_{-1}; + Object::WeakRef kick_vote_starter_; + Object::WeakRef kick_vote_target_; + Object::Ref ad_completion_callback_; + millisecs_t last_ad_start_time_{}; + bool ad_completion_callback_pass_actually_showed_{}; + bool public_party_enabled_{false}; + int public_party_size_{1}; // Always count ourself (is that what we want?). + int public_party_max_size_{8}; + int public_party_player_count_{0}; + int public_party_max_player_count_{8}; + std::string public_party_name_; + std::string public_party_min_league_; + std::string public_party_stats_url_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_GAME_H_ diff --git a/src/ballistica/game/game_stream.cc b/src/ballistica/game/game_stream.cc new file mode 100644 index 00000000..8dd47091 --- /dev/null +++ b/src/ballistica/game/game_stream.cc @@ -0,0 +1,1242 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/game/game_stream.h" + +#include + +#include "ballistica/app/app_globals.h" +#include "ballistica/dynamics/bg/bg_dynamics.h" +#include "ballistica/dynamics/material/material.h" +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/dynamics/material/material_component.h" +#include "ballistica/dynamics/material/material_condition_node.h" +#include "ballistica/dynamics/part.h" +#include "ballistica/game/connection/connection_to_client.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/media/component/collide_model.h" +#include "ballistica/media/component/data.h" +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/media/media_server.h" +#include "ballistica/networking/networking.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +GameStream::GameStream(HostSession* host_session, bool saveReplay) + : time_(0), + host_session_(host_session), + next_flush_time_(0), + last_physics_correction_time_(0), + last_send_time_(0), + writing_replay_(false) { + if (saveReplay) { + // Sanity check - we should only ever be writing one replay at once. + if (g_app_globals->replay_open) { + Log("ERROR: g_replay_open true at replay start; shouldn't happen."); + } + assert(g_media_server); + g_media_server->PushBeginWriteReplayCall(); + writing_replay_ = true; + g_app_globals->replay_open = true; + } + + // If we're the live output-stream from a host-session, + // take responsibility for feeding all clients to this device. + if (host_session_) { + g_game->RegisterClientController(this); + } +} + +GameStream::~GameStream() { + // Ship our last commands (if it matters..) + Flush(); + + if (writing_replay_) { + // Sanity check: We should only ever be writing one replay at once. + if (!g_app_globals->replay_open) { + Log("ERROR: g_replay_open false at replay close; shouldn't happen."); + } + g_app_globals->replay_open = false; + assert(g_media_server); + g_media_server->PushEndWriteReplayCall(); + writing_replay_ = false; + } + + // If we're wired to the host-session, go ahead and release clients. + if (host_session_) { + g_game->UnregisterClientController(this); + + // Also, in the host-session case, make sure everything cleaned itself up. +#if BA_DEBUG_BUILD + size_t count; + count = GetPointerCount(scenes_); + if (count != 0) { + Log("ERROR: " + std::to_string(count) + + " scene graphs in output stream at shutdown"); + } + count = GetPointerCount(nodes_); + if (count != 0) { + Log("ERROR: " + std::to_string(count) + + " nodes in output stream at shutdown"); + } + count = GetPointerCount(materials_); + if (count != 0) { + Log("ERROR: " + std::to_string(count) + + " materials in output stream at shutdown"); + } + count = GetPointerCount(textures_); + if (count != 0) { + Log("ERROR: " + std::to_string(count) + + " textures in output stream at shutdown"); + } + count = GetPointerCount(models_); + if (count != 0) { + Log("ERROR: " + std::to_string(count) + + " models in output stream at shutdown"); + } + count = GetPointerCount(sounds_); + if (count != 0) { + Log("ERROR: " + std::to_string(count) + + " sounds in output stream at shutdown"); + } + count = GetPointerCount(collide_models_); + if (count != 0) { + Log("ERROR: " + std::to_string(count) + + " collide_models in output stream at shutdown"); + } +#endif // BA_DEBUG_BUILD + } +} + +// Pull the current built-up message. +auto GameStream::GetOutMessage() const -> std::vector { + assert(!host_session_); // this should only be getting used for + // standalone temp ones.. + if (!out_command_.empty()) { + Log("Error: GameStream shutting down with non-empty outCommand"); + } + return out_message_; +} + +template +auto GameStream::GetPointerCount(const std::vector& vec) -> size_t { + size_t count = 0; + + auto size = vec.size(); + T* const* vals = vec.data(); + for (size_t i = 0; i < size; i++) { + if (vals[i] != nullptr) { + count++; + } + } + return count; +} + +// Given a vector of pointers, return an index to an available (nullptr) entry, +// expanding the vector if need be. +template +auto GameStream::GetFreeIndex(std::vector* vec, + std::vector* free_indices) -> size_t { + // If we have any free indices, use one of them. + if (!free_indices->empty()) { + size_t val = free_indices->back(); + free_indices->pop_back(); + return val; + } + + // No free indices; expand the vec and return the new index. + vec->push_back(nullptr); + return vec->size() - 1; +} + +// Add an entry. +template +void GameStream::Add(T* val, std::vector* vec, + std::vector* free_indices) { + // This should only get used when we're being driven by the host-session. + assert(host_session_); + assert(val); + assert(val->stream_id() == -1); + size_t index = GetFreeIndex(vec, free_indices); + (*vec)[index] = val; + val->set_stream_id(index); +} + +// Remove an entry. +template +void GameStream::Remove(T* val, std::vector* vec, + std::vector* free_indices) { + assert(val); + assert(val->stream_id() >= 0); + assert(static_cast(vec->size()) > val->stream_id()); + assert((*vec)[val->stream_id()] == val); + (*vec)[val->stream_id()] = nullptr; + + // Add this to our list of available slots to recycle. + free_indices->push_back(val->stream_id()); + val->clear_stream_id(); +} + +void GameStream::Fail() { + Log("Error writing replay file"); + if (writing_replay_) { + // Sanity check: We should only ever be writing one replay at once. + if (!g_app_globals->replay_open) { + Log("ERROR: g_replay_open false at replay close; shouldn't happen."); + } + assert(g_media_server); + g_media_server->PushEndWriteReplayCall(); + writing_replay_ = false; + g_app_globals->replay_open = false; + } +} + +void GameStream::Flush() { + if (!out_command_.empty()) + Log("Error: GameStream flushing down with non-empty outCommand"); + if (!out_message_.empty()) { + ShipSessionCommandsMessage(); + } +} + +// Writes just a command. +void GameStream::WriteCommand(SessionCommand cmd) { + assert(out_command_.empty()); + + // For now just use full size values. + size_t size = 0; + out_command_.resize(size + 1); + uint8_t* ptr = &out_command_[size]; + *ptr = static_cast(cmd); +} + +// Writes a command plus an int to the stream, using whatever size is optimal. +void GameStream::WriteCommandInt32(SessionCommand cmd, int32_t value) { + assert(out_command_.empty()); + + // For now just use full size values. + size_t size = 0; + out_command_.resize(size + 5); + uint8_t* ptr = &out_command_[size]; + *(ptr++) = static_cast(cmd); + int32_t vals[] = {value}; + memcpy(ptr, vals, 4); +} + +void GameStream::WriteCommandInt32_2(SessionCommand cmd, int32_t value1, + int32_t value2) { + assert(out_command_.empty()); + + // For now just use full size vals. + size_t size = 0; + out_command_.resize(size + 9); + uint8_t* ptr = &out_command_[size]; + *(ptr++) = static_cast(cmd); + int32_t vals[] = {value1, value2}; + memcpy(ptr, vals, 8); +} + +void GameStream::WriteCommandInt32_3(SessionCommand cmd, int32_t value1, + int32_t value2, int32_t value3) { + assert(out_command_.empty()); + + // For now just use full size vals. + size_t size = 0; + out_command_.resize(size + 13); + uint8_t* ptr = &out_command_[size]; + *(ptr++) = static_cast(cmd); + int32_t vals[] = {value1, value2, value3}; + memcpy(ptr, vals, 12); +} + +void GameStream::WriteCommandInt32_4(SessionCommand cmd, int32_t value1, + int32_t value2, int32_t value3, + int32_t value4) { + assert(out_command_.empty()); + + // For now just use full size vals. + size_t size = 0; + out_command_.resize(size + 17); + uint8_t* ptr = &out_command_[size]; + *(ptr++) = static_cast(cmd); + int32_t vals[] = {value1, value2, value3, value4}; + memcpy(ptr, vals, 16); +} + +// FIXME: We don't actually support sending out 64 bit values yet, but +// adding these placeholders for if/when we do. +// They will also catch values greater than 32 bits in debug mode. +// We'll need a protocol update to add support for 64 bit over the wire. +void GameStream::WriteCommandInt64(SessionCommand cmd, int64_t value) { + WriteCommandInt32(cmd, static_cast_check_fit(value)); +} + +void GameStream::WriteCommandInt64_2(SessionCommand cmd, int64_t value1, + int64_t value2) { + WriteCommandInt32_2(cmd, static_cast_check_fit(value1), + static_cast_check_fit(value2)); +} + +void GameStream::WriteCommandInt64_3(SessionCommand cmd, int64_t value1, + int64_t value2, int64_t value3) { + WriteCommandInt32_3(cmd, static_cast_check_fit(value1), + static_cast_check_fit(value2), + static_cast_check_fit(value3)); +} + +void GameStream::WriteCommandInt64_4(SessionCommand cmd, int64_t value1, + int64_t value2, int64_t value3, + int64_t value4) { + WriteCommandInt32_4(cmd, static_cast_check_fit(value1), + static_cast_check_fit(value2), + static_cast_check_fit(value3), + static_cast_check_fit(value4)); +} + +void GameStream::WriteString(const std::string& s) { + // Write length int. + auto string_size = s.size(); + auto size = out_command_.size(); + out_command_.resize(size + 4 + s.size()); + memcpy(&out_command_[size], &string_size, 4); + if (string_size > 0) { + memcpy(&out_command_[size + 4], s.c_str(), string_size); + } +} + +void GameStream::WriteFloat(float val) { + auto size = static_cast(out_command_.size()); + out_command_.resize(size + sizeof(val)); + memcpy(&out_command_[size], &val, 4); +} + +void GameStream::WriteFloats(size_t count, const float* vals) { + assert(count > 0); + auto size = out_command_.size(); + size_t vals_size = sizeof(float) * count; + out_command_.resize(size + vals_size); + memcpy(&(out_command_[size]), vals, vals_size); +} + +void GameStream::WriteInts32(size_t count, const int32_t* vals) { + assert(count > 0); + auto size = out_command_.size(); + size_t vals_size = sizeof(int32_t) * count; + out_command_.resize(size + vals_size); + memcpy(&(out_command_[size]), vals, vals_size); +} + +void GameStream::WriteInts64(size_t count, const int64_t* vals) { + // FIXME: we don't actually support writing 64 bit values to the wire + // at the moment; will need a protocol update for that. + // This is just implemented as a placeholder. + std::vector vals32(count); + for (size_t i = 0; i < count; i++) { + vals32[i] = static_cast_check_fit(vals[i]); + } + WriteInts32(count, vals32.data()); +} + +void GameStream::WriteChars(size_t count, const char* vals) { + assert(count > 0); + auto size = out_command_.size(); + auto vals_size = static_cast(count); + out_command_.resize(size + vals_size); + memcpy(&(out_command_[size]), vals, vals_size); +} + +void GameStream::ShipSessionCommandsMessage() { + BA_PRECONDITION(!out_message_.empty()); + + // Send this message to all client-connections we're attached to. + for (auto& connection : connections_to_clients_) { + (*connection).SendReliableMessage(out_message_); + } + if (writing_replay_) { + AddMessageToReplay(out_message_); + } + out_message_.clear(); + last_send_time_ = GetRealTime(); +} + +void GameStream::AddMessageToReplay(const std::vector& message) { + assert(writing_replay_); + assert(g_media_server); + + assert(!message.empty()); +#if BA_DEBUG_BUILD + switch (message[0]) { + case BA_MESSAGE_SESSION_RESET: + case BA_MESSAGE_SESSION_COMMANDS: + case BA_MESSAGE_SESSION_DYNAMICS_CORRECTION: + break; + default: + throw Exception("unexpected message going to replay: " + + std::to_string(static_cast(message[0]))); + } +#endif // BA_DEBUG_BUILD + g_media_server->PushAddMessageToReplayCall(message); +} + +void GameStream::SendPhysicsCorrection(bool blend) { + assert(host_session_); + + std::vector > messages; + host_session_->GetCorrectionMessages(blend, &messages); + + // FIXME - have to send reliably at the moment since these will most likely be + // bigger than our unreliable packet limit. :-( + for (auto& message : messages) { + for (auto& connections_to_client : connections_to_clients_) { + (*connections_to_client).SendReliableMessage(message); + } + if (writing_replay_) { + AddMessageToReplay(message); + } + } +} + +void GameStream::EndCommand(bool is_time_set) { + assert(!out_command_.empty()); + + int out_message_size; + if (out_message_.empty()) { + // Init the message if we're the first command on it. + out_message_.resize(1); + out_message_[0] = BA_MESSAGE_SESSION_COMMANDS; + out_message_size = 1; + } else { + out_message_size = static_cast(out_message_.size()); + } + + out_message_.resize(out_message_size + 2 + + out_command_.size()); // command length plus data + + auto val = static_cast(out_command_.size()); + memcpy(&(out_message_[out_message_size]), &val, 2); + memcpy(&(out_message_[out_message_size + 2]), &(out_command_[0]), + out_command_.size()); + + // When attached to a host-session, send this message to clients if it's been + // long enough. + + // Also send off occasional correction packets. + if (host_session_) { + // Now if its been long enough and this is a time-step command, send. + millisecs_t real_time = GetRealTime(); + millisecs_t diff = real_time - last_send_time_; + if (is_time_set && diff > g_app_globals->buffer_time) { + ShipSessionCommandsMessage(); + + // Also, as long as we're here, fire off a physics-correction packet every + // now and then. + + // IMPORTANT: We only do this right after shipping off our pending session + // commands; otherwise the client will get the correction that accounts + // for commands that they haven't been sent yet. + diff = real_time - last_physics_correction_time_; + if (diff > g_app_globals->dynamics_sync_time) { + last_physics_correction_time_ = real_time; + SendPhysicsCorrection(true); + } + } + } + out_command_.clear(); +} + +auto GameStream::IsValidScene(Scene* s) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (s != nullptr && s->stream_id() >= 0 + && s->stream_id() < static_cast(scenes_.size()) + && scenes_[s->stream_id()] == s); +} + +auto GameStream::IsValidNode(Node* n) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (n != nullptr && n->stream_id() >= 0 + && n->stream_id() < static_cast(nodes_.size()) + && nodes_[n->stream_id()] == n); +} + +auto GameStream::IsValidTexture(Texture* n) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (n != nullptr && n->stream_id() >= 0 + && n->stream_id() < static_cast(textures_.size()) + && textures_[n->stream_id()] == n); +} + +auto GameStream::IsValidModel(Model* n) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (n != nullptr && n->stream_id() >= 0 + && n->stream_id() < static_cast(models_.size()) + && models_[n->stream_id()] == n); +} + +auto GameStream::IsValidSound(Sound* n) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (n != nullptr && n->stream_id() >= 0 + && n->stream_id() < static_cast(sounds_.size()) + && sounds_[n->stream_id()] == n); +} + +auto GameStream::IsValidData(Data* n) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (n != nullptr && n->stream_id() >= 0 + && n->stream_id() < static_cast(datas_.size()) + && datas_[n->stream_id()] == n); +} + +auto GameStream::IsValidCollideModel(CollideModel* n) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (n != nullptr && n->stream_id() >= 0 + && n->stream_id() < static_cast(collide_models_.size()) + && collide_models_[n->stream_id()] == n); +} + +auto GameStream::IsValidMaterial(Material* n) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (n != nullptr && n->stream_id() >= 0 + && n->stream_id() < static_cast(materials_.size()) + && materials_[n->stream_id()] == n); +} + +void GameStream::SetTime(millisecs_t t) { + if (time_ == t) { + return; // Ignore redundants. + } + millisecs_t diff = t - time_; + if (diff > 255) { + Log("Error: GameStream got time diff > 255; not expected."); + diff = 255; + } + WriteCommandInt64(SessionCommand::kBaseTimeStep, diff); + time_ = t; + EndCommand(true); +} + +void GameStream::AddScene(Scene* s) { + // Host mode. + if (host_session_) { + Add(s, &scenes_, &free_indices_scene_graphs_); + s->SetOutputStream(this); + } else { + // Dump mode. + assert(s->stream_id() != -1); + } + WriteCommandInt64_2(SessionCommand::kAddSceneGraph, s->stream_id(), + s->time()); + EndCommand(); +} + +void GameStream::RemoveScene(Scene* s) { + WriteCommandInt64(SessionCommand::kRemoveSceneGraph, s->stream_id()); + Remove(s, &scenes_, &free_indices_scene_graphs_); + EndCommand(); +} + +void GameStream::StepScene(Scene* s) { + assert(IsValidScene(s)); + WriteCommandInt64(SessionCommand::kStepSceneGraph, s->stream_id()); + EndCommand(); +} + +void GameStream::AddNode(Node* n) { + assert(n); + if (host_session_) { + Add(n, &nodes_, &free_indices_nodes_); + } else { + assert(n && n->stream_id() != -1); + } + + Scene* sg = n->scene(); + assert(IsValidScene(sg)); + WriteCommandInt64_3(SessionCommand::kAddNode, sg->stream_id(), + n->type()->id(), n->stream_id()); + EndCommand(); +} + +void GameStream::NodeOnCreate(Node* n) { + assert(IsValidNode(n)); + WriteCommandInt64(SessionCommand::kNodeOnCreate, n->stream_id()); + EndCommand(); +} + +void GameStream::SetForegroundScene(Scene* sg) { + assert(IsValidScene(sg)); + WriteCommandInt64(SessionCommand::kSetForegroundSceneGraph, sg->stream_id()); + EndCommand(); +} + +void GameStream::RemoveNode(Node* n) { + assert(IsValidNode(n)); + WriteCommandInt64(SessionCommand::kRemoveNode, n->stream_id()); + Remove(n, &nodes_, &free_indices_nodes_); + EndCommand(); +} + +void GameStream::AddTexture(Texture* t) { + // Register an ID in host mode. + if (host_session_) { + Add(t, &textures_, &free_indices_textures_); + } else { + assert(t && t->stream_id() != -1); + } + Scene* sg = t->scene(); + assert(IsValidScene(sg)); + WriteCommandInt64_2(SessionCommand::kAddTexture, sg->stream_id(), + t->stream_id()); + WriteString(t->name()); + EndCommand(); +} + +void GameStream::RemoveTexture(Texture* t) { + assert(IsValidTexture(t)); + WriteCommandInt64(SessionCommand::kRemoveTexture, t->stream_id()); + Remove(t, &textures_, &free_indices_textures_); + EndCommand(); +} + +void GameStream::AddModel(Model* t) { + // Register an ID in host mode. + if (host_session_) { + Add(t, &models_, &free_indices_models_); + } else { + assert(t && t->stream_id() != -1); + } + Scene* sg = t->scene(); + assert(IsValidScene(sg)); + WriteCommandInt64_2(SessionCommand::kAddModel, sg->stream_id(), + t->stream_id()); + WriteString(t->name()); + EndCommand(); +} + +void GameStream::RemoveModel(Model* t) { + assert(IsValidModel(t)); + WriteCommandInt64(SessionCommand::kRemoveModel, t->stream_id()); + Remove(t, &models_, &free_indices_models_); + EndCommand(); +} + +void GameStream::AddSound(Sound* t) { + // Register an ID in host mode. + if (host_session_) { + Add(t, &sounds_, &free_indices_sounds_); + } else { + assert(t && t->stream_id() != -1); + } + Scene* sg = t->scene(); + assert(IsValidScene(sg)); + WriteCommandInt64_2(SessionCommand::kAddSound, sg->stream_id(), + t->stream_id()); + WriteString(t->name()); + EndCommand(); +} + +void GameStream::RemoveSound(Sound* t) { + assert(IsValidSound(t)); + WriteCommandInt64(SessionCommand::kRemoveSound, t->stream_id()); + Remove(t, &sounds_, &free_indices_sounds_); + EndCommand(); +} + +void GameStream::AddData(Data* t) { + // Register an ID in host mode. + if (host_session_) { + Add(t, &datas_, &free_indices_datas_); + } else { + assert(t && t->stream_id() != -1); + } + Scene* sg = t->scene(); + assert(IsValidScene(sg)); + WriteCommandInt64_2(SessionCommand::kAddData, sg->stream_id(), + t->stream_id()); + WriteString(t->name()); + EndCommand(); +} + +void GameStream::RemoveData(Data* t) { + assert(IsValidData(t)); + WriteCommandInt64(SessionCommand::kRemoveData, t->stream_id()); + Remove(t, &datas_, &free_indices_datas_); + EndCommand(); +} + +void GameStream::AddCollideModel(CollideModel* t) { + if (host_session_) { + Add(t, &collide_models_, &free_indices_collide_models_); + } else { + assert(t && t->stream_id() != -1); + } + Scene* sg = t->scene(); + assert(IsValidScene(sg)); + WriteCommandInt64_2(SessionCommand::kAddCollideModel, sg->stream_id(), + t->stream_id()); + WriteString(t->name()); + EndCommand(); +} + +void GameStream::RemoveCollideModel(CollideModel* t) { + assert(IsValidCollideModel(t)); + WriteCommandInt64(SessionCommand::kRemoveCollideModel, t->stream_id()); + Remove(t, &collide_models_, &free_indices_collide_models_); + EndCommand(); +} + +void GameStream::AddMaterial(Material* m) { + if (host_session_) { + Add(m, &materials_, &free_indices_materials_); + } else { + assert(m && m->stream_id() != -1); + } + Scene* sg = m->scene(); + assert(IsValidScene(sg)); + WriteCommandInt64_2(SessionCommand::kAddMaterial, sg->stream_id(), + m->stream_id()); + EndCommand(); +} + +void GameStream::RemoveMaterial(Material* m) { + assert(IsValidMaterial(m)); + WriteCommandInt64(SessionCommand::kRemoveMaterial, m->stream_id()); + Remove(m, &materials_, &free_indices_materials_); + EndCommand(); +} + +void GameStream::AddMaterialComponent(Material* m, MaterialComponent* c) { + assert(IsValidMaterial(m)); + auto flattened_size = c->GetFlattenedSize(); + assert(flattened_size > 0 && flattened_size < 10000); + WriteCommandInt64_2(SessionCommand::kAddMaterialComponent, m->stream_id(), + static_cast_check_fit(flattened_size)); + size_t size = out_command_.size(); + out_command_.resize(size + flattened_size); + char* ptr = reinterpret_cast(&out_command_[size]); + char* ptr2 = ptr; + c->Flatten(&ptr2, this); + size_t actual_size = ptr2 - ptr; + if (actual_size != flattened_size) { + throw Exception("Expected flattened_size " + std::to_string(flattened_size) + + " got " + std::to_string(actual_size)); + } + EndCommand(); +} + +void GameStream::ConnectNodeAttribute(Node* src_node, + NodeAttributeUnbound* src_attr, + Node* dst_node, + NodeAttributeUnbound* dst_attr) { + assert(IsValidNode(src_node)); + assert(IsValidNode(dst_node)); + assert(src_attr->node_type() == src_node->type()); + assert(dst_attr->node_type() == dst_node->type()); + if (src_node->scene() != dst_node->scene()) { + throw Exception("Nodes are from different scenes"); + } + assert(src_node->scene() == dst_node->scene()); + WriteCommandInt64_4(SessionCommand::kConnectNodeAttribute, + src_node->stream_id(), src_attr->index(), + dst_node->stream_id(), dst_attr->index()); + EndCommand(); +} + +void GameStream::NodeMessage(Node* node, const char* buffer, size_t size) { + assert(IsValidNode(node)); + BA_PRECONDITION(size > 0 && size < 10000); + WriteCommandInt64_2(SessionCommand::kNodeMessage, node->stream_id(), + static_cast_check_fit(size)); + WriteChars(size, buffer); + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, float val) { + assert(IsValidNode(attr.node)); + WriteCommandInt64_2(SessionCommand::kSetNodeAttrFloat, attr.node->stream_id(), + attr.index()); + WriteFloat(val); + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, int64_t val) { + assert(IsValidNode(attr.node)); + WriteCommandInt64_3(SessionCommand::kSetNodeAttrInt32, attr.node->stream_id(), + attr.index(), val); + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, bool val) { + assert(IsValidNode(attr.node)); + WriteCommandInt64_3(SessionCommand::kSetNodeAttrBool, attr.node->stream_id(), + attr.index(), val); + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); + size_t count{vals.size()}; + WriteCommandInt64_3(SessionCommand::kSetNodeAttrFloats, + attr.node->stream_id(), attr.index(), + static_cast_check_fit(count)); + if (count > 0) { + WriteFloats(count, vals.data()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); + size_t count{vals.size()}; + WriteCommandInt64_3(SessionCommand::kSetNodeAttrInt32s, + attr.node->stream_id(), attr.index(), + static_cast_check_fit(count)); + if (count > 0) { + WriteInts64(count, vals.data()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::string& val) { + assert(IsValidNode(attr.node)); + WriteCommandInt64_2(SessionCommand::kSetNodeAttrString, + attr.node->stream_id(), attr.index()); + WriteString(val); + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, Node* val) { + assert(IsValidNode(attr.node)); + if (val) { + assert(IsValidNode(val)); + if (attr.node->scene() != val->scene()) { + throw Exception("nodes are from different scenes"); + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrNode, + attr.node->stream_id(), attr.index(), val->stream_id()); + } else { + WriteCommandInt64_2(SessionCommand::kSetNodeAttrNodeNull, + attr.node->stream_id(), attr.index()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); +#if BA_DEBUG_BUILD + for (auto val : vals) { + assert(IsValidNode(val)); + } +#endif + size_t count{vals.size()}; + std::vector vals_out; + if (count > 0) { + vals_out.resize(count); + Scene* scene = attr.node->scene(); + for (size_t i = 0; i < count; i++) { + if (vals[i]->scene() != scene) { + throw Exception("nodes are from different scenes"); + } + vals_out[i] = static_cast_check_fit(vals[i]->stream_id()); + } + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrNodes, attr.node->stream_id(), + attr.index(), static_cast_check_fit(count)); + if (count > 0) { + WriteInts32(count, vals_out.data()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, Player* val) { + // cout << "SET PLAYER ATTR " << attr.getIndex() << endl; +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); + + if (g_buildconfig.debug_build()) { + for (auto val : vals) { + assert(IsValidMaterial(val)); + } + } + + size_t count = vals.size(); + std::vector vals_out; + if (count > 0) { + vals_out.resize(count); + Scene* scene = attr.node->scene(); + for (size_t i = 0; i < count; i++) { + if (vals[i]->scene() != scene) { + throw Exception("material/node are from different scenes"); + } + vals_out[i] = static_cast_check_fit(vals[i]->stream_id()); + } + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrMaterials, + attr.node->stream_id(), attr.index(), + static_cast_check_fit(count)); + if (count > 0) { + WriteInts32(count, &(vals_out[0])); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, Texture* val) { + if (val) { + assert(IsValidNode(attr.node)); + assert(IsValidTexture(val)); + if (attr.node->scene() != val->scene()) { + throw Exception("texture/node are from different scenes"); + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrTexture, + attr.node->stream_id(), attr.index(), val->stream_id()); + } else { + WriteCommandInt64_2(SessionCommand::kSetNodeAttrTextureNull, + attr.node->stream_id(), attr.index()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); + if (g_buildconfig.debug_build()) { + for (auto val : vals) { + assert(IsValidTexture(val)); + } + } + size_t count{vals.size()}; + std::vector vals_out; + if (count > 0) { + vals_out.resize(count); + Scene* scene{attr.node->scene()}; + for (size_t i = 0; i < count; i++) { + if (vals[i]->scene() != scene) { + throw Exception("texture/node are from different scenes"); + } + vals_out[i] = static_cast_check_fit(vals[i]->stream_id()); + } + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrTextures, + attr.node->stream_id(), attr.index(), + static_cast_check_fit(count)); + if (count > 0) { + WriteInts32(count, vals_out.data()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, Sound* val) { + if (val) { + assert(IsValidNode(attr.node)); + assert(IsValidSound(val)); + if (attr.node->scene() != val->scene()) { + throw Exception("sound/node are from different scenes"); + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrSound, + attr.node->stream_id(), attr.index(), val->stream_id()); + } else { + WriteCommandInt64_2(SessionCommand::kSetNodeAttrSoundNull, + attr.node->stream_id(), attr.index()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); + if (g_buildconfig.debug_build()) { + for (auto val : vals) { + assert(IsValidSound(val)); + } + } + size_t count{vals.size()}; + std::vector vals_out; + if (count > 0) { + vals_out.resize(count); + Scene* scene = attr.node->scene(); + for (size_t i = 0; i < count; i++) { + if (vals[i]->scene() != scene) { + throw Exception("sound/node are from different scenes"); + } + vals_out[i] = static_cast_check_fit(vals[i]->stream_id()); + } + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrSounds, + attr.node->stream_id(), attr.index(), + static_cast_check_fit(count)); + if (count > 0) { + WriteInts32(count, &(vals_out[0])); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, Model* val) { + if (val) { + assert(IsValidNode(attr.node)); + assert(IsValidModel(val)); + if (attr.node->scene() != val->scene()) { + throw Exception("model/node are from different scenes"); + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrModel, + attr.node->stream_id(), attr.index(), val->stream_id()); + } else { + WriteCommandInt64_2(SessionCommand::kSetNodeAttrModelNull, + attr.node->stream_id(), attr.index()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); + if (g_buildconfig.debug_build()) { + for (auto val : vals) { + assert(IsValidModel(val)); + } + } + size_t count = vals.size(); + std::vector vals_out; + if (count > 0) { + vals_out.resize(count); + Scene* scene = attr.node->scene(); + for (size_t i = 0; i < count; i++) { + if (vals[i]->scene() != scene) { + throw Exception("model/node are from different scenes"); + } + vals_out[i] = static_cast_check_fit(vals[i]->stream_id()); + } + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrModels, + attr.node->stream_id(), attr.index(), + static_cast_check_fit(count)); + if (count > 0) { + WriteInts32(count, &(vals_out[0])); + } + EndCommand(); +} +void GameStream::SetNodeAttr(const NodeAttribute& attr, CollideModel* val) { + if (val) { + assert(IsValidNode(attr.node)); + assert(IsValidCollideModel(val)); + if (attr.node->scene() != val->scene()) { + throw Exception("collide_model/node are from different scenes"); + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrCollideModel, + attr.node->stream_id(), attr.index(), val->stream_id()); + } else { + WriteCommandInt64_2(SessionCommand::kSetNodeAttrCollideModelNull, + attr.node->stream_id(), attr.index()); + } + EndCommand(); +} +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); + if (g_buildconfig.debug_build()) { + for (auto val : vals) { + assert(IsValidCollideModel(val)); + } + } + size_t count = vals.size(); + std::vector vals_out; + if (count > 0) { + vals_out.resize(count); + Scene* scene = attr.node->scene(); + for (size_t i = 0; i < count; i++) { + if (vals[i]->scene() != scene) { + throw Exception("collide_model/node are from different scenes"); + } + vals_out[i] = static_cast_check_fit(vals[i]->stream_id()); + } + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrCollideModels, + attr.node->stream_id(), attr.index(), + static_cast_check_fit(count)); + if (count > 0) { + WriteInts32(count, &(vals_out[0])); + } + EndCommand(); +} + +void GameStream::PlaySoundAtPosition(Sound* sound, float volume, float x, + float y, float z) { + assert(IsValidSound(sound)); + assert(IsValidScene(sound->scene())); + + // FIXME: We shouldn't need to be passing all these as full floats. :-( + WriteCommandInt64(SessionCommand::kPlaySoundAtPosition, sound->stream_id()); + WriteFloat(volume); + WriteFloat(x); + WriteFloat(y); + WriteFloat(z); + EndCommand(); +} + +void GameStream::EmitBGDynamics(const BGDynamicsEmission& e) { + WriteCommandInt64_4(SessionCommand::kEmitBGDynamics, + static_cast(e.emit_type), e.count, + static_cast(e.chunk_type), + static_cast(e.tendril_type)); + float fvals[8]; + fvals[0] = e.position.x; + fvals[1] = e.position.y; + fvals[2] = e.position.z; + fvals[3] = e.velocity.x; + fvals[4] = e.velocity.y; + fvals[5] = e.velocity.z; + fvals[6] = e.scale; + fvals[7] = e.spread; + WriteFloats(8, fvals); + EndCommand(); +} + +void GameStream::PlaySound(Sound* sound, float volume) { + assert(IsValidSound(sound)); + assert(IsValidScene(sound->scene())); + + // FIXME: We shouldn't need to be passing all these as full floats. :-( + WriteCommandInt64(SessionCommand::kPlaySound, sound->stream_id()); + WriteFloat(volume); + EndCommand(); +} + +void GameStream::ScreenMessageTop(const std::string& val, float r, float g, + float b, Texture* texture, + Texture* tint_texture, float tint_r, + float tint_g, float tint_b, float tint2_r, + float tint2_g, float tint2_b) { + assert(IsValidTexture(texture)); + assert(IsValidTexture(tint_texture)); + assert(IsValidScene(texture->scene())); + assert(IsValidScene(tint_texture->scene())); + WriteCommandInt64_2(SessionCommand::kScreenMessageTop, texture->stream_id(), + tint_texture->stream_id()); + WriteString(val); + float f[9]; + f[0] = r; + f[1] = g; + f[2] = b; + f[3] = tint_r; + f[4] = tint_g; + f[5] = tint_b; + f[6] = tint2_r; + f[7] = tint2_g; + f[8] = tint2_b; + WriteFloats(9, f); + EndCommand(); +} + +void GameStream::ScreenMessageBottom(const std::string& val, float r, float g, + float b) { + WriteCommand(SessionCommand::kScreenMessageBottom); + WriteString(val); + float color[3]; + color[0] = r; + color[1] = g; + color[2] = b; + WriteFloats(3, color); + EndCommand(); +} + +auto GameStream::GetSoundID(Sound* s) -> int64_t { + assert(IsValidSound(s)); + return s->stream_id(); +} + +auto GameStream::GetMaterialID(Material* m) -> int64_t { + assert(IsValidMaterial(m)); + return m->stream_id(); +} + +void GameStream::OnClientConnected(ConnectionToClient* c) { + // Sanity check - abort if its on either of our lists already. + for (auto& connections_to_client : connections_to_clients_) { + if (connections_to_client == c) { + Log("Error: GameStream::OnClientConnected() got duplicate connection."); + return; + } + } + for (auto& i : connections_to_clients_ignored_) { + if (i == c) { + Log("Error: GameStream::OnClientConnected() got duplicate connection."); + return; + } + } + + { + // First thing, we need to flush all pending session-commands to clients. + // The host-session's current state is the result of having already run + // these commands locally, so if we leave them on the list while 'restoring' + // the new client to our state they'll get essentially double-applied, which + // is bad. (ie: a delete-node command will get called but the node will + // already be gone) + Flush(); + + connections_to_clients_.push_back(c); + + // We create a temporary output stream just for the purpose of building + // a giant session-commands message to reconstruct everything in our + // host-session in its current form. + GameStream out(nullptr, false); + + // Ask the host-session that we came from to dump it's complete state. + host_session_->DumpFullState(&out); + + // Grab the message that's been built up. + // If its not empty, send it to the client. + std::vector out_message = out.GetOutMessage(); + if (!out_message.empty()) { + c->SendReliableMessage(out_message); + } + + // Also send a correction packet to sync up all our dynamics. + // (technically could do this *just* for the new client) + SendPhysicsCorrection(false); + } +} + +void GameStream::OnClientDisconnected(ConnectionToClient* c) { + // Search for it on either our ignored or regular lists. + for (auto i = connections_to_clients_.begin(); + i != connections_to_clients_.end(); i++) { + if (*i == c) { + connections_to_clients_.erase(i); + return; + } + } + for (auto i = connections_to_clients_ignored_.begin(); + i != connections_to_clients_ignored_.end(); i++) { + if (*i == c) { + connections_to_clients_ignored_.erase(i); + return; + } + } + Log("Error: GameStream::OnClientDisconnected() called for connection not on " + "lists"); +} + +} // namespace ballistica diff --git a/src/ballistica/game/game_stream.h b/src/ballistica/game/game_stream.h new file mode 100644 index 00000000..794e522e --- /dev/null +++ b/src/ballistica/game/game_stream.h @@ -0,0 +1,159 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_GAME_STREAM_H_ +#define BALLISTICA_GAME_GAME_STREAM_H_ + +#include +#include + +#include "ballistica/core/object.h" +#include "ballistica/game/client_controller_interface.h" + +namespace ballistica { + +// A mechanism for dumping a live session or session-creation-commands to a +// stream of messages that can be saved to file or sent over the network. +class GameStream : public Object, public ClientControllerInterface { + public: + GameStream(HostSession* host_session, bool saveReplay); + ~GameStream() override; + void SetTime(millisecs_t t); + void AddScene(Scene* s); + void RemoveScene(Scene* s); + void StepScene(Scene* s); + void AddNode(Node* n); + void NodeOnCreate(Node* n); + void RemoveNode(Node* n); + void SetForegroundScene(Scene* sg); + void AddMaterial(Material* m); + void RemoveMaterial(Material* m); + void AddMaterialComponent(Material* m, MaterialComponent* c); + void AddTexture(Texture* t); + void RemoveTexture(Texture* t); + void AddModel(Model* t); + void RemoveModel(Model* t); + void AddSound(Sound* t); + void RemoveSound(Sound* t); + void AddData(Data* d); + void RemoveData(Data* d); + void AddCollideModel(CollideModel* t); + void RemoveCollideModel(CollideModel* t); + void ConnectNodeAttribute(Node* src_node, NodeAttributeUnbound* src_attr, + Node* dst_node, NodeAttributeUnbound* dst_attr); + void NodeMessage(Node* node, const char* buffer, size_t size); + void SetNodeAttr(const NodeAttribute& attr, float val); + void SetNodeAttr(const NodeAttribute& attr, int64_t val); + void SetNodeAttr(const NodeAttribute& attr, bool val); + void SetNodeAttr(const NodeAttribute& attr, const std::vector& vals); + void SetNodeAttr(const NodeAttribute& attr, const std::vector& vals); + void SetNodeAttr(const NodeAttribute& attr, const std::string& val); + void SetNodeAttr(const NodeAttribute& attr, Node* n); + void SetNodeAttr(const NodeAttribute& attr, const std::vector& vals); + void SetNodeAttr(const NodeAttribute& attr, Player* n); + void SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals); + void SetNodeAttr(const NodeAttribute& attr, Texture* n); + void SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals); + void SetNodeAttr(const NodeAttribute& attr, Sound* n); + void SetNodeAttr(const NodeAttribute& attr, const std::vector& vals); + void SetNodeAttr(const NodeAttribute& attr, Model* n); + void SetNodeAttr(const NodeAttribute& attr, const std::vector& vals); + void SetNodeAttr(const NodeAttribute& attr, CollideModel* n); + void SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals); + void PlaySoundAtPosition(Sound* sound, float volume, float x, float y, + float z); + void PlaySound(Sound* sound, float volume); + void EmitBGDynamics(const BGDynamicsEmission& e); + auto GetSoundID(Sound* s) -> int64_t; + auto GetMaterialID(Material* m) -> int64_t; + void ScreenMessageBottom(const std::string& val, float r, float g, float b); + void ScreenMessageTop(const std::string& val, float r, float g, float b, + Texture* texture, Texture* tint_texture, float tint_r, + float tint_g, float tint_b, float tint2_r, + float tint2_g, float tint2_b); + void OnClientConnected(ConnectionToClient* c) override; + void OnClientDisconnected(ConnectionToClient* c) override; + auto GetOutMessage() const -> std::vector; + + private: + HostSession* host_session_; + + // Make sure the scene is in our stream. + auto IsValidScene(Scene* val) -> bool; + auto IsValidNode(Node* val) -> bool; + auto IsValidTexture(Texture* val) -> bool; + auto IsValidModel(Model* val) -> bool; + auto IsValidSound(Sound* val) -> bool; + auto IsValidData(Data* val) -> bool; + auto IsValidCollideModel(CollideModel* val) -> bool; + auto IsValidMaterial(Material* val) -> bool; + millisecs_t next_flush_time_; + void Flush(); + void AddMessageToReplay(const std::vector& message); + + // Individual command going into the commands-messages. + std::vector out_command_; + + // The complete message full of commands. + std::vector out_message_; + std::vector connections_to_clients_; + std::vector connections_to_clients_ignored_; + bool writing_replay_; + void Fail(); + millisecs_t last_physics_correction_time_; + millisecs_t last_send_time_; + void ShipSessionCommandsMessage(); + void SendPhysicsCorrection(bool blend); + void EndCommand(bool is_time_set = false); + void WriteString(const std::string& s); + void WriteFloat(float val); + void WriteFloats(size_t count, const float* vals); + void WriteInts32(size_t count, const int32_t* vals); + void WriteInts64(size_t count, const int64_t* vals); + void WriteChars(size_t count, const char* vals); + void WriteCommand(SessionCommand cmd); + void WriteCommandInt32(SessionCommand cmd, int32_t value); + void WriteCommandInt64(SessionCommand cmd, int64_t value); + void WriteCommandInt32_2(SessionCommand cmd, int32_t value1, int32_t value2); + void WriteCommandInt64_2(SessionCommand cmd, int64_t value1, int64_t value2); + void WriteCommandInt32_3(SessionCommand cmd, int32_t value1, int32_t value2, + int32_t value3); + void WriteCommandInt64_3(SessionCommand cmd, int64_t value1, int64_t value2, + int64_t value3); + void WriteCommandInt32_4(SessionCommand cmd, int32_t value1, int32_t value2, + int32_t value3, int32_t value4); + void WriteCommandInt64_4(SessionCommand cmd, int64_t value1, int64_t value2, + int64_t value3, int64_t value4); + template + auto GetPointerCount(const std::vector& vec) -> size_t; + template + auto GetFreeIndex(std::vector* vec, std::vector* free_indices) + -> size_t; + template + void Add(T* val, std::vector* vec, std::vector* free_indices); + template + void Remove(T* val, std::vector* vec, std::vector* free_indices); + millisecs_t time_; + std::vector scenes_; + std::vector free_indices_scene_graphs_; + std::vector nodes_; + std::vector free_indices_nodes_; + std::vector materials_; + std::vector free_indices_materials_; + std::vector textures_; + std::vector free_indices_textures_; + std::vector models_; + std::vector free_indices_models_; + std::vector sounds_; + std::vector free_indices_sounds_; + std::vector datas_; + std::vector free_indices_datas_; + std::vector collide_models_; + std::vector free_indices_collide_models_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_GAME_STREAM_H_ diff --git a/src/ballistica/game/host_activity.cc b/src/ballistica/game/host_activity.cc new file mode 100644 index 00000000..ce4a1f58 --- /dev/null +++ b/src/ballistica/game/host_activity.cc @@ -0,0 +1,528 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/game/host_activity.h" + +#include + +#include "ballistica/dynamics/material/material.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/game/player.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/generic/lambda_runnable.h" +#include "ballistica/generic/timer.h" +#include "ballistica/input/device/input_device.h" +#include "ballistica/media/component/collide_model.h" +#include "ballistica/media/component/data.h" +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_context_call.h" +#include "ballistica/python/python_sys.h" +#include "ballistica/scene/node/globals_node.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +HostActivity::HostActivity(HostSession* host_session) { + // Store a link to the HostSession and add ourself to it. + host_session_ = host_session; + + // Create our game timer - gets called whenever game should step. + step_scene_timer_ = + base_timers_.NewTimer(base_time_, kGameStepMilliseconds, 0, -1, + NewLambdaRunnable([this] { StepScene(); })); + SetGameSpeed(1.0f); + { + ScopedSetContext cp(this); // So scene picks us up as context. + scene_ = Object::New(0); + + // If there's an output stream, add to it. + if (GameStream* out = host_session->GetGameStream()) { + out->AddScene(scene_.get()); + } + } +} + +HostActivity::~HostActivity() { + shutting_down_ = true; + + // Put the scene in shut-down mode before we start killing stuff. + // (this generates warnings, suppresses messages, etc) + scene_->set_shutting_down(true); + + // Clear out all python calls registered in our context. + // (should wipe out refs to our activity and prevent them from running without + // a valid activity context) + for (auto&& i : python_calls_) { + if (i.exists()) { + i->MarkDead(); + } + } + + // Mark all our media dead to clear it out of our output-stream cleanly + for (auto&& i : textures_) { + if (i.second.exists()) { + i.second->MarkDead(); + } + } + for (auto&& i : models_) { + if (i.second.exists()) { + i.second->MarkDead(); + } + } + for (auto&& i : sounds_) { + if (i.second.exists()) { + i.second->MarkDead(); + } + } + for (auto&& i : collide_models_) { + if (i.second.exists()) { + i.second->MarkDead(); + } + } + for (auto&& i : materials_) { + if (i.exists()) { + i->MarkDead(); + } + } + + // Clear our timers and scene; this should wipe out any remaining refs to our + // python activity, allowing it to die. + base_timers_.Clear(); + sim_timers_.Clear(); + scene_.Clear(); + + // Report outstanding calls. There shouldn't be any at this point. Actually it + // turns out there's generally 1; whichever call was responsible for killing + // this activity will still be in progress.. so let's report on 2 or more I + // guess. +#if BA_DEBUG_BUILD + PruneDeadRefs(&python_calls_); + if (python_calls_.size() > 1) { + std::string s = "WARNING: " + std::to_string(python_calls_.size()) + + " live PythonContextCalls at shutdown for " + + "HostActivity" + " (1 call is expected):"; + int count = 1; + for (auto& python_call : python_calls_) + s += "\n " + std::to_string(count++) + ": " + + (*python_call).GetObjectDescription(); + Log(s); + } +#endif // BA_DEBUG_BUILD +} + +auto HostActivity::GetGameStream() const -> GameStream* { + if (!host_session_.exists()) return nullptr; + return host_session_->GetGameStream(); +} + +void HostActivity::StepScene() { + int cycle_count = 1; + if (host_session_->benchmark_type() == BenchmarkType::kCPU) { + cycle_count = 100; + } + + for (int cycle = 0; cycle < cycle_count; ++cycle) { + assert(InGameThread()); + + // Clear our player-positions for this step. + // FIXME: Move this to scene and/or player node. + assert(host_session_.exists()); + for (auto&& player : host_session_->players()) { + assert(player.exists()); + player->set_have_position(false); + } + + // Run our sim-time timers. + sim_timers_.Run(scene()->time()); + + // Send die-messages/etc to out-of-bounds stuff. + HandleOutOfBoundsNodes(); + + scene()->Step(); + } +} + +void HostActivity::RegisterCall(PythonContextCall* call) { + assert(call); + python_calls_.emplace_back(call); + + // If we're shutting down, just kill the call immediately. + // (we turn all of our calls to no-ops as we shut down) + if (shutting_down_) { + Log("WARNING: adding call to expired activity; call will not function: " + + call->GetObjectDescription()); + call->MarkDead(); + } +} + +void HostActivity::start() { + if (_started) { + Log("Error: Start called twice for activity."); + } + _started = true; +} + +auto HostActivity::GetAsHostActivity() -> HostActivity* { return this; } + +auto HostActivity::NewMaterial(const std::string& name) + -> Object::Ref { + if (shutting_down_) { + throw Exception("can't create materials during activity shutdown"); + } + + auto m(Object::New(name, scene())); + materials_.emplace_back(m); + return m; +} + +auto HostActivity::GetTexture(const std::string& name) -> Object::Ref { + if (shutting_down_) { + throw Exception("can't load assets during activity shutdown"); + } + return Media::GetMedia(&textures_, name, scene()); +} + +auto HostActivity::GetSound(const std::string& name) -> Object::Ref { + if (shutting_down_) { + throw Exception("can't load assets during activity shutdown"); + } + return Media::GetMedia(&sounds_, name, scene()); +} + +auto HostActivity::GetData(const std::string& name) -> Object::Ref { + if (shutting_down_) { + throw Exception("can't load assets during activity shutdown"); + } + return Media::GetMedia(&datas_, name, scene()); +} + +auto HostActivity::GetModel(const std::string& name) -> Object::Ref { + if (shutting_down_) { + throw Exception("can't load assets during activity shutdown"); + } + return Media::GetMedia(&models_, name, scene()); +} + +auto HostActivity::GetCollideModel(const std::string& name) + -> Object::Ref { + if (shutting_down_) { + throw Exception("can't load assets during activity shutdown"); + } + return Media::GetMedia(&collide_models_, name, scene()); +} + +void HostActivity::SetPaused(bool val) { + if (paused_ == val) { + return; + } + paused_ = val; + UpdateStepTimerLength(); +} + +void HostActivity::SetGameSpeed(float speed) { + if (speed == game_speed_) { + return; + } + assert(speed >= 0.0f); + game_speed_ = speed; + UpdateStepTimerLength(); +} + +void HostActivity::UpdateStepTimerLength() { + if (game_speed_ == 0.0f || paused_) { + step_scene_timer_->SetLength(-1, true, base_time_); + } else { + step_scene_timer_->SetLength( + std::max(1, static_cast( + round(static_cast(kGameStepMilliseconds) + / (game_speed_ * g_game->debug_speed_mult())))), + true, base_time_); + } +} + +void HostActivity::HandleOutOfBoundsNodes() { + if (scene()->out_of_bounds_nodes().empty()) { + out_of_bounds_in_a_row_ = 0; + return; + } + + // Make sure someone's handling our out-of-bounds messages. + out_of_bounds_in_a_row_++; + if (out_of_bounds_in_a_row_ > 100) { + Log("Warning: 100 consecutive out-of-bounds messages sent." + " They are probably not being handled properly"); + int j = 0; + for (auto&& i : scene()->out_of_bounds_nodes()) { + j++; + Node* n = i.get(); + if (n) { + std::string dstr; + PyObject* delegate = n->GetDelegate(); + if (delegate) { + dstr = PythonRef(delegate, PythonRef::kAcquire).Str(); + } + Log(" node #" + std::to_string(j) + ": type='" + n->type()->name() + + "' addr=" + Utils::PtrToString(i.get()) + " name='" + n->label() + + "' delegate=" + dstr); + } + } + out_of_bounds_in_a_row_ = 0; + } + + // Send out-of-bounds messages to newly out-of-bounds nodes. + for (auto&& i : scene()->out_of_bounds_nodes()) { + Node* n = i.get(); + if (n) { + n->DispatchOutOfBoundsMessage(); + } + } +} + +void HostActivity::RegisterPyActivity(PyObject* pyActivityObj) { + assert(pyActivityObj && pyActivityObj != Py_None); + assert(!py_activity_weak_ref_.exists()); + + // Store a python weak-ref to this activity. + py_activity_weak_ref_.Steal(PyWeakref_NewRef(pyActivityObj, nullptr)); +} + +auto HostActivity::GetPyActivity() const -> PyObject* { + PyObject* obj = py_activity_weak_ref_.get(); + if (!obj) return Py_None; + return PyWeakref_GetObject(obj); +} + +auto HostActivity::GetHostSession() -> HostSession* { + return host_session_.get(); +} + +auto HostActivity::GetMutableScene() -> Scene* { + Scene* sg = scene_.get(); + assert(sg); + return sg; +} + +void HostActivity::SetIsForeground(bool val) { + // If we're foreground, set our scene as foreground. + Scene* sg = scene(); + if (val && sg) { + // Set it locally. + g_game->SetForegroundScene(sg); + + // Also push it to clients. + if (GameStream* out = GetGameStream()) { + out->SetForegroundScene(scene_.get()); + } + } +} + +auto HostActivity::globals_node() const -> GlobalsNode* { + return globals_node_.get(); +} + +auto HostActivity::NewSimTimer(millisecs_t length, bool repeat, + const Object::Ref& runnable) -> int { + if (shutting_down_) { + BA_LOG_PYTHON_TRACE_ONCE( + "WARNING: Creating game timer during host-activity shutdown"); + return 123; // Dummy. + } + if (length == 0 && repeat) { + throw Exception("Can't add game-timer with length 0 and repeat on"); + } + if (length < 0) { + throw Exception("Timer length cannot be < 0 (got " + std::to_string(length) + + ")"); + } + + int offset = 0; + Timer* t = sim_timers_.NewTimer(scene()->time(), length, offset, + repeat ? -1 : 0, runnable); + return t->id(); +} + +auto HostActivity::NewBaseTimer(millisecs_t length, bool repeat, + const Object::Ref& runnable) -> int { + if (shutting_down_) { + BA_LOG_PYTHON_TRACE_ONCE( + "WARNING: Creating session-time timer during host-activity shutdown"); + return 123; // dummy... + } + if (length == 0 && repeat) { + throw Exception("Can't add session-time timer with length 0 and repeat on"); + } + if (length < 0) { + throw Exception("Timer length cannot be < 0"); + } + + int offset = 0; + Timer* t = base_timers_.NewTimer(base_time_, length, offset, repeat ? -1 : 0, + runnable); + return t->id(); +} + +void HostActivity::DeleteSimTimer(int timer_id) { + assert(InGameThread()); + if (shutting_down_) return; + sim_timers_.DeleteTimer(timer_id); +} + +void HostActivity::DeleteBaseTimer(int timer_id) { + assert(InGameThread()); + if (shutting_down_) return; + base_timers_.DeleteTimer(timer_id); +} + +auto HostActivity::Update(millisecs_t time_advance) -> millisecs_t { + assert(InGameThread()); + + // We can be killed at any time, so let's keep an eye out for that. + WeakRef test_ref(this); + assert(test_ref.exists()); + + // If we haven't been told to start yet, don't do anything more. + if (!_started) { + return 1000; + } + + // Advance base time by the specified amount, stopping at all timers along the + // way. + millisecs_t target_base_time = base_time_ + time_advance; + while (!base_timers_.empty() + && (base_time_ + base_timers_.GetTimeToNextExpire(base_time_) + <= target_base_time)) { + base_time_ += base_timers_.GetTimeToNextExpire(base_time_); + base_timers_.Run(base_time_); + if (!test_ref.exists()) { + return 1000; // The last timer run might have killed us. + } + } + base_time_ = target_base_time; + + // Periodically prune various dead refs. + if (base_time_ > next_prune_time_) { + PruneDeadMapRefs(&textures_); + PruneDeadMapRefs(&sounds_); + PruneDeadMapRefs(&collide_models_); + PruneDeadMapRefs(&models_); + PruneDeadRefs(&materials_); + PruneDeadRefs(&python_calls_); + next_prune_time_ = base_time_ + 5000; + } + + // Return the time until the next timer goes off. + return base_timers_.empty() ? 1000 + : base_timers_.GetTimeToNextExpire(base_time_); +} + +void HostActivity::ScreenSizeChanged() { scene()->ScreenSizeChanged(); } +void HostActivity::LanguageChanged() { scene()->LanguageChanged(); } +void HostActivity::DebugSpeedMultChanged() { UpdateStepTimerLength(); } +void HostActivity::GraphicsQualityChanged(GraphicsQuality q) { + scene()->GraphicsQualityChanged(q); +} + +void HostActivity::Draw(FrameDef* frame_def) { + if (!_started) return; + scene()->Draw(frame_def); +} + +void HostActivity::DumpFullState(GameStream* out) { + // Add our scene. + if (scene_.exists()) { + scene_->Dump(out); + } + + // Before doing any nodes, we need to create all materials. + // (but *not* their components, which may reference the nodes that we haven't + // made yet) + for (auto&& i : materials_) { + if (Material* m = i.get()) { + out->AddMaterial(m); + } + } + + // Add our media. + for (auto&& i : textures_) { + if (Texture* t = i.second.get()) { + out->AddTexture(t); + } + } + for (auto&& i : sounds_) { + if (Sound* s = i.second.get()) { + out->AddSound(s); + } + } + for (auto&& i : models_) { + if (Model* s = i.second.get()) { + out->AddModel(s); + } + } + for (auto&& i : collide_models_) { + if (CollideModel* m = i.second.get()) { + out->AddCollideModel(m); + } + } + + // Add scene's nodes. + if (scene_.exists()) { + scene_->DumpNodes(out); + } + + // Ok, now we can fill out our materials since nodes/etc they reference + // exists. + for (auto&& i : materials_) { + if (Material* m = i.get()) { + m->DumpComponents(out); + } + } +} + +auto HostActivity::NewTimer(TimeType timetype, TimerMedium length, bool repeat, + const Object::Ref& runnable) -> int { + // Make sure the runnable passed in is reference-managed already. + // (we may not add an initial reference ourself) + assert(runnable->is_valid_refcounted_object()); + + // We currently support game and base timers. + switch (timetype) { + case TimeType::kSim: + return NewSimTimer(length, repeat, runnable); + case TimeType::kBase: + return NewBaseTimer(length, repeat, runnable); + default: + // Fall back to default for descriptive error otherwise. + return ContextTarget::NewTimer(timetype, length, repeat, runnable); + } +} + +void HostActivity::DeleteTimer(TimeType timetype, int timer_id) { + switch (timetype) { + case TimeType::kSim: + DeleteSimTimer(timer_id); + break; + case TimeType::kBase: + DeleteBaseTimer(timer_id); + break; + default: + // Fall back to default for descriptive error otherwise. + ContextTarget::DeleteTimer(timetype, timer_id); + break; + } +} + +auto HostActivity::GetTime(TimeType timetype) -> millisecs_t { + switch (timetype) { + case TimeType::kSim: + return scene()->time(); + case TimeType::kBase: + return base_time(); + default: + // Fall back to default for descriptive error otherwise. + return ContextTarget::GetTime(timetype); + } +} + +} // namespace ballistica diff --git a/src/ballistica/game/host_activity.h b/src/ballistica/game/host_activity.h new file mode 100644 index 00000000..c8c9f530 --- /dev/null +++ b/src/ballistica/game/host_activity.h @@ -0,0 +1,120 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_HOST_ACTIVITY_H_ +#define BALLISTICA_GAME_HOST_ACTIVITY_H_ + +#include +#include +#include + +#include "ballistica/core/context.h" +#include "ballistica/generic/timer_list.h" +#include "ballistica/python/python_ref.h" + +namespace ballistica { + +class HostActivity : public ContextTarget { + public: + explicit HostActivity(HostSession* host_session); + ~HostActivity() override; + auto GetHostSession() -> HostSession* override; + void SetGameSpeed(float speed); + auto game_speed() const -> float { return game_speed_; } + + // ContextTarget time/timer support. + auto NewTimer(TimeType timetype, TimerMedium length, bool repeat, + const Object::Ref& runnable) -> int override; + void DeleteTimer(TimeType timetype, int timer_id) override; + auto GetTime(TimeType timetype) -> millisecs_t override; + + /// Return a borrowed ref to the python activity; Py_None if nonexistent. + auto GetPyActivity() const -> PyObject*; + + // All these commands are propagated into the output stream + // in addition to being applied locally. + auto NewMaterial(const std::string& name) -> Object::Ref; + auto GetTexture(const std::string& name) -> Object::Ref override; + auto GetSound(const std::string& name) -> Object::Ref override; + auto GetData(const std::string& name) -> Object::Ref override; + auto GetModel(const std::string& name) -> Object::Ref override; + auto GetCollideModel(const std::string& name) + -> Object::Ref override; + auto Update(millisecs_t time_advance) -> millisecs_t; + auto base_time() const -> millisecs_t { return base_time_; } + auto scene() -> Scene* { + assert(scene_.exists()); + return scene_.get(); + } + void start(); + + // A utility function; faster than dynamic_cast. + auto GetAsHostActivity() -> HostActivity* override; + auto GetMutableScene() -> Scene* override; + void Draw(FrameDef* frame_def); + void ScreenSizeChanged(); + void LanguageChanged(); + void DebugSpeedMultChanged(); + void GraphicsQualityChanged(GraphicsQuality q); + + // Used to register python calls created in this context so we can make sure + // they got properly cleaned up. + void RegisterCall(PythonContextCall* call); + auto shutting_down() const -> bool { return shutting_down_; } + auto globals_node() const -> GlobalsNode*; + void SetPaused(bool val); + auto paused() const -> bool { return paused_; } + void setAllowKickIdlePlayers(bool val) { allow_kick_idle_players_ = val; } + auto getAllowKickIdlePlayers() const -> bool { + return allow_kick_idle_players_; + } + auto GetGameStream() const -> GameStream*; + void DumpFullState(GameStream* out); + + private: + auto NewSimTimer(millisecs_t length, bool repeat, + const Object::Ref& runnable) -> int; + void DeleteSimTimer(int timer_id); + auto NewBaseTimer(millisecs_t length, bool repeat, + const Object::Ref& runnable) -> int; + void DeleteBaseTimer(int timer_id); + void UpdateStepTimerLength(); + Object::WeakRef globals_node_; + void SetIsForeground(bool val); + bool allow_kick_idle_players_ = false; + void StepScene(); + Timer* step_scene_timer_ = nullptr; + std::map > textures_; + std::map > sounds_; + std::map > datas_; + std::map > collide_models_; + std::map > models_; + std::list > materials_; + bool shutting_down_ = false; + + // Our list of python calls created in the context of this activity; + // we clear them as we are shutting down and ensure nothing runs after + // that point. + std::list > python_calls_; + millisecs_t next_prune_time_ = 0; + bool _started = false; + int out_of_bounds_in_a_row_ = 0; + void HandleOutOfBoundsNodes(); + bool paused_ = false; + float game_speed_ = 0.0f; + millisecs_t base_time_ = 0; + Object::Ref scene_; + Object::WeakRef host_session_; + PythonRef py_activity_weak_ref_; + void RegisterPyActivity(PyObject* pyActivity); + + // Want this at the bottom so it dies first since this may cause python stuff + // to access us. + TimerList sim_timers_; + TimerList base_timers_; + friend class HostSession; + friend class GlobalsNode; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_HOST_ACTIVITY_H_ diff --git a/src/ballistica/game/player.cc b/src/ballistica/game/player.cc new file mode 100644 index 00000000..2afdd229 --- /dev/null +++ b/src/ballistica/game/player.cc @@ -0,0 +1,418 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/game/player.h" + +#include + +#include "ballistica/game/host_activity.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/generic/utils.h" +#include "ballistica/input/device/joystick.h" +#include "ballistica/python/class/python_class_session_player.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_context_call.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" + +namespace ballistica { + +Player::Player(int id_in, HostSession* host_session) + : id_(id_in), creation_time_(GetRealTime()), host_session_(host_session) { + assert(host_session); + assert(InGameThread()); +} + +Player::~Player() { + assert(InGameThread()); + + // If we have an input-device attached to us, detach it. + InputDevice* input_device = input_device_.get(); + if (input_device) { + input_device->DetachFromPlayer(); + } + + // Release our ref to ourself if we have one. + if (py_ref_) { + Py_DECREF(py_ref_); + } +} + +auto Player::GetName(bool full, bool icon) const -> std::string { + std::string n = full ? full_name_ : name_; + + // Quasi-hacky: if they ask for no icon, strip the first char off our string + // if its in the custom-use-range. + if (!icon) { + std::vector uni = Utils::UnicodeFromUTF8(n, "3f94f4f"); + if (!uni.empty() && uni[0] >= 0xE000 && uni[0] <= 0xF8FF) { + uni.erase(uni.begin()); + } + return Utils::UTF8FromUnicode(uni); + } else { + return n; + } +} + +auto Player::GetHostActivity() const -> HostActivity* { + return host_activity_.get(); +} + +void Player::SetHostActivity(HostActivity* a) { + assert(InGameThread()); + + // Make sure we get pulled out of one activity before being added to another. + if (a && in_activity_) { + std::string old_name = + host_activity_.exists() + ? PythonRef(host_activity_->GetPyActivity(), PythonRef::kAcquire) + .Str() + : ""; + std::string new_name = + PythonRef(a->GetPyActivity(), PythonRef::kAcquire).Str(); + BA_LOG_PYTHON_TRACE_ONCE( + "Player::SetHostActivity() called when already in an activity (old=" + + old_name + ", new=" + new_name + ")"); + } else if (!a && !in_activity_) { + BA_LOG_PYTHON_TRACE_ONCE( + "Player::SetHostActivity() called with nullptr when not in an " + "activity"); + } + host_activity_ = a; + in_activity_ = (a != nullptr); +} + +void Player::SetPosition(const Vector3f& position) { + position_ = position; + have_position_ = true; +} + +void Player::ResetInput() { + // Hold a ref to ourself while clearing this to make sure + // we don't die midway as a result of freeing something. + Object::Ref ref(this); + calls_.clear(); + left_held_ = right_held_ = up_held_ = down_held_ = have_position_ = false; +} + +void Player::SetPyTeam(PyObject* team) { + if (team != nullptr && team != Py_None) { + // We store a weak-ref to this. + py_team_weak_ref_.Steal(PyWeakref_NewRef(team, nullptr)); + } else { + py_team_weak_ref_.Release(); + } +} + +auto Player::GetPyTeam() -> PyObject* { + PyObject* obj = py_team_weak_ref_.get(); + if (!obj) { + return Py_None; + } + return PyWeakref_GetObject(obj); +} + +void Player::SetPyCharacter(PyObject* character) { + if (character != nullptr && character != Py_None) { + py_character_.Acquire(character); + } else { + py_character_.Release(); + } +} + +auto Player::GetPyCharacter() -> PyObject* { + return py_character_.exists() ? py_character_.get() : Py_None; +} + +void Player::SetPyColor(PyObject* c) { py_color_.Acquire(c); } +auto Player::GetPyColor() -> PyObject* { + return py_color_.exists() ? py_color_.get() : Py_None; +} + +void Player::SetPyHighlight(PyObject* c) { py_highlight_.Acquire(c); } +auto Player::GetPyHighlight() -> PyObject* { + return py_highlight_.exists() ? py_highlight_.get() : Py_None; +} + +void Player::SetPyActivityPlayer(PyObject* c) { py_activityplayer_.Acquire(c); } +auto Player::GetPyActivityPlayer() -> PyObject* { + return py_activityplayer_.exists() ? py_activityplayer_.get() : Py_None; +} + +auto Player::GetPyRef(bool new_ref) -> PyObject* { + assert(InGameThread()); + if (py_ref_ == nullptr) { + py_ref_ = PythonClassSessionPlayer::Create(this); + } + if (new_ref) { + Py_INCREF(py_ref_); + } + return py_ref_; +} + +void Player::AssignInputCall(InputType type, PyObject* call_obj) { + assert(InGameThread()); + assert(static_cast(type) >= 0 + && static_cast(type) < static_cast(InputType::kLast)); + + // Special case: if they're assigning hold-position-press or + // hold-position-release, or any direction events, we add in a hold-position + // press/release event before we deliver any other events.. that way newly + // created stuff is informed of the hold state and doesn't wrongly think they + // should start moving. + switch (type) { + case InputType::kHoldPositionPress: + case InputType::kHoldPositionRelease: + case InputType::kLeftPress: + case InputType::kLeftRelease: + case InputType::kRightPress: + case InputType::kUpPress: + case InputType::kUpRelease: + case InputType::kDownPress: + case InputType::kDownRelease: + case InputType::kUpDown: + case InputType::kLeftRight: { + send_hold_state_ = true; + break; + } + default: + break; + } + if (call_obj) { + calls_[static_cast(type)] = Object::New(call_obj); + } else { + calls_[static_cast(type)].Clear(); + } + + // If they assigned l/r, immediately send an update for its current value. + if (type == InputType::kLeftRight) { + RunInput(type, lr_state_); + } + + // Same for up/down. + if (type == InputType::kUpDown) { + RunInput(type, ud_state_); + } + + // Same for run. + if (type == InputType::kRun) { + RunInput(type, run_state_); + } + + // Same for fly. + if (type == InputType::kFlyPress && fly_held_) { + RunInput(type); + } +} + +void Player::RunInput(InputType type, float value) { + assert(InGameThread()); + + const float threshold = kJoystickDiscreteThresholdFloat; + + // Most input commands cause us to reset the player's time-out + // there are a few exceptions though - very small analog values + // get ignored since they can come through without user intervention. + bool reset_time_out = true; + if (type == InputType::kLeftRight || type == InputType::kUpDown) { + if (std::abs(value) < 0.3f) { + reset_time_out = false; + } + } + if (type == InputType::kRun) { + if (value < 0.3f) { + reset_time_out = false; + } + } + + // Also ignore hold-position stuff since it can come through without user + // interaction. + if ((type == InputType::kHoldPositionPress) + || (type == InputType::kHoldPositionRelease)) + reset_time_out = false; + + if (reset_time_out) { + time_out_ = BA_PLAYER_TIME_OUT; + } + + // Keep track of the hold-position state that comes through here. + // any-time hold position buttons are re-assigned, we subsequently + // re-send the current hold-state so whatever its driving starts out correctly + // held if need be. + if (type == InputType::kHoldPositionPress) { + hold_position_ = true; + } else if (type == InputType::kHoldPositionRelease) { + hold_position_ = false; + } else if (type == InputType::kFlyPress) { + fly_held_ = true; + } else if (type == InputType::kFlyRelease) { + fly_held_ = false; + } + + // If we were supposed to deliver hold-state, go ahead and do that first. + if (send_hold_state_) { + send_hold_state_ = false; + if (hold_position_) { + RunInput(InputType::kHoldPositionPress); + } else { + RunInput(InputType::kHoldPositionRelease); + } + } + + // Let's make our life simpler by converting held-position-joystick-events.. + { + // We need to store these since we might look at them during a hold-position + // event when we don't have their originating events available. + if (type == InputType::kLeftRight) { + lr_state_ = value; + } + if (type == InputType::kUpDown) { + ud_state_ = value; + } + if (type == InputType::kRun) { + run_state_ = value; + } + + // Special input commands - keep track of left/right and up/down positions + // so we can deliver simple "leftUp", "leftDown", etc type of events + // in addition to the standard absolute leftRight positions, etc. + if (type == InputType::kLeftRight || type == InputType::kHoldPositionPress + || type == InputType::kHoldPositionRelease) { + float arg = lr_state_; + if (hold_position_) { + arg = 0.0f; // Throttle is off. + } + if (left_held_) { + if (arg > -threshold) { + left_held_ = false; + RunInput(InputType::kLeftRelease); + } + } else if (right_held_) { + if (arg < threshold) { + right_held_ = false; + RunInput(InputType::kRightRelease); + } + } else { + if (arg >= threshold) { + if (!left_held_ && !up_held_ && !down_held_) { + right_held_ = true; + RunInput(InputType::kRightPress); + } + } else if (arg <= -threshold) { + if (!right_held_ && !up_held_ && !down_held_) { + left_held_ = true; + RunInput(InputType::kLeftPress); + } + } + } + } + if (type == InputType::kUpDown || type == InputType::kHoldPositionPress + || type == InputType::kHoldPositionRelease) { + float arg = ud_state_; + if (hold_position_) arg = 0.0f; // throttle is off; + if (up_held_) { + if (arg < threshold) { + up_held_ = false; + RunInput(InputType::kUpRelease); + } + } else if (down_held_) { + if (arg > -threshold) { + down_held_ = false; + RunInput(InputType::kDownRelease); + } + } else { + if (arg <= -threshold) { + if (!left_held_ && !right_held_ && !up_held_) { + down_held_ = true; + RunInput(InputType::kDownPress); + } + } else if (arg >= threshold) { + if (!left_held_ && !up_held_ && !right_held_) { + up_held_ = true; + RunInput(InputType::kUpPress); + } + } + } + } + } + + auto j = calls_.find(static_cast(type)); + if (j != calls_.end() && j->second.exists()) { + if (type == InputType::kRun) { + PythonRef args( + Py_BuildValue("(f)", std::min(1.0f, std::max(0.0f, value))), + PythonRef::kSteal); + j->second->Run(args.get()); + } else if (type == InputType::kLeftRight || type == InputType::kUpDown) { + PythonRef args( + Py_BuildValue("(f)", std::min(1.0f, std::max(-1.0f, value))), + PythonRef::kSteal); + j->second->Run(args.get()); + } else { + j->second->Run(); + } + } +} + +auto Player::GetHostSession() const -> HostSession* { + return host_session_.get(); +} + +void Player::SetName(const std::string& name, const std::string& full_name, + bool is_real) { + assert(InGameThread()); + HostSession* host_session = GetHostSession(); + BA_PRECONDITION(host_session); + name_is_real_ = is_real; + name_ = host_session->GetUnusedPlayerName(this, name); + full_name_ = full_name; + + // If we're already in the game and our name is changing, we need to update + // the roster. + if (accepted_) { + g_game->UpdateGameRoster(); + } +} + +void Player::InputCommand(InputType type, float value) { + assert(InGameThread()); + switch (type) { + case InputType::kUpDown: + case InputType::kLeftRight: + case InputType::kRun: + RunInput(type, value); + break; + // case InputType::kReset: + // Log("Error: FIXME: player-input-reset command unimplemented"); + // break; + default: + RunInput(type); + break; + } +} + +void Player::SetInputDevice(InputDevice* input_device) { + input_device_ = input_device; +} + +auto Player::GetPublicAccountID() const -> std::string { + assert(InGameThread()); + if (input_device_.exists()) { + return input_device_->GetPublicAccountID(); + } + return ""; +} + +void Player::SetIcon(const std::string& tex_name, + const std::string& tint_tex_name, + const std::vector& tint_color, + const std::vector& tint2_color) { + assert(tint_color.size() == 3); + assert(tint2_color.size() == 3); + icon_tex_name_ = tex_name; + icon_tint_tex_name_ = tint_tex_name; + icon_tint_color_ = tint_color; + icon_tint2_color_ = tint2_color; + icon_set_ = true; +} + +} // namespace ballistica diff --git a/src/ballistica/game/player.h b/src/ballistica/game/player.h new file mode 100644 index 00000000..00b33c4c --- /dev/null +++ b/src/ballistica/game/player.h @@ -0,0 +1,167 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_PLAYER_H_ +#define BALLISTICA_GAME_PLAYER_H_ + +#include +#include +#include + +#include "ballistica/core/object.h" +#include "ballistica/input/input.h" +#include "ballistica/math/vector3f.h" +#include "ballistica/scene/scene.h" + +// How much time should pass before we kick idle players (in milliseconds). +#define BA_PLAYER_TIME_OUT 60000 +#define BA_PLAYER_TIME_OUT_WARN 10000 + +namespace ballistica { + +// A player (from the game's point of view). +class Player : public Object { + public: + Player(int id, HostSession* host_session); + ~Player() override; + + void SetInputDevice(InputDevice* input_device); + void AssignInputCall(InputType type, PyObject* call_obj); + void InputCommand(InputType type, float value = 0.0f); + + void SetName(const std::string& name, const std::string& full_name, + bool real); + auto GetName(bool full = false, bool icon = true) const -> std::string; + auto name_is_real() const -> bool { return name_is_real_; } + void ResetInput(); + auto GetHostSession() const -> HostSession*; + + auto id() const -> int { return id_; } + + auto NewPyRef() -> PyObject* { return GetPyRef(true); } + auto BorrowPyRef() -> PyObject* { return GetPyRef(false); } + + // Set the player node for the current activity. + void set_node(Node* node) { + assert(InGameThread()); + node_ = node; + } + auto node() const -> Node* { + assert(InGameThread()); + return node_.get(); + } + + void SetPyTeam(PyObject* team); + auto GetPyTeam() -> PyObject*; // Returns a borrowed ref. + + void SetPyCharacter(PyObject* team); + auto GetPyCharacter() -> PyObject*; // Returns a borrowed ref. + + void SetPyColor(PyObject* team); + auto GetPyColor() -> PyObject*; // Returns a borrowed ref. + + void SetPyHighlight(PyObject* team); + auto GetPyHighlight() -> PyObject*; // Returns a borrowed ref. + + void SetPyActivityPlayer(PyObject* team); + auto GetPyActivityPlayer() -> PyObject*; // Returns a borrowed ref. + + void set_has_py_data(bool has) { has_py_data_ = has; } + auto has_py_data() const -> bool { return has_py_data_; } + + auto GetInputDevice() const -> InputDevice* { return input_device_.get(); } + auto GetAge() const -> millisecs_t { return GetRealTime() - creation_time_; } + auto accepted() const -> bool { return accepted_; } + + void SetPosition(const Vector3f& position); + + // If an public account-id can be determined with relative + // certainty for this player, returns it. Otherwise returns + // an empty string. + auto GetPublicAccountID() const -> std::string; + + void SetHostActivity(HostActivity* host_activity); + auto GetHostActivity() const -> HostActivity*; + + auto has_py_ref() -> bool { return (py_ref_ != nullptr); } + + void SetIcon(const std::string& tex_name, const std::string& tint_tex_name, + const std::vector& tint_color, + const std::vector& tint2_color); + + auto icon_tex_name() const -> const std::string& { + BA_PRECONDITION(icon_set_); + return icon_tex_name_; + } + auto icon_tint_tex_name() const -> const std::string& { + BA_PRECONDITION(icon_set_); + return icon_tint_tex_name_; + } + auto icon_tint_color() const -> const std::vector& { + BA_PRECONDITION(icon_set_); + return icon_tint_color_; + } + auto icon_tint2_color() const -> const std::vector& { + BA_PRECONDITION(icon_set_); + return icon_tint2_color_; + } + void set_accepted(bool value) { accepted_ = value; } + auto time_out() const -> millisecs_t { return time_out_; } + void set_time_out(millisecs_t value) { time_out_ = value; } + void set_have_position(bool value) { have_position_ = value; } + + private: + auto GetPyRef(bool new_ref) -> PyObject*; + void RunInput(InputType type, float value = 0.0f); + bool icon_set_{}; + std::string icon_tex_name_; + std::string icon_tint_tex_name_; + std::vector icon_tint_color_; + std::vector icon_tint2_color_; + Object::WeakRef host_session_; + Object::WeakRef host_activity_; + Object::WeakRef node_; + bool in_activity_{}; + Object::WeakRef input_device_; + PyObject* py_ref_{}; + bool accepted_{}; + bool has_py_data_{}; + millisecs_t creation_time_{}; + int id_{}; + std::string name_; + std::string full_name_; + + // Is the current name real (as opposed to a standin + // title such as '') + bool name_is_real_{}; + bool left_held_{}; + bool right_held_{}; + bool up_held_{}; + bool down_held_{}; + bool hold_position_{}; + bool send_hold_state_{}; + bool fly_held_{}; + float lr_state_{}; + float ud_state_{}; + float run_state_{}; + millisecs_t time_out_{BA_PLAYER_TIME_OUT}; + + // Player's position for use by input devices and whatnot for guides. + // FIXME: This info should be acquired through the player node. + bool have_position_{false}; + Vector3f position_{0.0f, 0.0f, 0.0f}; + + // These should be destructed before the rest of our class goes down, + // so they should be here at the bottom.. + // (they might access our name string or other stuff declared above) + // PythonRef py_actor_; + PythonRef py_team_weak_ref_; + PythonRef py_character_; + PythonRef py_color_; + PythonRef py_highlight_; + PythonRef py_activityplayer_; + std::map > calls_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_PLAYER_H_ diff --git a/src/ballistica/game/player_spec.cc b/src/ballistica/game/player_spec.cc new file mode 100644 index 00000000..ecdb7462 --- /dev/null +++ b/src/ballistica/game/player_spec.cc @@ -0,0 +1,109 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/game/player_spec.h" + +#include + +#include "ballistica/app/app_globals.h" +#include "ballistica/game/account.h" +#include "ballistica/generic/json.h" +#include "ballistica/generic/utils.h" +#include "ballistica/platform/platform.h" + +namespace ballistica { + +PlayerSpec::PlayerSpec() : account_type_(AccountType::kInvalid) {} + +PlayerSpec::PlayerSpec(const std::string& s) { + cJSON* root_obj = cJSON_Parse(s.c_str()); + bool success = false; + if (root_obj) { + cJSON* name_obj = cJSON_GetObjectItem(root_obj, "n"); + cJSON* short_name_obj = cJSON_GetObjectItem(root_obj, "sn"); + cJSON* account_obj = cJSON_GetObjectItem(root_obj, "a"); + if (name_obj && short_name_obj && account_obj) { + name_ = Utils::GetValidUTF8(name_obj->valuestring, "psps"); + short_name_ = Utils::GetValidUTF8(short_name_obj->valuestring, "psps2"); + + // Account type may technically be something we don't recognize, + // but that's ok.. it'll just be 'invalid' to us in that case + account_type_ = Account::AccountTypeFromString(account_obj->valuestring); + success = true; + } + cJSON_Delete(root_obj); + } + if (!success) { + Log("Error creating PlayerSpec from string: '" + s + "'"); + name_ = ""; + short_name_ = ""; + account_type_ = AccountType::kInvalid; + } +} + +auto PlayerSpec::GetDisplayString() const -> std::string { + return Account::AccountTypeToIconString(account_type_) + name_; +} + +auto PlayerSpec::GetShortName() const -> std::string { + if (short_name_.empty()) { + return name_; + } + return short_name_; +} + +auto PlayerSpec::operator==(const PlayerSpec& spec) const -> bool { + // NOTE: need to add account ID in here once that's available + return (spec.name_ == name_ && spec.short_name_ == short_name_ + && spec.account_type_ == account_type_); +} + +auto PlayerSpec::GetSpecString() const -> std::string { + cJSON* root; + root = cJSON_CreateObject(); + cJSON_AddStringToObject(root, "n", name_.c_str()); + cJSON_AddStringToObject(root, "a", + Account::AccountTypeToString(account_type_).c_str()); + cJSON_AddStringToObject(root, "sn", short_name_.c_str()); + char* out = cJSON_PrintUnformatted(root); + std::string out_s = out; + free(out); + cJSON_Delete(root); + + // We should never allow ourself to have all this add up to more than 256. + assert(out_s.size() < 256); + + return out_s; +} + +auto PlayerSpec::GetAccountPlayerSpec() -> PlayerSpec { + PlayerSpec spec; + if (g_account->GetAccountState() == AccountState::kSignedIn) { + spec.account_type_ = g_app_globals->account_type; + spec.name_ = + Utils::GetValidUTF8(g_account->GetAccountName().c_str(), "bsgaps"); + } else { + spec.name_ = + Utils::GetValidUTF8(g_platform->GetDeviceName().c_str(), "bsgaps2"); + } + if (spec.name_.size() > 100) { + // FIXME should perhaps clamp this in unicode space + Log("account name size too long: '" + spec.name_ + "'"); + spec.name_.resize(100); + spec.name_ = Utils::GetValidUTF8(spec.name_.c_str(), "bsgaps3"); + } + return spec; +} + +auto PlayerSpec::GetDummyPlayerSpec(const std::string& name) -> PlayerSpec { + PlayerSpec spec; + spec.name_ = Utils::GetValidUTF8(name.c_str(), "bsgdps1"); + if (spec.name_.size() > 100) { + // FIXME should perhaps clamp this in unicode space + Log("dummy player spec name too long: '" + spec.name_ + "'"); + spec.name_.resize(100); + spec.name_ = Utils::GetValidUTF8(spec.name_.c_str(), "bsgdps2"); + } + return spec; +} + +} // namespace ballistica diff --git a/src/ballistica/game/player_spec.h b/src/ballistica/game/player_spec.h new file mode 100644 index 00000000..a6bf948f --- /dev/null +++ b/src/ballistica/game/player_spec.h @@ -0,0 +1,56 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_PLAYER_SPEC_H_ +#define BALLISTICA_GAME_PLAYER_SPEC_H_ + +#include + +#include "ballistica/ballistica.h" + +namespace ballistica { + +// a PlayerSpec is a portable description of an entity such as a player or +// client. It can contain long and short names, optional info linking it to a +// real account, and can be passed around easily in string form. +class PlayerSpec { + public: + // inits an invalid player-spec + PlayerSpec(); + auto operator==(const PlayerSpec& spec) const -> bool; + + // create a player-spec from a given spec-string. + // in the case of an error, defaults will be used + // (though the error will be reported) + explicit PlayerSpec(const std::string& s); + + // this returns a full display string for the spec, + // which may include the account icon + auto GetDisplayString() const -> std::string; + + // returns a short version of the player's name + // ideal for displaying in-game; this includes + // no icon and may just be the first name + auto GetShortName() const -> std::string; + + // return the full string form to be passed around + auto GetSpecString() const -> std::string; + + // returns a PlayerSpec for the currently logged in account + // if there is no current logged in account, a dummy-spec is created + // using the device name (so this always returns something reasonable) + static auto GetAccountPlayerSpec() -> PlayerSpec; + + // returns a 'dummy' PlayerSpec using the given name; can be + // used for non-account player profiles, names for non-logged-in + // party hosts, etc. + static auto GetDummyPlayerSpec(const std::string& name) -> PlayerSpec; + + private: + std::string name_; + std::string short_name_; + AccountType account_type_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_PLAYER_SPEC_H_ diff --git a/src/ballistica/game/score_to_beat.h b/src/ballistica/game/score_to_beat.h new file mode 100644 index 00000000..39b084b8 --- /dev/null +++ b/src/ballistica/game/score_to_beat.h @@ -0,0 +1,28 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_SCORE_TO_BEAT_H_ +#define BALLISTICA_GAME_SCORE_TO_BEAT_H_ + +#include +#include + +namespace ballistica { + +// Do we still need this? +class ScoreToBeat { + public: + ScoreToBeat(std::string player_in, std::string type_in, std::string value_in, + double timeIn) + : player(std::move(player_in)), + type(std::move(type_in)), + value(std::move(value_in)), + time(timeIn) {} + std::string player; + std::string type; + std::string value; + double time; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_SCORE_TO_BEAT_H_ diff --git a/src/ballistica/game/session/client_session.cc b/src/ballistica/game/session/client_session.cc new file mode 100644 index 00000000..d0013f3c --- /dev/null +++ b/src/ballistica/game/session/client_session.cc @@ -0,0 +1,1070 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/game/session/client_session.h" + +#include "ballistica/app/app_globals.h" +#include "ballistica/audio/audio.h" +#include "ballistica/dynamics/bg/bg_dynamics.h" +#include "ballistica/dynamics/material/material.h" +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/dynamics/material/material_component.h" +#include "ballistica/dynamics/material/material_condition_node.h" +#include "ballistica/dynamics/rigid_body.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/media/component/collide_model.h" +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/networking/networking.h" +#include "ballistica/python/python.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +// How many buckets we keep around for calculating max-lag-spikes. +const int kDelayBufferBuckets = 20; + +ClientSession::ClientSession() + : base_time_(0), + least_buffered_count_list_(kDelayBufferBuckets, 0), + most_buffered_count_list_(kDelayBufferBuckets, 0), + adjust_counter_(0), + buffer_count_list_index_(0), + steps_on_list_(0), + current_cmd_ptr_(nullptr), + shutting_down_(false) { + ClearSessionObjs(); +} + +void ClientSession::Reset(bool rewind) { + assert(!shutting_down_); + OnReset(rewind); +} + +void ClientSession::OnReset(bool rewind) { + ClearSessionObjs(); + target_base_time_ = base_time_ = 0; +} + +void ClientSession::ClearSessionObjs() { + scenes_.clear(); + nodes_.clear(); + textures_.clear(); + models_.clear(); + sounds_.clear(); + collide_models_.clear(); + materials_.clear(); + commands_pending_.clear(); + commands_.clear(); + steps_on_list_ = 0; +} + +auto ClientSession::DoesFillScreen() const -> bool { + // Look for any scene that has something that covers the background. + for (const auto& scene : scenes_) { + if ((scene.exists()) && (*scene).has_bg_cover()) { + return true; + } + } + return false; +} + +void ClientSession::Draw(FrameDef* f) { + // Just go through and draw all of our scenes. + for (auto&& i : scenes_) { + // NOTE - here we draw scenes in the order they were created, but + // in a host-session we draw session first followed by activities + // (that should be the same order in both cases, but just something to keep + // in mind...) + if (i.exists()) { + i->Draw(f); + } + } +} + +auto ClientSession::ReadByte() -> uint8_t { + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - 1) { + throw Exception("state read error"); + } + return *(current_cmd_ptr_++); +} + +auto ClientSession::ReadInt32() -> int32_t { + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - 4) { + throw Exception("state read error"); + } + int32_t val; + memcpy(&val, current_cmd_ptr_, sizeof(val)); + current_cmd_ptr_ += 4; + return val; +} + +auto ClientSession::ReadFloat() -> float { + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - 4) { + throw Exception("state read error"); + } + float val; + memcpy(&val, current_cmd_ptr_, 4); + current_cmd_ptr_ += 4; + return val; +} + +void ClientSession::ReadFloats(int count, float* vals) { + int size = 4 * count; + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - size) { + throw Exception("state read error"); + } + memcpy(vals, current_cmd_ptr_, static_cast(size)); + current_cmd_ptr_ += size; +} + +void ClientSession::ReadInt32s(int count, int32_t* vals) { + int size = 4 * count; + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - size) { + throw Exception("state read error"); + } + memcpy(vals, current_cmd_ptr_, static_cast(size)); + current_cmd_ptr_ += size; +} + +void ClientSession::ReadChars(int count, char* vals) { + int size = count; + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - size) { + throw Exception("state read error"); + } + memcpy(vals, current_cmd_ptr_, static_cast(size)); + current_cmd_ptr_ += size; +} + +void ClientSession::ReadInt32_3(int32_t* vals) { + size_t size = 3 * 4; + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - size) { + throw Exception("state read error"); + } + memcpy(vals, current_cmd_ptr_, size); + current_cmd_ptr_ += size; +} + +void ClientSession::ReadInt32_4(int32_t* vals) { + size_t size = 4 * 4; + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - size) { + throw Exception("state read error"); + } + memcpy(vals, current_cmd_ptr_, size); + current_cmd_ptr_ += size; +} + +void ClientSession::ReadInt32_2(int32_t* vals) { + size_t size = 2 * 4; + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - size) { + throw Exception("state read error"); + } + memcpy(vals, current_cmd_ptr_, size); + current_cmd_ptr_ += size; +} + +auto ClientSession::ReadString() -> std::string { + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - 4) { + throw Exception("state read error"); + } + int32_t size; + memcpy(&size, current_cmd_ptr_, sizeof(size)); + current_cmd_ptr_ += 4; + std::vector buffer(static_cast(size + 1)); + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - size) { + throw Exception("state read error"); + } + memcpy(&(buffer[0]), current_cmd_ptr_, static_cast(size)); + current_cmd_ptr_ += size; + return &(buffer[0]); +} + +void ClientSession::Update(int time_advance) { + if (shutting_down_) { + return; + } + + // Allow replays to modulate speed, etc. + time_advance = GetActualTimeAdvance(time_advance); + + target_base_time_ += static_cast(time_advance) * correction_; + + try { + // Read and run all events up to our target time. + while (base_time_ < target_base_time_) { + // If we need to do something explicit to keep messages flowing in. + // (informing the replay thread to feed us more, etc). + FetchMessages(); + + // If we've got another command on the list, pull it and run it. + if (!commands_.empty()) { + // Debugging: if this was previously pointed at a buffer, make sure we + // went exactly to the end. +#if BA_DEBUG_BUILD + if (current_cmd_ptr_ != nullptr) { + if (current_cmd_ptr_ != &(current_cmd_[0]) + current_cmd_.size()) { + Log("SIZE ERROR FOR CMD " + + std::to_string(static_cast(current_cmd_[0])) + + " expected " + std::to_string(current_cmd_.size()) + " got " + + std::to_string(current_cmd_ptr_ - &(current_cmd_[0]))); + } + } + assert(current_cmd_ptr_ == current_cmd_.data() + current_cmd_.size()); +#endif + + current_cmd_ = commands_.front(); + commands_.pop_front(); + current_cmd_ptr_ = &(current_cmd_[0]); + } else { + // Any time we run out of commands we immediately pull our target time + // back to where we currently are at. We want to stay *behind* the + // "buffering line". + target_base_time_ = base_time_; + OnCommandBufferUnderrun(); + return; + } + + auto cmd = static_cast(ReadByte()); + + switch (cmd) { + case SessionCommand::kBaseTimeStep: { + int32_t stepsize = ReadInt32(); + BA_PRECONDITION(stepsize > 0); + if (stepsize > 10000) { + throw Exception( + "got abnormally large stepsize; probably a corrupt stream"); + } + steps_on_list_ -= stepsize; + BA_PRECONDITION(steps_on_list_ >= 0); + base_time_ += stepsize; + break; + } + case SessionCommand::kDynamicsCorrection: { + bool blend = current_cmd_[1]; + uint32_t offset = 2; + uint16_t node_count; + memcpy(&node_count, current_cmd_.data() + offset, sizeof(node_count)); + offset += 2; + for (int i = 0; i < node_count; i++) { + uint32_t node_id; + memcpy(&node_id, current_cmd_.data() + offset, sizeof(node_id)); + offset += 4; + int body_count = current_cmd_[offset++]; + Node* n = + (node_id < nodes_.size()) ? nodes_[node_id].get() : nullptr; + for (int j = 0; j < body_count; j++) { + int bodyid = current_cmd_[offset++]; + uint16_t body_data_len; + memcpy(&body_data_len, current_cmd_.data() + offset, + sizeof(body_data_len)); + RigidBody* b = n ? n->GetRigidBody(bodyid) : nullptr; + offset += 2; + const char* p1 = reinterpret_cast(&(current_cmd_[offset])); + const char* p2 = p1; + if (b) { + dBodyID body = b->body(); + const dReal* p = dBodyGetPosition(body); + float old_x = p[0]; + float old_y = p[1]; + float old_z = p[2]; + b->ExtractFull(&p2); + if (p2 - p1 != body_data_len) + throw Exception("Invalid rbd correction data"); + if (blend) { + b->AddBlendOffset(old_x - p[0], old_y - p[1], old_z - p[2]); + } + } + offset += body_data_len; + if (offset > current_cmd_.size()) { + throw Exception("Invalid rbd correction data"); + } + } + if (offset > current_cmd_.size()) + throw Exception("Invalid rbd correction data"); + // extract custom per-node data + uint16_t custom_data_len; + memcpy(&custom_data_len, current_cmd_.data() + offset, + sizeof(custom_data_len)); + offset += 2; + if (custom_data_len != 0) { + std::vector data(custom_data_len); + memcpy(&(data[0]), &(current_cmd_[offset]), custom_data_len); + if (n) n->ApplyResyncData(data); + offset += custom_data_len; + } + if (offset > current_cmd_.size()) { + throw Exception("Invalid rbd correction data"); + } + } + if (offset != current_cmd_.size()) { + throw Exception("invalid rbd correction data"); + } + current_cmd_ptr_ = &(current_cmd_[0]) + offset; + + break; + } + case SessionCommand::kEndOfFile: { + // EOF can happen anytime if they run out of disk space/etc. + // We should expect any state. + Reset(true); + break; + } + case SessionCommand::kAddSceneGraph: { + int32_t cmdvals[2]; + ReadInt32_2(cmdvals); + int32_t id = cmdvals[0]; + millisecs_t starttime = cmdvals[1]; + if (id < 0 || id > 100) { + throw Exception("invalid scene id"); + } + if (static_cast(scenes_.size()) < (id + 1)) { + scenes_.resize(static_cast(id) + 1); + } + assert(!scenes_[id].exists()); + scenes_[id] = Object::New(starttime); + scenes_[id]->stream_id_ = id; + break; + } + case SessionCommand::kRemoveSceneGraph: { + int32_t id = ReadInt32(); + GetScene(id); // Make sure it's valid. + scenes_[id].Clear(); + break; + } + case SessionCommand::kStepSceneGraph: { + int32_t val = ReadInt32(); + Scene* sg = GetScene(val); + sg->Step(); + break; + } + case SessionCommand::kAddNode: { + int32_t vals[3]; // scene-id, nodetype-id, node-id + ReadInt32_3(vals); + Scene* scene = GetScene(vals[0]); + assert(g_app_globals != nullptr); + if (vals[1] < 0 + || vals[1] >= static_cast( + g_app_globals->node_types_by_id.size())) { + throw Exception("invalid node type id"); + } + + NodeType* node_type = g_app_globals->node_types_by_id[vals[1]]; + + // Fail if we get a ridiculous number of nodes. + // FIXME: should enforce this on the server side too. + int id = vals[2]; + if (id < 0 || id > 10000) throw Exception("invalid node id"); + if (static_cast(nodes_.size()) < (id + 1)) { + nodes_.resize(static_cast(id) + 1); + } + assert(!nodes_[id].exists()); + { + ScopedSetContext _cp(this); + nodes_[id] = scene->NewNode(node_type->name(), "", nullptr); + nodes_[id]->set_stream_id(id); + } + break; + } + case SessionCommand::kSetForegroundSceneGraph: { + Scene* scene = GetScene(ReadInt32()); + g_game->SetForegroundScene(scene); + break; + } + case SessionCommand::kNodeMessage: { + int32_t vals[2]; + ReadInt32_2(vals); + Node* n = GetNode(vals[0]); + int32_t msg_size = vals[1]; + if (msg_size < 1 || msg_size > 10000) { + throw Exception("invalid message"); + } + std::vector buffer(static_cast(msg_size)); + ReadChars(msg_size, &buffer[0]); + n->DispatchNodeMessage(&buffer[0]); + break; + } + case SessionCommand::kConnectNodeAttribute: { + int32_t vals[4]; + ReadInt32_4(vals); + Node* src_node = GetNode(vals[0]); + Node* dst_node = GetNode(vals[2]); + NodeAttributeUnbound* src_attr = + src_node->type()->GetAttribute(static_cast(vals[1])); + NodeAttributeUnbound* dst_attr = + dst_node->type()->GetAttribute(static_cast(vals[3])); + src_node->ConnectAttribute(src_attr, dst_node, dst_attr); + break; + } + case SessionCommand::kNodeOnCreate: { + Node* n = GetNode(ReadInt32()); + n->OnCreate(); + break; + } + case SessionCommand::kAddMaterial: { + int32_t vals[2]; // scene-id, material-id + ReadInt32_2(vals); + Scene* scene = GetScene(vals[0]); + // Fail if we get a ridiculous number of materials. + // FIXME: should enforce this on the server side too. + int id = vals[1]; + if (vals[1] < 0 || vals[1] >= 1000) { + throw Exception("invalid material id"); + } + if (static_cast(materials_.size()) < (id + 1)) + materials_.resize(static_cast(id) + 1); + assert(!materials_[id].exists()); + materials_[id] = Object::New("", scene); + materials_[id]->stream_id_ = id; + break; + } + case SessionCommand::kRemoveMaterial: { + int id = ReadInt32(); + GetMaterial(id); // make sure its valid + materials_[id].Clear(); + break; + } + case SessionCommand::kAddMaterialComponent: { + int32_t cmdvals[2]; + ReadInt32_2(cmdvals); + Material* m = GetMaterial(cmdvals[0]); + int component_size = cmdvals[1]; + if (component_size < 1 || component_size > 10000) { + throw Exception("invalid component"); + } + std::vector buffer(static_cast(component_size)); + ReadChars(component_size, &buffer[0]); + auto c(Object::New()); + const char* ptr1 = &buffer[0]; + const char* ptr2 = ptr1; + c->Restore(&ptr2, this); + BA_PRECONDITION(ptr2 - ptr1 == component_size); + m->AddComponent(c); + break; + } + case SessionCommand::kAddTexture: { + int32_t vals[2]; // scene-id, texture-id + ReadInt32_2(vals); + std::string name = ReadString(); + Scene* scene = GetScene(vals[0]); + // Fail if we get a ridiculous number of textures. + // FIXME: Should enforce this on the server side too. + int id = vals[1]; + if (vals[1] < 0 || vals[1] >= 1000) { + throw Exception("invalid texture id"); + } + if (static_cast(textures_.size()) < (id + 1)) { + textures_.resize(static_cast(id) + 1); + } + assert(!textures_[id].exists()); + textures_[id] = Object::New(name, scene); + textures_[id]->stream_id_ = id; + break; + } + case SessionCommand::kRemoveTexture: { + int id = ReadInt32(); + GetTexture(id); // make sure its valid + textures_[id].Clear(); + break; + } + case SessionCommand::kAddModel: { + int32_t vals[2]; // scene-id, model-id + ReadInt32_2(vals); + std::string name = ReadString(); + Scene* scene = GetScene(vals[0]); + + // Fail if we get a ridiculous number of models. + // FIXME: Should enforce this on the server side too. + int id = vals[1]; + if (vals[1] < 0 || vals[1] >= 1000) { + throw Exception("invalid model id"); + } + if (static_cast(models_.size()) < (id + 1)) { + models_.resize(static_cast(id) + 1); + } + assert(!models_[id].exists()); + models_[id] = Object::New(name, scene); + models_[id]->stream_id_ = id; + break; + } + case SessionCommand::kRemoveModel: { + int id = ReadInt32(); + GetModel(id); // make sure its valid + models_[id].Clear(); + break; + } + case SessionCommand::kAddSound: { + int32_t vals[2]; // scene-id, sound-id + ReadInt32_2(vals); + std::string name = ReadString(); + Scene* scene = GetScene(vals[0]); + // Fail if we get a ridiculous number of sounds. + // FIXME: Should enforce this on the server side too. + int id = vals[1]; + if (vals[1] < 0 || vals[1] >= 1000) { + throw Exception("invalid sound id"); + } + if (static_cast(sounds_.size()) < (id + 1)) { + sounds_.resize(static_cast(id) + 1); + } + assert(!sounds_[id].exists()); + sounds_[id] = Object::New(name, scene); + sounds_[id]->stream_id_ = id; + break; + } + case SessionCommand::kRemoveSound: { + int id = ReadInt32(); + GetSound(id); // make sure its valid + sounds_[id].Clear(); + break; + } + case SessionCommand::kAddCollideModel: { + int32_t vals[2]; // scene-id, collide_model-id + ReadInt32_2(vals); + std::string name = ReadString(); + Scene* scene = GetScene(vals[0]); + // Fail if we get a ridiculous number of collide_models. + // FIXME: Should enforce this on the server side too. + int id = vals[1]; + if (vals[1] < 0 || vals[1] >= 1000) { + throw Exception("invalid collide_model id"); + } + if (static_cast(collide_models_.size()) < (id + 1)) { + collide_models_.resize(static_cast(id) + 1); + } + assert(!collide_models_[id].exists()); + collide_models_[id] = Object::New(name, scene); + collide_models_[id]->stream_id_ = id; + break; + } + case SessionCommand::kRemoveCollideModel: { + int id = ReadInt32(); + GetCollideModel(id); // make sure its valid + collide_models_[id].Clear(); + break; + } + case SessionCommand::kRemoveNode: { + int id = ReadInt32(); + Node* n = GetNode(id); + n->scene()->DeleteNode(n); + assert(!nodes_[id].exists()); + break; + } + case SessionCommand::kSetNodeAttrFloat: { + int vals[2]; + ReadInt32_2(vals); + GetNode(vals[0])->GetAttribute(vals[1]).Set(ReadFloat()); + break; + } + case SessionCommand::kSetNodeAttrInt32: { + int32_t vals[3]; + ReadInt32_3(vals); + + // Note; we currently deal in 64 bit ints locally but read/write 32 + // bit over the wire. + GetNode(vals[0])->GetAttribute(vals[1]).Set( + static_cast(vals[2])); + break; + } + case SessionCommand::kSetNodeAttrBool: { + int vals[3]; + ReadInt32_3(vals); + GetNode(vals[0])->GetAttribute(vals[1]).Set( + static_cast(vals[2])); + break; + } + case SessionCommand::kSetNodeAttrFloats: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals(static_cast(count)); + if (count > 0) { + ReadFloats(count, &(vals[0])); + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); + break; + } + case SessionCommand::kSetNodeAttrInt32s: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals(static_cast(count)); + if (count > 0) { + ReadInt32s(count, &(vals[0])); + } + // Note: we currently deal in 64 bit ints locally but read/write 32 + // bit over the wire. Convert. + std::vector vals64(static_cast(count)); + for (int i = 0; i < count; i++) { + vals64[i] = vals[i]; + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals64); + break; + } + case SessionCommand::kSetNodeAttrString: { + int vals[2]; + ReadInt32_2(vals); + GetNode(vals[0])->GetAttribute(vals[1]).Set(ReadString()); + break; + } + case SessionCommand::kSetNodeAttrNode: { + int vals[3]; + ReadInt32_3(vals); + GetNode(vals[0])->GetAttribute(vals[1]).Set(GetNode(vals[2])); + break; + } + case SessionCommand::kSetNodeAttrNodeNull: { + int cmdvals[2]; + ReadInt32_2(cmdvals); + Node* val = nullptr; + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrTextureNull: { + int cmdvals[2]; + ReadInt32_2(cmdvals); + Texture* val = nullptr; + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrSoundNull: { + int cmdvals[2]; + ReadInt32_2(cmdvals); + Sound* val = nullptr; + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrModelNull: { + int cmdvals[2]; + ReadInt32_2(cmdvals); + Model* val = nullptr; + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrCollideModelNull: { + int cmdvals[2]; + ReadInt32_2(cmdvals); + CollideModel* val = nullptr; + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrNodes: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals_in(static_cast(count)); + std::vector vals(static_cast(count)); + if (count > 0) { + ReadInt32s(count, &(vals_in[0])); + } + for (int i = 0; i < count; i++) { + vals[i] = GetNode(vals_in[i]); + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); + break; + } + case SessionCommand::kSetNodeAttrTexture: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + Texture* val = GetTexture(cmdvals[2]); + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrTextures: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals_in(static_cast(count)); + std::vector vals(static_cast(count)); + if (count > 0) { + ReadInt32s(count, &(vals_in[0])); + } + for (int i = 0; i < count; i++) { + vals[i] = GetTexture(vals_in[i]); + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); + break; + } + case SessionCommand::kSetNodeAttrSound: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + Sound* val = GetSound(cmdvals[2]); + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrSounds: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals_in(static_cast(count)); + std::vector vals(static_cast(count)); + if (count > 0) { + ReadInt32s(count, &(vals_in[0])); + } + for (int i = 0; i < count; i++) { + vals[i] = GetSound(vals_in[i]); + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); + break; + } + case SessionCommand::kSetNodeAttrModel: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + Model* val = GetModel(cmdvals[2]); + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrModels: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals_in(static_cast(count)); + std::vector vals(static_cast(count)); + if (count > 0) { + ReadInt32s(count, &(vals_in[0])); + } + for (int i = 0; i < count; i++) { + vals[i] = GetModel(vals_in[i]); + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); + break; + } + case SessionCommand::kSetNodeAttrCollideModel: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + CollideModel* val = GetCollideModel(cmdvals[2]); + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrCollideModels: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals_in(static_cast(count)); + std::vector vals(static_cast(count)); + if (count > 0) { + ReadInt32s(count, &(vals_in[0])); + } + for (int i = 0; i < count; i++) { + vals[i] = GetCollideModel(vals_in[i]); + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); + break; + } + case SessionCommand::kSetNodeAttrMaterials: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals_in(static_cast(count)); + std::vector vals(static_cast(count)); + if (count > 0) { + ReadInt32s(count, &(vals_in[0])); + } + for (int i = 0; i < count; i++) { + vals[i] = GetMaterial(vals_in[i]); + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); + break; + } + case SessionCommand::kPlaySound: { + Sound* sound = GetSound(ReadInt32()); + float volume = ReadFloat(); + g_audio->PlaySound(sound->GetSoundData(), volume); + break; + } + case SessionCommand::kScreenMessageBottom: { + std::string val = ReadString(); + Vector3f color{}; + ReadFloats(3, color.v); + ScreenMessage(val, color); + break; + } + case SessionCommand::kScreenMessageTop: { + int cmdvals[2]; + ReadInt32_2(cmdvals); + Texture* texture = GetTexture(cmdvals[0]); + Texture* tint_texture = GetTexture(cmdvals[1]); + std::string s = ReadString(); + float f[9]; + ReadFloats(9, f); + g_graphics->AddScreenMessage( + s, Vector3f(f[0], f[1], f[2]), true, texture, tint_texture, + Vector3f(f[3], f[4], f[5]), Vector3f(f[6], f[7], f[8])); + break; + } + case SessionCommand::kPlaySoundAtPosition: { + Sound* sound = GetSound(ReadInt32()); + float volume = ReadFloat(); + float x = ReadFloat(); + float y = ReadFloat(); + float z = ReadFloat(); + g_audio->PlaySoundAtPosition(sound->GetSoundData(), volume, x, y, z); + break; + } + case SessionCommand::kEmitBGDynamics: { +#if !BA_HEADLESS_BUILD + BGDynamicsEmission e; +#endif + int cmdvals[4]; + ReadInt32_4(cmdvals); +#if !BA_HEADLESS_BUILD + e.emit_type = (BGDynamicsEmitType)cmdvals[0]; + e.count = cmdvals[1]; + e.chunk_type = (BGDynamicsChunkType)cmdvals[2]; + e.tendril_type = (BGDynamicsTendrilType)cmdvals[3]; +#endif + float vals[8]; + ReadFloats(8, vals); +#if !BA_HEADLESS_BUILD + e.position.x = vals[0]; + e.position.y = vals[1]; + e.position.z = vals[2]; + e.velocity.x = vals[3]; + e.velocity.y = vals[4]; + e.velocity.z = vals[5]; + e.scale = vals[6]; + e.spread = vals[7]; + g_bg_dynamics->Emit(e); +#endif + break; + } + default: + throw Exception("unrecognized stream command: " + + std::to_string(static_cast(cmd))); + } + } + } catch (const std::exception& e) { + Error(e.what()); + } +} // NOLINT (yes this is too long) + +ClientSession::~ClientSession() = default; + +void ClientSession::ScreenSizeChanged() { + // Let all our scenes know. + for (auto&& i : scenes_) { + if (Scene* sg = i.get()) { + sg->ScreenSizeChanged(); + } + } +} + +void ClientSession::LanguageChanged() { + // Let all our scenes know. + for (auto&& i : scenes_) { + if (Scene* sg = i.get()) { + sg->LanguageChanged(); + } + } +} + +auto ClientSession::GetScene(int id) const -> Scene* { + if (id < 0 || id >= static_cast(scenes_.size())) { + throw Exception("Invalid scene id"); + } + Scene* sg = scenes_[id].get(); + if (!sg) { + throw Exception("Invalid scene id"); + } + return sg; +} +auto ClientSession::GetNode(int id) const -> Node* { + if (id < 0 || id >= static_cast(nodes_.size())) { + throw Exception("Invalid node (out of range)"); + } + Node* n = nodes_[id].get(); + if (!n) { + throw Exception("Invalid node id (empty slot)"); + } + return n; +} +auto ClientSession::GetMaterial(int id) const -> Material* { + if (id < 0 || id >= static_cast(materials_.size())) { + throw Exception("Invalid material (out of range)"); + } + Material* n = materials_[id].get(); + if (!n) { + throw Exception("Invalid material id (empty slot)"); + } + return n; +} +auto ClientSession::GetTexture(int id) const -> Texture* { + if (id < 0 || id >= static_cast(textures_.size())) { + throw Exception("Invalid texture (out of range)"); + } + Texture* n = textures_[id].get(); + if (!n) { + throw Exception("Invalid texture id (empty slot)"); + } + return n; +} +auto ClientSession::GetModel(int id) const -> Model* { + if (id < 0 || id >= static_cast(models_.size())) { + throw Exception("Invalid model (out of range)"); + } + Model* n = models_[id].get(); + if (!n) { + throw Exception("Invalid model id (empty slot)"); + } + return n; +} +auto ClientSession::GetSound(int id) const -> Sound* { + if (id < 0 || id >= static_cast(sounds_.size())) { + throw Exception("Invalid sound (out of range)"); + } + Sound* n = sounds_[id].get(); + if (!n) { + throw Exception("Invalid sound id (empty slot)"); + } + return n; +} +auto ClientSession::GetCollideModel(int id) const -> CollideModel* { + if (id < 0 || id >= static_cast(collide_models_.size())) { + throw Exception("Invalid collide_model (out of range)"); + } + CollideModel* n = collide_models_[id].get(); + if (!n) { + throw Exception("Invalid collide_model id (empty slot)"); + } + return n; +} + +void ClientSession::Error(const std::string& description) { + Log("ERROR: client session error: " + description); + End(); +} + +void ClientSession::End() { + if (shutting_down_) return; + shutting_down_ = true; + g_python->PushObjCall(Python::ObjID::kLaunchMainMenuSessionCall); +} + +void ClientSession::HandleSessionMessage(const std::vector& buffer) { + assert(InGameThread()); + + BA_PRECONDITION(!buffer.empty()); + + switch (buffer[0]) { + case BA_MESSAGE_SESSION_RESET: { + // Hmmm; been a while since I wrote this, but wondering why reset isn't + // just a session-command. (Do we not want it added to replays?...) + Reset(false); + break; + } + + case BA_MESSAGE_SESSION_COMMANDS: { + // This is simply 16 bit length followed by command up to the end of the + // packet. Break it apart and feed each command to the client session. + uint32_t offset = 1; + std::vector subBuffer; + while (true) { + uint16_t size; + memcpy(&size, &(buffer[offset]), 2); + if (offset + size > buffer.size()) { + Error("invalid state message"); + return; + } + subBuffer.resize(size); + memcpy(&(subBuffer[0]), &(buffer[offset + 2]), subBuffer.size()); + AddCommand(subBuffer); + offset += 2 + size; // move to next command + if (offset == buffer.size()) { + // lets also use this opportunity to graph our command-buffer size for + // network debugging.. if (NetGraph *graph = + // g_graphics->GetClientSessionStepBufferGraph()) { + // graph->addSample(GetRealTime(), steps_on_list_); + // } + + break; + } + } + break; + } + + case BA_MESSAGE_SESSION_DYNAMICS_CORRECTION: { + // Just drop this in the game's command-stream verbatim, except switch its + // state-ID to a command-ID. + std::vector bufferOut = buffer; + bufferOut[0] = static_cast(SessionCommand::kDynamicsCorrection); + AddCommand(bufferOut); + break; + } + + default: + throw Exception("ClientSession::HandleSessionMessage " + ObjToString(this) + + "got unrecognized message : " + + std::to_string(static_cast(buffer[0])) + + " of size " + std::to_string(buffer.size())); + break; + } +} + +// Add a single command in. +void ClientSession::AddCommand(const std::vector& command) { + // If this is a time-step command, we can dump everything we've been building + // up onto the list to be chewed through by the interpreter (we don't want to + // add things until we have the *entire* step so we don't wind up rendering + // things halfway through some change, etc). + commands_pending_.push_back(command); + if (!command.empty()) { + if (command[0] == static_cast(SessionCommand::kBaseTimeStep)) { + // Keep a tally of how much stepped time we've built up. + steps_on_list_ += command[1]; + for (auto&& i : commands_pending_) { + commands_.push_back(i); + } + commands_pending_.clear(); + } + } +} + +auto ClientSession::GetForegroundContext() -> Context { return Context(this); } + +void ClientSession::GetCorrectionMessages( + bool blend, std::vector >* messages) { + std::vector message; + for (auto&& i : scenes_) { + if (Scene* sg = i.get()) { + message = sg->GetCorrectionMessage(blend); + // A correction packet of size 4 is empty; ignore it. + if (message.size() > 4) { + messages->push_back(message); + } + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/game/session/client_session.h b/src/ballistica/game/session/client_session.h new file mode 100644 index 00000000..60330a47 --- /dev/null +++ b/src/ballistica/game/session/client_session.h @@ -0,0 +1,99 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_SESSION_CLIENT_SESSION_H_ +#define BALLISTICA_GAME_SESSION_CLIENT_SESSION_H_ + +#include +#include +#include + +#include "ballistica/game/client_controller_interface.h" +#include "ballistica/game/session/session.h" + +namespace ballistica { + +class ClientSession : public Session { + public: + ClientSession(); + ~ClientSession() override; + + // Allows for things like replay speed. + virtual auto GetActualTimeAdvance(int advance_in) -> int { + return advance_in; + } + void Update(int time_advance) override; + void Draw(FrameDef* f) override; + virtual void HandleSessionMessage(const std::vector& buffer); + void Reset(bool rewind); + auto GetForegroundContext() -> Context override; + auto DoesFillScreen() const -> bool override; + void ScreenSizeChanged() override; + void LanguageChanged() override; + auto shutting_down() const -> bool { return shutting_down_; } + void GetCorrectionMessages(bool blend, + std::vector >* messages); + + // Called when attempting to step without input data available. + virtual void OnCommandBufferUnderrun() {} + + // Returns existing objects; throws exceptions if not available. + auto GetScene(int id) const -> Scene*; + auto GetNode(int id) const -> Node*; + auto GetTexture(int id) const -> Texture*; + auto GetModel(int id) const -> Model*; + auto GetCollideModel(int id) const -> CollideModel*; + auto GetMaterial(int id) const -> Material*; + auto GetSound(int id) const -> Sound*; + + protected: + virtual void OnReset(bool rewind); + virtual void FetchMessages() {} + int steps_on_list_; + std::list > commands_; // ready-to-go commands + virtual void Error(const std::string& description); + void End(); + millisecs_t base_time_; + double target_base_time_ = 0.0f; + bool shutting_down_; + std::vector least_buffered_count_list_; // move this to net-client?.. + std::vector most_buffered_count_list_; + int buffer_count_list_index_; + int adjust_counter_; + float correction_ = 1.0f; + float largest_spike_smoothed_ = 0.0f; + float low_pass_smoothed_ = 0.0f; + + private: + void ClearSessionObjs(); + void AddCommand(const std::vector& command); + + // commands being built up for the next time step + // (we want to be able to run *everything* for a given timestep at once + // to avoid drawing things in half-changed states, etc) + std::list > commands_pending_; // commands for the next + std::vector current_cmd_; + uint8_t* current_cmd_ptr_; + auto ReadByte() -> uint8_t; + auto ReadInt32() -> int32_t; + void ReadInt32_2(int32_t* vals); + void ReadInt32_3(int32_t* vals); + void ReadInt32_4(int32_t* vals); + auto ReadString() -> std::string; + auto ReadFloat() -> float; + void ReadFloats(int count, float* vals); + void ReadInt32s(int count, int32_t* vals); + void ReadChars(int count, char* vals); + + protected: + std::vector > scenes_; + std::vector > nodes_; + std::vector > textures_; + std::vector > models_; + std::vector > sounds_; + std::vector > collide_models_; + std::vector > materials_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_SESSION_CLIENT_SESSION_H_ diff --git a/src/ballistica/game/session/host_session.cc b/src/ballistica/game/session/host_session.cc new file mode 100644 index 00000000..914beba7 --- /dev/null +++ b/src/ballistica/game/session/host_session.cc @@ -0,0 +1,765 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/game/session/host_session.h" + +#include "ballistica/core/context.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/game/host_activity.h" +#include "ballistica/game/player.h" +#include "ballistica/generic/lambda_runnable.h" +#include "ballistica/generic/timer.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/input/device/input_device.h" +#include "ballistica/media/component/data.h" +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/sound.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_command.h" +#include "ballistica/python/python_context_call.h" +#include "ballistica/python/python_sys.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +HostSession::HostSession(PyObject* session_type_obj) + : last_kick_idle_players_decrement_time_(GetRealTime()) { + assert(g_game); + assert(InGameThread()); + assert(session_type_obj != nullptr); + + ScopedSetContext cp(this); + + // FIXME: Should be an attr of the session class, not hard-coded. + is_main_menu_ = + static_cast(strstr(Python::ObjToString(session_type_obj).c_str(), + "bastd.mainmenu.MainMenuSession")); + // Log("MAIN MENU? " + std::to_string(is_main_menu())); + + kick_idle_players_ = g_game->kick_idle_players(); + + // Create a timer to step our session scene. + step_scene_timer_ = + base_timers_.NewTimer(base_time_, kGameStepMilliseconds, 0, -1, + NewLambdaRunnable([this] { StepScene(); })); + + // Set up our output-stream, which will go to a replay and/or the network. + // We don't dump to a replay if we're doing the main menu; that replay + // would be boring. + bool do_replay = !is_main_menu_; + // Log("DO REPLAY? " + std::to_string(do_replay)); + + // At the moment headless-server don't write replays. +#if BA_HEADLESS_BUILD + do_replay = false; +#endif // BA_HEADLESS_BUILD + output_stream_ = Object::New(this, do_replay); + + // Make a scene for our session-level nodes, etc. + scene_ = Object::New(0); + if (output_stream_.exists()) { + output_stream_->AddScene(scene_.get()); + } + + // Fade in from our current blackness. + g_graphics->FadeScreen(true, 250, nullptr); + + // Start by showing the progress bar instead of hitching. + g_graphics->EnableProgressBar(true); + + // Now's a good time to run garbage collection; there should be pretty much + // no game stuff to speak of in existence (provided the last session went + // down peacefully). + g_python->obj(Python::ObjID::kGarbageCollectCall).Call(); + + // Instantiate our python Session instance. + PythonRef obj; + PythonRef session_type(session_type_obj, PythonRef::kAcquire); + { + Python::ScopedCallLabel label("Session instantiation"); + obj = session_type.Call(); + } + if (!obj.exists()) { + throw Exception("Error creating game session: '" + session_type.Str() + + "'"); + } + + // The session python object should have called + // _ba.register_session() in its constructor to set session_py_obj_. + if (session_py_obj_ != obj) { + throw Exception("session not set up correctly"); + } + + // Lastly, keep the python layer fed with our latest player count in case + // it is updating the master-server with our current/max player counts. + g_game->SetPublicPartyPlayerCount(static_cast(players_.size())); +} + +auto HostSession::GetHostSession() -> HostSession* { return this; } + +void HostSession::DestroyHostActivity(HostActivity* a) { + BA_PRECONDITION(a); + BA_PRECONDITION(a->GetHostSession() == this); + if (a == foreground_host_activity_.get()) { + foreground_host_activity_.Clear(); + } + + // Clear it from our activities list if its still on there. + for (auto i = host_activities_.begin(); i < host_activities_.end(); i++) { + if (i->get() == a) { + host_activities_.erase(i); + return; + } + } + + // The only reason it wouldn't be there should be because the activity is + // dying due our clearing of the list in our destructor; make sure that's + // the case. + assert(shutting_down_); +} + +auto HostSession::GetMutableScene() -> Scene* { + assert(scene_.exists()); + return scene_.get(); +} + +void HostSession::DebugSpeedMultChanged() { + // FIXME - should we progress our own scene faster/slower depending on + // this too? Is there really a need to? + + // Let all our activities know. + for (auto&& i : host_activities_) { + i->DebugSpeedMultChanged(); + } +} + +void HostSession::ScreenSizeChanged() { + // Let our internal scene know. + scene()->ScreenSizeChanged(); + + // Also let all our activities know. + for (auto&& i : host_activities_) { + i->ScreenSizeChanged(); + } +} + +void HostSession::LanguageChanged() { + // Let our internal scene know. + scene()->LanguageChanged(); + + // Also let all our activities know. + for (auto&& i : host_activities_) { + i->LanguageChanged(); + } +} + +void HostSession::GraphicsQualityChanged(GraphicsQuality q) { + // Let our internal scene know. + scene()->GraphicsQualityChanged(q); + + // Let all our activities know. + for (auto&& i : host_activities_) { + i->GraphicsQualityChanged(q); + } +} + +auto HostSession::DoesFillScreen() const -> bool { + // FIXME not necessarily the case. + return true; +} + +void HostSession::Draw(FrameDef* f) { + // First draw our session scene. + scene()->Draw(f); + + // Let all our activities draw their own scenes/etc. + for (auto&& i : host_activities_) { + i->Draw(f); + } +} + +auto HostSession::NewTimer(TimerMedium length, bool repeat, + const Object::Ref& runnable) -> int { + if (shutting_down_) { + BA_LOG_PYTHON_TRACE_ONCE( + "WARNING: Creating game timer during host-session shutdown"); + return 123; // dummy... + } + if (length == 0 && repeat) { + throw Exception("Can't add game-timer with length 0 and repeat on"); + } + if (length < 0) { + throw Exception("Timer length cannot be < 0 (got " + std::to_string(length) + + ")"); + } + int offset = 0; + Timer* t = sim_timers_.NewTimer(scene()->time(), length, offset, + repeat ? -1 : 0, runnable); + return t->id(); +} + +void HostSession::DeleteTimer(int timer_id) { + assert(InGameThread()); + if (shutting_down_) return; + sim_timers_.DeleteTimer(timer_id); +} + +auto HostSession::GetSound(const std::string& name) -> Object::Ref { + if (shutting_down_) { + throw Exception("can't load assets during session shutdown"); + } + return Media::GetMedia(&sounds_, name, scene()); +} + +auto HostSession::GetData(const std::string& name) -> Object::Ref { + if (shutting_down_) { + throw Exception("can't load assets during session shutdown"); + } + return Media::GetMedia(&datas_, name, scene()); +} + +auto HostSession::GetTexture(const std::string& name) -> Object::Ref { + if (shutting_down_) { + throw Exception("can't load assets during session shutdown"); + } + return Media::GetMedia(&textures_, name, scene()); +} +auto HostSession::GetModel(const std::string& name) -> Object::Ref { + if (shutting_down_) { + throw Exception("can't load media during session shutdown"); + } + return Media::GetMedia(&models_, name, scene()); +} + +auto HostSession::GetForegroundContext() -> Context { + HostActivity* a = foreground_host_activity_.get(); + if (a) { + return Context(a); + } + return Context(this); +} + +void HostSession::RequestPlayer(InputDevice* device) { + assert(InGameThread()); + + // Ignore if we have no python session obj. + if (!GetSessionPyObj()) { + Log("Error: HostSession::RequestPlayer() called w/no session_py_obj_."); + return; + } + + // Need to at least temporarily create and attach to a player for passing to + // the callback. + int player_id = next_player_id_++; + auto player(Object::New(player_id, this)); + players_.push_back(player); + device->AttachToLocalPlayer(player.get()); + + // Ask the python layer to accept/deny this guy. + bool accept; + { + // Set the session as context. + ScopedSetContext cp(this); + accept = static_cast( + session_py_obj_.GetAttr("_request_player") + .Call(PythonRef(Py_BuildValue("(O)", player->BorrowPyRef()), + PythonRef::kSteal)) + .ValueAsInt()); + if (accept) { + player->set_accepted(true); + } else { + RemovePlayer(player.get()); + } + } + + // If he was accepted, update our game roster with the new info. + if (accept) { + g_game->UpdateGameRoster(); + } + + // Lastly, keep the python layer fed with our latest player count in case it + // is updating the master-server with our current/max player counts. + g_game->SetPublicPartyPlayerCount(static_cast(players_.size())); +} + +void HostSession::RemovePlayer(Player* player) { + assert(player); + + for (auto i = players_.begin(); i != players_.end(); ++i) { + if (i->get() == player) { + // Grab a ref to keep the player alive, pull him off the list, then call + // his leaving callback. + Object::Ref player2 = *i; + players_.erase(i); + + // Only make the callback for this player if they were accepted. + if (player2->accepted()) { + IssuePlayerLeft(player2.get()); + } + + // Update our game roster with the departure. + g_game->UpdateGameRoster(); + + // Lastly, keep the python layer fed with our latest player count in case + // it is updating the master-server with our current/max player counts. + g_game->SetPublicPartyPlayerCount(static_cast(players_.size())); + + return; + } + } + BA_LOG_ERROR_TRACE("Player not found in HostSession::RemovePlayer()"); +} + +void HostSession::IssuePlayerLeft(Player* player) { + assert(player); + assert(InGameThread()); + + try { + if (GetSessionPyObj()) { + if (player) { + // Make sure we're the context for session callbacks. + ScopedSetContext cp(this); + Python::ScopedCallLabel label("Session on_player_leave"); + session_py_obj_.GetAttr("on_player_leave") + .Call(PythonRef(Py_BuildValue("(O)", player->BorrowPyRef()), + PythonRef::kSteal)); + } else { + BA_LOG_PYTHON_TRACE_ONCE("missing player on IssuePlayerLeft"); + } + } else { + Log("WARNING: HostSession: IssuePlayerLeft caled with no " + "session_py_obj_"); + } + } catch (const std::exception& e) { + Log(std::string("Error calling on_player_leave(): ") + e.what()); + } +} + +void HostSession::SetKickIdlePlayers(bool enable) { + // If this has changed, reset our disconnect-time reporting. + assert(InGameThread()); + if (enable != kick_idle_players_) { + last_kick_idle_players_decrement_time_ = GetRealTime(); + } + kick_idle_players_ = enable; +} + +void HostSession::SetForegroundHostActivity(HostActivity* a) { + assert(a); + assert(InGameThread()); + + if (shutting_down_) { + Log("WARNING: SetForegroundHostActivity called during session shutdown; " + "ignoring."); + return; + } + + // Sanity check: make sure the one provided is part of this session. + bool found = false; + for (auto&& i : host_activities_) { + if (i == a) { + found = true; + break; + } + } + if ((a->GetHostSession() != this) || !found) { + throw Exception("HostActivity is not part of this HostSession"); + } + + foreground_host_activity_ = a; + + // Now go through telling each host-activity whether it's foregrounded or not. + // FIXME: Dying sessions never get told they're un-foregrounded.. could that + // ever be a problem? + bool session_is_foreground = (g_game->GetForegroundSession() != nullptr); + for (auto&& i : host_activities_) { + i->SetIsForeground(session_is_foreground && (i == a)); + } +} + +void HostSession::AddHostActivity(HostActivity* a) { + host_activities_.emplace_back(a); +} + +// Called by the constructor of the session python object. +void HostSession::RegisterPySession(PyObject* obj) { + session_py_obj_.Acquire(obj); +} + +// Given an activity python type, instantiates and returns a new activity. +auto HostSession::NewHostActivity(PyObject* activity_type_obj, + PyObject* settings_obj) -> PyObject* { + PythonRef activity_type(activity_type_obj, PythonRef::kAcquire); + if (!activity_type.CallableCheck()) { + throw Exception("Invalid HostActivity type passed; not callable"); + } + + // First generate our C++ activity instance and point the context at it. + auto activity(Object::New(this)); + AddHostActivity(activity.get()); + + ScopedSetContext cp(activity.get()); + + // Now instantiate the python instance.. pass args if some were provided, or + // an empty dict otherwise. + PythonRef args; + if (settings_obj == Py_None) { + args.Steal(Py_BuildValue("({})")); + } else { + args.Steal(Py_BuildValue("(O)", settings_obj)); + } + + PythonRef result = activity_type.Call(args); + if (!result.exists()) { + throw Exception("HostActivity creation failed"); + } + + // If all went well, the python activity constructor should have called + // _ba.register_activity(), so we should be able to get at the same python + // activity we just instantiated through the c++ class. + if (activity->GetPyActivity() != result.get()) { + throw Exception("Error on HostActivity construction"); + } + + PyObject* obj = result.get(); + Py_INCREF(obj); + return obj; +} + +auto HostSession::RegisterPyActivity(PyObject* activity_obj) -> HostActivity* { + // The context should be pointing to an unregistered HostActivity; + // register and return it. + HostActivity* activity = Context::current().GetHostActivity(); + if (!activity) + throw Exception( + "No current activity in RegisterPyActivity; did you remember to call " + "ba.newHostActivity() to instantiate your activity?"); + activity->RegisterPyActivity(activity_obj); + return activity; +} + +void HostSession::DecrementPlayerTimeOuts(millisecs_t millisecs) { + for (auto&& i : players_) { + Player* player = i.get(); + assert(player); + if (player->time_out() < millisecs) { + std::string kick_str = + g_game->GetResourceString("kickIdlePlayersKickedText"); + Utils::StringReplaceOne(&kick_str, "${NAME}", player->GetName()); + ScreenMessage(kick_str); + RemovePlayer(player); + return; // Bail for this round since we prolly mucked with the list. + } else if (player->time_out() > BA_PLAYER_TIME_OUT_WARN + && (player->time_out() - millisecs <= BA_PLAYER_TIME_OUT_WARN)) { + std::string kick_str_1 = + g_game->GetResourceString("kickIdlePlayersWarning1Text"); + Utils::StringReplaceOne(&kick_str_1, "${NAME}", player->GetName()); + Utils::StringReplaceOne(&kick_str_1, "${COUNT}", + std::to_string(BA_PLAYER_TIME_OUT_WARN / 1000)); + ScreenMessage(kick_str_1); + ScreenMessage(g_game->GetResourceString("kickIdlePlayersWarning2Text")); + } + player->set_time_out(player->time_out() - millisecs); + } +} + +void HostSession::ProcessPlayerTimeOuts() { + millisecs_t real_time = GetRealTime(); + + if (foreground_host_activity_.exists() + && foreground_host_activity_->game_speed() > 0.0 + && !foreground_host_activity_->paused() + && foreground_host_activity_->getAllowKickIdlePlayers() + && kick_idle_players_) { + // Let's only do this every now and then. + if (real_time - last_kick_idle_players_decrement_time_ > 1000) { + DecrementPlayerTimeOuts(real_time + - last_kick_idle_players_decrement_time_); + last_kick_idle_players_decrement_time_ = real_time; + } + } else { + // If we're not kicking, we still store the latest time (so it doesnt + // accumulate for when we start again). + last_kick_idle_players_decrement_time_ = real_time; + } +} + +void HostSession::StepScene() { + // Run up our game-time timers. + sim_timers_.Run(scene()->time()); + + // And step. + scene()->Step(); +} + +void HostSession::Update(int time_advance) { + assert(InGameThread()); + + // We can be killed at any time, so let's keep an eye out for that. + WeakRef test_ref(this); + assert(test_ref.exists()); + + ProcessPlayerTimeOuts(); + + GameStream* output_stream = GetGameStream(); + + // Advance base time by the specified amount, + // stopping at all timers along the way. + millisecs_t target_base_time = base_time_ + time_advance; + while (!base_timers_.empty() + && (base_time_ + base_timers_.GetTimeToNextExpire(base_time_) + <= target_base_time)) { + base_time_ += base_timers_.GetTimeToNextExpire(base_time_); + if (output_stream) { + output_stream->SetTime(base_time_); + } + base_timers_.Run(base_time_); + } + base_time_ = target_base_time; + if (output_stream) { + output_stream->SetTime(base_time_); + } + assert(test_ref.exists()); + + // Update our activities (iterate via weak-refs as this list may change under + // us at any time). + std::vector > activities = + PointersToWeakRefs(RefsToPointers(host_activities_)); + for (auto&& i : activities) { + if (i.exists()) { + i->Update(time_advance); + assert(test_ref.exists()); + } + } + assert(test_ref.exists()); + + // Periodically prune various dead refs. + if (base_time_ > next_prune_time_) { + PruneDeadMapRefs(&textures_); + PruneDeadMapRefs(&sounds_); + PruneDeadMapRefs(&models_); + PruneDeadRefs(&python_calls_); + next_prune_time_ = base_time_ + 5000; + } + assert(test_ref.exists()); +} + +HostSession::~HostSession() { + try { + shutting_down_ = true; + + // Put the scene in shut-down mode before we start killing stuff + // (this generates warnings, suppresses messages, etc). + scene_->set_shutting_down(true); + + // Clear out all python calls registered in our context + // (should wipe out refs to our session and prevent them from running + // without a valid session context). + for (auto&& i : python_calls_) { + if (i.exists()) { + i->MarkDead(); + } + } + + // Mark all our media dead to clear it out of our output-stream cleanly. + for (auto&& i : textures_) { + if (i.second.exists()) { + i.second->MarkDead(); + } + } + for (auto&& i : models_) { + if (i.second.exists()) { + i.second->MarkDead(); + } + } + for (auto&& i : sounds_) { + if (i.second.exists()) { + i.second->MarkDead(); + } + } + + // Clear our timers and scene; this should wipe out any remaining refs + // to our session scene. + base_timers_.Clear(); + sim_timers_.Clear(); + scene_.Clear(); + + // Kill our python session object. + { + ScopedSetContext cp(this); + session_py_obj_.Release(); + } + + // Kill any remaining activity data. Generally all activities should die + // when the session python object goes down, but lets clean up in case any + // didn't. + for (auto&& i : host_activities_) { + ScopedSetContext cp{Object::Ref(i)}; + i.Clear(); + } + + // Report outstanding calls. There shouldn't be any at this point. Actually + // it turns out there's generally 1; whichever call was responsible for + // killing this activity will still be in progress.. so let's report on 2 or + // more I guess. +#if BA_DEBUG_BUILD + PruneDeadRefs(&python_calls_); + if (python_calls_.size() > 1) { + std::string s = "WARNING: " + std::to_string(python_calls_.size()) + + " live PythonContextCalls at shutdown for " + + "HostSession" + " (1 call is expected):"; + int count = 1; + for (auto&& i : python_calls_) { + s += ("\n " + std::to_string(count++) + ": " + + i->GetObjectDescription()); + } + Log(s); + } +#endif // BA_DEBUG_BUILD + } catch (const std::exception& e) { + Log("Exception in HostSession destructor: " + std::string(e.what())); + } +} + +void HostSession::RegisterCall(PythonContextCall* call) { + assert(call); + python_calls_.emplace_back(call); + + // If we're shutting down, just kill the call immediately. + // (we turn all of our calls to no-ops as we shut down). + if (shutting_down_) { + Log("WARNING: adding call to expired session; call will not function: " + + call->GetObjectDescription()); + call->MarkDead(); + } +} + +auto HostSession::GetUnusedPlayerName(Player* p, const std::string& base_name) + -> std::string { + // Now find the first non-taken variation. + int index = 1; + std::string name_test; + while (true) { + if (index > 1) { + name_test = base_name + " " + std::to_string(index); + } else { + name_test = base_name; + } + bool name_found = false; + for (auto&& j : players_) { + if ((j->GetName() == name_test) && (j.get() != p)) { + name_found = true; + break; + } + } + if (!name_found) break; + index += 1; + } + return name_test; +} + +void HostSession::DumpFullState(GameStream* out) { + // Add session-scene. + if (scene_.exists()) { + scene_->Dump(out); + } + + // Dump media associated with session-scene. + for (auto&& i : textures_) { + if (Texture* t = i.second.get()) { + out->AddTexture(t); + } + } + for (auto&& i : sounds_) { + if (Sound* s = i.second.get()) { + out->AddSound(s); + } + } + for (auto&& i : models_) { + if (Model* s = i.second.get()) { + out->AddModel(s); + } + } + + // Dump session-scene's nodes. + if (scene_.exists()) { + scene_->DumpNodes(out); + } + + // Now let our activities dump themselves. + for (auto&& i : host_activities_) { + i->DumpFullState(out); + } +} + +void HostSession::GetCorrectionMessages( + bool blend, std::vector >* messages) { + std::vector message; + + // Grab correction for session scene (though there shouldn't be one). + if (scene_.exists()) { + message = scene_->GetCorrectionMessage(blend); + if (message.size() > 4) { + // A correction packet of size 4 is empty; ignore it. + messages->push_back(message); + } + } + + // Now do same for activity scenes. + for (auto&& i : host_activities_) { + if (HostActivity* ha = i.get()) { + if (Scene* sg = ha->scene()) { + message = sg->GetCorrectionMessage(blend); + if (message.size() > 4) { + // A correction packet of size 4 is empty; ignore it. + messages->push_back(message); + } + } + } + } +} + +auto HostSession::NewTimer(TimeType timetype, TimerMedium length, bool repeat, + const Object::Ref& runnable) -> int { + // Make sure the runnable passed in is reference-managed already + // (we may not add an initial reference ourself). + assert(runnable->is_valid_refcounted_object()); + + // We currently support game and base timers. + switch (timetype) { + case TimeType::kSim: + case TimeType::kBase: + // Game and base timers are the same thing for us. + return NewTimer(length, repeat, runnable); + default: + // Gall back to default for descriptive error otherwise. + return ContextTarget::NewTimer(timetype, length, repeat, runnable); + } +} + +void HostSession::DeleteTimer(TimeType timetype, int timer_id) { + switch (timetype) { + case TimeType::kSim: + case TimeType::kBase: + // Game and base timers are the same thing for us. + DeleteTimer(timer_id); + break; + default: + // Fall back to default for descriptive error otherwise. + ContextTarget::DeleteTimer(timetype, timer_id); + break; + } +} + +auto HostSession::GetTime(TimeType timetype) -> millisecs_t { + switch (timetype) { + case TimeType::kSim: + case TimeType::kBase: + return scene_->time(); + default: + // Fall back to default for descriptive error otherwise. + return ContextTarget::GetTime(timetype); + } +} + +} // namespace ballistica diff --git a/src/ballistica/game/session/host_session.h b/src/ballistica/game/session/host_session.h new file mode 100644 index 00000000..812c9799 --- /dev/null +++ b/src/ballistica/game/session/host_session.h @@ -0,0 +1,131 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_SESSION_HOST_SESSION_H_ +#define BALLISTICA_GAME_SESSION_HOST_SESSION_H_ + +#include +#include +#include +#include + +#include "ballistica/core/context.h" +#include "ballistica/game/session/session.h" +#include "ballistica/generic/timer_list.h" +#include "ballistica/python/python_ref.h" + +namespace ballistica { + +class HostSession : public Session { + public: + explicit HostSession(PyObject* session_type_obj); + ~HostSession() override; + + // Return a borrowed python ref. + auto GetSessionPyObj() const -> PyObject* { return session_py_obj_.get(); } + + // Set focus to a Context (it must belong to this session). + void SetForegroundHostActivity(HostActivity* sgc); + auto GetSound(const std::string& name) -> Object::Ref override; + auto GetData(const std::string& name) -> Object::Ref override; + auto GetTexture(const std::string& name) -> Object::Ref override; + auto GetModel(const std::string& name) -> Object::Ref override; + + void SetKickIdlePlayers(bool enable); + + // Update the session. + void Update(int time_advance) override; + + // ContextTarget time/timer support + auto NewTimer(TimeType timetype, TimerMedium length, bool repeat, + const Object::Ref& runnable) -> int override; + void DeleteTimer(TimeType timetype, int timer_id) override; + auto GetTime(TimeType timetype) -> millisecs_t override; + + // Given an activity python type, instantiate a new activity + // and return a new reference. + auto NewHostActivity(PyObject* activity_type_obj, PyObject* settings_obj) + -> PyObject*; + void DestroyHostActivity(HostActivity* a); + void RemovePlayer(Player* player); + void RequestPlayer(InputDevice* device); + + // Return either a host-activity context or the session-context. + auto GetForegroundContext() -> Context override; + auto DoesFillScreen() const -> bool override; + void Draw(FrameDef* f) override; + void ScreenSizeChanged() override; + void LanguageChanged() override; + void GraphicsQualityChanged(GraphicsQuality q) override; + void DebugSpeedMultChanged() override; + auto GetHostSession() -> HostSession* override; + auto GetMutableScene() -> Scene* override; + auto scene() -> Scene* { + assert(scene_.exists()); + return scene_.get(); + } + void RegisterCall(PythonContextCall* call); + auto GetGameStream() const -> GameStream* { return output_stream_.get(); } + auto is_main_menu() const -> bool { + return is_main_menu_; + } // fixme remove this + void DumpFullState(GameStream* out) override; + void GetCorrectionMessages(bool blend, + std::vector >* messages); + auto base_time() const -> millisecs_t { return base_time_; } + auto players() const -> const std::vector >& { + return players_; + } + + // Called by new py Session to pass themselves to us. + void RegisterPySession(PyObject* obj); + + // Called by new py Activities to pass themselves to us. + auto RegisterPyActivity(PyObject* activity_obj) -> HostActivity*; + + // New HostActivities should call this in their constructors. + void AddHostActivity(HostActivity* sgc); + + auto GetUnusedPlayerName(Player* p, const std::string& base_name) + -> std::string; + + private: + auto NewTimer(TimerMedium length, bool repeat, + const Object::Ref& runnable) -> int; + void DeleteTimer(int timer_id); + void StepScene(); + void ProcessPlayerTimeOuts(); + void DecrementPlayerTimeOuts(millisecs_t millisecs); + void IssuePlayerLeft(Player* player); + + bool is_main_menu_; // FIXME: Remove this. + Object::Ref output_stream_; + Timer* step_scene_timer_; + millisecs_t base_time_ = 0; + TimerList sim_timers_; + TimerList base_timers_; + Object::Ref scene_; + bool shutting_down_ = false; + + // Our list of python calls created in the context of this activity. We + // clear them as we are shutting down and ensure nothing runs after that + // point. + std::list > python_calls_; + std::vector > players_; + int next_player_id_ = 0; + + // Which host-activity has focus at the moment (Players talking to it, etc). + Object::WeakRef foreground_host_activity_; + std::vector > host_activities_; + PythonRef session_py_obj_; + bool kick_idle_players_ = false; + millisecs_t last_kick_idle_players_decrement_time_; + millisecs_t next_prune_time_ = 0; + std::map > textures_; + std::map > sounds_; + std::map > datas_; + std::map > models_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_SESSION_HOST_SESSION_H_ diff --git a/src/ballistica/game/session/net_client_session.cc b/src/ballistica/game/session/net_client_session.cc new file mode 100644 index 00000000..c555cebe --- /dev/null +++ b/src/ballistica/game/session/net_client_session.cc @@ -0,0 +1,147 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/game/session/net_client_session.h" + +#include +#include + +#include "ballistica/app/app_globals.h" +#include "ballistica/game/connection/connection_to_host.h" +#include "ballistica/media/media_server.h" + +namespace ballistica { + +NetClientSession::NetClientSession() { + // Sanity check: we should only ever be writing one replay at once. + if (g_app_globals->replay_open) { + Log("ERROR: g_replay_open true at netclient start; shouldn't happen."); + } + assert(g_media_server); + g_media_server->PushBeginWriteReplayCall(); + writing_replay_ = true; + g_app_globals->replay_open = true; +} + +NetClientSession::~NetClientSession() { + if (writing_replay_) { + // Sanity check: we should only ever be writing one replay at once. + if (!g_app_globals->replay_open) { + Log("ERROR: g_replay_open false at net-client close; shouldn't happen."); + } + g_app_globals->replay_open = false; + assert(g_media_server); + g_media_server->PushEndWriteReplayCall(); + writing_replay_ = false; + } +} + +void NetClientSession::SetConnectionToHost(ConnectionToHost* c) { + connection_to_host_ = c; +} + +void NetClientSession::OnCommandBufferUnderrun() { + // Any time we run out of data, hit the brakes on our playback speed. + // Update: maybe not. + // correction_ *= 0.99f; +} + +void NetClientSession::Update(int time_advance) { + if (shutting_down_) { + return; + } + + // Now do standard step. + ClientSession::Update(time_advance); + + // And update our timing to try and ensure we don't run out of buffer. + UpdateBuffering(); +} + +void NetClientSession::UpdateBuffering() { + // if (NetGraph *graph = g_graphics->debug_graph_1()) { + // graph->addSample(GetRealTime(), steps_on_list_); + // } + + // Keep record of the most and least amount of time we've had buffered + // recently, and slow down/speed up a bit based on that. + { + int bucket_count = static_cast(least_buffered_count_list_.size()); + + // Change bucket every g_delay_samples samples. + int bucket = (buffer_count_list_index_ / g_app_globals->delay_samples) + % bucket_count; + int bucket_iteration = + buffer_count_list_index_ % g_app_globals->delay_samples; + + // *Set* the value the first iteration in each bucket; do *min* after that. + if (bucket_iteration == 0) { + least_buffered_count_list_[bucket] = steps_on_list_; + most_buffered_count_list_[bucket] = steps_on_list_; + } else { + least_buffered_count_list_[bucket] = + std::min(least_buffered_count_list_[bucket], steps_on_list_); + most_buffered_count_list_[bucket] = + std::max(most_buffered_count_list_[bucket], steps_on_list_); + + // After the last sample in each bucket, feed the max bucket value in + // as the 'low pass' buffer-count. The low-pass curve minus our largest + // spike value should be where we want to aim for in the buffer. + if (bucket_iteration == g_app_globals->delay_samples - 1) { + float smoothing = 0.5f; + low_pass_smoothed_ = + smoothing * low_pass_smoothed_ + + (1.0f - smoothing) + * static_cast(most_buffered_count_list_[bucket]); + } + } + + // Keep track of the largest min/max difference in our sample segments. + int largest_spike = 0; + + buffer_count_list_index_++; + for (int i = 1; i < bucket_count; i++) { + int spike = most_buffered_count_list_[i] - least_buffered_count_list_[i]; + if (spike > largest_spike) { + largest_spike = spike; + } + } + + // Slowly adjust largest spike value based on the biggest in recent history. + { + float smoothing = 0.95f; + largest_spike_smoothed_ = + smoothing * largest_spike_smoothed_ + + (1.0f - smoothing) * static_cast(largest_spike); + } + + // Low pass is the most buffered data we've had in the most recent slot. + float ideal_offset = low_pass_smoothed_ - largest_spike_smoothed_ * 1.0f; + + // Any time we've got no current buffered data, slow down fast. + // (otherwise we can get stuck cruising along with no 0 buffered data and + // things get real jerky looking) + if (steps_on_list_ == 0) { + ideal_offset -= 100.0f; + } + float smoothing = 0.0f; + correction_ = smoothing * correction_ + + (1.0f - smoothing) * (1.0f + 0.002f * ideal_offset); + correction_ = std::min(1.5f, std::max(0.5f, correction_)); + // if (NetGraph *graph = g_graphics->debug_graph_2()) { + // graph->addSample(GetRealTime(), correction_); + // } + } +} +void NetClientSession::HandleSessionMessage( + const std::vector& message) { + // Do the standard thing, but also write this message straight to our replay + // stream if we have one. + ClientSession::HandleSessionMessage(message); + + if (writing_replay_) { + assert(g_media_server); + g_media_server->PushAddMessageToReplayCall(message); + } +} + +} // namespace ballistica diff --git a/src/ballistica/game/session/net_client_session.h b/src/ballistica/game/session/net_client_session.h new file mode 100644 index 00000000..303d598a --- /dev/null +++ b/src/ballistica/game/session/net_client_session.h @@ -0,0 +1,35 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_SESSION_NET_CLIENT_SESSION_H_ +#define BALLISTICA_GAME_SESSION_NET_CLIENT_SESSION_H_ + +#include + +#include "ballistica/game/session/client_session.h" + +namespace ballistica { + +// A client-session fed by a connection to a host. +class NetClientSession : public ClientSession { + public: + NetClientSession(); + ~NetClientSession() override; + auto connection_to_host() const -> ConnectionToHost* { + return connection_to_host_.get(); + } + void SetConnectionToHost(ConnectionToHost* c); + void HandleSessionMessage(const std::vector& buffer) override; + void OnCommandBufferUnderrun() override; + + protected: + void Update(int time_advance) override; + + private: + void UpdateBuffering(); + bool writing_replay_ = false; + Object::WeakRef connection_to_host_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_SESSION_NET_CLIENT_SESSION_H_ diff --git a/src/ballistica/game/session/replay_client_session.cc b/src/ballistica/game/session/replay_client_session.cc new file mode 100644 index 00000000..f38c8362 --- /dev/null +++ b/src/ballistica/game/session/replay_client_session.cc @@ -0,0 +1,321 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/game/session/replay_client_session.h" + +#include +#include +#include +#include + +#include "ballistica/dynamics/material/material.h" +#include "ballistica/game/connection/connection_to_client.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/generic/huffman.h" +#include "ballistica/generic/utils.h" +#include "ballistica/math/vector3f.h" +#include "ballistica/networking/networking.h" +#include "ballistica/platform/platform.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +auto ReplayClientSession::GetActualTimeAdvance(int advance_in) -> int { + return static_cast( + round(advance_in * pow(2.0f, g_game->replay_speed_exponent()))); +} + +ReplayClientSession::ReplayClientSession(std::string filename) + : file_name_(std::move(filename)), + file_(nullptr), + message_fetch_num_(0), + have_sent_client_message_(false) { + // take responsibility for feeding all clients to this device.. + g_game->RegisterClientController(this); + + // go ahead and just do a reset here, which will get things going.. + Reset(true); +} + +ReplayClientSession::~ReplayClientSession() { + // we no longer are responsible for feeding clients to this device.. + g_game->UnregisterClientController(this); + + if (file_) { + fclose(file_); + file_ = nullptr; + } +} + +void ReplayClientSession::OnClientConnected(ConnectionToClient* c) { + // sanity check - abort if its on either of our lists already + for (ConnectionToClient* i : connections_to_clients_) { + if (i == c) { + Log("Error: ReplayClientSession::OnClientConnected()" + " got duplicate connection"); + return; + } + } + for (ConnectionToClient* i : connections_to_clients_ignored_) { + if (i == c) { + Log("Error: ReplayClientSession::OnClientConnected()" + " got duplicate connection"); + return; + } + } + + // if we've sent *any* commands out to clients so far, we currently have to + // ignore new connections (need to rebuild state to match current session + // state) + { + connections_to_clients_.push_back(c); + + // we create a temporary output stream just for the purpose of building + // a giant session-commands message that we can send to the client + // to build its state up to where we are currently. + GameStream out(nullptr, false); + + // go ahead and dump our full state.. + DumpFullState(&out); + + // grab the message that's been built up.. + // if its not empty, send it to the client. + std::vector out_message = out.GetOutMessage(); + if (!out_message.empty()) c->SendReliableMessage(out_message); + + // also send a correction packet to sync up all our dynamics + // (technically could do this *just* for the new client) + { + std::vector > messages; + bool blend = false; + GetCorrectionMessages(blend, &messages); + + // FIXME - have to send reliably at the moment since these will most + // likely be bigger than our unreliable packet limit.. :-( + for (auto&& i : messages) { + for (auto&& j : connections_to_clients_) { + j->SendReliableMessage(i); + } + } + } + } +} + +void ReplayClientSession::OnClientDisconnected(ConnectionToClient* c) { + // search for it on either our ignored or regular lists.. + for (auto i = connections_to_clients_.begin(); + i != connections_to_clients_.end(); i++) { + if (*i == c) { + connections_to_clients_.erase(i); + return; + } + } + for (auto i = connections_to_clients_ignored_.begin(); + i != connections_to_clients_ignored_.end(); i++) { + if (*i == c) { + connections_to_clients_ignored_.erase(i); + return; + } + } + Log("Error: ReplayClientSession::OnClientDisconnected()" + " called for connection not on lists"); +} + +void ReplayClientSession::FetchMessages() { + if (!file_ || shutting_down()) { + return; + } + + // If we have no messages left, read from the file until we get some. + while (commands_.empty()) { + std::vector buffer; + uint8_t len8; + uint32_t len32; + + // read the size of the message.. + // the first byte represents the actual size if the value is < 254 + // if it is 254, the 2 bytes after it represent size + // if it is 255, the 4 bytes after it represent size + if (fread(&len8, 1, 1, file_) != 1) { + // so they know to be done when they reach the end of the command list + // (instead of just waiting for more commands) + commands_.emplace_back(1, + static_cast(SessionCommand::kEndOfFile)); + fclose(file_); + file_ = nullptr; + return; + } + if (len8 < 254) { + len32 = len8; + } else { + // pull 16 bit len.. + if (len8 == 254) { + uint16_t len16; + if (fread(&len16, 2, 1, file_) != 1) { + // so they know to be done when they reach the end of the command list + // (instead of just waiting for more commands) + commands_.emplace_back( + 1, static_cast(SessionCommand::kEndOfFile)); + fclose(file_); + file_ = nullptr; + return; + } + assert(len16 >= 254); + len32 = len16; + } else { + // pull 32 bit len... + if (fread(&len32, 4, 1, file_) != 1) { + // so they know to be done when they reach the end of the command list + // (instead of just waiting for more commands) + commands_.emplace_back( + 1, static_cast(SessionCommand::kEndOfFile)); + fclose(file_); + file_ = nullptr; + return; + } + assert(len32 > 65535); + } + } + + // read and decompress the actual message.. + BA_PRECONDITION(len32 > 0); + buffer.resize(len32); + if (fread(&(buffer[0]), len32, 1, file_) != 1) { + commands_.emplace_back(1, + static_cast(SessionCommand::kEndOfFile)); + fclose(file_); + file_ = nullptr; + return; + } + std::vector data_decompressed = + g_utils->huffman()->decompress(buffer); + HandleSessionMessage(data_decompressed); + + // Also send it to all client-connections we're attached to. + // NOTE: We currently are sending everything as reliable; we can maybe do + // unreliable for certain type of messages. Though perhaps when passing + // around replays maybe its best to keep everything intact. + have_sent_client_message_ = true; + for (auto&& i : connections_to_clients_) { + i->SendReliableMessage(data_decompressed); + } + message_fetch_num_++; + } +} + +void ReplayClientSession::Error(const std::string& description) { + // Close the replay, announce something went wrong with it, and then do + // standard error response.. + ScreenMessage(g_game->GetResourceString("replayReadErrorText"), {1, 0, 0}); + if (file_) { + fclose(file_); + file_ = nullptr; + } + ClientSession::Error(description); +} + +void ReplayClientSession::OnReset(bool rewind) { + // Handles base resetting. + ClientSession::OnReset(rewind); + + // If we've got any clients attached to us, tell them to reset as well. + for (auto&& i : connections_to_clients_) { + i->SendReliableMessage(std::vector(1, BA_MESSAGE_SESSION_RESET)); + } + + // If rewinding, pop back to the start of our file. + if (rewind) { + if (file_) { + fclose(file_); + file_ = nullptr; + } + + file_ = g_platform->FOpen(file_name_.c_str(), "rb"); + if (!file_) { + Error("can't open file for reading"); + return; + } + + // Read file ID and version to make sure we support this file. + uint32_t file_id; + if ((fread(&file_id, sizeof(file_id), 1, file_) != 1)) { + Error("error reading file_id"); + return; + } + if (file_id != kBrpFileID) { + Error("incorrect file_id"); + return; + } + + // Make sure its a compatible protocol version. + uint16_t version; + if (fread(&version, sizeof(version), 1, file_) != 1) { + Error("error reading version"); + return; + } + if (version > kProtocolVersion || version < kProtocolVersionMin) { + ScreenMessage(g_game->GetResourceString("replayVersionErrorText"), + {1, 0, 0}); + End(); + return; + } + } +} + +void ReplayClientSession::DumpFullState(GameStream* out) { + // This shouldn't actually be replay-specific. Should move this up to + // ClientSession perhaps? + + // Add all scenes. + for (auto&& i : scenes_) { + if (Scene* sg = i.get()) { + sg->Dump(out); + } + } + + // Before doing any nodes, we need to create all materials. + // (but *not* their components, which may reference the nodes that we haven't + // made yet) + for (auto&& i : materials_) { + if (Material* m = i.get()) { + out->AddMaterial(m); + } + } + + // Add all media. + for (auto&& i : textures_) { + if (Texture* t = i.get()) { + out->AddTexture(t); + } + } + for (auto&& i : models_) { + if (Model* s = i.get()) { + out->AddModel(s); + } + } + for (auto&& i : sounds_) { + if (Sound* s = i.get()) { + out->AddSound(s); + } + } + for (auto&& i : collide_models_) { + if (CollideModel* s = i.get()) { + out->AddCollideModel(s); + } + } + + // Add all scene nodes. + for (auto&& i : scenes_) { + if (Scene* sg = i.get()) { + sg->DumpNodes(out); + } + } + + // Now fill out materials since all the nodes/etc they reference exist. + for (auto&& i : materials_) { + if (Material* m = i.get()) { + m->DumpComponents(out); + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/game/session/replay_client_session.h b/src/ballistica/game/session/replay_client_session.h new file mode 100644 index 00000000..c6b146b6 --- /dev/null +++ b/src/ballistica/game/session/replay_client_session.h @@ -0,0 +1,43 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_SESSION_REPLAY_CLIENT_SESSION_H_ +#define BALLISTICA_GAME_SESSION_REPLAY_CLIENT_SESSION_H_ + +#include +#include + +#include "ballistica/game/client_controller_interface.h" +#include "ballistica/game/session/client_session.h" + +namespace ballistica { + +// A client-session fed by a connection to a host. +class ReplayClientSession : public ClientSession, + public ClientControllerInterface { + public: + explicit ReplayClientSession(std::string filename); + ~ReplayClientSession() override; + void OnReset(bool rewind) override; + + // Our ClientControllerInterface implementation. + auto GetActualTimeAdvance(int advance_in) -> int override; + void OnClientConnected(ConnectionToClient* c) override; + void OnClientDisconnected(ConnectionToClient* c) override; + void DumpFullState(GameStream* out) override; + + protected: + void Error(const std::string& description) override; + void FetchMessages() override; + + private: + uint32_t message_fetch_num_; + bool have_sent_client_message_; + std::vector connections_to_clients_; + std::vector connections_to_clients_ignored_; + std::string file_name_; + FILE* file_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_SESSION_REPLAY_CLIENT_SESSION_H_ diff --git a/src/ballistica/game/session/session.cc b/src/ballistica/game/session/session.cc new file mode 100644 index 00000000..756e5d9f --- /dev/null +++ b/src/ballistica/game/session/session.cc @@ -0,0 +1,36 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/game/session/session.h" + +#include "ballistica/app/app_globals.h" +#include "ballistica/game/game.h" + +namespace ballistica { + +Session::Session() { + g_app_globals->session_count++; + // new sessions immediately become foreground + g_game->SetForegroundSession(this); +} + +Session::~Session() { g_app_globals->session_count--; } + +void Session::Update(int time_advance) {} + +auto Session::GetForegroundContext() -> Context { return Context(); } + +void Session::Draw(FrameDef*) {} + +void Session::ScreenSizeChanged() {} + +void Session::LanguageChanged() {} + +void Session::GraphicsQualityChanged(GraphicsQuality q) {} + +void Session::DebugSpeedMultChanged() {} + +void Session::DumpFullState(GameStream* out) { + Log("Session::DumpFullState() being called; shouldn't happen."); +} + +} // namespace ballistica diff --git a/src/ballistica/game/session/session.h b/src/ballistica/game/session/session.h new file mode 100644 index 00000000..a6e4f7ca --- /dev/null +++ b/src/ballistica/game/session/session.h @@ -0,0 +1,44 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_SESSION_SESSION_H_ +#define BALLISTICA_GAME_SESSION_SESSION_H_ + +#include "ballistica/core/context.h" +#include "ballistica/core/object.h" + +namespace ballistica { + +class Session : public ContextTarget { + public: + Session(); + ~Session() override; + + // Update the session. Should return real milliseconds until next + // update is needed. + virtual void Update(int time_advance); + + // If this returns false, the screen will be cleared as part of rendering. + virtual auto DoesFillScreen() const -> bool = 0; + + // Draw!!! + virtual void Draw(FrameDef* f); + + // Return the 'frontmost' context in the session. + // This is used for executing console command or other UI hotkeys that should + // apply to whatever the user is seeing. + virtual auto GetForegroundContext() -> Context; + virtual void ScreenSizeChanged(); + virtual void LanguageChanged(); + virtual void GraphicsQualityChanged(GraphicsQuality q); + virtual void DebugSpeedMultChanged(); + auto benchmark_type() const -> BenchmarkType { return benchmark_type_; } + void set_benchmark_type(BenchmarkType val) { benchmark_type_ = val; } + virtual void DumpFullState(GameStream* s); + + private: + BenchmarkType benchmark_type_ = BenchmarkType::kNone; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_SESSION_SESSION_H_ diff --git a/src/ballistica/graphics/area_of_interest.cc b/src/ballistica/graphics/area_of_interest.cc new file mode 100644 index 00000000..645aacc5 --- /dev/null +++ b/src/ballistica/graphics/area_of_interest.cc @@ -0,0 +1,19 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/area_of_interest.h" + +#include "ballistica/ballistica.h" + +namespace ballistica { + +AreaOfInterest::AreaOfInterest(bool in_focus) : in_focus_(in_focus) {} + +AreaOfInterest::~AreaOfInterest() = default; + +void AreaOfInterest::SetRadius(float r_in) { + // We slightly scale this for phone situations. + float extrascale = (GetInterfaceType() == UIScale::kSmall) ? 0.85f : 1.0f; + radius_ = r_in * extrascale; +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/area_of_interest.h b/src/ballistica/graphics/area_of_interest.h new file mode 100644 index 00000000..3fd61feb --- /dev/null +++ b/src/ballistica/graphics/area_of_interest.h @@ -0,0 +1,33 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_AREA_OF_INTEREST_H_ +#define BALLISTICA_GRAPHICS_AREA_OF_INTEREST_H_ + +#include + +#include "ballistica/math/vector3f.h" + +namespace ballistica { + +class AreaOfInterest { + public: + explicit AreaOfInterest(bool in_focus); + ~AreaOfInterest(); + void set_position(const Vector3f& position) { position_ = position; } + void set_velocity(const Vector3f& velocity) { velocity_ = velocity; } + auto position() const -> const Vector3f& { return position_; } + auto velocity() const -> const Vector3f& { return velocity_; } + void SetRadius(float r); + auto in_focus() const -> bool { return in_focus_; } + auto radius() const -> float { return radius_; } + + private: + Vector3f position_ = {0.0f, 0.0f, 0.0f}; + Vector3f velocity_ = {0.0f, 0.0f, 0.0f}; + float radius_ = 1.0f; + bool in_focus_ = false; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_AREA_OF_INTEREST_H_ diff --git a/src/ballistica/graphics/camera.cc b/src/ballistica/graphics/camera.cc new file mode 100644 index 00000000..4db67f21 --- /dev/null +++ b/src/ballistica/graphics/camera.cc @@ -0,0 +1,1016 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/camera.h" + +#include + +#include "ballistica/audio/audio.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/area_of_interest.h" +#include "ballistica/graphics/frame_def.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/render_pass.h" +#include "ballistica/graphics/vr_graphics.h" +#include "ode/ode_collision_util.h" + +namespace ballistica { + +const float kCameraOffsetX = 0.0f; +const float kCameraOffsetY = -8.3f; +const float kCameraOffsetZ = -7.4f; +const float kMaxFOV = 150.0f; +const float kPanMax = 9.0f; +const float kPanMin = -9.0f; + +Camera::Camera() + : last_mode_set_time_(GetRealTime()), + lock_panning_(IsVRMode()), + pan_speed_scale_(IsVRMode() ? 0.3f : 1.0f) {} + +Camera::~Camera() = default; + +#define DEG2RAD(a) (0.0174532925f * (a)) + +static auto DotProduct(const dVector3 v1, const dVector3 v2) -> float { + return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]; +} + +static void ProjectPointOnPlane(dVector3 dst, const dVector3 p, + const dVector3 normal) { + float d; + dVector3 n; + float inv_denom; + inv_denom = 1.0F / DotProduct(normal, normal); + d = DotProduct(normal, p) * inv_denom; + n[0] = normal[0] * inv_denom; + n[1] = normal[1] * inv_denom; + n[2] = normal[2] * inv_denom; + dst[0] = p[0] - d * n[0]; + dst[1] = p[1] - d * n[1]; + dst[2] = p[2] - d * n[2]; +} + +void PerpendicularVector(dVector3 dst, const dVector3 src) { + int pos; + int i; + float minelem = 1.0f; + dVector3 tempvec; + + // Find the smallest magnitude axially aligned vector. + for (pos = 0, i = 0; i < 3; i++) { + if (std::abs(src[i]) < minelem) { + pos = i; + minelem = std::abs(src[i]); + } + } + tempvec[0] = tempvec[1] = tempvec[2] = 0.0f; + tempvec[pos] = 1.0f; + + // Project the point onto the plane defined by src. + ProjectPointOnPlane(dst, tempvec, src); + + // Normalize the result. + dNormalize3(dst); +} + +static void MatrixMultiply(float in1[3][3], float in2[3][3], float out[3][3]) { + out[0][0] = + in1[0][0] * in2[0][0] + in1[0][1] * in2[1][0] + in1[0][2] * in2[2][0]; + out[0][1] = + in1[0][0] * in2[0][1] + in1[0][1] * in2[1][1] + in1[0][2] * in2[2][1]; + out[0][2] = + in1[0][0] * in2[0][2] + in1[0][1] * in2[1][2] + in1[0][2] * in2[2][2]; + out[1][0] = + in1[1][0] * in2[0][0] + in1[1][1] * in2[1][0] + in1[1][2] * in2[2][0]; + out[1][1] = + in1[1][0] * in2[0][1] + in1[1][1] * in2[1][1] + in1[1][2] * in2[2][1]; + out[1][2] = + in1[1][0] * in2[0][2] + in1[1][1] * in2[1][2] + in1[1][2] * in2[2][2]; + out[2][0] = + in1[2][0] * in2[0][0] + in1[2][1] * in2[1][0] + in1[2][2] * in2[2][0]; + out[2][1] = + in1[2][0] * in2[0][1] + in1[2][1] * in2[1][1] + in1[2][2] * in2[2][1]; + out[2][2] = + in1[2][0] * in2[0][2] + in1[2][1] * in2[1][2] + in1[2][2] * in2[2][2]; +} + +static void Cross(const dVector3 v1, const dVector3 v2, dVector3 cross) { + cross[0] = v1[1] * v2[2] - v1[2] * v2[1]; + cross[1] = v1[2] * v2[0] - v1[0] * v2[2]; + cross[2] = v1[0] * v2[1] - v1[1] * v2[0]; +} + +static void RotatePointAroundVector(dVector3 dst, const dVector3 dir, + const dVector3 point, float degrees) { + float m[3][3]; + float im[3][3]; + float zrot[3][3]; + float tmpmat[3][3]; + float rot[3][3]; + int i; + dVector3 vr, vup, vf; + float rad; + + vf[0] = dir[0]; + vf[1] = dir[1]; + vf[2] = dir[2]; + + PerpendicularVector(vr, dir); + Cross(vr, vf, vup); + + m[0][0] = vr[0]; + m[1][0] = vr[1]; + m[2][0] = vr[2]; + + m[0][1] = vup[0]; + m[1][1] = vup[1]; + m[2][1] = vup[2]; + + m[0][2] = vf[0]; + m[1][2] = vf[1]; + m[2][2] = vf[2]; + + memcpy(im, m, sizeof(im)); + + im[0][1] = m[1][0]; + im[0][2] = m[2][0]; + im[1][0] = m[0][1]; + im[1][2] = m[2][1]; + im[2][0] = m[0][2]; + im[2][1] = m[1][2]; + + memset(zrot, 0, sizeof(zrot)); + zrot[0][0] = zrot[1][1] = zrot[2][2] = 1.0F; + + rad = DEG2RAD(degrees); + zrot[0][0] = cosf(rad); + zrot[0][1] = sinf(rad); + zrot[1][0] = -sinf(rad); + zrot[1][1] = cosf(rad); + + MatrixMultiply(m, zrot, tmpmat); + MatrixMultiply(tmpmat, im, rot); + + for (i = 0; i < 3; i++) { + dst[i] = rot[i][0] * point[0] + rot[i][1] * point[1] + rot[i][2] * point[2]; + } +} + +void Camera::Shake(float amount) { shake_amount_ += 0.12f * amount; } + +void Camera::UpdateManualMode() { + panning_ = orbiting_ = trucking_ = rolling_ = false; + if (!manual_) { + return; + } + if ((alt_down_ || cmd_down_) && mouse_middle_down_ && mouse_left_down_) { + trucking_ = true; + } else if (ctrl_down_ && mouse_left_down_) { + panning_ = true; + } else if ((alt_down_ || cmd_down_) && mouse_left_down_) { + orbiting_ = true; + } else if ((alt_down_ || cmd_down_) && mouse_right_down_) { + rolling_ = true; + } +} + +void Camera::UpdatePosition() { + // We re-calc our area-of-interest-points here. + area_of_interest_points_.clear(); + + // In non-manual modes, update our position and target automatically. + if (manual_) { + area_of_interest_points_.emplace_back(target_.x, target_.y, target_.z); + } else { + // Non-manual. + + // If we're orbiting, just put a single AOI point in the middle. + if (mode_ == CameraMode::kOrbit) { + target_radius_ = 11; + float dist = 28; + float dist_v = 4.5f; + float altitude = 12; + float world_offset_z = -3; + SetTarget(0, dist_v, world_offset_z); + SetPosition(dist * sinf(heading_), altitude, + dist * cosf(heading_) + world_offset_z); + area_of_interest_points_.emplace_back(target_.x, target_.y, target_.z); + + have_real_areas_of_interest_ = false; + } else { + // Follow mode. + if (explicit_bool(true)) { + float lr_jitter; + { + if (IsVRMode()) { + lr_jitter = 0.0f; + } else { + lr_jitter = + sinf(static_cast(GetRealTime()) / 108.0f) * 0.4f + + sinf(static_cast(GetRealTime()) / 268.0f) * 1.0f; + lr_jitter *= 0.05f; + } + } + + if (!smooth_next_frame_ || lock_panning_) { + pan_pos_ = 0.0f; + pan_speed_ = 0.0f; + pan_target_ = 0.0f; + } + + SetPosition(pan_pos_ + lr_jitter, 20 + 0.5f, 22); + SetTarget(0, 0, 0); // Default. + + float x_min, y_min, z_min, x_max, y_max, z_max; + + if (!areas_of_interest_.empty()) { + float angle_x_min = 0.0f, angle_x_max = 0.0f, angle_y_min = 0.0f, + angle_y_max = 0.0f; + float center_x, center_y, center_z; + + x_min = y_min = z_min = 99999; + x_max = y_max = z_max = -99999; + + // Find the center of all AOI points (clamped to our bounds plus their + // radius as a buffer) + for (auto&& i : areas_of_interest_) { + float x_clamped, y_clamped, z_clamped; + float diameter = i.radius() * 2.0f; + + if (diameter + > (area_of_interest_bounds_[3] - area_of_interest_bounds_[0])) { + x_clamped = + 0.5f + * (area_of_interest_bounds_[3] + area_of_interest_bounds_[0]); + } else { + x_clamped = + std::min(area_of_interest_bounds_[3] - i.radius(), + std::max(area_of_interest_bounds_[0] + i.radius(), + i.position().x)); + } + + if (diameter + > (area_of_interest_bounds_[4] - area_of_interest_bounds_[1])) { + y_clamped = + 0.5f + * (area_of_interest_bounds_[4] + area_of_interest_bounds_[1]); + } else { + y_clamped = + std::min(area_of_interest_bounds_[4] - i.radius(), + std::max(area_of_interest_bounds_[1] + i.radius(), + i.position().y)); + } + + if (diameter + > (area_of_interest_bounds_[5] - area_of_interest_bounds_[2])) { + z_clamped = 0.5f + * ((area_of_interest_bounds_[5] + + area_of_interest_bounds_[2])); + } else { + z_clamped = + std::min(area_of_interest_bounds_[5] - i.radius(), + std::max(area_of_interest_bounds_[2] + i.radius(), + i.position().z)); + } + + x_min = std::min(x_min, x_clamped - i.radius()); + y_min = std::min(y_min, y_clamped - i.radius()); + z_min = std::min(z_min, z_clamped - i.radius()); + x_max = std::max(x_max, x_clamped - i.radius()); + y_max = std::max(y_max, y_clamped - i.radius()); + z_max = std::max(z_max, z_clamped - i.radius()); + + x_min = std::min(x_min, x_clamped + i.radius()); + y_min = std::min(y_min, y_clamped + i.radius()); + z_min = std::min(z_min, z_clamped + i.radius()); + x_max = std::max(x_max, x_clamped + i.radius()); + y_max = std::max(y_max, y_clamped + i.radius()); + z_max = std::max(z_max, z_clamped + i.radius()); + } + + center_x = 0.5f * (x_min + x_max); + center_y = 0.5f * (y_min + y_max); + center_z = 0.5f * (z_min + z_max); + + // As a starting point, aim at the center of these. + SetTarget(center_x, center_y, center_z); + + // Ok, now have a cam position point and base target point. + // now for each point, calc its horizontal and vertical angle from the + // camera's forward vector. + Vector3f cam_forward(target_.x - position_.x, target_.y - position_.y, + target_.z - position_.z); + cam_forward.Normalize(); + Vector3f cam_side = Vector3f::Cross(cam_forward, Vector3f(0, 1, 0)); + cam_side.Normalize(); + Vector3f cam_up = Vector3f::Cross(cam_side, cam_forward); + cam_up.Normalize(); + + int num = 0; + + for (auto&& i : areas_of_interest_) { + // If this point is used for focusing, add it to that list. + if (i.in_focus()) { + // Get the AOI center point clamped to AOI bounds (not taking + // radius into account) + float x_clamped_focus = std::min( + area_of_interest_bounds_[3], + std::max(area_of_interest_bounds_[0], i.position().x)); + float y_clamped_focus = std::min( + area_of_interest_bounds_[4], + std::max(area_of_interest_bounds_[1], i.position().y)); + float z_clamped_focus = std::min( + area_of_interest_bounds_[5], + std::max(area_of_interest_bounds_[2], i.position().z)); + area_of_interest_points_.emplace_back( + x_clamped_focus, y_clamped_focus, z_clamped_focus); + } + + // Now, for camera aiming purposes, add some of their velocity and + // clamp to bounds, taking their radius into account. if our AOI + // sphere is bigger than a given dimension, center it; otherwise + // clamp to the box inset by our radius. + float x_clamped, y_clamped, z_clamped, x_mirrored_clamped; + float diameter = i.radius() * 2.0f; + + if (diameter + > (area_of_interest_bounds_[3] - area_of_interest_bounds_[0])) { + x_clamped = + 0.5f + * (area_of_interest_bounds_[3] + area_of_interest_bounds_[0]); + } else { + x_clamped = + std::min(area_of_interest_bounds_[3] - i.radius(), + std::max(area_of_interest_bounds_[0] + i.radius(), + i.position().x)); + } + + if (diameter + > (area_of_interest_bounds_[4] - area_of_interest_bounds_[1])) { + y_clamped = + 0.5f + * (area_of_interest_bounds_[4] + area_of_interest_bounds_[1]); + } else { + y_clamped = + std::min(area_of_interest_bounds_[4] - i.radius(), + std::max(area_of_interest_bounds_[1] + i.radius(), + i.position().y)); + } + + if (diameter + > (area_of_interest_bounds_[5] - area_of_interest_bounds_[2])) { + z_clamped = 0.5f + * ((area_of_interest_bounds_[5] + + area_of_interest_bounds_[2])); + } else { + z_clamped = + std::min(area_of_interest_bounds_[5] - i.radius(), + std::max(area_of_interest_bounds_[2] + i.radius(), + i.position().z)); + } + + // Let's also do a version mirrored across the camera's x coordinate + // (adding this to our tracked point set causes us zoom out instead + // of rotating generally) + float x_mirrored = position_.x - (i.position().x - position_.x); + if (diameter + > (area_of_interest_bounds_[3] - area_of_interest_bounds_[0])) { + x_mirrored_clamped = + 0.5f + * (area_of_interest_bounds_[3] + area_of_interest_bounds_[0]); + } else { + x_mirrored_clamped = + std::min(area_of_interest_bounds_[3] - i.radius(), + std::max(area_of_interest_bounds_[0] + i.radius(), + x_mirrored)); + } + + Vector3f corner_offs = (cam_side + cam_up) * i.radius(); + + for (int sample = 0; sample < 2; sample++) { + Vector3f to_point{x_clamped - position_.x, + y_clamped - position_.y, + z_clamped - position_.z}; + + // For sample 0, subtract AOI radius in camera-space x and y. + // For sample 1, add them. + // this way we should get the whole sphere. + if (sample == 0) { + to_point -= corner_offs; + } else if (sample == 1) { + to_point += corner_offs; + } else if (sample == 2) { + to_point.v[0] = x_mirrored_clamped - position_.x; + } + + to_point.Normalize(); + float up_amt = Vector3f::Dot(to_point, cam_up); + float side_amt = Vector3f::Dot(to_point, cam_side); + + // Get the vector from the cam to this point, subtract out the + // component parallel to the camera's up vector, and then measure + // the angle to the camera's forward vector. + + float angle_x, angle_y; + + if (std::abs(up_amt) < 0.001f) { + angle_y = 0.0f; + } else { + angle_y = Vector3f::Angle(to_point - cam_side * side_amt, + cam_forward); + } + if (std::abs(side_amt) < 0.001f) { + angle_x = 0.0f; + } else { + angle_x = + Vector3f::Angle(to_point - cam_up * up_amt, cam_forward); + } + if (side_amt > 0) { + angle_x *= -1; + } + if (up_amt > 0) { + angle_y *= -1; + } + if (num == 0) { + angle_x_min = angle_x_max = angle_x; + angle_y_min = angle_y_max = angle_y; + } else { + angle_x_min = std::min(angle_x_min, angle_x); + angle_x_max = std::max(angle_x_max, angle_x); + angle_y_min = std::min(angle_y_min, angle_y); + angle_y_max = std::max(angle_y_max, angle_y); + } + num++; + } + } + + float turn_angle_x = 0.5f * (angle_x_min + angle_x_max); + float turn_angle_y = 0.5f * (angle_y_min + angle_y_max); + + // Get cam target relative to the camera, rotate it on cam left/right, + // and set it. + Vector3f p(target_.x - position_.x, target_.y - position_.y, + target_.z - position_.z); + p = Matrix44fRotate(cam_up, turn_angle_x) * p; + SetTarget(position_.x + p.v[0], position_.y + p.v[1], + position_.z + p.v[2]); + + // Now the same for cam up/down. + // Note: technically we should recalc angles since we just rotated, + // but this should be close enough. + Vector3f p2(target_.x - position_.x, target_.y - position_.y, + target_.z - position_.z); + p2 = Matrix44fRotate(cam_side, -turn_angle_y) * p2; + SetTarget(position_.x + p2.v[0], position_.y + p2.v[1], + position_.z + p2.v[2]); + + field_of_view_x_ = angle_x_max - angle_x_min; + field_of_view_y_ = angle_y_max - angle_y_min; + } else { + // Look at the center of the AOI bounds. + if (area_of_interest_bounds_[0] != -9999) { + x_min = x_max = + 0.5f + * (area_of_interest_bounds_[3] + area_of_interest_bounds_[0]); + y_min = y_max = area_of_interest_bounds_[4] + + 0.5f + * (area_of_interest_bounds_[1] + - area_of_interest_bounds_[4]); + z_min = z_max = + 0.5f + * (area_of_interest_bounds_[5] + area_of_interest_bounds_[2]); + } else { + // Our default area of interest position is a bit higher + // in vr since we want to drag our UI up a bit by default. + x_min = x_max = 0.0f; + y_min = y_max = 3.0f; + z_min = z_max = -5.0f; + + // In vr mode we want or default area-of-interest to line up so that + // our fixed-overlay matrix and our regular overlay matrix come out + // the same. +#if BA_VR_BUILD + if (IsVRMode()) { + // Only apply map's X offset if camera is locked. + x_min = x_max = position_.x + + (kCameraOffsetX + + (lock_panning_ ? vr_offset_smooth_.x : 0.0f) + + vr_extra_offset_.x); + y_min = y_max = + position_.y + + (kCameraOffsetY + vr_offset_smooth_.y + vr_extra_offset_.y) + + kVRFixedOverlayOffsetY; + z_min = z_max = + position_.z + + (kCameraOffsetZ + vr_offset_smooth_.z + vr_extra_offset_.z) + + kVRFixedOverlayOffsetZ; + } +#endif + } + field_of_view_x_ = 45.0f; + field_of_view_y_ = 30.0f; + SetTarget(0.5f * (x_min + x_max), 0.5f * (y_min + y_max), + 0.5f * (z_min + z_max)); + } + + // If we don't have any focusable points, drop in a default. + if (area_of_interest_points_.empty()) { + area_of_interest_points_.emplace_back(0, 0, 0); + have_real_areas_of_interest_ = false; + } else { + have_real_areas_of_interest_ = true; + } + pan_target_ = (x_max + x_min) / 2; + if (pan_target_ > kPanMax) { + pan_target_ = kPanMax; + } else if (pan_target_ < kPanMin) { + pan_target_ = kPanMin; + } + } + } + } + + // If they're on manual, we don't do smoothing or anything fancy. + if (manual_) { + target_.x = target_smoothed_.x = target_.x; + target_.y = target_smoothed_.y = target_.y; + target_.z = target_smoothed_.z = target_.z; + smooth_speed_ = {0.0f, 0.0f, 0.0f}; + smooth_next_frame_ = false; + } else { + if (mode_ == CameraMode::kFollow) { + // Useful to test camera. + if (explicit_bool(false)) { + field_of_view_x_smoothed_ = field_of_view_x_; + field_of_view_y_smoothed_ = field_of_view_y_; + target_smoothed_.x = target_.x; + target_smoothed_.y = target_.y; + target_smoothed_.z = target_.z; + pan_pos_ = pan_target_; + xy_constrain_blend_ = x_constrained_ ? 1.0f : 0.0f; + } + } else { + float dx = target_smoothed_.x - position_.x; + float dy = target_smoothed_.y - position_.y; + float dz = target_smoothed_.z - position_.z; + float target_dist = sqrtf(dx * dx + dy * dy + dz * dz); + + // If we're not smoothing this upcoming frame, snap this value. + if (!smooth_next_frame_) target_radius_smoothed_ = target_radius_; + + float angle = tanf(target_radius_smoothed_ / target_dist); + field_of_view_x_ = 0.001f; // Always want y to be the constrained one. + field_of_view_y_ = (2 * 360 * (angle / (2 * 3.1415f))); + } + } + + // Extra cam-space tweakage (via accelerometer if available). + { + Vector3f to_cam(target_smoothed_.x - position_.x, + target_smoothed_.y - position_.y, + target_smoothed_.z - position_.z); + to_cam.Normalize(); + Vector3f cam_space_lr = Vector3f::Cross(to_cam, Vector3f(0, 1, 0)); + Vector3f cam_space_ud = Vector3f::Cross(cam_space_lr, to_cam); + Vector3f tilt = 0.1f * g_graphics->tilt(); + if (manual_) { + tilt.x = 0.0f; + tilt.y = 0.0f; + } + extra_pos_ = -0.1f * tilt.y * cam_space_lr + 0.1f * tilt.x * cam_space_ud; + extra_pos_2_ = extra_pos_; + extra_pos_2_ += 0.35f * tilt.y * cam_space_lr; + extra_pos_2_ -= 0.35f * tilt.x * cam_space_ud; + up_ = cam_space_ud; + + // A tiny bit of random jitter to our camera pos. + if (!manual_) { + float mag = 2.0f; + extra_pos_2_.x += mag * position_offset_smoothed_.x; + extra_pos_2_.y += mag * position_offset_smoothed_.y; + extra_pos_2_.z += mag * position_offset_smoothed_.z; + } + } +} + +void Camera::Update(millisecs_t elapsed) { + float rand_component = 0.000005f; + float zoom_speed = 0.001f; + float fov_speed_out = 0.0025f; + float fov_speed_in = 0.001f; + float speed = 0.000012f; + float speed_2 = 0.00005f; + float damping = 0.006f; + float damping2 = 0.006f; + float xy_blend_speed = 0.0002f; + millisecs_t real_time = GetRealTime(); + + // Prevent camera "explosions" if we've been unable to update for a while. + elapsed = std::min(millisecs_t{100}, elapsed); + + // In normal mode we orbit; in vr mode we don't. + if (IsVRMode()) { + heading_ = -0.3f; + } else { + heading_ += static_cast(elapsed) / 10000.0f; + } + + int rand_incr_1 = 309; + int rand_incr_2 = 273; + int rand_incr_3 = 247; + + if (mode_ == CameraMode::kOrbit) { + rand_component *= 2.5f; + rand_incr_1 /= 2; + rand_incr_2 /= 2; + rand_incr_3 /= 2; + } + + target_radius_smoothed_ += + elapsed * (target_radius_ - target_radius_smoothed_) * zoom_speed; + + float diff = field_of_view_x_ - field_of_view_x_smoothed_; + field_of_view_x_smoothed_ += + elapsed * diff * (diff > 0.0f ? fov_speed_out : fov_speed_in); + + diff = field_of_view_y_ - field_of_view_y_smoothed_; + field_of_view_y_smoothed_ += + elapsed * diff * (diff > 0.0f ? fov_speed_out : fov_speed_in); + + if (x_constrained_) { + xy_constrain_blend_ += + elapsed * (1.0f - xy_constrain_blend_) * xy_blend_speed; + xy_constrain_blend_ = std::min(1.0f, xy_constrain_blend_); + } else { + xy_constrain_blend_ += + elapsed * (0.0f - xy_constrain_blend_) * xy_blend_speed * elapsed; + xy_constrain_blend_ = std::max(0.0f, xy_constrain_blend_); + } + + if (!IsVRMode()) { + smooth_speed_.x += elapsed * rand_component + * (-0.5f + + Utils::precalc_rands_1[(real_time / rand_incr_1) + % kPrecalcRandsCount]); + smooth_speed_.y += elapsed * rand_component + * (-0.5f + + Utils::precalc_rands_2[(real_time / rand_incr_2) + % kPrecalcRandsCount]); + smooth_speed_.z += elapsed * rand_component + * (-0.5f + + Utils::precalc_rands_3[(real_time / rand_incr_3) + % kPrecalcRandsCount]); + } + + if (RandomFloat() < 0.1f && !IsVRMode()) { + smooth_speed_2_.x += + elapsed * rand_component * 4.0f * (-0.5f + RandomFloat()); + smooth_speed_2_.y += + elapsed * rand_component * 4.0f * (-0.5f + RandomFloat()); + smooth_speed_2_.z += + elapsed * rand_component * 4.0f * (-0.5f + RandomFloat()); + } + + // If we have no important areas of interest, keep our camera from moving too + // fast. + if (!have_real_areas_of_interest_) { + speed *= 0.5f; + } + + for (millisecs_t i = 0; i < elapsed; i++) { + { + float smoothing = 0.8f; + float inv_smoothing = 1.0f - smoothing; + vr_offset_smooth_.x = + smoothing * vr_offset_smooth_.x + inv_smoothing * vr_offset_.x; + vr_offset_smooth_.y = + smoothing * vr_offset_smooth_.y + inv_smoothing * vr_offset_.y; + vr_offset_smooth_.z = + smoothing * vr_offset_smooth_.z + inv_smoothing * vr_offset_.z; + } + smooth_speed_ += (target_ - target_smoothed_) * speed; + smooth_speed_ *= (1.0f - damping); + smooth_speed_2_ += (-position_offset_smoothed_) * speed_2; + smooth_speed_2_ *= (1.0f - damping2); + target_smoothed_ += smooth_speed_; + position_offset_smoothed_ += smooth_speed_2_; + + pan_speed_ += 0.00004f * pan_speed_scale_ * (pan_target_ - position_.x); + pan_speed_ *= 0.97f; + if (position_.x > kPanMax) pan_speed_ -= (position_.x - kPanMax) * 0.00003f; + if (position_.x < kPanMin) pan_speed_ -= (position_.x - kPanMin) * 0.00003f; + pan_pos_ += pan_speed_; + + int iterations = 1; + + // Jostle the camera occasionally if we're shaking. + if (i % iterations == 0 && shake_amount_ > 0.0001f) { + shake_amount_ *= 0.97f; + shake_vel_.x += 0.05f * shake_amount_ + * (0.5f + - Utils::precalc_rands_1[real_time % 122 * i + % kPrecalcRandsCount]); + shake_vel_.y += 0.05f * shake_amount_ + * (0.5f + - Utils::precalc_rands_2[real_time % 323 * i + % kPrecalcRandsCount]); + shake_vel_.z += + 0.05f * shake_amount_ + * (0.5f + - Utils::precalc_rands_3[real_time % 76 * i % kPrecalcRandsCount]); + } + + for (int j = 0; j < iterations; j++) { + shake_pos_ += shake_vel_; + shake_vel_ += -0.001f * shake_pos_; + shake_vel_ *= 0.99f; + } + + if (g_graphics->camera_shake_disabled()) { + shake_vel_ = {0, 0, 0}; + } + } + + // Update audio position more often in vr since we can whip our head around. + uint32_t interval = IsVRMode() ? 50 : 100; + + // Every now and then, update microphone position for audio. + if (real_time - last_listener_update_time_ > interval) { + last_listener_update_time_ = real_time; + bool do_regular_update = true; + if (IsVRMode()) { +#if BA_VR_MODE + VRGraphics* vrgraphics = VRGraphics::get(); + do_regular_update = false; + Vector3f listener_pos = vrgraphics->vr_head_translate() + + vrgraphics->vr_head_forward() * 5.0f; + assert(g_audio_server); + g_audio->SetListenerPosition(listener_pos); + g_audio->SetListenerOrientation(vrgraphics->vr_head_forward(), + vrgraphics->vr_head_up()); +#endif + } + if (explicit_bool(do_regular_update)) { + float to_target = 0.5f; + Vector3f listener_pos( + position_.x + to_target * (target_smoothed_.x - position_.x), + position_.y + to_target * (target_smoothed_.y - position_.y), + position_.z + to_target * (target_smoothed_.z - position_.z)); + assert(g_audio_server); + g_audio->SetListenerPosition(listener_pos); + } + } +} + +void Camera::SetPosition(float x, float y, float z) { + position_.x = x; + position_.y = y; + position_.z = z; +} + +void Camera::SetTarget(float x, float y, float z) { + target_.x = x; + target_.y = y; + target_.z = z; +} + +void Camera::ManualHandleMouseWheel(float value) { + if (!manual_) { + return; + } + + // Make this tiny so that Y is always the constraint. + field_of_view_x_ = 0.1f; + field_of_view_y_ *= (1.0f - 0.1f * value); + if (field_of_view_y_ > kMaxFOV) { + field_of_view_y_ = kMaxFOV; + } else if (field_of_view_y_ < 1.0f) { + field_of_view_y_ = 1.0f; + } +} + +void Camera::ManualHandleMouseMove(float move_h, float move_v) { + if (!manual_) return; + + if (panning_ || trucking_ || orbiting_ || rolling_) { + // get cam vector + dVector3 cam_vec = {target_.x - position_.x, target_.y - position_.y, + target_.z - position_.z}; + float len = dVector3Length(cam_vec); + dNormalize3(cam_vec); + + float fov_width = + 2 * (len * tanf(((field_of_view_y_) / 2) * 0.0174532925f)); + + // get cam side vector + dVector3 up = {0, 1, 0}; + dVector3 side_vec; + dVector3Cross(up, cam_vec, side_vec); + dNormalize3(side_vec); + + // get cam's up vector + dVector3 cam_up; + dVector3Cross(side_vec, cam_vec, cam_up); + dNormalize3(cam_up); + + if (panning_) { + move_h *= fov_width; + move_v *= fov_width; + side_vec[0] *= move_h; + side_vec[1] *= move_h; + side_vec[2] *= move_h; + cam_up[0] *= move_v; + cam_up[1] *= move_v; + cam_up[2] *= move_v; + + position_.x += side_vec[0] + cam_up[0]; + position_.y += side_vec[1] + cam_up[1]; + position_.z += side_vec[2] + cam_up[2]; + target_.x += side_vec[0] + cam_up[0]; + target_.y += side_vec[1] + cam_up[1]; + target_.z += side_vec[2] + cam_up[2]; + } else if (orbiting_) { + dVector3 cam_pos = {position_.x - target_.x, position_.y - target_.y, + position_.z - target_.z}; + RotatePointAroundVector(cam_pos, side_vec, cam_pos, move_v * -100); + RotatePointAroundVector(cam_pos, up, cam_pos, move_h * -100); + position_.x = cam_pos[0] + target_.x; + position_.y = cam_pos[1] + target_.y; + position_.z = cam_pos[2] + target_.z; + + } else if (rolling_) { + // _roll += (move_h + move_v) * 100.0f; + } else if (trucking_) { + cam_vec[0] *= (move_h + move_v) * len; + cam_vec[1] *= (move_h + move_v) * len; + cam_vec[2] *= (move_h + move_v) * len; + position_.x += cam_vec[0]; + position_.y += cam_vec[1]; + position_.z += cam_vec[2]; + } + } +} + +auto Camera::NewAreaOfInterest(bool in_focus) -> AreaOfInterest* { + assert(InGameThread()); + areas_of_interest_.emplace_back(in_focus); + return &areas_of_interest_.back(); +} + +void Camera::DeleteAreaOfInterest(AreaOfInterest* a) { + assert(InGameThread()); + for (auto i = areas_of_interest_.begin(); i != areas_of_interest_.end(); + ++i) { + if (&(*i) == a) { + areas_of_interest_.erase(i); + return; + } + } + throw Exception("Area-of-interest not found"); +} + +void Camera::SetManual(bool enable) { + manual_ = enable; + if (manual_) { + // Reset our target settings to our current smoothed ones + // so we don't see an instant jump to the target. + target_.x = target_smoothed_.x; + target_.y = target_smoothed_.y; + target_.z = target_smoothed_.z; + } else { + smooth_next_frame_ = false; + } +} + +void Camera::SetMode(CameraMode m) { + if (mode_ != m) { + mode_ = m; + smooth_next_frame_ = false; + last_mode_set_time_ = GetRealTime(); + heading_ = kInitialHeading; + } +} + +void Camera::ApplyToFrameDef(FrameDef* frame_def) { + frame_def->set_camera_mode(mode_); + + // FIXME - we should have some sort of support + // for multiple cameras each with their own pass... + // for now, though, there's just a single beauty pass + // which is us + + RenderPass* passes[] = { + frame_def->beauty_pass(), + frame_def->beauty_pass_bg(), +#if BA_VR_BUILD + frame_def->overlay_pass(), + frame_def->GetOverlayFixedPass(), + frame_def->vr_cover_pass(), +#endif + frame_def->overlay_3d_pass(), + frame_def->blit_pass(), + nullptr + }; + + // Currently our x/y fovs are simply enough to fit everything. + // Check the aspect ratio of what we're rendering to and fit them. + + // Add a few degrees just to keep things away from the edges a bit + // since we have various UI elements there. + float extra = 0.0f; + + // If we don't want to smooth this frame, snap these values. + if (!smooth_next_frame_) { + field_of_view_x_smoothed_ = field_of_view_x_; + field_of_view_y_smoothed_ = field_of_view_y_; + } + + float final_fov_y = field_of_view_y_smoothed_ + extra; + if (final_fov_y < 1.0f) { + final_fov_y = 1.0f; + } else if (final_fov_y > 120.0f) { + final_fov_y = 120.0f; + } + float final_fov_x = field_of_view_x_smoothed_ + extra; + if (final_fov_x < 1.0f) { + final_fov_x = 1.0f; + } else if (final_fov_x > 120.0f) { + final_fov_x = 120.0f; + } + float ratio = final_fov_x / final_fov_y; + + // Need to look at a pass to know if we're x or y constrained. + float render_ratio = passes[0]->GetPhysicalAspectRatio(); + + // Update whether we're x-constrained or not. + x_constrained_ = (ratio >= render_ratio); + + // When we're x-constrained, we calc y so that x fits. + float final_fov_y2 = final_fov_x / render_ratio; + + // If we're not smoothing this frame, snap immediately. + if (!smooth_next_frame_) xy_constrain_blend_ = x_constrained_ ? 1.0f : 0.0f; + + // We smoothly blend between our x-constrained and non-x-constrained y values + // so that we don't see a hitch when it switches. + final_fov_y = xy_constrain_blend_ * final_fov_y2 + + (1.0f - xy_constrain_blend_) * final_fov_y; + + final_fov_y = std::max(5.0f, final_fov_y); + + // Reset some last things if we're non-smoothed. + if (!smooth_next_frame_) { + smooth_speed_ = {0.0f, 0.0f, 0.0f}; + shake_amount_ = 0; + shake_pos_ = {0.0f, 0.0f, 0.0f}; + shake_vel_ = {0.0f, 0.0f, 0.0f}; + target_smoothed_ = target_; + up_.x = 0.0f; + up_.y = 1.0f; + up_.z = 0.0f; + vr_offset_smooth_ = vr_offset_; + } + + // Also store original positions with the frame_def in case we want to muck + // with them later (VR, etc). + frame_def->set_cam_original(Vector3f(position_.x + extra_pos_2_.x, + position_.y + extra_pos_2_.y, + position_.z + extra_pos_2_.z)); + + // If we're vr, apply current vr offsets. + if (IsVRMode()) { + if (mode_ == CameraMode::kFollow) { + Vector3f cam_original = frame_def->cam_original(); + + // Only apply map's X offset if our camera is locked. + cam_original.x += kCameraOffsetX + + (lock_panning_ ? vr_offset_smooth_.x : 0.0f) + + vr_extra_offset_.x; + cam_original.y += + kCameraOffsetY + vr_offset_smooth_.y + vr_extra_offset_.y; + cam_original.z += + kCameraOffsetZ + vr_offset_smooth_.z + vr_extra_offset_.z; + frame_def->set_cam_original(cam_original); + } else { + Vector3f cam_original = frame_def->cam_original(); + cam_original.y += 3.0f; + frame_def->set_cam_original(cam_original); + } + } + frame_def->set_cam_target_original(target_smoothed_); + frame_def->set_shake_original(shake_pos_); + + for (RenderPass** p = passes; *p != nullptr; p++) { + assert(!area_of_interest_points_.empty()); + (**p).SetCamera( + position_ + extra_pos_2_, target_smoothed_ + shake_pos_ + extra_pos_, + up_, 4, 1000.0f, + -1.0f, // Auto x fov. + final_fov_y * (g_graphics->tv_border() ? (1.0f + kTVBorder) : 1.0f), + false, 0, 0, 0, 0, // Not using tangent fovs. + area_of_interest_points_); + } + smooth_next_frame_ = true; +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/camera.h b/src/ballistica/graphics/camera.h new file mode 100644 index 00000000..bd0dffdc --- /dev/null +++ b/src/ballistica/graphics/camera.h @@ -0,0 +1,152 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_CAMERA_H_ +#define BALLISTICA_GRAPHICS_CAMERA_H_ + +#include +#include + +#include "ballistica/core/object.h" +#include "ballistica/math/vector3f.h" + +namespace ballistica { + +// Hmm this shouldn't be here. +const float kHappyThoughtsZPlane = -5.52f; + +// Default horizontal camera field of view. +const float kCameraFOVY = 60.0f; +const float kInitialHeading = -1.0f; + +// FIXME: looks like this guy gets accessed from a few different threads. +class Camera : public Object { + public: + Camera(); + ~Camera() override; + void Shake(float amount); + void SetManual(bool enable); + auto manual() const -> bool { return manual_; } + void ManualHandleMouseMove(float move_h, float move_v); + void ManualHandleMouseWheel(float val); + + // Update the camera position values - done once per render + void UpdatePosition(); + + // Update camera velocities/etc - done as often as possible. + void Update(millisecs_t elapsed); + void SetPosition(float x, float y, float z); + void SetTarget(float x, float y, float z); + void SetMode(CameraMode m); + void set_area_of_interest_bounds(float min_x, float min_y, float min_z, + float max_x, float max_y, float max_z) { + area_of_interest_bounds_[0] = min_x; + area_of_interest_bounds_[1] = min_y; + area_of_interest_bounds_[2] = min_z; + area_of_interest_bounds_[3] = max_x; + area_of_interest_bounds_[4] = max_y; + area_of_interest_bounds_[5] = max_z; + } + void area_of_interest_bounds(float* min_x, float* min_y, float* min_z, + float* max_x, float* max_y, float* max_z) { + *min_x = area_of_interest_bounds_[0]; + *min_y = area_of_interest_bounds_[1]; + *min_z = area_of_interest_bounds_[2]; + *max_x = area_of_interest_bounds_[3]; + *max_y = area_of_interest_bounds_[4]; + *max_z = area_of_interest_bounds_[5]; + } + void UpdateManualMode(); + + // Sets up the render in the passes we're associated with. Call this anytime + // during a render. + void ApplyToFrameDef(FrameDef* frame_def); + auto field_of_view_y() const -> float { return field_of_view_y_; } + void get_position(float* x, float* y, float* z) const { + *x = position_.x; + *y = position_.y; + *z = position_.z; + } + void target_smoothed(float* x, float* y, float* z) const { + *x = target_smoothed_.x; + *y = target_smoothed_.y; + *z = target_smoothed_.z; + } + void set_alt_down(bool d) { alt_down_ = d; } + void set_cmd_down(bool d) { cmd_down_ = d; } + void set_ctrl_down(bool d) { ctrl_down_ = d; } + void set_mouse_left_down(bool d) { mouse_left_down_ = d; } + void set_mouse_right_down(bool d) { mouse_right_down_ = d; } + void set_mouse_middle_down(bool d) { mouse_middle_down_ = d; } + void set_happy_thoughts_mode(bool h) { happy_thoughts_mode_ = h; } + auto happy_thoughts_mode() const -> bool { return happy_thoughts_mode_; } + auto NewAreaOfInterest(bool inFocus = true) -> AreaOfInterest*; + void DeleteAreaOfInterest(AreaOfInterest* a); + auto mode() const -> CameraMode { return mode_; } + void set_vr_offset(const Vector3f& val) { vr_offset_ = val; } + void set_vr_extra_offset(const Vector3f& val) { vr_extra_offset_ = val; } + auto vr_extra_offset() const -> const Vector3f& { return vr_extra_offset_; } + void set_lock_panning(bool val) { lock_panning_ = val; } + auto lock_panning() const -> bool { return lock_panning_; } + auto pan_speed_scale() const -> float { return pan_speed_scale_; } + void set_pan_speed_scale(float val) { pan_speed_scale_ = val; } + + private: + float pan_speed_scale_{1.0f}; + bool lock_panning_{}; + Vector3f vr_offset_{0.0f, 0.0f, 0.0f}; + Vector3f vr_extra_offset_{0.0f, 0.0f, 0.0f}; + Vector3f vr_offset_smooth_{0.0f, 0.0f, 0.0f}; + millisecs_t last_mode_set_time_{}; + std::list areas_of_interest_; + CameraMode mode_{CameraMode::kFollow}; + bool manual_{}; + bool smooth_next_frame_{}; + bool have_real_areas_of_interest_{}; + + // Manual stuff. + bool panning_{}; + bool orbiting_{}; + bool rolling_{}; + bool trucking_{}; + bool alt_down_{}; + bool cmd_down_{}; + bool ctrl_down_{}; + bool mouse_left_down_{}; + bool mouse_middle_down_{}; + bool mouse_right_down_{}; + float heading_{kInitialHeading}; + Vector3f extra_pos_{0.0f, 0.0f, 0.0f}; + Vector3f extra_pos_2_{0.0f, 0.0f, 0.0f}; + float area_of_interest_bounds_[6]{-9999, -9999, -9999, 9999, 9999, 9999}; + float pan_pos_{}; + float pan_speed_{}; + float pan_target_{}; + float shake_amount_{}; + Vector3f shake_pos_{0.0f, 0.0f, 0.0f}; + Vector3f shake_vel_{0.0f, 0.0f, 0.0f}; + Vector3f position_{0.0f, 1.0f, -1.0f}; + Vector3f target_{0.0f, 1.0f, -1.0f}; + Vector3f target_smoothed_{0.0f, 0.0f, 0.0f}; + Vector3f position_offset_smoothed_{0.0f, 0.0f, 0.0f}; + Vector3f smooth_speed_{0.0f, 0.0f, 0.0f}; + Vector3f smooth_speed_2_{0.0f, 0.0f, 0.0f}; + float target_radius_{2.0f}; + float target_radius_smoothed_{2.0f}; + float field_of_view_x_{5.0f}; + float field_of_view_y_{kCameraFOVY}; + float field_of_view_x_smoothed_{1.0f}; + float field_of_view_y_smoothed_{1.0f}; + millisecs_t last_listener_update_time_{}; + bool happy_thoughts_mode_{}; + float min_target_radius_{5.0f}; + Vector3f up_{0.0f, 1.0f, 0.0f}; + float area_of_interest_near_{1.0f}; + float area_of_interest_far_{2.0f}; + bool x_constrained_{true}; + float xy_constrain_blend_{0.5f}; + std::vector area_of_interest_points_{{0.0f, 0.0f, 0.0f}}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_CAMERA_H_ diff --git a/src/ballistica/graphics/component/empty_component.h b/src/ballistica/graphics/component/empty_component.h new file mode 100644 index 00000000..3b70970a --- /dev/null +++ b/src/ballistica/graphics/component/empty_component.h @@ -0,0 +1,30 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_COMPONENT_EMPTY_COMPONENT_H_ +#define BALLISTICA_GRAPHICS_COMPONENT_EMPTY_COMPONENT_H_ + +#include "ballistica/graphics/component/render_component.h" + +namespace ballistica { + +// Empty component - has no shader but can be useful for spitting out +// transform/scissor/etc state changes. +class EmptyComponent : public RenderComponent { + public: + explicit EmptyComponent(RenderPass* pass) + : RenderComponent(pass), transparent_(false) {} + void SetTransparent(bool val) { + EnsureConfiguring(); + transparent_ = val; + } + + protected: + void WriteConfig() override { ConfigForEmpty(transparent_); } + + private: + bool transparent_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_COMPONENT_EMPTY_COMPONENT_H_ diff --git a/src/ballistica/graphics/component/object_component.cc b/src/ballistica/graphics/component/object_component.cc new file mode 100644 index 00000000..ce333d5e --- /dev/null +++ b/src/ballistica/graphics/component/object_component.cc @@ -0,0 +1,195 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/component/object_component.h" + +#include "ballistica/graphics/renderer.h" + +namespace ballistica { + +void ObjectComponent::WriteConfig() { + // If they didn't give us a texture, just use a blank white texture. + // This is not a common case and easier than forking all our shaders to + // create non-textured versions. + if (!texture_.exists()) { + texture_ = g_media->GetTexture(SystemTextureID::kWhite); + } + if (reflection_ == ReflectionType::kNone) { + assert(!double_sided_); // Unsupported combo. + assert(!colorize_texture_.exists()); // Unsupported combo. + assert(!have_color_add_); // Unsupported combo. + if (light_shadow_ == LightShadowType::kNone) { + if (transparent_) { + ConfigForShading(ShadingType::kObjectTransparent); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_); + cmd_buffer_->PutTexture(texture_); + } else { + ConfigForShading(ShadingType::kObject); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_); + cmd_buffer_->PutTexture(texture_); + } + } else { + if (transparent_) { + assert(!world_space_); // Unsupported combo. + ConfigForShading(ShadingType::kObjectLightShadowTransparent); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutInt(static_cast(light_shadow_)); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_); + cmd_buffer_->PutTexture(texture_); + } else { + ConfigForShading(ShadingType::kObjectLightShadow); + cmd_buffer_->PutInt(static_cast(light_shadow_)); + cmd_buffer_->PutInt(world_space_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_); + cmd_buffer_->PutTexture(texture_); + } + } + } else { + if (light_shadow_ == LightShadowType::kNone) { + assert(!double_sided_); // Unsupported combo. + assert(!colorize_texture_.exists()); // Unsupported combo. + if (transparent_) { + assert(!world_space_); // Unsupported combo. + if (have_color_add_) { + ConfigForShading(ShadingType::kObjectReflectAddTransparent); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_, + color_add_r_, color_add_g_, color_add_b_, + reflection_scale_r_, reflection_scale_g_, + reflection_scale_b_); + cmd_buffer_->PutTexture(texture_); + SystemCubeMapTextureID r = + Graphics::CubeMapFromReflectionType(reflection_); + cmd_buffer_->PutCubeMapTexture(g_media->GetCubeMapTexture(r)); + } else { + ConfigForShading(ShadingType::kObjectReflectTransparent); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_, + reflection_scale_r_, reflection_scale_g_, + reflection_scale_b_); + cmd_buffer_->PutTexture(texture_); + SystemCubeMapTextureID r = + Graphics::CubeMapFromReflectionType(reflection_); + cmd_buffer_->PutCubeMapTexture(g_media->GetCubeMapTexture(r)); + } + } else { + ConfigForShading(ShadingType::kObjectReflect); + cmd_buffer_->PutInt(world_space_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, + reflection_scale_r_, reflection_scale_g_, + reflection_scale_b_); + cmd_buffer_->PutTexture(texture_); + SystemCubeMapTextureID r = + Graphics::CubeMapFromReflectionType(reflection_); + cmd_buffer_->PutCubeMapTexture(g_media->GetCubeMapTexture(r)); + } + } else { + // With add. + assert(!transparent_); // Unsupported combo. + if (!have_color_add_) { + if (colorize_texture_.exists()) { + assert(!double_sided_); // Unsupported combo. + assert(!world_space_); // Unsupported combo. + if (do_colorize_2_) { + ConfigForShading(ShadingType::kObjectReflectLightShadowColorized2); + cmd_buffer_->PutInt(static_cast(light_shadow_)); + cmd_buffer_->PutFloats( + color_r_, color_g_, color_b_, reflection_scale_r_, + reflection_scale_g_, reflection_scale_b_, colorize_color_r_, + colorize_color_g_, colorize_color_b_, colorize_color2_r_, + colorize_color2_g_, colorize_color2_b_); + cmd_buffer_->PutTexture(texture_); + cmd_buffer_->PutTexture(colorize_texture_); + SystemCubeMapTextureID r = + Graphics::CubeMapFromReflectionType(reflection_); + cmd_buffer_->PutCubeMapTexture(g_media->GetCubeMapTexture(r)); + } else { + ConfigForShading(ShadingType::kObjectReflectLightShadowColorized); + cmd_buffer_->PutInt(static_cast(light_shadow_)); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, + reflection_scale_r_, reflection_scale_g_, + reflection_scale_b_, colorize_color_r_, + colorize_color_g_, colorize_color_b_); + cmd_buffer_->PutTexture(texture_); + cmd_buffer_->PutTexture(colorize_texture_); + SystemCubeMapTextureID r = + Graphics::CubeMapFromReflectionType(reflection_); + cmd_buffer_->PutCubeMapTexture(g_media->GetCubeMapTexture(r)); + } + } else { + if (double_sided_) { + ConfigForShading(ShadingType::kObjectReflectLightShadowDoubleSided); + cmd_buffer_->PutInt(static_cast(light_shadow_)); + cmd_buffer_->PutInt(world_space_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, + reflection_scale_r_, reflection_scale_g_, + reflection_scale_b_); + cmd_buffer_->PutTexture(texture_); + SystemCubeMapTextureID r = + Graphics::CubeMapFromReflectionType(reflection_); + cmd_buffer_->PutCubeMapTexture(g_media->GetCubeMapTexture(r)); + } else { + ConfigForShading(ShadingType::kObjectReflectLightShadow); + cmd_buffer_->PutInt(static_cast(light_shadow_)); + cmd_buffer_->PutInt(world_space_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, + reflection_scale_r_, reflection_scale_g_, + reflection_scale_b_); + cmd_buffer_->PutTexture(texture_); + SystemCubeMapTextureID r = + Graphics::CubeMapFromReflectionType(reflection_); + cmd_buffer_->PutCubeMapTexture(g_media->GetCubeMapTexture(r)); + } + } + } else { + assert(!double_sided_); // Unsupported combo. + assert(!world_space_); // Unsupported config. + if (colorize_texture_.exists()) { + if (do_colorize_2_) { + ConfigForShading( + ShadingType::kObjectReflectLightShadowAddColorized2); + cmd_buffer_->PutInt(static_cast(light_shadow_)); + cmd_buffer_->PutFloats( + color_r_, color_g_, color_b_, color_add_r_, color_add_g_, + color_add_b_, reflection_scale_r_, reflection_scale_g_, + reflection_scale_b_, colorize_color_r_, colorize_color_g_, + colorize_color_b_, colorize_color2_r_, colorize_color2_g_, + colorize_color2_b_); + cmd_buffer_->PutTexture(texture_); + cmd_buffer_->PutTexture(colorize_texture_); + SystemCubeMapTextureID r = + Graphics::CubeMapFromReflectionType(reflection_); + cmd_buffer_->PutCubeMapTexture(g_media->GetCubeMapTexture(r)); + } else { + ConfigForShading( + ShadingType::kObjectReflectLightShadowAddColorized); + cmd_buffer_->PutInt(static_cast(light_shadow_)); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_add_r_, + color_add_g_, color_add_b_, + reflection_scale_r_, reflection_scale_g_, + reflection_scale_b_, colorize_color_r_, + colorize_color_g_, colorize_color_b_); + cmd_buffer_->PutTexture(texture_); + cmd_buffer_->PutTexture(colorize_texture_); + SystemCubeMapTextureID r = + Graphics::CubeMapFromReflectionType(reflection_); + cmd_buffer_->PutCubeMapTexture(g_media->GetCubeMapTexture(r)); + } + } else { + ConfigForShading(ShadingType::kObjectReflectLightShadowAdd); + cmd_buffer_->PutInt(static_cast(light_shadow_)); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_add_r_, + color_add_g_, color_add_b_, + reflection_scale_r_, reflection_scale_g_, + reflection_scale_b_); + cmd_buffer_->PutTexture(texture_); + SystemCubeMapTextureID r = + Graphics::CubeMapFromReflectionType(reflection_); + cmd_buffer_->PutCubeMapTexture(g_media->GetCubeMapTexture(r)); + } + } + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/component/object_component.h b/src/ballistica/graphics/component/object_component.h new file mode 100644 index 00000000..bd0a2165 --- /dev/null +++ b/src/ballistica/graphics/component/object_component.h @@ -0,0 +1,166 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_COMPONENT_OBJECT_COMPONENT_H_ +#define BALLISTICA_GRAPHICS_COMPONENT_OBJECT_COMPONENT_H_ + +#include "ballistica/graphics/component/render_component.h" + +namespace ballistica { + +class ObjectComponent : public RenderComponent { + public: + explicit ObjectComponent(RenderPass* pass) : RenderComponent(pass) {} + + void SetTexture(const Object::Ref& t_in) { + EnsureConfiguring(); + if (t_in.exists()) { + texture_ = t_in->texture_data(); + } else { + texture_.Clear(); + } + } + + void SetTexture(TextureData* t) { + EnsureConfiguring(); + texture_ = t; + } + + void SetColorizeTexture(const Object::Ref& t_in) { + EnsureConfiguring(); + if (t_in.exists()) { + colorize_texture_ = t_in->texture_data(); + } else { + colorize_texture_.Clear(); + } + } + + void SetColorizeTexture(TextureData* t) { + EnsureConfiguring(); + colorize_texture_ = t; + } + + void SetDoubleSided(bool enable) { + EnsureConfiguring(); + double_sided_ = enable; + } + + void SetReflection(ReflectionType r) { + EnsureConfiguring(); + reflection_ = r; + } + + void SetReflectionScale(float r, float g, float b) { + EnsureConfiguring(); + reflection_scale_r_ = r; + reflection_scale_g_ = g; + reflection_scale_b_ = b; + } + + void SetPremultiplied(bool val) { + EnsureConfiguring(); + premultiplied_ = val; + } + + void SetTransparent(bool val) { + EnsureConfiguring(); + transparent_ = val; + } + + void SetColor(float r, float g, float b, float a = 1.0f) { + // We support fast inline color changes with drawing streams. + // (avoids having to re-send a whole configure for every color change) + if (state_ == State::kDrawing) { + cmd_buffer_->PutCommand( + RenderCommandBuffer::Command::kObjectComponentInlineColor); + cmd_buffer_->PutFloats(r, g, b, a); + } else { + EnsureConfiguring(); + } + color_r_ = r; + color_g_ = g; + color_b_ = b; + color_a_ = a; + } + + void SetColorizeColor(float r, float g, float b, float a = 1.0f) { + EnsureConfiguring(); + colorize_color_r_ = r; + colorize_color_g_ = g; + colorize_color_b_ = b; + colorize_color_a_ = a; + } + + void SetColorizeColor2(float r, float g, float b, float a = 1.0f) { + EnsureConfiguring(); + colorize_color2_r_ = r; + colorize_color2_g_ = g; + colorize_color2_b_ = b; + colorize_color2_a_ = a; + do_colorize_2_ = true; + } + + void SetAddColor(float r, float g, float b) { + // We support fast inline add-color changes with drawing streams + // (avoids having to re-send a whole configure for every change). + // Make sure to only allow this if we have an add color already; + // otherwise we need to config since we might be switching shaders. + if (state_ == State::kDrawing && have_color_add_) { + cmd_buffer_->PutCommand( + RenderCommandBuffer::Command::kObjectComponentInlineAddColor); + cmd_buffer_->PutFloats(r, g, b); + } else { + EnsureConfiguring(); + } + color_add_r_ = r; + color_add_g_ = g; + color_add_b_ = b; + have_color_add_ = true; + } + + void SetLightShadow(LightShadowType t) { + EnsureConfiguring(); + light_shadow_ = t; + } + + void SetWorldSpace(bool w) { + EnsureConfiguring(); + world_space_ = w; + } + + protected: + void WriteConfig() override; + + protected: + float color_r_{1.0f}; + float color_g_{1.0f}; + float color_b_{1.0f}; + float color_a_{1.0f}; + float colorize_color_r_{1.0f}; + float colorize_color_g_{1.0f}; + float colorize_color_b_{1.0f}; + float colorize_color_a_{1.0f}; + float colorize_color2_r_{}; + float colorize_color2_g_{}; + float colorize_color2_b_{}; + float colorize_color2_a_{}; + float color_add_r_{}; + float color_add_g_{}; + float color_add_b_{}; + float reflection_scale_r_{1.0f}; + float reflection_scale_g_{1.0f}; + float reflection_scale_b_{1.0f}; + Object::Ref texture_; + Object::Ref colorize_texture_; + ReflectionType reflection_{ReflectionType::kNone}; + LightShadowType light_shadow_{LightShadowType::kObject}; + bool world_space_{}; + bool transparent_{}; + bool premultiplied_{}; + bool have_color_add_{}; + bool double_sided_{}; + bool do_colorize_2_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_COMPONENT_OBJECT_COMPONENT_H_ diff --git a/src/ballistica/graphics/component/post_process_component.cc b/src/ballistica/graphics/component/post_process_component.cc new file mode 100644 index 00000000..b79dd475 --- /dev/null +++ b/src/ballistica/graphics/component/post_process_component.cc @@ -0,0 +1,21 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/component/post_process_component.h" + +namespace ballistica { + +void PostProcessComponent::WriteConfig() { + if (eyes_) { + assert(normal_distort_ == 0.0f); // unsupported config + ConfigForShading(ShadingType::kPostProcessEyes); + } else { + if (normal_distort_ != 0.0f) { + ConfigForShading(ShadingType::kPostProcessNormalDistort); + cmd_buffer_->PutFloat(normal_distort_); + } else { + ConfigForShading(ShadingType::kPostProcess); + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/component/post_process_component.h b/src/ballistica/graphics/component/post_process_component.h new file mode 100644 index 00000000..6f47e9b9 --- /dev/null +++ b/src/ballistica/graphics/component/post_process_component.h @@ -0,0 +1,31 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_COMPONENT_POST_PROCESS_COMPONENT_H_ +#define BALLISTICA_GRAPHICS_COMPONENT_POST_PROCESS_COMPONENT_H_ + +#include "ballistica/graphics/component/render_component.h" + +namespace ballistica { + +class PostProcessComponent : public RenderComponent { + public: + explicit PostProcessComponent(RenderPass* pass) + : RenderComponent(pass), normal_distort_(0.0f), eyes_(false) {} + void setNormalDistort(float d) { + EnsureConfiguring(); + normal_distort_ = d; + } + void setEyes(bool enable) { + EnsureConfiguring(); + eyes_ = enable; + } + + protected: + void WriteConfig() override; + bool eyes_; + float normal_distort_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_COMPONENT_POST_PROCESS_COMPONENT_H_ diff --git a/src/ballistica/graphics/component/render_component.cc b/src/ballistica/graphics/component/render_component.cc new file mode 100644 index 00000000..fdb1cc48 --- /dev/null +++ b/src/ballistica/graphics/component/render_component.cc @@ -0,0 +1,83 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/component/render_component.h" + +#include "ballistica/dynamics/rigid_body.h" +#include "ballistica/graphics/graphics_server.h" + +namespace ballistica { + +void RenderComponent::ScissorPush(const Rect& rIn) { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kScissorPush); + cmd_buffer_->PutFloats(rIn.l, rIn.b, rIn.r, rIn.t); +} + +#if BA_DEBUG_BUILD +void RenderComponent::ConfigForEmptyDebugChecks(bool transparent) { + assert(InGameThread()); + if (g_graphics->drawing_opaque_only() && transparent) { + throw Exception("Transparent component submitted in opaque-only section"); + } + if (g_graphics->drawing_transparent_only() && !transparent) { + throw Exception("Opaque component submitted in transparent-only section"); + } +} + +void RenderComponent::ConfigForShadingDebugChecks(ShadingType shading_type) { + assert(InGameThread()); + if (g_graphics->drawing_opaque_only() + && Graphics::IsShaderTransparent(shading_type)) { + throw Exception("Transparent component submitted in opaque-only section"); + } + if (g_graphics->drawing_transparent_only() + && !Graphics::IsShaderTransparent(shading_type)) { + throw Exception("Opaque component submitted in transparent-only section"); + } +} +#endif // BA_DEBUG_BUILD + +void RenderComponent::TransformToBody(const RigidBody& b) { + dBodyID body = b.body(); + dGeomID geom = b.geom(); + const dReal* pos_in; + const dReal* r_in; + if (b.type() == RigidBody::Type::kBody) { + pos_in = dBodyGetPosition(body); + r_in = dBodyGetRotation(body); + } else { + pos_in = dGeomGetPosition(geom); + r_in = dGeomGetRotation(geom); + } + float pos[3]; + float r[12]; + for (int x = 0; x < 3; x++) { + pos[x] = pos_in[x]; + } + pos[0] += b.blend_offset().x; + pos[1] += b.blend_offset().y; + pos[2] += b.blend_offset().z; + for (int x = 0; x < 12; x++) { + r[x] = r_in[x]; + } + float matrix[16]; + matrix[0] = r[0]; + matrix[1] = r[4]; + matrix[2] = r[8]; + matrix[3] = 0; + matrix[4] = r[1]; + matrix[5] = r[5]; + matrix[6] = r[9]; + matrix[7] = 0; + matrix[8] = r[2]; + matrix[9] = r[6]; + matrix[10] = r[10]; + matrix[11] = 0; + matrix[12] = pos[0]; + matrix[13] = pos[1]; + matrix[14] = pos[2]; + matrix[15] = 1; + MultMatrix(matrix); +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/component/render_component.h b/src/ballistica/graphics/component/render_component.h new file mode 100644 index 00000000..08ac4fea --- /dev/null +++ b/src/ballistica/graphics/component/render_component.h @@ -0,0 +1,262 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_COMPONENT_RENDER_COMPONENT_H_ +#define BALLISTICA_GRAPHICS_COMPONENT_RENDER_COMPONENT_H_ + +#include + +#include "ballistica/graphics/renderer.h" + +namespace ballistica { + +class RenderComponent { + public: + explicit RenderComponent(RenderPass* pass) + : state_(State::kConfiguring), pass_(pass), cmd_buffer_(nullptr) {} + ~RenderComponent() { + if (state_ != State::kSubmitted) { + Log("Error: RenderComponent dying without submit() having been called."); + } + } + void DrawModel(ModelData* model, uint32_t flags = 0) { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kDrawModel); + cmd_buffer_->PutInt(flags); + cmd_buffer_->PutModel(model); + } + void DrawModelInstanced(ModelData* model, + const std::vector& matrices, + int flags = 0) { + assert(!matrices.empty()); + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kDrawModelInstanced); + cmd_buffer_->PutInt(flags); + cmd_buffer_->PutModel(model); + cmd_buffer_->PutMatrices(matrices); + } + void DrawMesh(Mesh* m, int flags = 0) { + EnsureDrawing(); + if (m->IsValid()) { + cmd_buffer_->frame_def()->AddMesh(m); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kDrawMesh); + cmd_buffer_->PutInt(flags); + cmd_buffer_->PutMeshData(m->mesh_data_client_handle()->mesh_data); + } + } + void DrawScreenQuad() { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kDrawScreenQuad); + } + // draw triangles using old-school gl format.. only for debugging + // and not supported in all configurations + void BeginDebugDrawTriangles() { + EnsureDrawing(); + cmd_buffer_->PutCommand( + RenderCommandBuffer::Command::kBeginDebugDrawTriangles); + } + void BeginDebugDrawLines() { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kBeginDebugDrawLines); + } + void Vertex(float x, float y, float z) { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kDebugDrawVertex3); + cmd_buffer_->PutFloats(x, y, z); + } + void End() { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kEndDebugDraw); + } + void ScissorPush(const Rect& rIn); + void ScissorPop() { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kScissorPop); + } + void PushTransform() { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kPushTransform); + } + void PopTransform() { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kPopTransform); + } + void Translate(float x, float y) { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kTranslate2); + cmd_buffer_->PutFloats(x, y); + } + void Translate(float x, float y, float z) { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kTranslate3); + cmd_buffer_->PutFloats(x, y, z); + } + void CursorTranslate() { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kCursorTranslate); + } + void Rotate(float angle, float x, float y, float z) { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kRotate); + cmd_buffer_->PutFloats(angle, x, y, z); + } + void Scale(float x, float y) { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kScale2); + cmd_buffer_->PutFloats(x, y); + } + void Scale(float x, float y, float z) { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kScale3); + cmd_buffer_->PutFloats(x, y, z); + } + void ScaleUniform(float s) { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kScaleUniform); + cmd_buffer_->PutFloat(s); + } + void MultMatrix(const float* t) { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kMultMatrix); + cmd_buffer_->PutFloatArray16(t); + } + void TransformToBody(const RigidBody& b); +#if BA_VR_BUILD + void VRTransformToRightHand() { + EnsureDrawing(); + cmd_buffer_->PutCommand( + RenderCommandBuffer::Command::kTransformToRightHand); + } + void VRTransformToLeftHand() { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kTransformToLeftHand); + } + void VRTransformToHead() { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kTransformToHead); + } +#endif // BA_VR_BUILD + void TranslateToProjectedPoint(float x, float y, float z) { + EnsureDrawing(); + cmd_buffer_->PutCommand( + RenderCommandBuffer::Command::kTranslateToProjectedPoint); + cmd_buffer_->PutFloats(x, y, z); + } + void FlipCullFace() { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kFlipCullFace); + } + void Submit() { + if (state_ != State::kSubmitted) { + // if we were drawing, make note that we're done + if (state_ == State::kDrawing) { +#if BA_DEBUG_BUILD + assert(pass_->frame_def()->defining_component()); + pass_->frame_def()->set_defining_component(false); +#endif // BA_DEBUG_BUILD + } + state_ = State::kSubmitted; + } + } + + protected: + enum class State { kConfiguring, kDrawing, kSubmitted }; + void EnsureConfiguring() { + if (state_ != State::kConfiguring) { + // if we were drawing, make note that we're done +#if BA_DEBUG_BUILD + if (state_ == State::kDrawing) { + assert(pass_->frame_def()->defining_component()); + pass_->frame_def()->set_defining_component(false); + } +#endif // BA_DEBUG_BUILD + state_ = State::kConfiguring; + } + } +#if BA_DEBUG_BUILD + void ConfigForEmptyDebugChecks(bool transparent); + void ConfigForShadingDebugChecks(ShadingType shading_type); +#endif + + // Given a shader type, returns a buffer to write the command stream to. + void ConfigForEmpty(bool transparent) { +#if BA_DEBUG_BUILD + ConfigForEmptyDebugChecks(transparent); +#endif + + assert(!pass_->UsesWorldLists()); + if (transparent) { + cmd_buffer_ = pass_->commands_flat_transparent(); + } else { + cmd_buffer_ = pass_->commands_flat(); + } + } + + // Given a shader type, sets up the config target buffer. + void ConfigForShading(ShadingType shading_type) { + // Determine which buffer to write to, etc. + // Debugging: if we've got transparent-only or opaque-only mode flipped on, + // make sure only those type of components are being submitted. +#if BA_DEBUG_BUILD + ConfigForShadingDebugChecks(shading_type); + // Also make sure only transparent stuff is going into the + // light/shadow/overlay3D passes (we skip rendering the opaque lists there + // since there shouldn't be anything in them, and we're not using depth + // for those so it wouldn't be much of an optimization..) + if ((pass_->type() == RenderPass::Type::kLightPass + || pass_->type() == RenderPass::Type::kLightShadowPass + || pass_->type() == RenderPass::Type::kOverlay3DPass) + && !Graphics::IsShaderTransparent(shading_type)) { + throw Exception( + "Opaque component submitted to light/shadow/overlay3d pass;" + " not cool man."); + } + + // Likewise the blit pass should consist solely of opaque stuff. + if (pass_->type() == RenderPass::Type::kBlitPass + && Graphics::IsShaderTransparent(shading_type)) { + throw Exception( + "Transparent component submitted to blit pass;" + " not cool man."); + } +#endif // BA_DEBUG_BUILD + // Certain passes (overlay, etc) draw objects in the order + // provided. Other passes group by shader for efficiency. + if (pass_->UsesWorldLists()) { + cmd_buffer_ = pass_->GetCommands(shading_type); + } else { + if (Graphics::IsShaderTransparent(shading_type)) { + cmd_buffer_ = pass_->commands_flat_transparent(); + } else { + cmd_buffer_ = pass_->commands_flat(); + } + } + + // Go ahead and throw down the shader command. + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kShader); + cmd_buffer_->PutInt(static_cast(shading_type)); + } + + void EnsureDrawing() { + if (state_ != State::kDrawing) { + WriteConfig(); + state_ = State::kDrawing; + // make sure we're the only one drawing until we're submitted +#if BA_DEBUG_BUILD + assert(!pass_->frame_def()->defining_component()); + pass_->frame_def()->set_defining_component(true); +#endif // BA_DEBUG_BUILD + } + } + // subclasses should override this to dump + // their needed data to the stream + virtual void WriteConfig() = 0; + + protected: + RenderCommandBuffer* cmd_buffer_; + State state_; + RenderPass* pass_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_COMPONENT_RENDER_COMPONENT_H_ diff --git a/src/ballistica/graphics/component/shield_component.cc b/src/ballistica/graphics/component/shield_component.cc new file mode 100644 index 00000000..216e846c --- /dev/null +++ b/src/ballistica/graphics/component/shield_component.cc @@ -0,0 +1,9 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/component/shield_component.h" + +namespace ballistica { + +void ShieldComponent::WriteConfig() { ConfigForShading(ShadingType::kShield); } + +} // namespace ballistica diff --git a/src/ballistica/graphics/component/shield_component.h b/src/ballistica/graphics/component/shield_component.h new file mode 100644 index 00000000..447df953 --- /dev/null +++ b/src/ballistica/graphics/component/shield_component.h @@ -0,0 +1,21 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_COMPONENT_SHIELD_COMPONENT_H_ +#define BALLISTICA_GRAPHICS_COMPONENT_SHIELD_COMPONENT_H_ + +#include "ballistica/graphics/component/render_component.h" + +namespace ballistica { + +// handles special cases such as drawing light/shadow/back buffers. +class ShieldComponent : public RenderComponent { + public: + explicit ShieldComponent(RenderPass* pass) : RenderComponent(pass) {} + + protected: + void WriteConfig() override; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_COMPONENT_SHIELD_COMPONENT_H_ diff --git a/src/ballistica/graphics/component/simple_component.cc b/src/ballistica/graphics/component/simple_component.cc new file mode 100644 index 00000000..6f8228b8 --- /dev/null +++ b/src/ballistica/graphics/component/simple_component.cc @@ -0,0 +1,228 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/component/simple_component.h" + +namespace ballistica { + +void SimpleComponent::WriteConfig() { + // if we're transparent we don't want to do optimization-based + // shader swapping (ie: when color is 1). This is because it can + // affect draw order, which is important unlike with opaque stuff. + if (transparent_) { + if (texture_.exists()) { + if (colorize_texture_.exists()) { + assert(flatness_ == 0.0f); // unimplemented combo + assert(glow_amount_ == 0.0f); // unimplemented combo + assert(shadow_opacity_ == 0.0f); // unimplemented combo + assert(!double_sided_); // unimplemented combo + assert(!mask_uv2_texture_.exists()); // unimplemented combo + if (do_colorize_2_) { + if (mask_texture_.exists()) { + ConfigForShading( + ShadingType:: + kSimpleTextureModulatedTransparentColorized2Masked); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_, + colorize_color_r_, colorize_color_g_, + colorize_color_b_, colorize_color2_r_, + colorize_color2_g_, colorize_color2_b_); + cmd_buffer_->PutTexture(texture_); + cmd_buffer_->PutTexture(colorize_texture_); + cmd_buffer_->PutTexture(mask_texture_); + } else { + ConfigForShading( + ShadingType::kSimpleTextureModulatedTransparentColorized2); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_, + colorize_color_r_, colorize_color_g_, + colorize_color_b_, colorize_color2_r_, + colorize_color2_g_, colorize_color2_b_); + cmd_buffer_->PutTexture(texture_); + cmd_buffer_->PutTexture(colorize_texture_); + } + } else { + assert(!mask_texture_.exists()); // unimplemented combo + ConfigForShading( + ShadingType::kSimpleTextureModulatedTransparentColorized); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_, + colorize_color_r_, colorize_color_g_, + colorize_color_b_); + cmd_buffer_->PutTexture(texture_); + cmd_buffer_->PutTexture(colorize_texture_); + } + } else { + // non-colorized with texture + if (double_sided_) { + assert(!mask_texture_.exists()); // unimplemented combo + assert(flatness_ == 0.0f); // unimplemented combo + assert(glow_amount_ == 0.0f); // unimplemented combo + assert(shadow_opacity_ == 0.0f); // unimplemented combo + assert(!mask_texture_.exists()); // unimplemented combo + assert(!mask_uv2_texture_.exists()); // unimplemented combo + ConfigForShading( + ShadingType::kSimpleTextureModulatedTransparentDoubleSided); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_); + cmd_buffer_->PutTexture(texture_); + } else { + if (shadow_opacity_ > 0.0f) { + assert(!mask_texture_.exists()); // unimplemented combo + assert(glow_amount_ == 0.0f); // unimplemented combo + assert(mask_uv2_texture_.exists()); + if (flatness_ != 0.0f) { + ConfigForShading( + ShadingType::kSimpleTexModulatedTransShadowFlatness); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_, + shadow_offset_x_, shadow_offset_y_, + shadow_blur_, shadow_opacity_, flatness_); + cmd_buffer_->PutTexture(texture_); + cmd_buffer_->PutTexture(mask_uv2_texture_); + } else { + ConfigForShading( + ShadingType::kSimpleTextureModulatedTransparentShadow); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_, + shadow_offset_x_, shadow_offset_y_, + shadow_blur_, shadow_opacity_); + cmd_buffer_->PutTexture(texture_); + cmd_buffer_->PutTexture(mask_uv2_texture_); + } + } else { + if (glow_amount_ > 0.0f) { + assert(!mask_texture_.exists()); // unimplemented combo + assert(flatness_ == 0.0f); // unimplemented combo + if (mask_uv2_texture_.exists()) { + ConfigForShading( + ShadingType::kSimpleTextureModulatedTransparentGlowMaskUV2); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_, + glow_amount_, glow_blur_); + cmd_buffer_->PutTexture(texture_); + cmd_buffer_->PutTexture(mask_uv2_texture_); + } else { + ConfigForShading( + ShadingType::kSimpleTextureModulatedTransparentGlow); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_, + glow_amount_, glow_blur_); + cmd_buffer_->PutTexture(texture_); + } + } else { + if (flatness_ != 0.0f) { + assert(!mask_texture_.exists()); // unimplemented + ConfigForShading( + ShadingType::kSimpleTextureModulatedTransFlatness); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_, + flatness_); + cmd_buffer_->PutTexture(texture_); + } else { + if (mask_texture_.exists()) { + // currently mask functionality requires colorize too, so + // just send a black texture for that.. + ConfigForShading( + ShadingType:: + kSimpleTextureModulatedTransparentColorized2Masked); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats( + color_r_, color_g_, color_b_, color_a_, colorize_color_r_, + colorize_color_g_, colorize_color_b_, colorize_color2_r_, + colorize_color2_g_, colorize_color2_b_); + cmd_buffer_->PutTexture(texture_); + cmd_buffer_->PutTexture( + g_media->GetTexture(SystemTextureID::kBlack)); + cmd_buffer_->PutTexture(mask_texture_); + } else { + ConfigForShading( + ShadingType::kSimpleTextureModulatedTransparent); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, + color_a_); + cmd_buffer_->PutTexture(texture_); + } + } + } + } + } + } + } else { + assert(flatness_ == 0.0f); // unimplemented combo + assert(glow_amount_ == 0.0f); // unimplemented combo + assert(shadow_opacity_ == 0.0f); // unimplemented combo + assert(!colorize_texture_.exists()); // unimplemented combo + assert(!mask_texture_.exists()); // unimplemented combo + assert(!mask_uv2_texture_.exists()); // unimplemented combo + if (double_sided_) { + ConfigForShading(ShadingType::kSimpleColorTransparentDoubleSided); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_); + } else { + ConfigForShading(ShadingType::kSimpleColorTransparent); + cmd_buffer_->PutInt(premultiplied_); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_); + } + } + } else { + // when we're opaque we can do some shader-swapping optimizations + // since draw order doesn't matter. + assert(flatness_ == 0.0f); // unimplemented combo + assert(glow_amount_ == 0.0f); // unimplemented combo + assert(shadow_opacity_ == 0.0f); // unimplemented combo + assert(!double_sided_); // not implemented + assert(!mask_uv2_texture_.exists()); // unimplemented combo + if (texture_.exists()) { + if (colorize_texture_.exists()) { + assert(!mask_texture_.exists()); // unimplemented combo + if (do_colorize_2_) { + ConfigForShading(ShadingType::kSimpleTextureModulatedColorized2); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, + colorize_color_r_, colorize_color_g_, + colorize_color_b_, colorize_color2_r_, + colorize_color2_g_, colorize_color2_b_); + cmd_buffer_->PutTexture(texture_); + cmd_buffer_->PutTexture(colorize_texture_); + } else { + ConfigForShading(ShadingType::kSimpleTextureModulatedColorized); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, + colorize_color_r_, colorize_color_g_, + colorize_color_b_); + cmd_buffer_->PutTexture(texture_); + cmd_buffer_->PutTexture(colorize_texture_); + } + } else { + assert(!do_colorize_2_); // unsupported combo + if (mask_texture_.exists()) { + // currently mask functionality requires colorize too, so + // we have to send a black texture along for that.. + ConfigForShading( + ShadingType::kSimpleTextureModulatedColorized2Masked); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_, + colorize_color_r_, colorize_color_g_, + colorize_color_b_, colorize_color2_r_, + colorize_color2_g_, colorize_color2_b_); + cmd_buffer_->PutTexture(texture_); + cmd_buffer_->PutTexture(g_media->GetTexture(SystemTextureID::kBlack)); + cmd_buffer_->PutTexture(mask_texture_); + } else { + // if no color was provided we can do a super-cheap version + if (!have_color_) { + ConfigForShading(ShadingType::kSimpleTexture); + cmd_buffer_->PutTexture(texture_); + } else { + ConfigForShading(ShadingType::kSimpleTextureModulated); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_); + cmd_buffer_->PutTexture(texture_); + } + } + } + } else { + assert(!mask_texture_.exists()); // unimplemented combo + assert(!colorize_texture_.exists()); // unsupported here + ConfigForShading(ShadingType::kSimpleColor); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_); + } + } +} +} // namespace ballistica diff --git a/src/ballistica/graphics/component/simple_component.h b/src/ballistica/graphics/component/simple_component.h new file mode 100644 index 00000000..f6e23fd1 --- /dev/null +++ b/src/ballistica/graphics/component/simple_component.h @@ -0,0 +1,185 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_COMPONENT_SIMPLE_COMPONENT_H_ +#define BALLISTICA_GRAPHICS_COMPONENT_SIMPLE_COMPONENT_H_ + +#include "ballistica/graphics/component/render_component.h" + +namespace ballistica { + +// used for UI and overlays and things - no world tinting/etc is applied +class SimpleComponent : public RenderComponent { + public: + explicit SimpleComponent(RenderPass* pass) + : RenderComponent(pass), + color_r_(1.0f), + color_g_(1.0f), + color_b_(1.0f), + color_a_(1.0f), + colorize_color_r_(1.0f), + colorize_color_g_(1.0f), + colorize_color_b_(1.0f), + colorize_color_a_(1.0f), + colorize_color2_r_(1.0f), + colorize_color2_g_(1.0f), + colorize_color2_b_(1.0f), + colorize_color2_a_(1.0f), + shadow_offset_x_(0.0f), + shadow_offset_y_(0.0f), + shadow_blur_(0.0f), + shadow_opacity_(0.0f), + glow_amount_(0.0f), + glow_blur_(0.0f), + flatness_(0.0f), + transparent_(false), + premultiplied_(false), + have_color_(false), + double_sided_(false), + do_colorize_2_(false) {} + void SetPremultiplied(bool val) { + EnsureConfiguring(); + premultiplied_ = val; + } + void SetTransparent(bool val) { + EnsureConfiguring(); + transparent_ = val; + } + void SetTexture(TextureData* t) { + EnsureConfiguring(); + texture_ = t; + } + void SetTexture(const Object::Ref& t_in) { + EnsureConfiguring(); + if (t_in.exists()) { + texture_ = t_in->texture_data(); + } else { + texture_.Clear(); + } + } + // used with colorize color 1 and 2 + // red areas of the texture will get multiplied by colorize-color1 + // and green areas by colorize-color2 + void SetColorizeTexture(const Object::Ref& t_in) { + EnsureConfiguring(); + if (t_in.exists()) { + colorize_texture_ = t_in->texture_data(); + } else { + colorize_texture_.Clear(); + } + } + void SetColorizeTexture(TextureData* t) { + EnsureConfiguring(); + colorize_texture_ = t; + } + // red multiplies source color, green adds colorize1-color, + // and blue adds white + // (currently requires colorize1 and colorize 2 to be set) + void SetMaskTexture(const Object::Ref& t_in) { + EnsureConfiguring(); + if (t_in.exists()) { + mask_texture_ = t_in->texture_data(); + } else { + mask_texture_.Clear(); + } + } + void SetMaskTexture(TextureData* t) { + EnsureConfiguring(); + mask_texture_ = t; + } + void SetMaskUV2Texture(const Object::Ref& t_in) { + EnsureConfiguring(); + if (t_in.exists()) { + mask_uv2_texture_ = t_in->texture_data(); + } else { + mask_uv2_texture_.Clear(); + } + } + void SetMaskUV2Texture(TextureData* t) { + EnsureConfiguring(); + mask_uv2_texture_ = t; + } + void clearMaskUV2Texture() { + EnsureConfiguring(); + mask_uv2_texture_.Clear(); + } + void SetDoubleSided(bool enable) { + EnsureConfiguring(); + double_sided_ = enable; + } + void SetColor(float r, float g, float b, float a = 1.0f) { + // we support fast inline color changes with drawing streams + // (avoids having to re-send a whole configure for every color change) + // ..make sure to only allow this if we have a color already; otherwise we + // need to config since we might be implicitly switch shaders by setting + // color + if (state_ == State::kDrawing && have_color_) { + cmd_buffer_->PutCommand( + RenderCommandBuffer::Command::kSimpleComponentInlineColor); + cmd_buffer_->PutFloats(r, g, b, a); + } else { + EnsureConfiguring(); + have_color_ = true; + } + color_r_ = r; + color_g_ = g; + color_b_ = b; + color_a_ = a; + } + void SetColorizeColor(float r, float g, float b, float a = 1.0f) { + EnsureConfiguring(); + colorize_color_r_ = r; + colorize_color_g_ = g; + colorize_color_b_ = b; + colorize_color_a_ = a; + } + void SetColorizeColor2(float r, float g, float b, float a = 1.0f) { + EnsureConfiguring(); + colorize_color2_r_ = r; + colorize_color2_g_ = g; + colorize_color2_b_ = b; + colorize_color2_a_ = a; + do_colorize_2_ = true; + } + void SetShadow(float offsetX, float offsetY, float blur, float opacity) { + EnsureConfiguring(); + shadow_offset_x_ = offsetX; + shadow_offset_y_ = offsetY; + shadow_blur_ = blur; + shadow_opacity_ = opacity; + } + void setGlow(float amount, float blur) { + EnsureConfiguring(); + glow_amount_ = amount; + glow_blur_ = blur; + } + void SetFlatness(float flatness) { + EnsureConfiguring(); + flatness_ = flatness; + } + + protected: + void WriteConfig() override; + + protected: + float color_r_, color_g_, color_b_, color_a_; + float colorize_color_r_, colorize_color_g_, colorize_color_b_, + colorize_color_a_; + float colorize_color2_r_, colorize_color2_g_, colorize_color2_b_, + colorize_color2_a_; + float shadow_offset_x_, shadow_offset_y_, shadow_blur_, shadow_opacity_; + float glow_amount_, glow_blur_; + float flatness_; + Object::Ref texture_; + Object::Ref colorize_texture_; + Object::Ref mask_texture_; + Object::Ref mask_uv2_texture_; + bool do_colorize_2_; + bool transparent_; + bool premultiplied_; + bool have_color_; + bool double_sided_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_COMPONENT_SIMPLE_COMPONENT_H_ diff --git a/src/ballistica/graphics/component/smoke_component.cc b/src/ballistica/graphics/component/smoke_component.cc new file mode 100644 index 00000000..22a9a52b --- /dev/null +++ b/src/ballistica/graphics/component/smoke_component.cc @@ -0,0 +1,19 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/component/smoke_component.h" + +namespace ballistica { + +void SmokeComponent::WriteConfig() { + if (overlay_) { + ConfigForShading(ShadingType::kSmokeOverlay); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_); + cmd_buffer_->PutTexture(g_media->GetTexture(SystemTextureID::kSmoke)); + } else { + ConfigForShading(ShadingType::kSmoke); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_); + cmd_buffer_->PutTexture(g_media->GetTexture(SystemTextureID::kSmoke)); + } +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/component/smoke_component.h b/src/ballistica/graphics/component/smoke_component.h new file mode 100644 index 00000000..698044b9 --- /dev/null +++ b/src/ballistica/graphics/component/smoke_component.h @@ -0,0 +1,39 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_COMPONENT_SMOKE_COMPONENT_H_ +#define BALLISTICA_GRAPHICS_COMPONENT_SMOKE_COMPONENT_H_ + +#include "ballistica/graphics/component/render_component.h" + +namespace ballistica { + +class SmokeComponent : public RenderComponent { + public: + explicit SmokeComponent(RenderPass* pass) + : RenderComponent(pass), + color_r_(1.0f), + color_g_(1.0f), + color_b_(1.0f), + color_a_(1.0f), + overlay_(false) {} + void SetColor(float r, float g, float b, float a = 1.0f) { + EnsureConfiguring(); + color_r_ = r; + color_g_ = g; + color_b_ = b; + color_a_ = a; + } + void SetOverlay(bool overlay) { + EnsureConfiguring(); + overlay_ = overlay; + } + + protected: + void WriteConfig() override; + float color_r_, color_g_, color_b_, color_a_; + bool overlay_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_COMPONENT_SMOKE_COMPONENT_H_ diff --git a/src/ballistica/graphics/component/special_component.cc b/src/ballistica/graphics/component/special_component.cc new file mode 100644 index 00000000..9be04c15 --- /dev/null +++ b/src/ballistica/graphics/component/special_component.cc @@ -0,0 +1,12 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/component/special_component.h" + +namespace ballistica { + +void SpecialComponent::WriteConfig() { + ConfigForShading(ShadingType::kSpecial); + cmd_buffer_->PutInt(static_cast(source_)); +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/component/special_component.h b/src/ballistica/graphics/component/special_component.h new file mode 100644 index 00000000..ba6ee66b --- /dev/null +++ b/src/ballistica/graphics/component/special_component.h @@ -0,0 +1,26 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_COMPONENT_SPECIAL_COMPONENT_H_ +#define BALLISTICA_GRAPHICS_COMPONENT_SPECIAL_COMPONENT_H_ + +#include "ballistica/graphics/component/render_component.h" + +namespace ballistica { + +// handles special cases such as drawing light/shadow/back buffers. +class SpecialComponent : public RenderComponent { + public: + enum class Source { kLightBuffer, kLightShadowBuffer, kVROverlayBuffer }; + SpecialComponent(RenderPass* pass, Source s) + : RenderComponent(pass), source_(s) {} + + protected: + void WriteConfig() override; + + private: + Source source_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_COMPONENT_SPECIAL_COMPONENT_H_ diff --git a/src/ballistica/graphics/component/sprite_component.cc b/src/ballistica/graphics/component/sprite_component.cc new file mode 100644 index 00000000..4c438f5e --- /dev/null +++ b/src/ballistica/graphics/component/sprite_component.cc @@ -0,0 +1,25 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/component/sprite_component.h" + +namespace ballistica { + +void SpriteComponent::WriteConfig() { + // if they didn't give us a texture, just use a blank white texture; + // this is not a common case and easier than forking all our shaders + // to create non-textured versions. + if (!texture_.exists()) { + texture_ = g_media->GetTexture(SystemTextureID::kWhite); + } + if (exponent_ == 1) { + ConfigForShading(ShadingType::kSprite); + cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_); + cmd_buffer_->PutInt(overlay_); + cmd_buffer_->PutInt(camera_aligned_); + cmd_buffer_->PutTexture(texture_); + } else { + throw Exception(); + } +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/component/sprite_component.h b/src/ballistica/graphics/component/sprite_component.h new file mode 100644 index 00000000..2836034d --- /dev/null +++ b/src/ballistica/graphics/component/sprite_component.h @@ -0,0 +1,61 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_COMPONENT_SPRITE_COMPONENT_H_ +#define BALLISTICA_GRAPHICS_COMPONENT_SPRITE_COMPONENT_H_ + +#include "ballistica/graphics/component/render_component.h" + +namespace ballistica { + +class SpriteComponent : public RenderComponent { + public: + explicit SpriteComponent(RenderPass* pass) : RenderComponent(pass) {} + void SetColor(float r, float g, float b, float a = 1.0f) { + EnsureConfiguring(); + color_r_ = r; + color_g_ = g; + color_b_ = b; + color_a_ = a; + have_color_ = true; + } + void SetCameraAligned(bool c) { + EnsureConfiguring(); + camera_aligned_ = c; + } + void SetOverlay(bool enable) { + EnsureConfiguring(); + overlay_ = enable; + } + void SetExponent(int i) { + EnsureConfiguring(); + exponent_ = static_cast_check_fit(i); + } + void SetTexture(const Object::Ref& t_in) { + EnsureConfiguring(); + if (t_in.exists()) { + texture_ = t_in->texture_data(); + } else { + texture_.Clear(); + } + } + void SetTexture(TextureData* t) { + EnsureConfiguring(); + texture_ = t; + } + + protected: + void WriteConfig() override; + bool have_color_{}; + bool camera_aligned_{}; + bool overlay_{}; + uint8_t exponent_{1}; + float color_r_{1.0f}; + float color_g_{1.0f}; + float color_b_{1.0f}; + float color_a_{1.0f}; + Object::Ref texture_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_COMPONENT_SPRITE_COMPONENT_H_ diff --git a/src/ballistica/graphics/frame_def.cc b/src/ballistica/graphics/frame_def.cc new file mode 100644 index 00000000..d298fa5f --- /dev/null +++ b/src/ballistica/graphics/frame_def.cc @@ -0,0 +1,173 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/frame_def.h" + +#include "ballistica/graphics/camera.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/graphics/render_pass.h" +#include "ballistica/graphics/renderer.h" + +namespace ballistica { + +FrameDef::FrameDef() + : light_pass_(new RenderPass(RenderPass::Type::kLightPass, this)), + light_shadow_pass_( + new RenderPass(RenderPass::Type::kLightShadowPass, this)), + beauty_pass_(new RenderPass(RenderPass::Type::kBeautyPass, this)), + beauty_pass_bg_(new RenderPass(RenderPass::Type::kBeautyPassBG, this)), + overlay_pass_(new RenderPass(RenderPass::Type::kOverlayPass, this)), + overlay_front_pass_( + new RenderPass(RenderPass::Type::kOverlayFrontPass, this)), + overlay_3d_pass_(new RenderPass(RenderPass::Type::kOverlay3DPass, this)), + vr_cover_pass_(new RenderPass(RenderPass::Type::kVRCoverPass, this)), + overlay_fixed_pass_( + new RenderPass(RenderPass::Type::kOverlayFixedPass, this)), + overlay_flat_pass_( + new RenderPass(RenderPass::Type::kOverlayFlatPass, this)), + blit_pass_(new RenderPass(RenderPass::Type::kBlitPass, this)) {} + +FrameDef::~FrameDef() { assert(InGameThread()); } + +void FrameDef::Reset() { + assert(InGameThread()); + real_time_ = 0; + base_time_ = 0; + base_time_elapsed_ = 0; + frame_number_ = 0; + +#if BA_DEBUG_BUILD + defining_component_ = false; +#endif + + benchmark_type_ = BenchmarkType::kNone; + + mesh_data_creates_.clear(); + mesh_data_destroys_.clear(); + + media_components_.clear(); + meshes_.clear(); + mesh_index_sizes_.clear(); + mesh_buffers_.clear(); + + quality_ = g_graphics_server->quality(); + + assert(g_graphics->has_supports_high_quality_graphics_value()); + orbiting_ = (g_graphics->camera()->mode() == CameraMode::kOrbit); + + shadow_offset_ = g_graphics->shadow_offset(); + shadow_scale_ = g_graphics->shadow_scale(); + shadow_ortho_ = g_graphics->shadow_ortho(); + tint_ = g_graphics->tint(); + ambient_color_ = g_graphics->ambient_color(); + + vignette_outer_ = g_graphics->vignette_outer(); + vignette_inner_ = g_graphics->vignette_inner(); + + light_pass_->Reset(); + light_shadow_pass_->Reset(); + beauty_pass_->Reset(); + beauty_pass_bg_->Reset(); + overlay_pass_->Reset(); + overlay_front_pass_->Reset(); + if (IsVRMode()) { + overlay_flat_pass_->Reset(); + overlay_fixed_pass_->Reset(); + vr_cover_pass_->Reset(); + } + overlay_3d_pass_->Reset(); + blit_pass_->Reset(); + beauty_pass_->set_floor_reflection(g_graphics->floor_reflection()); +} + +void FrameDef::Finalize() { + assert(!defining_component_); + light_pass_->Finalize(); + light_shadow_pass_->Finalize(); + beauty_pass_->Finalize(); + beauty_pass_bg_->Finalize(); + overlay_pass_->Finalize(); + overlay_front_pass_->Finalize(); + if (IsVRMode()) { + overlay_fixed_pass_->Finalize(); + overlay_flat_pass_->Finalize(); + vr_cover_pass_->Finalize(); + } + overlay_3d_pass_->Finalize(); + blit_pass_->Finalize(); +} + +void FrameDef::AddMesh(Mesh* mesh) { + // Add this mesh's data to the frame only if we haven't yet. + if (mesh->last_frame_def_num() != frame_number_) { + mesh->set_last_frame_def_num(frame_number_); + meshes_.push_back(mesh->mesh_data_client_handle()); + switch (mesh->type()) { + case MeshDataType::kIndexedSimpleSplit: { + auto* m = static_cast(mesh); + assert(m); + assert(m == dynamic_cast(mesh)); + mesh_index_sizes_.push_back( + static_cast_check_fit(m->index_data_size())); + mesh_buffers_.emplace_back(m->GetIndexData()); + mesh_buffers_.emplace_back(m->static_data()); + mesh_buffers_.emplace_back(m->dynamic_data()); + break; + } + case MeshDataType::kIndexedObjectSplit: { + auto* m = static_cast(mesh); + assert(m); + assert(m == dynamic_cast(mesh)); + mesh_index_sizes_.push_back( + static_cast_check_fit(m->index_data_size())); + mesh_buffers_.emplace_back(m->GetIndexData()); + mesh_buffers_.emplace_back(m->static_data()); + mesh_buffers_.emplace_back(m->dynamic_data()); + break; + } + case MeshDataType::kIndexedSimpleFull: { + auto* m = static_cast(mesh); + assert(m); + assert(m == dynamic_cast(mesh)); + mesh_index_sizes_.push_back( + static_cast_check_fit(m->index_data_size())); + mesh_buffers_.emplace_back(m->GetIndexData()); + mesh_buffers_.emplace_back(m->data()); + break; + } + case MeshDataType::kIndexedDualTextureFull: { + auto* m = static_cast(mesh); + assert(m); + assert(m == dynamic_cast(mesh)); + mesh_index_sizes_.push_back( + static_cast_check_fit(m->index_data_size())); + mesh_buffers_.emplace_back(m->GetIndexData()); + mesh_buffers_.emplace_back(m->data()); + break; + } + case MeshDataType::kIndexedSmokeFull: { + auto* m = static_cast(mesh); + assert(m); + assert(m == dynamic_cast(mesh)); + mesh_index_sizes_.push_back( + static_cast_check_fit(m->index_data_size())); + mesh_buffers_.emplace_back(m->GetIndexData()); + mesh_buffers_.emplace_back(m->data()); + break; + } + case MeshDataType::kSprite: { + auto* m = static_cast(mesh); + assert(m); + assert(m == dynamic_cast(mesh)); + mesh_index_sizes_.push_back( + static_cast_check_fit(m->index_data_size())); + mesh_buffers_.emplace_back(m->GetIndexData()); + mesh_buffers_.emplace_back(m->data()); + break; + } + default: + throw Exception(); + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/frame_def.h b/src/ballistica/graphics/frame_def.h new file mode 100644 index 00000000..5df6de21 --- /dev/null +++ b/src/ballistica/graphics/frame_def.h @@ -0,0 +1,228 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_FRAME_DEF_H_ +#define BALLISTICA_GRAPHICS_FRAME_DEF_H_ + +#include +#include + +#include "ballistica/math/matrix44f.h" +#include "ballistica/math/vector2f.h" +#include "ballistica/media/data/media_component_data.h" + +namespace ballistica { + +/// A flattened representation of a frame; generated by the game thread and sent +/// to the graphics thread to render. +class FrameDef { + public: + auto light_pass() -> RenderPass* { return light_pass_.get(); } + auto light_shadow_pass() -> RenderPass* { return light_shadow_pass_.get(); } + auto beauty_pass() -> RenderPass* { return beauty_pass_.get(); } + auto beauty_pass_bg() -> RenderPass* { return beauty_pass_bg_.get(); } + auto overlay_pass() -> RenderPass* { return overlay_pass_.get(); } + auto overlay_front_pass() -> RenderPass* { return overlay_front_pass_.get(); } + auto vr_near_clip() const -> float { return vr_near_clip_; } + void set_vr_near_clip(float val) { vr_near_clip_ = val; } + auto benchmark_type() const -> BenchmarkType { return benchmark_type_; } + void set_benchmark_type(BenchmarkType val) { benchmark_type_ = val; } + + // Returns the fixed overlay pass if there is one; otherwise the regular. + auto GetOverlayFixedPass() -> RenderPass* { + if (IsVRMode()) { + return overlay_fixed_pass_.get(); + } else { + return overlay_pass_.get(); + } + } + + // Return either the overlay-flat pass (in vr) or regular overlay pass (for + // non-vr). + auto GetOverlayFlatPass() -> RenderPass* { + if (IsVRMode()) { + return overlay_flat_pass_.get(); + } else { + return overlay_pass_.get(); + } + } + auto overlay_3d_pass() -> RenderPass* { return overlay_3d_pass_.get(); } + auto blit_pass() -> RenderPass* { return blit_pass_.get(); } + auto vr_cover_pass() -> RenderPass* { return vr_cover_pass_.get(); } + + // Returns the real-time this frame_def originated at. + // For a more smoothly-incrementing value, + // use getbasetime() + auto real_time() const -> millisecs_t { return real_time_; } + auto frame_number() const -> int64_t { return frame_number_; } + + // Returns the bsGame master-net-time when this was made + // (tries to match real time but is incremented more smoothly + // so is better for drawing purposes) + auto base_time() const -> millisecs_t { return base_time_; } + + // How much base time does this frame-def represent. + auto base_time_elapsed() const -> millisecs_t { return base_time_elapsed_; } + + auto quality() const -> GraphicsQuality { return quality_; } + auto orbiting() const -> bool { return orbiting_; } + auto shadow_offset() const -> const Vector3f& { return shadow_offset_; } + auto shadow_scale() const -> const Vector2f& { return shadow_scale_; } + auto shadow_ortho() const -> bool { return shadow_ortho_; } + auto tint() const -> const Vector3f& { return tint_; } + auto ambient_color() const -> const Vector3f& { return ambient_color_; } + auto vignette_outer() const -> const Vector3f& { return vignette_outer_; } + auto vignette_inner() const -> const Vector3f& { return vignette_inner_; } + + // FIXME: what was this for?..(I think some vr thing?) + auto cam_original() const -> const Vector3f& { return cam_original_; } + auto cam_target_original() const -> const Vector3f& { + return cam_target_original_; + } + void set_cam_original(const Vector3f& val) { cam_original_ = val; } + void set_cam_target_original(const Vector3f& val) { + cam_target_original_ = val; + } + auto camera_mode() const -> CameraMode { return camera_mode_; } + auto vr_overlay_screen_matrix() const -> const Matrix44f& { + return vr_overlay_screen_matrix_; + } + void set_vr_overlay_screen_matrix(const Matrix44f& mat) { + vr_overlay_screen_matrix_ = mat; + } + auto vr_overlay_screen_matrix_fixed() const -> const Matrix44f& { + return vr_overlay_screen_matrix_fixed_; + } + void set_vr_overlay_screen_matrix_fixed(const Matrix44f& mat) { + vr_overlay_screen_matrix_fixed_ = mat; + } + + // Effects requiring availability of a depth texture should + // check this to determine whether they should draw. + auto has_depth_texture() const -> bool { + return (quality_ >= GraphicsQuality::kHigh); + } + void AddComponent(const Object::Ref& component) { + // Add a reference to this component only if we havn't yet. + if (component->last_frame_def_num() != frame_number_) { + component->set_last_frame_def_num(frame_number_); + media_components_.push_back(component); + } + } + void AddMesh(Mesh* mesh); + void set_needs_clear(bool val) { needs_clear_ = val; } + auto needs_clear() const -> bool { return needs_clear_; } + + FrameDef(); + ~FrameDef(); + void Reset(); + void Finalize(); + + void set_base_time_elapsed(millisecs_t val) { base_time_elapsed_ = val; } + void set_real_time(millisecs_t val) { real_time_ = val; } + void set_base_time(millisecs_t val) { base_time_ = val; } + void set_frame_number(int64_t val) { frame_number_ = val; } + + auto overlay_flat_pass() const -> RenderPass* { + return overlay_flat_pass_.get(); + } + auto overlay_fixed_pass() const -> RenderPass* { + return overlay_fixed_pass_.get(); + } + auto overlay_front_pass() const -> RenderPass* { + return overlay_front_pass_.get(); + } + auto overlay_pass() const -> RenderPass* { return overlay_pass_.get(); } + auto vr_cover_pass() const -> RenderPass* { return vr_cover_pass_.get(); } + + void set_mesh_data_creates(const std::vector& creates) { + mesh_data_creates_ = creates; + } + void set_mesh_data_destroys(const std::vector& destroys) { + mesh_data_destroys_ = destroys; + } + auto mesh_data_creates() const -> const std::vector& { + return mesh_data_creates_; + } + auto mesh_data_destroys() const -> const std::vector& { + return mesh_data_destroys_; + } + auto meshes() const -> const std::vector>& { + return meshes_; + } + auto mesh_buffers() const -> const std::vector>& { + return mesh_buffers_; + } + auto mesh_index_sizes() const -> const std::vector& { + return mesh_index_sizes_; + } + auto media_components() const + -> const std::vector>& { + return media_components_; + } + + void set_camera_mode(CameraMode val) { camera_mode_ = val; } + void set_rendering(bool val) { rendering_ = val; } + auto rendering() const -> bool { return rendering_; } + void set_shake_original(const Vector3f& val) { shake_original_ = val; } + auto shake_original() const -> const Vector3f& { return shake_original_; } + +#if BA_DEBUG_BUILD + auto defining_component() const -> bool { return defining_component_; } + void set_defining_component(bool val) { defining_component_ = val; } +#endif + + private: + bool needs_clear_{}; + BenchmarkType benchmark_type_{BenchmarkType::kNone}; + bool rendering_{}; + CameraMode camera_mode_{CameraMode::kFollow}; + Vector3f cam_original_{0.0f, 0.0f, 0.0f}; + Vector3f cam_target_original_{0.0f, 0.0f, 0.0f}; + Vector3f shake_original_{0.0f, 0.0f, 0.0f}; + float vr_near_clip_{}; + Matrix44f vr_overlay_screen_matrix_ = kMatrix44fIdentity; + Matrix44f vr_overlay_screen_matrix_fixed_ = kMatrix44fIdentity; + std::vector mesh_data_creates_; + std::vector mesh_data_destroys_; + + // Meshes/Buffers: + std::vector> meshes_; + std::vector> mesh_buffers_; + std::vector mesh_index_sizes_; + std::vector> media_components_; + +#if BA_DEBUG_BUILD + // Sanity checking: make sure components are completely submitted + // before new ones are started (so we dont get scrambled command buffers). + bool defining_component_{}; +#endif + + std::unique_ptr light_pass_; + std::unique_ptr light_shadow_pass_; + std::unique_ptr beauty_pass_; + std::unique_ptr beauty_pass_bg_; + std::unique_ptr overlay_pass_; + std::unique_ptr overlay_front_pass_; + std::unique_ptr overlay_fixed_pass_; + std::unique_ptr overlay_flat_pass_; + std::unique_ptr vr_cover_pass_; + std::unique_ptr overlay_3d_pass_; + std::unique_ptr blit_pass_; + GraphicsQuality quality_{GraphicsQuality::kLow}; + bool orbiting_{}; + millisecs_t real_time_{}; + millisecs_t base_time_{}; + millisecs_t base_time_elapsed_{}; + int64_t frame_number_{}; + Vector3f shadow_offset_{0.0f, 0.0f, 0.0f}; + Vector2f shadow_scale_{1.0f, 1.0f}; + bool shadow_ortho_{}; + Vector3f tint_{1.0f, 1.0f, 1.0f}; + Vector3f ambient_color_{1.0f, 1.0f, 1.0f}; + Vector3f vignette_outer_{1.0f, 1.0f, 1.0f}; + Vector3f vignette_inner_{1.0f, 1.0f, 1.0f}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_FRAME_DEF_H_ diff --git a/src/ballistica/graphics/framebuffer.h b/src/ballistica/graphics/framebuffer.h new file mode 100644 index 00000000..cf3d22e2 --- /dev/null +++ b/src/ballistica/graphics/framebuffer.h @@ -0,0 +1,19 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_FRAMEBUFFER_H_ +#define BALLISTICA_GRAPHICS_FRAMEBUFFER_H_ + +#include "ballistica/core/object.h" + +namespace ballistica { + +class Framebuffer : public Object { + public: + auto GetDefaultOwnerThread() const -> ThreadIdentifier override { + return ThreadIdentifier::kMain; + } +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_FRAMEBUFFER_H_ diff --git a/src/ballistica/graphics/gl/gl_sys.cc b/src/ballistica/graphics/gl/gl_sys.cc new file mode 100644 index 00000000..32a610a5 --- /dev/null +++ b/src/ballistica/graphics/gl/gl_sys.cc @@ -0,0 +1,368 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#if BA_ENABLE_OPENGL +#include "ballistica/graphics/gl/gl_sys.h" + +#include + +#include "ballistica/ballistica.h" +#include "ballistica/platform/sdl/sdl_app.h" + +#if BA_OSTYPE_ANDROID +#include +#if !BA_USE_ES3_INCLUDES +#include "ballistica/platform/android/android_gl3.h" +#endif +#endif + +#if BA_OSTYPE_WINDOWS +#pragma comment(lib, "opengl32.lib") +#pragma comment(lib, "glu32.lib") +#endif + +#if BA_OSTYPE_MACOS +#include +#include +#include +#endif + +#if BA_DEBUG_BUILD +#define DEBUG_CHECK_GL_ERROR \ + { \ + GLenum err = glGetError(); \ + if (err != GL_NO_ERROR) \ + Log("OPENGL ERROR AT LINE " + std::to_string(__LINE__) + ": " \ + + GLErrorToString(err)); \ + } +#else +#define DEBUG_CHECK_GL_ERROR +#endif + +#if BA_OSTYPE_ANDROID +PFNGLDISCARDFRAMEBUFFEREXTPROC _glDiscardFramebufferEXT = nullptr; +#endif + +#if BA_OSTYPE_WINDOWS +PFNGLGETINTERNALFORMATIVPROC glGetInternalformativ = nullptr; +PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC +glGetFramebufferAttachmentParameteriv = nullptr; +PFNGLBLENDFUNCSEPARATEPROC glBlendFuncSeparate = nullptr; +PFNGLACTIVETEXTUREPROC glActiveTexture = nullptr; +PFNGLCLIENTACTIVETEXTUREARBPROC glClientActiveTextureARB = nullptr; +PFNGLPOINTPARAMETERFVARBPROC glPointParameterfvARB = nullptr; +PFNGLPOINTPARAMETERFARBPROC glPointParameterfARB = nullptr; +PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT = nullptr; +PFNGLCREATEPROGRAMPROC glCreateProgram = nullptr; +PFNGLCREATESHADERPROC glCreateShader = nullptr; +PFNGLSHADERSOURCEPROC glShaderSource = nullptr; +PFNGLCOMPILESHADERPROC glCompileShader = nullptr; +PFNGLLINKPROGRAMPROC glLinkProgram = nullptr; +PFNGLGETINFOLOGARBPROC glGetInfoLogARB = nullptr; +PFNGLATTACHSHADERPROC glAttachShader = nullptr; +PFNGLUSEPROGRAMOBJECTARBPROC glUseProgram = nullptr; +PFNGLGENERATEMIPMAPPROC glGenerateMipmap = nullptr; +PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer = nullptr; +PFNGLBLITFRAMEBUFFERPROC glBlitFramebuffer = nullptr; +PFNGLBINDVERTEXARRAYPROC glBindVertexArray = nullptr; +PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation = nullptr; +PFNGLUNIFORM1IPROC glUniform1i = nullptr; +PFNGLUNIFORM1FPROC glUniform1f = nullptr; +PFNGLUNIFORM1FVPROC glUniform1fv = nullptr; +PFNGLUNIFORM2FPROC glUniform2f = nullptr; +PFNGLUNIFORM3FPROC glUniform3f = nullptr; +PFNGLUNIFORM4FPROC glUniform4f = nullptr; +PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers = nullptr; +PFNGLGENBUFFERSPROC glGenBuffers = nullptr; +PFNGLGENVERTEXARRAYSPROC glGenVertexArrays = nullptr; +PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D = nullptr; +PFNGLGENRENDERBUFFERSPROC glGenRenderbuffers = nullptr; +PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer = nullptr; +PFNGLBINDBUFFERPROC glBindBuffer = nullptr; +PFNGLBUFFERDATAPROC glBufferData = nullptr; +PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage = nullptr; +PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC glRenderbufferStorageMultisample = + nullptr; +PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer = nullptr; +PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus = nullptr; +PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers = nullptr; +PFNGLDELETERENDERBUFFERSPROC glDeleteRenderbuffers = nullptr; +PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer = nullptr; +PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray = nullptr; +PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray = nullptr; +PFNGLUNIFORMMATRIX4FVARBPROC glUniformMatrix4fv = nullptr; +PFNGLBINDATTRIBLOCATIONPROC glBindAttribLocation = nullptr; +PFNGLCOMPRESSEDTEXIMAGE2DPROC glCompressedTexImage2D = nullptr; +PFNGLGETSHADERIVPROC glGetShaderiv = nullptr; +PFNGLGETPROGRAMIVPROC glGetProgramiv = nullptr; +PFNGLDELETESHADERPROC glDeleteShader = nullptr; +PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays = nullptr; +PFNGLDELETEBUFFERSPROC glDeleteBuffers = nullptr; +PFNGLDELETEPROGRAMPROC glDeleteProgram = nullptr; +PFNGLDETACHSHADERPROC glDetachShader = nullptr; +PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog = nullptr; +PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog = nullptr; +#endif // BA_OSTYPE_WINDOWS + +namespace ballistica { + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#pragma ide diagnostic ignored "EmptyDeclOrStmt" + +GLContext::GLContext(int target_res_x, int target_res_y, bool fullscreen) + : fullscreen_(fullscreen) { + assert(InMainThread()); + bool need_window = true; +#if BA_RIFT_BUILD + // on the rift build we don't need a window when running in vr mode; we just + // use the context we're created into... + if (IsVRMode()) { + need_window = false; + } +#endif // BA_RIFT_BUILD + if (explicit_bool(need_window)) { +#if BA_SDL2_BUILD +#if BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID + int flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_BORDERLESS; +#else + // Things are a bit more varied on desktop.. + uint32_t flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN + | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE; + if (fullscreen_) { + flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; + } +#endif + sdl_window_ = SDL_CreateWindow(nullptr, SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, target_res_x, + target_res_y, flags); + if (!sdl_window_) { + throw Exception("Unable to create SDL Window of size " + + std::to_string(target_res_x) + " by " + + std::to_string(target_res_y)); + } + sdl_gl_context_ = SDL_GL_CreateContext(sdl_window_); + if (!sdl_gl_context_) { + throw Exception("Unable to create SDL GL Context"); + } + SDL_SetWindowTitle(sdl_window_, "BallisticaCore"); + + // Our actual drawable size could differ from the window size on retina + // devices. + int win_size_x, win_size_y; + SDL_GetWindowSize(sdl_window_, &win_size_x, &win_size_y); + SDLApp::get()->SetInitialScreenDimensions(Vector2f( + static_cast(win_size_x), static_cast(win_size_y))); +#if BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID + res_x_ = win_size_x; + res_y_ = win_size_y; +#else + SDL_GL_GetDrawableSize(sdl_window_, &res_x_, &res_y_); +#endif // BA_OSTYPE_ANDROID + + // This can come through as zero in some cases (on our cardboard build at + // least). + if (win_size_x != 0) { + pixel_density_ = + static_cast(res_x_) / static_cast(win_size_x); + } +#elif BA_SDL_BUILD // BA_SDL2_BUILD + + int v_flags; + v_flags = SDL_OPENGL; + if (fullscreen_) { + v_flags |= SDL_FULLSCREEN; + // convert to the closest valid fullscreen resolution + // (our last 1.2 build is mac and it's got hacked-in fullscreen-window + // support; so we don't need this) getValidResolution(target_res_x, + // target_res_y); + } else { + v_flags |= SDL_RESIZABLE; + } + surface_ = SDL_SetVideoMode(target_res_x, target_res_y, 32, v_flags); + + // if we failed, fall back to windowed mode. + if (surface_ == nullptr) { + throw Exception("SDL_SetVideoMode() failed for " + + std::to_string(target_res_x) + " by " + + std::to_string(target_res_y) + " fullscreen=" + + std::to_string(static_cast(fullscreen_))); + } + res_x_ = surface_->w; + res_y_ = surface_->h; + SDLApp::get()->SetInitialScreenDimensions(Vector2f(res_x_, res_y_)); + SDL_WM_SetCaption("BallisticaCore", "BallisticaCore"); +#elif BA_OSTYPE_ANDROID + // On Android the Java layer creates a GL setup before even calling us. + // So we have nothing to do here. Hooray! +#else + throw Exception("FIXME: Unimplemented"); +#endif // BA_SDL2_BUILD + } + + // Fetch needed android gl stuff. +#if BA_OSTYPE_ANDROID +#define GET(PTRTYPE, FUNC, REQUIRED) \ + FUNC = (PTRTYPE)eglGetProcAddress(#FUNC); \ + if (!FUNC) FUNC = (PTRTYPE)eglGetProcAddress(#FUNC "EXT"); \ + if (REQUIRED) { \ + BA_PRECONDITION(FUNC != nullptr); \ + } + GET(PFNGLDISCARDFRAMEBUFFEREXTPROC, _glDiscardFramebufferEXT, false); +#endif // BA_OSTYPE_ANDROID + + // Fetch needed windows gl stuff. +#if BA_OSTYPE_WINDOWS +#define GET(PTRTYPE, FUNC, REQUIRED) \ + FUNC = (PTRTYPE)wglGetProcAddress(#FUNC); \ + if (!FUNC) FUNC = (PTRTYPE)wglGetProcAddress(#FUNC "EXT"); \ + if (REQUIRED) { \ + BA_PRECONDITION(FUNC != nullptr); \ + } + GET(PFNGLGETINTERNALFORMATIVPROC, glGetInternalformativ, + false); // for checking msaa level support + GET(PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC, + glGetFramebufferAttachmentParameteriv, false); // for checking srgb stuff + GET(PFNGLBLENDFUNCSEPARATEPROC, glBlendFuncSeparate, + false); // needed for VR overlay + GET(PFNGLACTIVETEXTUREPROC, glActiveTexture, true); + GET(PFNGLCLIENTACTIVETEXTUREARBPROC, glClientActiveTextureARB, true); + GET(PFNWGLSWAPINTERVALEXTPROC, wglSwapIntervalEXT, true); + GET(PFNGLPOINTPARAMETERFVARBPROC, glPointParameterfvARB, true); + GET(PFNGLPOINTPARAMETERFARBPROC, glPointParameterfARB, true); + GET(PFNGLCREATEPROGRAMPROC, glCreateProgram, true); + GET(PFNGLCREATESHADERPROC, glCreateShader, true); + GET(PFNGLSHADERSOURCEPROC, glShaderSource, true); + GET(PFNGLCOMPILESHADERPROC, glCompileShader, true); + GET(PFNGLLINKPROGRAMPROC, glLinkProgram, true); + GET(PFNGLGETINFOLOGARBPROC, glGetInfoLogARB, true); + GET(PFNGLATTACHSHADERPROC, glAttachShader, true); + GET(PFNGLUSEPROGRAMOBJECTARBPROC, glUseProgram, true); + GET(PFNGLGENERATEMIPMAPPROC, glGenerateMipmap, true); + GET(PFNGLBINDFRAMEBUFFERPROC, glBindFramebuffer, true); + GET(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation, true); + GET(PFNGLUNIFORM1IPROC, glUniform1i, true); + GET(PFNGLUNIFORM1FPROC, glUniform1f, true); + GET(PFNGLUNIFORM1FVPROC, glUniform1fv, true); + GET(PFNGLUNIFORM2FPROC, glUniform2f, true); + GET(PFNGLUNIFORM3FPROC, glUniform3f, true); + GET(PFNGLUNIFORM4FPROC, glUniform4f, true); + GET(PFNGLGENFRAMEBUFFERSPROC, glGenFramebuffers, true); + GET(PFNGLGENBUFFERSPROC, glGenBuffers, true); + GET(PFNGLFRAMEBUFFERTEXTURE2DPROC, glFramebufferTexture2D, true); + GET(PFNGLGENRENDERBUFFERSPROC, glGenRenderbuffers, true); + GET(PFNGLBINDRENDERBUFFERPROC, glBindRenderbuffer, true); + GET(PFNGLBINDBUFFERPROC, glBindBuffer, true); + GET(PFNGLBUFFERDATAPROC, glBufferData, true); + GET(PFNGLRENDERBUFFERSTORAGEPROC, glRenderbufferStorage, true); + GET(PFNGLFRAMEBUFFERRENDERBUFFERPROC, glFramebufferRenderbuffer, true); + GET(PFNGLCHECKFRAMEBUFFERSTATUSPROC, glCheckFramebufferStatus, true); + GET(PFNGLDELETEFRAMEBUFFERSPROC, glDeleteFramebuffers, true); + GET(PFNGLDELETERENDERBUFFERSPROC, glDeleteRenderbuffers, true); + GET(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer, true); + GET(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray, true); + GET(PFNGLDISABLEVERTEXATTRIBARRAYPROC, glDisableVertexAttribArray, true); + GET(PFNGLUNIFORMMATRIX4FVARBPROC, glUniformMatrix4fv, true); + GET(PFNGLBINDATTRIBLOCATIONPROC, glBindAttribLocation, true); + GET(PFNGLCOMPRESSEDTEXIMAGE2DPROC, glCompressedTexImage2D, true); + GET(PFNGLGETSHADERIVPROC, glGetShaderiv, true); + GET(PFNGLGETPROGRAMIVPROC, glGetProgramiv, true); + GET(PFNGLDELETESHADERPROC, glDeleteShader, true); + GET(PFNGLDELETEBUFFERSPROC, glDeleteBuffers, true); + GET(PFNGLDELETEPROGRAMPROC, glDeleteProgram, true); + GET(PFNGLDETACHSHADERPROC, glDetachShader, true); + GET(PFNGLGETSHADERINFOLOGPROC, glGetShaderInfoLog, true); + GET(PFNGLGETPROGRAMINFOLOGPROC, glGetProgramInfoLog, true); + + // Stuff we can live without: + GET(PFNGLBINDVERTEXARRAYPROC, glBindVertexArray, false); + GET(PFNGLGENVERTEXARRAYSPROC, glGenVertexArrays, false); + GET(PFNGLDELETEVERTEXARRAYSPROC, glDeleteVertexArrays, false); + GET(PFNGLBLITFRAMEBUFFERPROC, glBlitFramebuffer, false); + GET(PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC, glRenderbufferStorageMultisample, + false); + +#undef GET +#endif // BA_OSTYPE_WINDOWS + + // So that our window comes up nice and black. + // FIXME should just make the window's blanking color black. + +#if BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID + // Not needed here. +#else + +#if BA_SDL2_BUILD + // Gonna wait and see if if still need this. +#else + glClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + SDL_GL_SwapBuffers(); +#endif // BA_SDL2_BUILD + +#endif // IOS/ANDROID +} +#pragma clang diagnostic pop + +void GLContext::SetVSync(bool enable) { + assert(InMainThread()); + +#if BA_OSTYPE_MACOS + CGLContextObj context = CGLGetCurrentContext(); + BA_PRECONDITION(context); + GLint sync = enable; + CGLSetParameter(context, kCGLCPSwapInterval, &sync); +#else + +#endif // BA_OSTYPE_MACOS +} + +GLContext::~GLContext() { + if (!InMainThread()) { + Log("Error: GLContext dying in non-graphics thread"); + } +#if BA_SDL2_BUILD + +#if BA_RIFT_BUILD + // (in rift we only have a window in 2d mode) + if (!IsVRMode()) { + BA_PRECONDITION_LOG(sdl_window_); + } +#else // BA_RIFT_MODE + BA_PRECONDITION_LOG(sdl_window_); +#endif // BA_RIFT_BUILD + + if (sdl_window_) { + SDL_DestroyWindow(sdl_window_); + sdl_window_ = nullptr; + } +#elif BA_SDL_BUILD + BA_PRECONDITION_LOG(surface_); + if (surface_) { + SDL_FreeSurface(surface_); + surface_ = nullptr; + } +#endif +} + +auto GLErrorToString(GLenum err) -> std::string { + switch (err) { + case GL_NO_ERROR: + return "GL_NO_ERROR"; + case GL_INVALID_ENUM: + return "GL_INVALID_ENUM"; + case GL_INVALID_VALUE: + return "GL_INVALID_VALUE"; + case GL_INVALID_OPERATION: + return "GL_INVALID_OPERATION"; + case GL_OUT_OF_MEMORY: + return "GL_OUT_OF_MEMORY"; + case GL_INVALID_FRAMEBUFFER_OPERATION: + return "GL_INVALID_FRAMEBUFFER_OPERATION"; + default: + return std::to_string(err); + } +} + +} // namespace ballistica + +#endif // BA_ENABLE_OPENGL diff --git a/src/ballistica/graphics/gl/gl_sys.h b/src/ballistica/graphics/gl/gl_sys.h new file mode 100644 index 00000000..6006502c --- /dev/null +++ b/src/ballistica/graphics/gl/gl_sys.h @@ -0,0 +1,201 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_GL_GL_SYS_H_ +#define BALLISTICA_GRAPHICS_GL_GL_SYS_H_ + +#if BA_ENABLE_OPENGL + +#if !BA_OSTYPE_WINDOWS +#define GL_GLEXT_PROTOTYPES +#endif + +#include + +#if BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID + +#if BA_USE_ES3_INCLUDES +#include +#include +#else +#if BA_SDL_BUILD +#include // needed for ios?... +#include +#else +// FIXME: According to https://developer.android.com/ndk/guides/stable_apis +// we can always link against ES3.1 now that we're API 21+, so we shouldn't +// need our funky stubs and function lookups anymore. +// (though we'll still need to check for availability of 3.x features) +#include +#include +#endif // BA_SDL_BUILD +#endif // BA_USE_ES3_INCLUDES + +// looks like these few defines are currently missing on android +// (s3tc works on some nvidia hardware) +#ifndef GL_COMPRESSED_RGB_S3TC_DXT1_EXT +#define GL_COMPRESSED_RGB_S3TC_DXT1_EXT 0x83F0 +#endif +#ifndef GL_COMPRESSED_RGBA_S3TC_DXT1_EXT +#define GL_COMPRESSED_RGBA_S3TC_DXT1_EXT 0x83F1 +#endif +#ifndef GL_COMPRESSED_RGBA_S3TC_DXT3_EXT +#define GL_COMPRESSED_RGBA_S3TC_DXT3_EXT 0x83F2 +#endif +#ifndef GL_COMPRESSED_RGBA_S3TC_DXT5_EXT +#define GL_COMPRESSED_RGBA_S3TC_DXT5_EXT 0x83F3 +#endif + +#else // BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID + +#if BA_SDL2_BUILD +#include +#elif BA_SDL_BUILD // BA_SDL2_BUILD +#define NO_SDL_GLEXT +#include +#endif // BA_SDL2_BUILD + +#if BA_OSTYPE_MACOS +#include +#endif // BA_OSTYPE_MACOS + +#endif // BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID + +#include "ballistica/core/object.h" +#include "ballistica/platform/min_sdl.h" + +#if BA_OSTYPE_ANDROID +extern PFNGLDISCARDFRAMEBUFFEREXTPROC _glDiscardFramebufferEXT; +#endif + +#if BA_OSTYPE_WINDOWS +#ifndef WGL_EXT_swap_control +#define WGL_EXT_swap_control 1 +typedef BOOL(WINAPI* PFNWGLSWAPINTERVALEXTPROC)(int interval); +typedef int(WINAPI* PFNWGLGETSWAPINTERVALEXTPROC)(VOID); // NOLINT +#endif // WGL_EXT_swap_control +extern PFNGLGETINTERNALFORMATIVPROC glGetInternalformativ; +extern PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC + glGetFramebufferAttachmentParameteriv; +extern PFNGLBLENDFUNCSEPARATEPROC glBlendFuncSeparate; +extern PFNGLACTIVETEXTUREPROC glActiveTexture; +extern PFNGLCLIENTACTIVETEXTUREARBPROC glClientActiveTextureARB; +extern PFNGLPOINTPARAMETERFARBPROC glPointParameterfARB; +extern PFNGLPOINTPARAMETERFVARBPROC glPointParameterfvARB; +extern PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT; +extern PFNGLCREATEPROGRAMPROC glCreateProgram; +extern PFNGLCREATESHADERPROC glCreateShader; +extern PFNGLSHADERSOURCEPROC glShaderSource; +extern PFNGLCOMPILESHADERPROC glCompileShader; +extern PFNGLLINKPROGRAMPROC glLinkProgram; +extern PFNGLGETINFOLOGARBPROC glGetInfoLogARB; +extern PFNGLATTACHSHADERPROC glAttachShader; +extern PFNGLUSEPROGRAMOBJECTARBPROC glUseProgram; +extern PFNGLGENERATEMIPMAPPROC glGenerateMipmap; +extern PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer; +extern PFNGLBLITFRAMEBUFFERPROC glBlitFramebuffer; +extern PFNGLBINDVERTEXARRAYPROC glBindVertexArray; +extern PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation; +extern PFNGLUNIFORM1IPROC glUniform1i; +extern PFNGLUNIFORM1FPROC glUniform1f; +extern PFNGLUNIFORM1FVPROC glUniform1fv; +extern PFNGLUNIFORM2FPROC glUniform2f; +extern PFNGLUNIFORM3FPROC glUniform3f; +extern PFNGLUNIFORM4FPROC glUniform4f; +extern PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers; +extern PFNGLGENBUFFERSPROC glGenBuffers; +extern PFNGLGENVERTEXARRAYSPROC glGenVertexArrays; +extern PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D; +extern PFNGLGENRENDERBUFFERSPROC glGenRenderbuffers; +extern PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer; +extern PFNGLBINDBUFFERPROC glBindBuffer; +extern PFNGLBUFFERDATAPROC glBufferData; +extern PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage; +extern PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC glRenderbufferStorageMultisample; +extern PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer; +extern PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus; +extern PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers; +extern PFNGLDELETERENDERBUFFERSPROC glDeleteRenderbuffers; +extern PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer; +extern PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray; +extern PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray; +extern PFNGLUNIFORMMATRIX4FVARBPROC glUniformMatrix4fv; +extern PFNGLBINDATTRIBLOCATIONPROC glBindAttribLocation; +extern PFNGLCOMPRESSEDTEXIMAGE2DPROC glCompressedTexImage2D; +extern PFNGLGETSHADERIVPROC glGetShaderiv; +extern PFNGLGETPROGRAMIVPROC glGetProgramiv; +extern PFNGLDELETESHADERPROC glDeleteShader; +extern PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays; +extern PFNGLDELETEBUFFERSPROC glDeleteBuffers; +extern PFNGLDELETEPROGRAMPROC glDeleteProgram; +extern PFNGLDETACHSHADERPROC glDetachShader; +extern PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog; +extern PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog; +#endif // BA_OSTYPE_WINDOWS + +#ifndef GL_NV_texture_rectangle +#define GL_TEXTURE_RECTANGLE_NV 0x84F5 +#define GL_TEXTURE_BINDING_RECTANGLE_NV 0x84F6 +#define GL_PROXY_TEXTURE_RECTANGLE_NV 0x84F7 +#define GL_MAX_RECTANGLE_TEXTURE_SIZE_NV 0x84F8 +#endif +#ifndef GL_NV_texture_rectangle +#define GL_NV_texture_rectangle 1 +#endif + +// Support for gl object debug labeling. +#if BA_OSTYPE_IOS_TVOS +#define GL_LABEL_OBJECT(type, obj, label) glLabelObjectEXT(type, obj, 0, label) +#define GL_PUSH_GROUP_MARKER(label) glPushGroupMarkerEXT(0, label) +#define GL_POP_GROUP_MARKER() glPopGroupMarkerEXT() +#else +#define GL_LABEL_OBJECT(type, obj, label) ((void)0) +#define GL_PUSH_GROUP_MARKER(label) ((void)0) +#define GL_POP_GROUP_MARKER() ((void)0) +#endif + +namespace ballistica { + +auto GLErrorToString(GLenum err) -> std::string; + +// Container for OpenGL rendering context data. +class GLContext { + public: + GLContext(int target_res_x, int target_res_y, bool fullScreen); + ~GLContext(); + auto res_x() const -> int { return res_x_; } + auto res_y() const -> int { return res_y_; } + auto pixel_density() const -> float { return pixel_density_; } + void SetVSync(bool enable); + + // Currently no surface/window in this case. +#if BA_SDL2_BUILD + auto sdl_window() const -> SDL_Window* { + assert(sdl_window_); + return sdl_window_; + } +#elif BA_SDL_BUILD // BA_SDL2_BUILD + SDL_Surface* sdl_screen_surface() const { + assert(surface_); + return surface_; + } +#endif // BA_SDL2_BUILD + + private: +#if BA_SDL2_BUILD + SDL_Window* sdl_window_{}; + SDL_GLContext sdl_gl_context_{}; +#endif // BA_SDL2_BUILD + bool fullscreen_{}; + int res_x_{}; + int res_y_{}; + float pixel_density_{1.0f}; +#if BA_SDL_BUILD && !BA_SDL2_BUILD + SDL_Surface* surface_{}; +#endif +}; // GLContext + +} // namespace ballistica + +#endif // BA_ENABLE_OPENGL + +#endif // BALLISTICA_GRAPHICS_GL_GL_SYS_H_ diff --git a/src/ballistica/graphics/gl/renderer_gl.cc b/src/ballistica/graphics/gl/renderer_gl.cc new file mode 100644 index 00000000..0ac10649 --- /dev/null +++ b/src/ballistica/graphics/gl/renderer_gl.cc @@ -0,0 +1,6617 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#if BA_ENABLE_OPENGL +#include "ballistica/graphics/gl/renderer_gl.h" + +#include +#include +#include +#include +#include + +#include "ballistica/graphics/component/special_component.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/graphics/mesh/mesh_renderer_data.h" +#include "ballistica/math/matrix44f.h" +#include "ballistica/media/data/model_data.h" +#include "ballistica/media/data/model_renderer_data.h" +#include "ballistica/media/data/texture_preload_data.h" +#include "ballistica/media/data/texture_renderer_data.h" + +#if BA_OSTYPE_IOS_TVOS +#include "ballistica/platform/apple/apple_utils.h" +#endif + +#define MSAA_ERROR_TEST 0 + +#if BA_OSTYPE_ANDROID +#include +#include +#if !BA_USE_ES3_INCLUDES +#include "ballistica/platform/android/android_gl3.h" +#endif +#define glDepthRange glDepthRangef +#define glDiscardFramebufferEXT _glDiscardFramebufferEXT +#ifndef GL_RGB565_OES +#define GL_RGB565_OES 0x8D62 +#endif // GL_RGB565_OES +#define GL_READ_FRAMEBUFFER 0x8CA8 +#define GL_DRAW_FRAMEBUFFER 0x8CA9 +#define GL_READ_FRAMEBUFFER_BINDING 0x8CAA +#define glClearDepth glClearDepthf +#endif // BA_OSTYPE_ANDROID + +#if BA_OSTYPE_MACOS +#include +#define glGenVertexArrays glGenVertexArraysAPPLE +#define glDeleteVertexArrays glDeleteVertexArraysAPPLE +#define glBindVertexArray glBindVertexArrayAPPLE +#endif // BA_OSTYPE_MACOS + +#if BA_OSTYPE_IOS_TVOS +void (*glInvalidateFramebuffer)(GLenum target, GLsizei num_attachments, + const GLenum* attachments) = nullptr; +#define glDepthRange glDepthRangef +#define glGenVertexArrays glGenVertexArraysOES +#define glDeleteVertexArrays glDeleteVertexArraysOES +#define glBindVertexArray glBindVertexArrayOES +#define glClearDepth glClearDepthf +#endif // BA_OSTYPE_IOS_TVOS + +// Turn this off to see how much blend overdraw is occurring. +#define ENABLE_BLEND 1 + +// Support legacy drawing purely for debugging (should migrate this to +// post-fixed pipeline). +#if BA_OSTYPE_MACOS +#define ENABLE_DEBUG_DRAWING 1 +#else +#define ENABLE_DEBUG_DRAWING 0 +#endif + +#ifndef GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG +#define GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG 0x8C02 +#endif +#ifndef GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG +#define GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG 0x8C03 +#endif + +#ifndef GL_ETC1_RGB8_OES +#define GL_ETC1_RGB8_OES 0x8D64 +#endif + +#ifndef GL_COMPRESSED_RGB8_ETC2 +#define GL_COMPRESSED_RGB8_ETC2 0x9274 +#endif +#ifndef GL_COMPRESSED_RGBA8_ETC2_EAC +#define GL_COMPRESSED_RGBA8_ETC2_EAC 0x9278 +#endif + +#define CHECK_GL_ERROR _check_gl_error(__LINE__) + +// Handy to check gl stuff on opt builds. +#define FORCE_CHECK_GL_ERRORS 0 + +#if BA_DEBUG_BUILD || FORCE_CHECK_GL_ERRORS +#define DEBUG_CHECK_GL_ERROR _check_gl_error(__LINE__) +#else +#define DEBUG_CHECK_GL_ERROR ((void)0) +#endif + +// OpenGL ES uses precision.. regular GL doesn't +#if (BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID) +#define LOWP "lowp " +#define MEDIUMP "mediump " +#define HIGHP "highp " +#else +#define LOWP +#define MEDIUMP +#define HIGHP +#endif // (BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID) + +// FIXME: Should make proper blur work in VR (perhaps just pass a uniform? +#if BA_VR_BUILD +#define BLURSCALE "0.3 * " +#else +#define BLURSCALE +#endif + +namespace ballistica { + +// Lots of signed bitwise stuff happening in there; should tidy it up. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#pragma ide diagnostic ignored "bugprone-macro-parentheses" + +bool RendererGL::funky_depth_issue_set_{}; +bool RendererGL::funky_depth_issue_{}; +bool RendererGL::draws_shields_funny_{}; +bool RendererGL::draws_shields_funny_set_{}; + +GLint g_combined_texture_image_unit_count{}; +bool g_anisotropic_support{}; +bool g_vao_support{}; +float g_max_anisotropy{}; +bool g_discard_framebuffer_support{}; +bool g_invalidate_framebuffer_support{}; +bool g_blit_framebuffer_support{}; +bool g_framebuffer_multisample_support{}; +bool g_running_es3{}; +bool g_seamless_cube_maps{}; +int g_msaa_max_samples_rgb565{}; +int g_msaa_max_samples_rgb8{}; + +#if BA_OSTYPE_ANDROID +bool RendererGL::is_speedy_android_device_{}; +bool RendererGL::is_extra_speedy_android_device_{}; +#endif // BA_OSTYPE_ANDROID + +static void _check_gl_error(int line) { + GLenum err = glGetError(); + if (err != GL_NO_ERROR) { + const char* version = (const char*)glGetString(GL_VERSION); + const char* vendor = (const char*)glGetString(GL_VENDOR); + const char* renderer = (const char*)glGetString(GL_RENDERER); + Log("Error: OpenGL Error at line " + std::to_string(line) + ": " + + GLErrorToString(err) + "\nrenderer: " + renderer + + "\nvendor: " + vendor + "\nversion: " + version + + "\ntime: " + std::to_string(GetRealTime())); + } +} + +// Flags affecting shader creation. +enum ShaderFlag { + SHD_REFLECTION = 1, + SHD_TEXTURE = 1 << 1, + SHD_MODULATE = 1 << 2, + SHD_COLORIZE = 1 << 3, + SHD_LIGHT_SHADOW = 1 << 4, + SHD_WORLD_SPACE_PTS = 1 << 5, + SHD_DEBUG_PRINT = 1 << 6, + SHD_ADD = 1 << 7, + SHD_OBJ_TRANSPARENT = 1 << 8, + SHD_COLOR = 1 << 9, + SHD_EXP2 = 1 << 10, + SHD_CAMERA_ALIGNED = 1 << 11, + SHD_DISTORT = 1 << 12, + SHD_PREMULTIPLY = 1 << 13, + SHD_OVERLAY = 1 << 14, + SHD_EYES = 1 << 15, + SHD_COLORIZE2 = 1 << 16, + SHD_HIGHER_QUALITY = 1 << 17, + SHD_SHADOW = 1 << 18, + SHD_GLOW = 1 << 19, + SHD_MASKED = 1 << 20, + SHD_MASK_UV2 = 1 << 21, + SHD_CONDITIONAL = 1 << 22, + SHD_FLATNESS = 1 << 23, + SHD_DEPTH_BUG_TEST = 1 << 24 +}; + +// Flags used internally by shaders. +enum ShaderPrivateFlags { + PFLAG_USES_POSITION_ATTR = 1, + PFLAG_USES_UV_ATTR = 1 << 1, + PFLAG_USES_NORMAL_ATTR = 1 << 2, + PFLAG_USES_MODEL_WORLD_MATRIX = 1 << 3, + PFLAG_USES_CAM_POS = 1 << 4, + PFLAG_USES_SHADOW_PROJECTION_MATRIX = 1 << 5, + PFLAG_WORLD_SPACE_PTS = 1 << 6, + PFLAG_USES_ERODE_ATTR = 1 << 7, + PFLAG_USES_COLOR_ATTR = 1 << 8, + PFLAG_USES_SIZE_ATTR = 1 << 9, + PFLAG_USES_DIFFUSE_ATTR = 1 << 10, + PFLAG_USES_CAM_ORIENT_MATRIX = 1 << 11, + PFLAG_USES_MODEL_VIEW_MATRIX = 1 << 12, + PFLAG_USES_UV2_ATTR = 1 << 13 +}; + +// Look for a gl extension prefixed by "GL_ARB", "GL_EXT", etc +// returns true if found. +static auto CheckGLExtension(const char* exts, const char* ext) -> bool { + char b[128]; + snprintf(b, sizeof(b), "OES_%s", ext); + if (strstr(exts, b)) return true; + snprintf(b, sizeof(b), "GL_ARB_%s", ext); + if (strstr(exts, b)) return true; + snprintf(b, sizeof(b), "GL_APPLE_%s", ext); + if (strstr(exts, b)) return true; + snprintf(b, sizeof(b), "GL_EXT_%s", ext); + if (strstr(exts, b)) return true; + snprintf(b, sizeof(b), "GL_NV_%s", ext); + if (strstr(exts, b)) return true; + snprintf(b, sizeof(b), "GL_SGIS_%s", ext); + if (strstr(exts, b)) return true; + snprintf(b, sizeof(b), "GL_IMG_%s", ext); + return strstr(exts, b) != nullptr; +} + +void RendererGL::CheckGLExtensions() { + DEBUG_CHECK_GL_ERROR; + assert(InGraphicsThread()); + // const char *version_str = (const char*)glGetString(GL_VERSION); + + const char* ex = (const char*)glGetString(GL_EXTENSIONS); + assert(ex); + // Log(ex); + + // Log(string("GL VERSION: ")+version_str); + + draws_shields_funny_set_ = true; + + // const char *renderer = (const char*)glGetString(GL_RENDERER); + // const char *vendor = (const char*)glGetString(GL_VENDOR); + // const char *version_str = (const char*)glGetString(GL_VERSION); + // printf("RENDERER %s\nVENDOR %s\nVERSION %s\n",renderer,vendor,version_str); + + // on android, look at the GL version and try to get gl3 funcs to determine if + // we're running ES3 or not +#if BA_OSTYPE_ANDROID + + const char* renderer = (const char*)glGetString(GL_RENDERER); + const char* vendor = (const char*)glGetString(GL_VENDOR); + const char* version_str = (const char*)glGetString(GL_VERSION); + // Log(string("VER ")+version_str); + + bool have_es3; + +#if BA_USE_ES3_INCLUDES + have_es3 = true; +#else + have_es3 = (strstr(version_str, "OpenGL ES 3.") && gl3stubInit()); +#endif + + // if we require ES3 + if (have_es3) { + g_running_es3 = true; + Log(std::string("Using OpenGL ES 3 (vendor: ") + vendor + + ", renderer: " + renderer + ", version: " + version_str + ")", + false, false); + + } else { +#if !BA_USE_ES3_INCLUDES + g_running_es3 = false; + Log(std::string("USING OPENGL ES2 (vendor: ") + vendor + + ", renderer: " + renderer + ", version: " + version_str + ")", + false, false); + + // Can still support some stuff like framebuffer-blit with es2 extensions. + assert(glBlitFramebuffer == nullptr || !first_extension_check_); + glBlitFramebuffer = + (decltype(glBlitFramebuffer))eglGetProcAddress("glBlitFramebufferNV"); + assert(glRenderbufferStorageMultisample == nullptr + || !first_extension_check_); + glRenderbufferStorageMultisample = + (decltype(glRenderbufferStorageMultisample))eglGetProcAddress( + "glRenderbufferStorageMultisampleNV"); + + assert(glGenVertexArrays == nullptr || !first_extension_check_); + glGenVertexArrays = + (decltype(glGenVertexArrays))eglGetProcAddress("glGenVertexArraysOES"); + assert(glDeleteVertexArrays == nullptr || !first_extension_check_); + glDeleteVertexArrays = (decltype(glDeleteVertexArrays))eglGetProcAddress( + "glDeleteVertexArraysOES"); + assert(glBindVertexArray == nullptr || !first_extension_check_); + glBindVertexArray = + (decltype(glBindVertexArray))eglGetProcAddress("glBindVertexArrayOES"); + +#endif // BA_USE_ES3_INCLUDES + } + + DEBUG_CHECK_GL_ERROR; + + // Flag certain devices as 'speedy' - we use this to enable high/higher + // quality and whatnot (even in cases where ES3 isnt available). + is_speedy_android_device_ = false; + is_extra_speedy_android_device_ = false; + is_adreno_ = (strstr(renderer, "Adreno") != nullptr); + draws_shields_funny_ = false; // start optimistic. + + // ali tv box + if (!strcmp(renderer, "Mali-450 MP")) { + is_speedy_android_device_ = true; // this is borderline speedy/extra-speedy + draws_shields_funny_ = true; + } + + // firetv, etc.. lets enable MSAA + if (!strcmp(renderer, "Adreno (TM) 320")) { + is_recent_adreno_ = true; + } + + // this is right on the borderline, but lets go with extra-speedy i guess + if (!strcmp(renderer, "Adreno (TM) 330")) { + is_recent_adreno_ = true; + is_extra_speedy_android_device_ = true; + } + + // *any* of the 4xx or 5xx series are extra-speedy + if (strstr(renderer, "Adreno (TM) 4") || strstr(renderer, "Adreno (TM) 5") + || strstr(renderer, "Adreno (TM) 6")) { + is_extra_speedy_android_device_ = true; + is_recent_adreno_ = true; + } + + // some speedy malis (Galaxy S6 / Galaxy S7-ish) + if (strstr(renderer, "Mali-T760") || strstr(renderer, "Mali-T860") + || strstr(renderer, "Mali-T880")) { + is_extra_speedy_android_device_ = true; + } + // Note 8 is speed-tastic + if (!strcmp(renderer, "Mali-G71") || !strcmp(renderer, "Mali-G72")) { + is_extra_speedy_android_device_ = true; + } + + // covers Nexus player + // HMM Scratch that - this winds up being too slow for phones using this chip. + if (strstr(renderer, "PowerVR Rogue G6430")) { + // is_extra_speedy_android_device_ = true; + } + + // Figure out if we're a Tegra 4/K1/etc since we do some special stuff on + // those... + if (!strcmp(renderer, "NVIDIA Tegra")) { + // tegra 4 won't have ES3 but will have framebuffer_multisample + if (!g_running_es3 && CheckGLExtension(ex, "framebuffer_multisample")) { + is_tegra_4_ = true; + is_speedy_android_device_ = true; + } else if (g_running_es3) { + // running ES3 - must be a K1 (for now) + is_tegra_k1_ = true; + is_extra_speedy_android_device_ = true; + } else { + // looks like Tegra-2 era stuff was just "NVIDIA Tegra" as well... + } + } + + // Also store this globally for a few other bits of the app to use.. + g_platform->set_is_tegra_k1(is_tegra_k1_); + + // Extra-speedy implies speedy too.. + if (is_extra_speedy_android_device_) { + is_speedy_android_device_ = true; + } + +#endif // BA_OSTYPE_ANDROID + + std::list c_types; + assert(g_graphics); + if (CheckGLExtension(ex, "texture_compression_s3tc")) + c_types.push_back(TextureCompressionType::kS3TC); + + // Limiting pvr support to iOS for the moment. +#if !BA_OSTYPE_ANDROID + if (CheckGLExtension(ex, "texture_compression_pvrtc")) + c_types.push_back(TextureCompressionType::kPVR); +#endif + + // All android devices should support etc1. + if (CheckGLExtension(ex, "compressed_ETC1_RGB8_texture")) { + c_types.push_back(TextureCompressionType::kETC1); + } else { +#if BA_OSTYPE_ANDROID + Log("Android device missing ETC1 support"); +#endif + } + + // ETC2 is required for ES3 support (and OpenGL 4.4 or something once we + // eventually get there) + if (g_running_es3) c_types.push_back(TextureCompressionType::kETC2); + + g_graphics_server->SetTextureCompressionTypes(c_types); + + // Check whether we support high-quality mode (requires a few things like + // depth textures) For now lets also disallow high-quality in some VR + // environments. + + if (CheckGLExtension(ex, "depth_texture")) { + supports_depth_textures_ = true; +#if BA_CARDBOARD_BUILD + g_graphics->SetSupportsHighQualityGraphics(false); +#else // BA_CARDBOARD_BUILD + g_graphics->SetSupportsHighQualityGraphics(true); +#endif // BA_CARDBOARD_BUILD + } else { + supports_depth_textures_ = false; + g_graphics->SetSupportsHighQualityGraphics(false); + } + + // Store the tex-compression type we support. + DEBUG_CHECK_GL_ERROR; + + g_anisotropic_support = CheckGLExtension(ex, "texture_filter_anisotropic"); + if (g_anisotropic_support) { + glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &g_max_anisotropy); + } + + DEBUG_CHECK_GL_ERROR; + + // We can run with our without VAOs but they're nice to have. + g_vao_support = + (glGenVertexArrays != nullptr && glDeleteVertexArrays != nullptr + && glBindVertexArray != nullptr + && (g_running_es3 || CheckGLExtension(ex, "vertex_array_object"))); + +#if BA_OSTYPE_IOS_TVOS + g_blit_framebuffer_support = false; + g_framebuffer_multisample_support = false; +#elif BA_OSTYPE_MACOS + g_blit_framebuffer_support = CheckGLExtension(ex, "framebuffer_blit"); + g_framebuffer_multisample_support = false; +#else + g_blit_framebuffer_support = + (glBlitFramebuffer != nullptr + && (g_running_es3 || CheckGLExtension(ex, "framebuffer_blit"))); + g_framebuffer_multisample_support = + (glRenderbufferStorageMultisample != nullptr + && (g_running_es3 || (CheckGLExtension(ex, "framebuffer_multisample")))); +#endif + +#if BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID + +#if BA_OSTYPE_IOS_TVOS + g_discard_framebuffer_support = CheckGLExtension(ex, "discard_framebuffer"); +#else + g_discard_framebuffer_support = + (glDiscardFramebufferEXT != nullptr + && CheckGLExtension(ex, "discard_framebuffer")); +#endif + + g_invalidate_framebuffer_support = + (g_running_es3 && glInvalidateFramebuffer != nullptr); +#else + g_discard_framebuffer_support = false; + g_invalidate_framebuffer_support = false; +#endif + + g_seamless_cube_maps = CheckGLExtension(ex, "seamless_cube_map"); + +#if BA_OSTYPE_WINDOWS + // the vmware gl driver breaks horrifically with VAOs turned on + const char* vendor = (const char*)glGetString(GL_VENDOR); + if (strstr(vendor, "VMware")) { + g_vao_support = false; + } +#endif + +#if BA_OSTYPE_ANDROID + // VAOs currently break my poor kindle fire hd to the point of rebooting it + if (!g_running_es3 && !is_tegra_4_) { + g_vao_support = false; + } + + // also they seem to be problematic on zenfone2's gpu. + if (strstr(renderer, "PowerVR Rogue G6430")) { + g_vao_support = false; + } +#endif + + glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, + &g_combined_texture_image_unit_count); + + // If we're running ES3, ask about our max multisample counts and whether we + // can enable MSAA. + // enable_msaa_ = false; // start pessimistic + g_msaa_max_samples_rgb565 = g_msaa_max_samples_rgb8 = 0; // start pessimistic + +#if BA_OSTYPE_ANDROID || BA_RIFT_BUILD + bool check_msaa = false; + +#if BA_OSTYPE_ANDROID + if (g_running_es3) { + check_msaa = true; + } +#endif // BA_OSTYPE_ANDROID +#if BA_RIFT_BUILD + check_msaa = true; +#endif // BA_RIFT_BUILD + + if (check_msaa) { + if (glGetInternalformativ != nullptr) { + GLint count; + glGetInternalformativ(GL_RENDERBUFFER, GL_RGB565, GL_NUM_SAMPLE_COUNTS, 1, + &count); + if (count > 0) { + std::vector samples; + samples.resize(static_cast(static_cast(count))); + glGetInternalformativ(GL_RENDERBUFFER, GL_RGB565, GL_SAMPLES, count, + &samples[0]); + g_msaa_max_samples_rgb565 = samples[0]; + } else { + BA_LOG_ONCE("Got 0 samplecounts for RGB565"); + g_msaa_max_samples_rgb565 = 0; + } + } + // RGB8 max multisamples + if (glGetInternalformativ != nullptr) { + GLint count; + glGetInternalformativ(GL_RENDERBUFFER, GL_RGB8, GL_NUM_SAMPLE_COUNTS, 1, + &count); + if (count > 0) { + std::vector samples; + samples.resize(static_cast(count)); + glGetInternalformativ(GL_RENDERBUFFER, GL_RGB8, GL_SAMPLES, count, + &samples[0]); + g_msaa_max_samples_rgb8 = samples[0]; + } else { + BA_LOG_ONCE("Got 0 samplecounts for RGB8"); + g_msaa_max_samples_rgb8 = 0; + } + } + } else { + if (is_tegra_4_) { + // HMM is there a way to query this without ES3? + g_msaa_max_samples_rgb8 = g_msaa_max_samples_rgb565 = 4; + } + } + +#if MSAA_ERROR_TEST + if (enable_msaa_) { + ScreenMessage("MSAA ENABLED"); + Log("Ballistica MSAA Test: MSAA ENABLED", false, false); + } else { + ScreenMessage("MSAA DISABLED"); + Log("Ballistica MSAA Test: MSAA DISABLED", false, false); + } +#endif // MSAA_ERROR_TEST + +#endif // BA_OSTYPE_ANDROID + + DEBUG_CHECK_GL_ERROR; + + first_extension_check_ = false; +} + +auto RendererGL::GetMSAASamplesForFramebuffer(int width, int height) -> int { +#if BA_RIFT_BUILD + return 4; +#else + // we currently aim for 4 up to 800 height and 2 beyond that.. + if (height > 800) { + return 2; + } else { + return 4; + } +#endif +} + +void RendererGL::UpdateMSAAEnabled() { +#if BA_RIFT_BUILD + if (g_msaa_max_samples_rgb8 > 0) { + enable_msaa_ = true; + } else { + enable_msaa_ = false; + } +#else + + // lets allow full 1080p msaa with newer stuff.. + int max_msaa_res = is_tegra_k1_ ? 1200 : 800; + + // to start, see if it looks like we support msaa on paper.. + enable_msaa_ = + ((screen_render_target()->physical_height() + <= static_cast(max_msaa_res)) + && (g_msaa_max_samples_rgb8 > 0) && (g_msaa_max_samples_rgb565 > 0)); + + // ok, lets be careful here.. msaa blitting/etc seems to be particular in + // terms of supported formats/etc so let's only enable it on explicitly-tested + // hardware. + if (!is_tegra_4_ && !is_tegra_k1_ && !is_recent_adreno_) { + enable_msaa_ = false; + } + +#endif // BA_RIFT_BUILD +} + +auto RendererGL::IsMSAAEnabled() const -> bool { return enable_msaa_; } + +static auto GetGLTextureFormat(TextureFormat f) -> GLenum { + switch (f) { + case TextureFormat::kDXT1: + return GL_COMPRESSED_RGBA_S3TC_DXT1_EXT; + break; + case TextureFormat::kDXT5: + return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; + break; + case TextureFormat::kPVR2: + return GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG; + break; + case TextureFormat::kPVR4: + return GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG; + break; + case TextureFormat::kETC1: + return GL_ETC1_RGB8_OES; + break; + case TextureFormat::kETC2_RGB: + return GL_COMPRESSED_RGB8_ETC2; + break; + case TextureFormat::kETC2_RGBA: + return GL_COMPRESSED_RGBA8_ETC2_EAC; + break; + default: + throw Exception("Invalid TextureFormat: " + + std::to_string(static_cast(f))); + } +} + +// a stand-in for vertex-array-objects for use on systems that don't support +// them directly +class RendererGL::FakeVertexArrayObject { + public: + struct AttrState { + bool enable; + GLuint buffer; + int elem_count; + GLenum elem_type; + bool normalized; + int stride; + size_t offset; + }; + + explicit FakeVertexArrayObject(RendererGL* renderer) + : renderer_(renderer), elem_buffer_(0) { + for (auto& attr : attrs_) { + attr.enable = false; + } + } + + void Bind() { + DEBUG_CHECK_GL_ERROR; + + // First bind our element buffer. + assert(elem_buffer_ != 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elem_buffer_); + + // Now bind/enable the buffers we use and disable the ones we don't. + for (GLuint i = 0; i < kVertexAttrCount; i++) { + if (attrs_[i].enable) { + renderer_->BindArrayBuffer(attrs_[i].buffer); + glVertexAttribPointer(i, attrs_[i].elem_count, attrs_[i].elem_type, + static_cast(attrs_[i].normalized), + attrs_[i].stride, + reinterpret_cast(attrs_[i].offset)); + } + renderer_->SetVertexAttribArrayEnabled(i, attrs_[i].enable); + } + DEBUG_CHECK_GL_ERROR; + } + void SetElementBuffer(GLuint vbo) { elem_buffer_ = vbo; } + void SetAttribBuffer(GLuint buffer, VertexAttr attr, int elem_count, + GLenum elem_type, bool normalized, int stride, + size_t offset) { + assert(attr < RendererGL::kVertexAttrCount); + assert(!attrs_[attr].enable); + attrs_[attr].enable = true; + attrs_[attr].buffer = buffer; + attrs_[attr].elem_count = elem_count; + attrs_[attr].elem_type = elem_type; + attrs_[attr].normalized = normalized; + attrs_[attr].stride = stride; + attrs_[attr].offset = offset; + } + + AttrState attrs_[RendererGL::kVertexAttrCount]{}; + RendererGL* renderer_{}; + GLuint elem_buffer_{}; +}; + +class RendererGL::FramebufferObjectGL : public Framebuffer { + public: + FramebufferObjectGL(RendererGL* renderer_in, int width_in, int height_in, + bool linear_interp_in, bool depth_in, bool is_texture_in, + bool depth_is_texture_in, bool high_quality_in, + bool msaa_in, bool alpha_in) + : width_(width_in), + height_(height_in), + linear_interp_(linear_interp_in), + depth_(depth_in), + is_texture_(is_texture_in), + depth_is_texture_(depth_is_texture_in), + renderer_(renderer_in), + high_quality_(high_quality_in), + msaa_(msaa_in), + alpha_(alpha_in) { + // Desktop stuff is always high-quality +#if BA_OSTYPE_MACOS || BA_OSTYPE_LINUX || BA_OSTYPE_WINDOWS + high_quality_ = true; +#endif + + // Things are finally getting to the point where we can default to + // desktop quality on some mobile stuff. +#if BA_OSTYPE_ANDROID + if (renderer_->is_tegra_k1_) { + high_quality_ = true; + } +#endif + + Load(); + } + + ~FramebufferObjectGL() override { Unload(); } + + void Load(bool force_low_quality = false) { + if (loaded_) return; + assert(InGraphicsThread()); + DEBUG_CHECK_GL_ERROR; + GLenum status; + DEBUG_CHECK_GL_ERROR; + glGenFramebuffers(1, &framebuffer_); + renderer_->BindFramebuffer(framebuffer_); + DEBUG_CHECK_GL_ERROR; + bool do_high_quality = high_quality_; + if (force_low_quality) do_high_quality = false; + int samples = 0; + if (msaa_) { + // Can't multisample with texture buffers currently. + assert(!is_texture_ && !depth_is_texture_); + + int target_samples = + renderer_->GetMSAASamplesForFramebuffer(width_, height_); + + if (do_high_quality) { + samples = std::min(target_samples, g_msaa_max_samples_rgb8); + } else { + samples = std::min(target_samples, g_msaa_max_samples_rgb565); + } + } + if (is_texture_) { + // attach a texture for the color target + glGenTextures(1, &texture_); + renderer_->BindTexture(GL_TEXTURE_2D, texture_); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, + linear_interp_ ? GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, + linear_interp_ ? GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + // On android/ios lets go with 16 bit unless they explicitly request high + // quality. +#if BA_OSTYPE_ANDROID || BA_OSTYPE_IOS_TVOS + GLenum format; + if (alpha_) { + format = do_high_quality ? GL_UNSIGNED_BYTE : GL_UNSIGNED_SHORT_4_4_4_4; + } else { + format = do_high_quality ? GL_UNSIGNED_BYTE : GL_UNSIGNED_SHORT_5_6_5; + } +#else + GLenum format = GL_UNSIGNED_BYTE; +#endif + // if (srgbTest) { + // Log("YOOOOOOO"); + // glTexImage2D(GL_TEXTURE_2D, 0, alpha_?GL_SRGB8_ALPHA8:GL_SRGB8, + // _width, _height, 0, alpha_?GL_RGBA:GL_RGB, format, nullptr); + // } else { + glTexImage2D(GL_TEXTURE_2D, 0, alpha_ ? GL_RGBA : GL_RGB, width_, height_, + 0, alpha_ ? GL_RGBA : GL_RGB, format, nullptr); + // } + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, texture_, 0); + } else { + // Regular renderbuffer. + assert(!alpha_); // fixme +#if BA_OSTYPE_IOS_TVOS + GLenum format = + GL_RGB565; // FIXME; need to pull ES3 headers in for GL_RGB8 +#elif BA_OSTYPE_ANDROID + GLenum format = do_high_quality ? GL_RGB8 : GL_RGB565; +#else + GLenum format = GL_RGB8; +#endif + glGenRenderbuffers(1, &render_buffer_); + DEBUG_CHECK_GL_ERROR; + glBindRenderbuffer(GL_RENDERBUFFER, render_buffer_); + DEBUG_CHECK_GL_ERROR; + if (samples > 0) { +#if BA_OSTYPE_IOS_TVOS + throw Exception(); +#else + glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, format, + width_, height_); +#endif + } else { + glRenderbufferStorage(GL_RENDERBUFFER, format, width_, height_); + } + DEBUG_CHECK_GL_ERROR; + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_RENDERBUFFER, render_buffer_); + DEBUG_CHECK_GL_ERROR; + } + DEBUG_CHECK_GL_ERROR; + if (depth_) { + if (depth_is_texture_) { + glGenTextures(1, &depth_texture_); + DEBUG_CHECK_GL_ERROR; + renderer_->BindTexture(GL_TEXTURE_2D, depth_texture_); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + DEBUG_CHECK_GL_ERROR; + // fixme - need to pull in ES3 stuff for iOS to get GL_DEPTH_COMPONENT24 +#if BA_OSTYPE_IOS_TVOS + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, width_, height_, 0, + GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, nullptr); +#else + if (do_high_quality) { +#if BA_OSTYPE_ANDROID + assert(g_running_es3); +#endif // BA_OSTYPE_ANDROID + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, width_, height_, + 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, nullptr); + } else { + glTexImage2D( + GL_TEXTURE_2D, 0, + g_running_es3 ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, width_, + height_, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, nullptr); + } +#endif // BA_OSTYPE_IOS_TVOS + + DEBUG_CHECK_GL_ERROR; + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, + GL_TEXTURE_2D, depth_texture_, 0); + + DEBUG_CHECK_GL_ERROR; + } else { + // Just use a plain old renderbuffer if we don't need it as a texture + // (this is more widely supported). + glGenRenderbuffers(1, &depth_render_buffer_); + DEBUG_CHECK_GL_ERROR; + glBindRenderbuffer(GL_RENDERBUFFER, depth_render_buffer_); + DEBUG_CHECK_GL_ERROR; + + if (samples > 0) { +#if BA_OSTYPE_IOS_TVOS + throw Exception(); +#else + // (GL_DEPTH_COMPONENT24 not available in ES2 it looks like) + bool do24; +#if BA_OSTYPE_ANDROID + do24 = (do_high_quality && g_running_es3); +#else + do24 = do_high_quality; +#endif + + glRenderbufferStorageMultisample( + GL_RENDERBUFFER, samples, + do24 ? GL_DEPTH_COMPONENT24 : GL_DEPTH_COMPONENT16, width_, + height_); + // (do_high_quality && + // g_running_es3)?GL_DEPTH_COMPONENT24:GL_DEPTH_COMPONENT16, _width, + // _height); +#endif + } else { + // FIXME - need to pull in es3 headers to get GL_DEPTH_COMPONENT24 on + // iOS +#if BA_OSTYPE_IOS_TVOS + GLenum format = GL_DEPTH_COMPONENT16; +#else + // (GL_DEPTH_COMPONENT24 not available in ES2 it looks like) + GLenum format = (do_high_quality && g_running_es3) + ? GL_DEPTH_COMPONENT24 + : GL_DEPTH_COMPONENT16; +#endif + + glRenderbufferStorage(GL_RENDERBUFFER, format, width_, height_); + } + + DEBUG_CHECK_GL_ERROR; + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, + GL_RENDERBUFFER, depth_render_buffer_); + DEBUG_CHECK_GL_ERROR; + } + } + + status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + + if (status != GL_FRAMEBUFFER_COMPLETE) { + const char* version = (const char*)glGetString(GL_VERSION); + const char* vendor = (const char*)glGetString(GL_VENDOR); + const char* renderer = (const char*)glGetString(GL_RENDERER); + throw Exception( + "Framebuffer setup failed for " + std::to_string(width_) + " by " + + std::to_string(height_) + " fb with depth " + std::to_string(depth_) + + " asTex " + std::to_string(depth_is_texture_) + " gl-version " + + version + " vendor " + vendor + " renderer " + renderer); + } + // GLint enc; + // glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, + // GL_COLOR_ATTACHMENT0, GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING, &enc); if + // (enc == GL_SRGB) { + // Log("GOT SRGB!!!!!!!!!!!"); + // } else if (enc == GL_LINEAR) { + // Log("GOT LINEAR..."); + // } else { + // Log("GOT OTHER.."); + // } + loaded_ = true; + } + + void Unload() { + assert(InGraphicsThread()); + if (!loaded_) return; + + // If our textures are currently bound as anything, clear that out. + // (otherwise a new texture with that same ID won't be bindable) + for (int& i : renderer_->bound_textures_2d_) { + if (i == texture_) { // NOLINT(bugprone-branch-clone) + i = -1; + } else if (depth_ && (i == depth_texture_)) { + i = -1; + } + } + + if (!g_graphics_server->renderer_context_lost()) { + // Tear down the FBO and texture attachment + if (is_texture_) { + glDeleteTextures(1, &texture_); + } else { + glDeleteRenderbuffers(1, &render_buffer_); + } + if (depth_) { + if (depth_is_texture_) { + glDeleteTextures(1, &depth_texture_); + } else { + glDeleteRenderbuffers(1, &depth_render_buffer_); + } + DEBUG_CHECK_GL_ERROR; + } + + // If this one is current, make sure we re-bind next time. + // (otherwise we might prevent a new framebuffer with a recycled id from + // binding) + if (renderer_->active_framebuffer_ == framebuffer_) { + renderer_->active_framebuffer_ = -1; + } + glDeleteFramebuffers(1, &framebuffer_); + DEBUG_CHECK_GL_ERROR; + } + loaded_ = false; + } + + void Bind() { + assert(InGraphicsThread()); + renderer_->BindFramebuffer(framebuffer_); + // if (time(nullptr)%2 == 0) { + // glDisable(GL_FRAMEBUFFER_SRGB); + // } + } + + auto texture() const -> GLuint { + assert(is_texture_); + return texture_; + } + + auto depth_texture() const -> GLuint { + assert(depth_ && depth_is_texture_); + return depth_texture_; + } + + auto width() const -> int { return width_; } + auto height() const -> int { return height_; } + auto id() const -> GLuint { return framebuffer_; } + + private: + RendererGL* renderer_{}; + bool depth_{}; + bool is_texture_{}; + bool depth_is_texture_{}; + bool high_quality_{}; + bool msaa_{}; + bool alpha_{}; + bool linear_interp_{}; + bool loaded_{}; + int width_{}, height_{}; + GLuint framebuffer_{}, texture_{}, depth_texture_{}, render_buffer_{}, + depth_render_buffer_{}; +}; // FramebufferObject + +// Base class for fragment/vertex shaders. +class RendererGL::ShaderGL : public Object { + public: + auto GetDefaultOwnerThread() const -> ThreadIdentifier override { + return ThreadIdentifier::kMain; + } + + ShaderGL(GLenum type_in, const std::string& src_in) : type_(type_in) { + assert(InGraphicsThread()); + DEBUG_CHECK_GL_ERROR; + assert(type_ == GL_FRAGMENT_SHADER || type_ == GL_VERTEX_SHADER); + shader_ = glCreateShader(type_); + DEBUG_CHECK_GL_ERROR; + BA_PRECONDITION(shader_); + const char* s = src_in.c_str(); + glShaderSource(shader_, 1, &s, nullptr); + glCompileShader(shader_); + GLint compile_status; + glGetShaderiv(shader_, GL_COMPILE_STATUS, &compile_status); + if (compile_status == GL_FALSE) { + const char* version = (const char*)glGetString(GL_VERSION); + const char* vendor = (const char*)glGetString(GL_VENDOR); + const char* renderer = (const char*)glGetString(GL_RENDERER); + // Let's not crash here. We have a better chance of calling home this way + // and theres a chance the game will still be playable. + Log(std::string("Compile failed for ") + GetTypeName() + + " shader:\n------------SOURCE BEGIN-------------\n" + src_in + + "\n-----------SOURCE END-------------\n" + GetInfo() + + "\nrenderer: " + renderer + "\nvendor: " + vendor + + "\nversion:" + version); + } else { + assert(compile_status == GL_TRUE); + std::string info = GetInfo(); + if (!info.empty() + && (strstr(info.c_str(), "error:") || strstr(info.c_str(), "warning:") + || strstr(info.c_str(), "Error:") + || strstr(info.c_str(), "Warning:"))) { + const char* version = (const char*)glGetString(GL_VERSION); + const char* vendor = (const char*)glGetString(GL_VENDOR); + const char* renderer = (const char*)glGetString(GL_RENDERER); + Log(std::string("WARNING: info returned for ") + GetTypeName() + + " shader:\n------------SOURCE BEGIN-------------\n" + src_in + + "\n-----------SOURCE END-------------\n" + info + "\nrenderer: " + + renderer + "\nvendor: " + vendor + "\nversion:" + version); + } + } + DEBUG_CHECK_GL_ERROR; + } + ~ShaderGL() override { + assert(InGraphicsThread()); + if (!g_graphics_server->renderer_context_lost()) { + glDeleteShader(shader_); + DEBUG_CHECK_GL_ERROR; + } + } + auto shader() const -> GLuint { return shader_; } + + private: + auto GetTypeName() const -> const char* { + if (type_ == GL_VERTEX_SHADER) { + return "vertex"; + } else { + return "fragment"; + } + } + auto GetInfo() -> std::string { + static char log[1024]; + GLsizei log_size; + glGetShaderInfoLog(shader_, sizeof(log), &log_size, log); + return log; + } + std::string name_; + GLuint shader_{}; + GLenum type_{}; + BA_DISALLOW_CLASS_COPIES(ShaderGL); +}; // ShaderGL + +//----------------------------------------------------------------- + +class RendererGL::FragmentShaderGL : public RendererGL::ShaderGL { + public: + explicit FragmentShaderGL(const std::string& src_in) + : ShaderGL(GL_FRAGMENT_SHADER, src_in) {} +}; + +//------------------------------------------------------------------- + +class RendererGL::VertexShaderGL : public RendererGL::ShaderGL { + public: + explicit VertexShaderGL(const std::string& src_in) + : ShaderGL(GL_VERTEX_SHADER, src_in) {} +}; + +//------------------------------------------------------------------- + +class RendererGL::ProgramGL { + public: + ProgramGL(RendererGL* renderer, + const Object::Ref& vertex_shader_in, + const Object::Ref& fragment_shader_in, + std::string name, int pflags) + : fragment_shader_(fragment_shader_in), + vertex_shader_(vertex_shader_in), + renderer_(renderer), + pflags_(pflags), + name_(std::move(name)) { + assert(InGraphicsThread()); + DEBUG_CHECK_GL_ERROR; + program_ = glCreateProgram(); + BA_PRECONDITION(program_); + glAttachShader(program_, fragment_shader_->shader()); + glAttachShader(program_, vertex_shader_->shader()); + assert(pflags_ & PFLAG_USES_POSITION_ATTR); + if (pflags_ & PFLAG_USES_POSITION_ATTR) + glBindAttribLocation(program_, kVertexAttrPosition, "position"); + if (pflags_ & PFLAG_USES_UV_ATTR) + glBindAttribLocation(program_, kVertexAttrUV, "uv"); + if (pflags_ & PFLAG_USES_NORMAL_ATTR) + glBindAttribLocation(program_, kVertexAttrNormal, "normal"); + if (pflags_ & PFLAG_USES_ERODE_ATTR) + glBindAttribLocation(program_, kVertexAttrErode, "erode"); + if (pflags_ & PFLAG_USES_COLOR_ATTR) + glBindAttribLocation(program_, kVertexAttrColor, "color"); + if (pflags_ & PFLAG_USES_SIZE_ATTR) + glBindAttribLocation(program_, kVertexAttrSize, "size"); + if (pflags_ & PFLAG_USES_DIFFUSE_ATTR) + glBindAttribLocation(program_, kVertexAttrDiffuse, "diffuse"); + if (pflags_ & PFLAG_USES_UV2_ATTR) + glBindAttribLocation(program_, kVertexAttrUV2, "uv2"); + glLinkProgram(program_); + GLint linkStatus; + glGetProgramiv(program_, GL_LINK_STATUS, &linkStatus); + if (linkStatus == GL_FALSE) { + Log("Link failed for program '" + name_ + "':\n" + GetInfo()); + } else { + assert(linkStatus == GL_TRUE); + + std::string info = GetInfo(); + if (!info.empty() + && (strstr(info.c_str(), "error:") || strstr(info.c_str(), "warning:") + || strstr(info.c_str(), "Error:") + || strstr(info.c_str(), "Warning:"))) { + Log("WARNING: program using frag shader '" + name_ + + "' returned info:\n" + info); + } + } + + // go ahead and bind ourself so child classes can config uniforms and + // whatnot + Bind(); + mvp_uniform_ = glGetUniformLocation(program_, "modelViewProjectionMatrix"); + assert(mvp_uniform_ != -1); + if (pflags_ & PFLAG_USES_MODEL_WORLD_MATRIX) { + model_world_matrix_uniform_ = + glGetUniformLocation(program_, "modelWorldMatrix"); + assert(model_world_matrix_uniform_ != -1); + } + if (pflags_ & PFLAG_USES_MODEL_VIEW_MATRIX) { + model_view_matrix_uniform_ = + glGetUniformLocation(program_, "modelViewMatrix"); + assert(model_view_matrix_uniform_ != -1); + } + if (pflags_ & PFLAG_USES_CAM_POS) { + cam_pos_uniform_ = glGetUniformLocation(program_, "camPos"); + assert(cam_pos_uniform_ != -1); + } + if (pflags_ & PFLAG_USES_CAM_ORIENT_MATRIX) { + cam_orient_matrix_uniform_ = + glGetUniformLocation(program_, "camOrientMatrix"); + assert(cam_orient_matrix_uniform_ != -1); + } + if (pflags_ & PFLAG_USES_SHADOW_PROJECTION_MATRIX) { + light_shadow_projection_matrix_uniform_ = + glGetUniformLocation(program_, "lightShadowProjectionMatrix"); + assert(light_shadow_projection_matrix_uniform_ != -1); + } + } + + virtual ~ProgramGL() { + assert(InGraphicsThread()); + if (!g_graphics_server->renderer_context_lost()) { + glDetachShader(program_, fragment_shader_->shader()); + glDetachShader(program_, vertex_shader_->shader()); + glDeleteProgram(program_); + DEBUG_CHECK_GL_ERROR; + } + } + auto IsBound() const -> bool { + return (renderer()->GetActiveProgram() == this); + } + + auto program() const -> GLuint { return program_; } + + void Bind() { renderer_->UseProgram(this); } + + auto name() const -> const std::string& { return name_; } + + // should grab matrices from the renderer + // or whatever else it needs in prep for drawing + void PrepareToDraw() { + DEBUG_CHECK_GL_ERROR; + + assert(IsBound()); + + // update matrices as necessary... + + uint32_t mvpState = g_graphics_server->GetModelViewProjectionMatrixState(); + if (mvpState != mvp_state_) { + mvp_state_ = mvpState; + glUniformMatrix4fv(mvp_uniform_, 1, 0, + g_graphics_server->GetModelViewProjectionMatrix().m); + } + DEBUG_CHECK_GL_ERROR; + + if (pflags_ & PFLAG_USES_MODEL_WORLD_MATRIX) { + assert(!(pflags_ + & PFLAG_WORLD_SPACE_PTS)); // with world space points this would + // be identity; don't waste time. + uint32_t state = g_graphics_server->GetModelWorldMatrixState(); + if (state != model_world_matrix_state_) { + model_world_matrix_state_ = state; + glUniformMatrix4fv(model_world_matrix_uniform_, 1, 0, + g_graphics_server->GetModelWorldMatrix().m); + } + } + DEBUG_CHECK_GL_ERROR; + + if (pflags_ & PFLAG_USES_MODEL_VIEW_MATRIX) { + assert(!(pflags_ + & PFLAG_WORLD_SPACE_PTS)); // with world space points this would + // be identity; don't waste time. + // there's no state for just modelview but this works + uint32_t state = g_graphics_server->GetModelViewProjectionMatrixState(); + if (state != model_view_matrix_state_) { + model_view_matrix_state_ = state; + glUniformMatrix4fv(model_view_matrix_uniform_, 1, 0, + g_graphics_server->model_view_matrix().m); + } + } + DEBUG_CHECK_GL_ERROR; + + if (pflags_ & PFLAG_USES_CAM_POS) { + uint32_t state = g_graphics_server->cam_pos_state(); + if (state != cam_pos_state_) { + cam_pos_state_ = state; + const Vector3f& p(g_graphics_server->cam_pos()); + glUniform4f(cam_pos_uniform_, p.x, p.y, p.z, 1.0f); + } + } + DEBUG_CHECK_GL_ERROR; + + if (pflags_ & PFLAG_USES_CAM_ORIENT_MATRIX) { + uint32_t state = g_graphics_server->GetCamOrientMatrixState(); + if (state != cam_orient_matrix_state_) { + cam_orient_matrix_state_ = state; + glUniformMatrix4fv(cam_orient_matrix_uniform_, 1, 0, + g_graphics_server->GetCamOrientMatrix().m); + } + } + DEBUG_CHECK_GL_ERROR; + + if (pflags_ & PFLAG_USES_SHADOW_PROJECTION_MATRIX) { + uint32_t state = + g_graphics_server->light_shadow_projection_matrix_state(); + if (state != light_shadow_projection_matrix_state_) { + light_shadow_projection_matrix_state_ = state; + glUniformMatrix4fv( + light_shadow_projection_matrix_uniform_, 1, 0, + g_graphics_server->light_shadow_projection_matrix().m); + } + } + DEBUG_CHECK_GL_ERROR; + } + + protected: + void SetTextureUnit(const char* tex_name, int unit) { + assert(IsBound()); + int c = glGetUniformLocation(program_, tex_name); + if (c == -1) { +#if !MSAA_ERROR_TEST + Log("Error: ShaderGL: " + name_ + ": Can't set texture unit for texture '" + + tex_name + "'"); + DEBUG_CHECK_GL_ERROR; +#endif + } else { + glUniform1i(c, unit); + } + } + + auto GetInfo() -> std::string { + static char log[1024]; + GLsizei log_size; + glGetProgramInfoLog(program_, sizeof(log), &log_size, log); + return log; + } + + auto renderer() const -> RendererGL* { return renderer_; } + + private: + RendererGL* renderer_{}; + Object::Ref fragment_shader_; + 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_{}; + BA_DISALLOW_CLASS_COPIES(ProgramGL); +}; // ProgramGL + +class RendererGL::SimpleProgramGL : public RendererGL::ProgramGL { + public: + enum TextureUnit { + kColorTexUnit, + kColorizeTexUnit, + kMaskTexUnit, + kMaskUV2TexUnit, + kBlurTexUnit + }; + + SimpleProgramGL(RendererGL* renderer, int flags) + : RendererGL::ProgramGL( + renderer, Object::New(GetVertexCode(flags)), + Object::New(GetFragmentCode(flags)), GetName(flags), + GetPFlags(flags)), + flags_(flags) { + if (flags & SHD_TEXTURE) { + SetTextureUnit("colorTex", kColorTexUnit); + } + if (flags & SHD_COLORIZE) { + SetTextureUnit("colorizeTex", kColorizeTexUnit); + colorize_color_location_ = + glGetUniformLocation(program(), "colorizeColor"); + assert(colorize_color_location_ != -1); + } + if (flags & SHD_COLORIZE2) { + colorize2_color_location_ = + glGetUniformLocation(program(), "colorize2Color"); + assert(colorize2_color_location_ != -1); + } + if ((!(flags & SHD_TEXTURE)) || (flags & SHD_MODULATE)) { + color_location_ = glGetUniformLocation(program(), "color"); + assert(color_location_ != -1); + } + if (flags & SHD_SHADOW) { + shadow_params_location_ = glGetUniformLocation(program(), "shadowParams"); + assert(shadow_params_location_ != -1); + } + if (flags & SHD_GLOW) { + glow_params_location_ = glGetUniformLocation(program(), "glowParams"); + assert(glow_params_location_ != -1); + } + if (flags & SHD_FLATNESS) { + flatness_location = glGetUniformLocation(program(), "flatness"); + assert(flatness_location != -1); + } + if (flags & SHD_MASKED) { + SetTextureUnit("maskTex", kMaskTexUnit); + } + if (flags & SHD_MASK_UV2) { + SetTextureUnit("maskUV2Tex", kMaskUV2TexUnit); + } + } + void SetColorTexture(const TextureData* t) { + assert(flags_ & SHD_TEXTURE); + assert(IsBound()); + renderer()->BindTexture(GL_TEXTURE_2D, t, kColorTexUnit); + } + void SetColorTexture(GLuint t) { + renderer()->BindTexture(GL_TEXTURE_2D, t, kColorTexUnit); + } + void SetColor(float r, float g, float b, float a = 1.0f) { + assert((flags_ & SHD_MODULATE) || !(flags_ & SHD_TEXTURE)); + assert(IsBound()); + if (r != r_ || g != g_ || b != b_ || a != a_) { + r_ = r; + g_ = g; + b_ = b; + a_ = a; + glUniform4f(color_location_, r_, g_, b_, a_); + } + } + void SetColorizeColor(float r, float g, float b, float a = 1.0f) { + assert(flags_ & SHD_COLORIZE); + assert(IsBound()); + if (r != colorize_r_ || g != colorize_g_ || b != colorize_b_ + || a != colorize_a_) { + colorize_r_ = r; + colorize_g_ = g; + colorize_b_ = b; + colorize_a_ = a; + glUniform4f(colorize_color_location_, colorize_r_, colorize_g_, + colorize_b_, colorize_a_); + } + } + void SetShadow(float shadow_offset_x, float shadow_offset_y, + float shadow_blur, float shadow_density) { + assert(flags_ & SHD_SHADOW); + assert(IsBound()); + if (shadow_offset_x != shadow_offset_x_ + || shadow_offset_y != shadow_offset_y_ || shadow_blur != shadow_blur_ + || shadow_density != shadow_density_) { + shadow_offset_x_ = shadow_offset_x; + shadow_offset_y_ = shadow_offset_y; + shadow_blur_ = shadow_blur; + shadow_density_ = shadow_density; + glUniform4f(shadow_params_location_, shadow_offset_x_, shadow_offset_y_, + shadow_blur_, shadow_density_ * 0.4f); + } + } + void setGlow(float glow_amount, float glow_blur) { + assert(flags_ & SHD_GLOW); + assert(IsBound()); + if (glow_amount != glow_amount_ || glow_blur != glow_blur_) { + glow_amount_ = glow_amount; + glow_blur_ = glow_blur; + glUniform2f(glow_params_location_, glow_amount_, glow_blur_); + } + } + void SetFlatness(float flatness) { + assert(flags_ & SHD_FLATNESS); + assert(IsBound()); + if (flatness != flatness_) { + flatness_ = flatness; + glUniform1f(flatness_location, flatness_); + } + } + void SetColorize2Color(float r, float g, float b, float a = 1.0f) { + assert(flags_ & SHD_COLORIZE2); + assert(IsBound()); + if (r != colorize2_r_ || g != colorize2_g_ || b != colorize2_b_ + || a != colorize2_a_) { + colorize2_r_ = r; + colorize2_g_ = g; + colorize2_b_ = b; + colorize2_a_ = a; + glUniform4f(colorize2_color_location_, colorize2_r_, colorize2_g_, + colorize2_b_, colorize2_a_); + } + } + void SetColorizeTexture(const TextureData* t) { + assert(flags_ & SHD_COLORIZE); + renderer()->BindTexture(GL_TEXTURE_2D, t, kColorizeTexUnit); + } + void SetMaskTexture(const TextureData* t) { + assert(flags_ & SHD_MASKED); + renderer()->BindTexture(GL_TEXTURE_2D, t, kMaskTexUnit); + } + void SetMaskUV2Texture(const TextureData* t) { + assert(flags_ & SHD_MASK_UV2); + renderer()->BindTexture(GL_TEXTURE_2D, t, kMaskUV2TexUnit); + } + + private: + auto GetName(int flags) -> std::string { + return "SimpleProgramGL texture:" + + std::to_string((flags & SHD_TEXTURE) != 0) + + " modulate:" + std::to_string((flags & SHD_MODULATE) != 0) + + " colorize:" + std::to_string((flags & SHD_COLORIZE) != 0) + + " colorize2:" + std::to_string((flags & SHD_COLORIZE2) != 0) + + " premultiply:" + std::to_string((flags & SHD_PREMULTIPLY) != 0) + + " shadow:" + std::to_string((flags & SHD_SHADOW) != 0) + + " glow:" + std::to_string((flags & SHD_GLOW) != 0) + " masked:" + + std::to_string((flags & SHD_MASKED) != 0) + " maskedUV2:" + + std::to_string((flags & SHD_MASK_UV2) != 0) + " depthBugTest:" + + std::to_string((flags & SHD_DEPTH_BUG_TEST) != 0) + + " flatness:" + std::to_string((flags & SHD_MASK_UV2) != 0); + } + auto GetPFlags(int flags) -> int { + int pflags = PFLAG_USES_POSITION_ATTR; + if (flags & SHD_TEXTURE) pflags |= PFLAG_USES_UV_ATTR; + if (flags & SHD_MASK_UV2) pflags |= PFLAG_USES_UV2_ATTR; + return pflags; + } + auto GetVertexCode(int flags) -> std::string { + std::string s; + s = "uniform mat4 modelViewProjectionMatrix;\n" + "attribute vec4 position;\n"; + if ((flags & SHD_TEXTURE) || (flags & SHD_COLORIZE) + || (flags & SHD_COLORIZE2)) + s += "attribute vec2 uv;\n" + "varying vec2 vUV;\n"; + if (flags & SHD_MASK_UV2) + s += "attribute vec2 uv2;\n" + "varying vec2 vUV2;\n"; + if (flags & SHD_SHADOW) + s += "varying vec2 vUVShadow;\n" + "varying vec2 vUVShadow2;\n" + "varying vec2 vUVShadow3;\n" + "uniform " LOWP "vec4 shadowParams;\n"; + s += "void main() {\n"; + if (flags & SHD_TEXTURE) s += " vUV = uv;\n"; + if (flags & SHD_MASK_UV2) s += " vUV2 = uv2;\n"; + if (flags & SHD_SHADOW) + s += " vUVShadow = uv+0.4*vec2(shadowParams.x,shadowParams.y);\n"; + if (flags & SHD_SHADOW) + s += " vUVShadow2 = uv+0.8*vec2(shadowParams.x,shadowParams.y);\n"; + if (flags & SHD_SHADOW) + s += " vUVShadow3 = uv+1.3*vec2(shadowParams.x,shadowParams.y);\n"; + s += " gl_Position = modelViewProjectionMatrix*position;\n" + "}"; + + if (flags & SHD_DEBUG_PRINT) + Log("\nVertex code for shader '" + GetName(flags) + "':\n\n" + s); + return s; + } + auto GetFragmentCode(int flags) -> std::string { + std::string s; + if (flags & SHD_TEXTURE) s += "uniform " LOWP "sampler2D colorTex;\n"; + if ((flags & SHD_COLORIZE)) + s += "uniform " LOWP + "sampler2D colorizeTex;\n" + "uniform " LOWP "vec4 colorizeColor;\n"; + if ((flags & SHD_COLORIZE2)) s += "uniform " LOWP "vec4 colorize2Color;\n"; + if ((flags & SHD_TEXTURE) || (flags & SHD_COLORIZE) + || (flags & SHD_COLORIZE2)) + s += "varying " LOWP "vec2 vUV;\n"; + if (flags & SHD_MASK_UV2) s += "varying " LOWP "vec2 vUV2;\n"; + if (flags & SHD_FLATNESS) s += "uniform " LOWP "float flatness;\n"; + if (flags & SHD_SHADOW) { + s += "varying " LOWP + "vec2 vUVShadow;\n" + "varying " LOWP + "vec2 vUVShadow2;\n" + "varying " LOWP + "vec2 vUVShadow3;\n" + "uniform " LOWP "vec4 shadowParams;\n"; + } + if (flags & SHD_GLOW) { + s += "uniform " LOWP "vec2 glowParams;\n"; + } + if ((flags & SHD_MODULATE) || (!(flags & SHD_TEXTURE))) + s += "uniform " LOWP "vec4 color;\n"; + if (flags & SHD_MASKED) s += "uniform " LOWP "sampler2D maskTex;\n"; + if (flags & SHD_MASK_UV2) s += "uniform " LOWP "sampler2D maskUV2Tex;\n"; + s += "void main() {\n"; + if (!(flags & SHD_TEXTURE)) { + s += " gl_FragColor = color;\n"; + } else { + std::string blurArg; + if (flags & SHD_GLOW) { + s += " " LOWP + "vec4 cVal = texture2D(colorTex,vUV,glowParams.g);\n" + " gl_FragColor = vec4(color.rgb * cVal.rgb * cVal.a * " + "glowParams.r,0.0)"; // we premultiply this. + if (flags & SHD_MASK_UV2) s += " * vec4(texture2D(maskUV2Tex,vUV2).a)"; + s += ";\n"; + } else { + if ((flags & SHD_COLORIZE) || (flags & SHD_COLORIZE2)) + s += " " LOWP + "vec4 colorizeVal = texture2D(colorizeTex,vUV);\n"; // TEMP TEST + if (flags & SHD_COLORIZE) + s += " " LOWP "float colorizeA = colorizeVal.r;\n"; + if (flags & SHD_COLORIZE2) + s += " " LOWP "float colorizeB = colorizeVal.g;\n"; + if (flags & SHD_MASKED) + s += " " MEDIUMP "vec4 mask = texture2D(maskTex,vUV);"; + + if (flags & SHD_MODULATE) { + if (flags & SHD_FLATNESS) { + s += " " LOWP + "vec4 rawTexColor = texture2D(colorTex,vUV);\n" + " gl_FragColor = color * " + "vec4(mix(rawTexColor.rgb,vec3(1.0),flatness),rawTexColor.a)"; + } else { + s += " gl_FragColor = color * texture2D(colorTex,vUV)"; + } + } else { + s += " gl_FragColor = texture2D(colorTex,vUV)"; + } + + if (flags & SHD_COLORIZE) + s += " * (vec4(1.0-colorizeA)+colorizeColor*colorizeA)"; + if (flags & SHD_COLORIZE2) + s += " * (vec4(1.0-colorizeB)+colorize2Color*colorizeB)"; + if (flags & SHD_MASKED) + s += " * vec4(vec3(mask.r),mask.a) + " + "vec4(vec3(mask.g)*colorizeColor.rgb+vec3(mask.b),0.0)"; + s += ";\n"; + + if (flags & SHD_SHADOW) { + s += " " LOWP + "float shadowA = (texture2D(colorTex,vUVShadow).a + " + "texture2D(colorTex,vUVShadow2,1.0).a + " + "texture2D(colorTex,vUVShadow3,2.0).a) * shadowParams.a"; + + if (flags & SHD_MASK_UV2) s += " * texture2D(maskUV2Tex,vUV2).a"; + s += ";\n"; + s += " gl_FragColor = " + "vec4(gl_FragColor.rgb*gl_FragColor.a,gl_FragColor.a) + " + "(1.0-gl_FragColor.a) * vec4(0,0,0,shadowA);\n"; + s += " gl_FragColor = " + "vec4(gl_FragColor.rgb/" + "max(0.001,gl_FragColor.a),gl_FragColor.a);\n"; + } + } + if (flags & SHD_DEPTH_BUG_TEST) + s += " gl_FragColor = vec4(abs(gl_FragCoord.z-gl_FragColor.r));\n"; + if (flags & SHD_PREMULTIPLY) + s += " gl_FragColor = vec4(gl_FragColor.rgb * " + "gl_FragColor.a,gl_FragColor.a);"; + } + s += "}"; + + if (flags & SHD_DEBUG_PRINT) + Log("\nFragment code for shader '" + GetName(flags) + "':\n\n" + s); + return s; + } + float r_{}, g_{}, b_{}, a_{}; + float colorize_r_{}, colorize_g_{}, colorize_b_{}, colorize_a_{}; + float colorize2_r_{}, colorize2_g_{}, colorize2_b_{}, colorize2_a_{}; + float shadow_offset_x_{}, shadow_offset_y_{}, shadow_blur_{}, + shadow_density_{}; + float glow_amount_{}, glow_blur_{}; + float flatness_{}; + GLint color_location_{}; + GLint colorize_color_location_{}; + GLint colorize2_color_location_{}; + GLint shadow_params_location_{}; + GLint glow_params_location_{}; + GLint flatness_location{}; + int flags_{}; +}; // SimpleProgramGL + +class RendererGL::ObjectProgramGL : public RendererGL::ProgramGL { + public: + enum TextureUnit { + kColorTexUnit, + kReflectionTexUnit, + kVignetteTexUnit, + kLightShadowTexUnit, + kColorizeTexUnit + }; + + ObjectProgramGL(RendererGL* renderer, int flags) + : RendererGL::ProgramGL( + renderer, Object::New(GetVertexCode(flags)), + Object::New(GetFragmentCode(flags)), GetName(flags), + GetPFlags(flags)), + flags_(flags), + r_(0), + g_(0), + b_(0), + a_(0), + colorize_r_(0), + colorize_g_(0), + colorize_b_(0), + colorize_a_(0), + colorize2_r_(0), + colorize2_g_(0), + colorize2_b_(0), + colorize2_a_(0), + add_r_(0), + add_g_(0), + add_b_(0), + r_mult_r_(0), + r_mult_g_(0), + r_mult_b_(0), + r_mult_a_(0) { + SetTextureUnit("colorTex", kColorTexUnit); + SetTextureUnit("vignetteTex", kVignetteTexUnit); + color_location_ = glGetUniformLocation(program(), "color"); + assert(color_location_ != -1); + if (flags & SHD_REFLECTION) { + SetTextureUnit("reflectionTex", kReflectionTexUnit); + reflect_mult_location_ = glGetUniformLocation(program(), "reflectMult"); + assert(reflect_mult_location_ != -1); + } + if (flags & SHD_LIGHT_SHADOW) { + SetTextureUnit("lightShadowTex", kLightShadowTexUnit); + } + if (flags & SHD_ADD) { + color_add_location_ = glGetUniformLocation(program(), "colorAdd"); + assert(color_add_location_ != -1); + } + if (flags & SHD_COLORIZE) { + SetTextureUnit("colorizeTex", kColorizeTexUnit); + colorize_color_location_ = + glGetUniformLocation(program(), "colorizeColor"); + assert(colorize_color_location_ != -1); + } + if (flags & SHD_COLORIZE2) { + colorize2_color_location_ = + glGetUniformLocation(program(), "colorize2Color"); + assert(colorize2_color_location_ != -1); + } + } + void SetColorTexture(const TextureData* t) { + renderer()->BindTexture(GL_TEXTURE_2D, t, kColorTexUnit); + } + void SetReflectionTexture(const TextureData* t) { + assert(flags_ & SHD_REFLECTION); + renderer()->BindTexture(GL_TEXTURE_CUBE_MAP, t, kReflectionTexUnit); + } + void SetColor(float r, float g, float b, float a = 1.0f) { + assert(IsBound()); + // include tint.. + if (r * renderer()->tint().x != r_ || g * renderer()->tint().y != g_ + || b * renderer()->tint().z != b_ || a != a_) { + r_ = r * renderer()->tint().x; + g_ = g * renderer()->tint().y; + b_ = b * renderer()->tint().z; + a_ = a; + glUniform4f(color_location_, r_, g_, b_, a_); + } + } + void SetAddColor(float r, float g, float b) { + assert(IsBound()); + if (r != add_r_ || g != add_g_ || b != add_b_) { + add_r_ = r; + add_g_ = g; + add_b_ = b; + glUniform4f(color_add_location_, add_r_, add_g_, add_b_, 0.0f); + } + } + void SetReflectionMult(float r, float g, float b, float a = 0.0f) { + assert(IsBound()); + // include tint and ambient color... + auto renderer = this->renderer(); + float rFin = r * renderer->tint().x * renderer->ambient_color().x; + float gFin = g * renderer->tint().y * renderer->ambient_color().y; + float bFin = b * renderer->tint().z * renderer->ambient_color().z; + if (rFin != r_mult_r_ || gFin != r_mult_g_ || bFin != r_mult_b_ + || a != r_mult_a_) { + r_mult_r_ = rFin; + r_mult_g_ = gFin; + r_mult_b_ = bFin; + r_mult_a_ = a; + assert(flags_ & SHD_REFLECTION); + glUniform4f(reflect_mult_location_, r_mult_r_, r_mult_g_, r_mult_b_, + r_mult_a_); + } + } + void SetVignetteTexture(GLuint t) { + renderer()->BindTexture(GL_TEXTURE_2D, t, kVignetteTexUnit); + } + void SetLightShadowTexture(GLuint t) { + renderer()->BindTexture(GL_TEXTURE_2D, t, kLightShadowTexUnit); + } + + void SetColorizeColor(float r, float g, float b, float a = 1.0f) { + assert(flags_ & SHD_COLORIZE); + assert(IsBound()); + if (r != colorize_r_ || g != colorize_g_ || b != colorize_b_ + || a != colorize_a_) { + colorize_r_ = r; + colorize_g_ = g; + colorize_b_ = b; + colorize_a_ = a; + glUniform4f(colorize_color_location_, colorize_r_, colorize_g_, + colorize_b_, colorize_a_); + } + } + void SetColorize2Color(float r, float g, float b, float a = 1.0f) { + assert(flags_ & SHD_COLORIZE2); + assert(IsBound()); + if (r != colorize2_r_ || g != colorize2_g_ || b != colorize2_b_ + || a != colorize2_a_) { + colorize2_r_ = r; + colorize2_g_ = g; + colorize2_b_ = b; + colorize2_a_ = a; + glUniform4f(colorize2_color_location_, colorize2_r_, colorize2_g_, + colorize2_b_, colorize2_a_); + } + } + void SetColorizeTexture(const TextureData* t) { + assert(flags_ & SHD_COLORIZE); + renderer()->BindTexture(GL_TEXTURE_2D, t, kColorizeTexUnit); + } + + private: + auto GetName(int flags) -> std::string { + return std::string("ObjectProgramGL") + + " reflect:" + std::to_string((flags & SHD_REFLECTION) != 0) + + " lightShadow:" + std::to_string((flags & SHD_LIGHT_SHADOW) != 0) + + " add:" + std::to_string((flags & SHD_ADD) != 0) + " colorize:" + + std::to_string((flags & SHD_COLORIZE) != 0) + " colorize2:" + + std::to_string((flags & SHD_COLORIZE2) != 0) + " transparent:" + + std::to_string((flags & SHD_OBJ_TRANSPARENT) != 0) + " worldSpace:" + + std::to_string((flags & SHD_WORLD_SPACE_PTS) != 0); + } + auto GetPFlags(int flags) -> int { + int pflags = PFLAG_USES_POSITION_ATTR | PFLAG_USES_UV_ATTR; + if (flags & SHD_REFLECTION) + pflags |= (PFLAG_USES_NORMAL_ATTR | PFLAG_USES_CAM_POS); + if (((flags & SHD_REFLECTION) || (flags & SHD_LIGHT_SHADOW)) + && !(flags & SHD_WORLD_SPACE_PTS)) + pflags |= PFLAG_USES_MODEL_WORLD_MATRIX; + if (flags & SHD_LIGHT_SHADOW) pflags |= PFLAG_USES_SHADOW_PROJECTION_MATRIX; + if (flags & SHD_WORLD_SPACE_PTS) pflags |= PFLAG_WORLD_SPACE_PTS; + return pflags; + } + auto GetVertexCode(int flags) -> std::string { + std::string s; + s = "uniform mat4 modelViewProjectionMatrix;\n" + "uniform vec4 camPos;\n" + "attribute vec4 position;\n" + "attribute " LOWP + "vec2 uv;\n" + "varying " LOWP + "vec2 vUV;\n" + "varying " MEDIUMP "vec4 vScreenCoord;\n"; + if ((flags & SHD_REFLECTION) || (flags & SHD_LIGHT_SHADOW)) + s += "uniform mat4 modelWorldMatrix;\n"; + if (flags & SHD_REFLECTION) + s += "attribute " MEDIUMP + "vec3 normal;\n" + "varying " MEDIUMP "vec3 vReflect;\n"; + if (flags & SHD_LIGHT_SHADOW) + s += "uniform mat4 lightShadowProjectionMatrix;\n" + "varying " MEDIUMP "vec4 vLightShadowUV;\n"; + s += + "void main() {\n" + " vUV = uv;\n" + " gl_Position = modelViewProjectionMatrix*position;\n" + " vScreenCoord = vec4(gl_Position.xy/gl_Position.w,gl_Position.zw);\n" + " vScreenCoord.xy += vec2(1.0);\n" + " vScreenCoord.xy *= vec2(0.5*vScreenCoord.w);\n"; + if (((flags & SHD_LIGHT_SHADOW) || (flags & SHD_REFLECTION)) + && !(flags & SHD_WORLD_SPACE_PTS)) { + s += " vec4 worldPos = modelWorldMatrix*position;\n"; + } + if (flags & SHD_LIGHT_SHADOW) { + if (flags & SHD_WORLD_SPACE_PTS) + s += " vLightShadowUV = (lightShadowProjectionMatrix*position);\n"; + else + s += " vLightShadowUV = (lightShadowProjectionMatrix*worldPos);\n"; + } + if (flags & SHD_REFLECTION) { + if (flags & SHD_WORLD_SPACE_PTS) + s += " vReflect = reflect(vec3(position - camPos),normal);\n"; + else + s += " vReflect = reflect(vec3(worldPos - " + "camPos),normalize(vec3(modelWorldMatrix * vec4(normal,0.0))));\n"; + } + s += "}"; + if (flags & SHD_DEBUG_PRINT) + Log("\nVertex code for shader '" + GetName(flags) + "':\n\n" + s); + return s; + } + auto GetFragmentCode(int flags) -> std::string { + std::string s; + s = "uniform " LOWP + "sampler2D colorTex;\n" + "uniform " LOWP + "sampler2D vignetteTex;\n" + "uniform " LOWP + "vec4 color;\n" + "varying " LOWP + "vec2 vUV;\n" + "varying " MEDIUMP "vec4 vScreenCoord;\n"; + if (flags & SHD_ADD) s += "uniform " LOWP "vec4 colorAdd;\n"; + if (flags & SHD_REFLECTION) + s += "uniform " LOWP + "samplerCube reflectionTex;\n" + "varying " MEDIUMP + "vec3 vReflect;\n" + "uniform " LOWP "vec4 reflectMult;\n"; + if (flags & SHD_COLORIZE) + s += "uniform " LOWP + "sampler2D colorizeTex;\n" + "uniform " LOWP "vec4 colorizeColor;\n"; + if (flags & SHD_COLORIZE2) s += "uniform " LOWP "vec4 colorize2Color;\n"; + if (flags & SHD_LIGHT_SHADOW) + s += "uniform " LOWP + "sampler2D lightShadowTex;\n" + "varying " MEDIUMP "vec4 vLightShadowUV;\n"; + s += "void main() {\n"; + if (flags & SHD_LIGHT_SHADOW) + s += + " " LOWP + "vec4 lightShadVal = texture2DProj(lightShadowTex,vLightShadowUV);\n"; + if ((flags & SHD_COLORIZE) || (flags & SHD_COLORIZE2)) + s += " " LOWP "vec4 colorizeVal = texture2D(colorizeTex,vUV);\n"; + if (flags & SHD_COLORIZE) + s += " " LOWP "float colorizeA = colorizeVal.r;\n"; + if (flags & SHD_COLORIZE2) + s += " " LOWP "float colorizeB = colorizeVal.g;\n"; + s += " gl_FragColor = (color*texture2D(colorTex,vUV)"; + if (flags & SHD_COLORIZE) + s += " * (vec4(1.0-colorizeA)+colorizeColor*colorizeA)"; + if (flags & SHD_COLORIZE2) + s += " * (vec4(1.0-colorizeB)+colorize2Color*colorizeB)"; + s += ")"; + + // add in lights/shadows + if (flags & SHD_LIGHT_SHADOW) { + if (flags & SHD_OBJ_TRANSPARENT) + s += " * vec4((2.0*lightShadVal).rgb,1) + " + "vec4((lightShadVal-0.5).rgb,0)"; + else + s += " * (2.0*lightShadVal) + (lightShadVal-0.5)"; + } + + // add glow and reflection + if (flags & SHD_REFLECTION) + s += " + (reflectMult*textureCube(reflectionTex,vReflect))"; + if (flags & SHD_ADD) s += " + colorAdd"; + + // subtract vignette + s += " - vec4(texture2DProj(vignetteTex,vScreenCoord).rgb,0)"; + + s += ";\n"; + // s += "gl_FragColor = 0.999 * texture2DProj(vignetteTex,vScreenCoord) + + // 0.01 * gl_FragColor;"; + + s += "}"; + + if (flags & SHD_DEBUG_PRINT) + Log("\nFragment code for shader '" + GetName(flags) + "':\n\n" + s); + return s; + } + float r_, g_, b_, a_; + float colorize_r_, colorize_g_, colorize_b_, colorize_a_; + float colorize2_r_, colorize2_g_, colorize2_b_, colorize2_a_; + float add_r_, add_g_, add_b_; + float r_mult_r_, r_mult_g_, r_mult_b_, r_mult_a_; + GLint color_location_; + GLint colorize_color_location_; + GLint colorize2_color_location_; + GLint color_add_location_; + GLint reflect_mult_location_; + int flags_; +}; // ObjectProgramGL + +class RendererGL::SmokeProgramGL : public RendererGL::ProgramGL { + public: + enum TextureUnit { kColorTexUnit, kDepthTexUnit, kBlurTexUnit }; + + SmokeProgramGL(RendererGL* renderer, int flags) + : RendererGL::ProgramGL( + renderer, Object::New(GetVertexCode(flags)), + Object::New(GetFragmentCode(flags)), GetName(flags), + GetPFlags(flags)), + flags_(flags), + r_(0), + g_(0), + b_(0), + a_(0) { + SetTextureUnit("colorTex", kColorTexUnit); + if (flags & SHD_OVERLAY) { + SetTextureUnit("depthTex", kDepthTexUnit); + SetTextureUnit("blurTex", kBlurTexUnit); + } + color_location_ = glGetUniformLocation(program(), "colorMult"); + assert(color_location_ != -1); + } + void SetColorTexture(const TextureData* t) { + renderer()->BindTexture(GL_TEXTURE_2D, t, kColorTexUnit); + } + void SetDepthTexture(GLuint t) { + assert(flags_ & SHD_OVERLAY); + renderer()->BindTexture(GL_TEXTURE_2D, t, kDepthTexUnit); + } + void SetBlurTexture(GLuint t) { + assert(flags_ & SHD_OVERLAY); + renderer()->BindTexture(GL_TEXTURE_2D, t, kBlurTexUnit); + } + void SetColor(float r, float g, float b, float a = 1.0f) { + assert(IsBound()); + // include tint.. + if (r * renderer()->tint().x != r_ || g * renderer()->tint().y != g_ + || b * renderer()->tint().z != b_ || a != a_) { + r_ = r * renderer()->tint().x; + g_ = g * renderer()->tint().y; + b_ = b * renderer()->tint().z; + a_ = a; + glUniform4f(color_location_, r_, g_, b_, a_); + } + } + + private: + auto GetName(int flags) -> std::string { + return std::string("SmokeProgramGL"); + } + auto GetPFlags(int flags) -> int { + int pflags = PFLAG_USES_POSITION_ATTR | PFLAG_USES_DIFFUSE_ATTR + | PFLAG_USES_UV_ATTR | PFLAG_WORLD_SPACE_PTS + | PFLAG_USES_ERODE_ATTR | PFLAG_USES_COLOR_ATTR; + return pflags; + } + auto GetVertexCode(int flags) -> std::string { + std::string s; + s = "uniform mat4 modelViewProjectionMatrix;\n" + "attribute vec4 position;\n" + "attribute " MEDIUMP + "vec2 uv;\n" + "varying " MEDIUMP + "vec2 vUV;\n" + "attribute " LOWP + "float erode;\n" + "attribute " MEDIUMP + "float diffuse;\n" + "varying " LOWP + "float vErode;\n" + "attribute " MEDIUMP + "vec4 color;\n" + "varying " LOWP + "vec4 vColor;\n" + "uniform " MEDIUMP "vec4 colorMult;\n"; + if (flags & SHD_OVERLAY) + s += "varying " LOWP + "vec4 cDiffuse;\n" + "varying " MEDIUMP "vec4 vScreenCoord;\n"; + s += "void main() {\n" + " vUV = uv;\n" + " gl_Position = modelViewProjectionMatrix*position;\n" + " vErode = erode;\n"; + // in overlay mode we pass color/diffuse to the pixel-shader since we + // combine them there with a blurred bg image to get a soft look. In the + // simple version we just use a flat ambient color here. + if (flags & SHD_OVERLAY) + s += " vScreenCoord = " + "vec4(gl_Position.xy/gl_Position.w,gl_Position.zw);\n" + " vColor = vec4(vec3(7.0*diffuse),0.7) * color * colorMult;\n" + " cDiffuse = colorMult*(0.3+0.8*diffuse);\n" + " vScreenCoord.xy += vec2(1.0);\n" + " vScreenCoord.xy *= vec2(0.5*vScreenCoord.w);\n"; + else + s += " vColor = " + "(vec4(vec3(7.0),1.0)*color+vec4(vec3(0.4),0))*vec4(vec3(diffuse),0." + "4) * colorMult;\n"; + s += " vColor *= vec4(vec3(vColor.a),1.0);\n"; // premultiply + s += "}"; + + if (flags & SHD_DEBUG_PRINT) + Log("\nVertex code for shader '" + GetName(flags) + "':\n\n" + s); + return s; + } + auto GetFragmentCode(int flags) -> std::string { + std::string s; + s = "uniform " LOWP + "sampler2D colorTex;\n" + "varying " MEDIUMP + "vec2 vUV;\n" + "varying " LOWP + "float vErode;\n" + "varying " LOWP "vec4 vColor;\n"; + if (flags & SHD_OVERLAY) + s += "varying " MEDIUMP + "vec4 vScreenCoord;\n" + "uniform " LOWP + "sampler2D depthTex;\n" + "uniform " LOWP + "sampler2D blurTex;\n" + "varying " LOWP "vec4 cDiffuse;\n"; + s += "void main() {\n"; + s += " " LOWP + "float erodeMult = smoothstep(vErode,1.0,texture2D(colorTex,vUV).r);\n" + " gl_FragColor = (vColor*vec4(erodeMult));"; + if (flags & SHD_OVERLAY) { + s += " gl_FragColor += vec4(vec3(gl_FragColor.a),0) * cDiffuse * " + "(0.11+0.8*texture2DProj(blurTex,vScreenCoord));\n"; + s += " " MEDIUMP + " float depth =texture2DProj(depthTex,vScreenCoord).r;\n"; + // adreno bug where depth is returned as 0..1 instead of glDepthRange() + if (GetFunkyDepthIssue()) { + s += " depth = " + std::to_string(kBackingDepth3) + "+depth*(" + + std::to_string(kBackingDepth4) + "-" + + std::to_string(kBackingDepth3) + ");\n"; + } + s += " gl_FragColor *= " + "(1.0-smoothstep(0.0,0.002,gl_FragCoord.z-depth));\n"; + } + + s += "}"; + + if (flags & SHD_DEBUG_PRINT) + Log("\nFragment code for shader '" + GetName(flags) + "':\n\n" + s); + return s; + } + float r_, g_, b_, a_; + GLint color_location_; + int flags_; +}; // SmokeProgramGL + +class RendererGL::BlurProgramGL : public RendererGL::ProgramGL { + public: + enum TextureUnit { + kColorTexUnit, + }; + + BlurProgramGL(RendererGL* renderer, int flags) + : RendererGL::ProgramGL( + renderer, Object::New(GetVertexCode(flags)), + Object::New(GetFragmentCode(flags)), GetName(flags), + GetPFlags(flags)), + flags_(flags), + pixel_size_x_(0.0f), + pixel_size_y_(0.0f) { + SetTextureUnit("colorTex", kColorTexUnit); + pixel_size_location_ = glGetUniformLocation(program(), "pixelSize"); + assert(pixel_size_location_ != -1); + } + void SetPixelSize(float x, float y) { + assert(IsBound()); + if (x != pixel_size_x_ || y != pixel_size_y_) { + pixel_size_x_ = x; + pixel_size_y_ = y; + glUniform2f(pixel_size_location_, pixel_size_x_, pixel_size_y_); + } + } + + void SetColorTexture(const TextureData* t) { + renderer()->BindTexture(GL_TEXTURE_2D, t, kColorTexUnit); + } + void SetColorTexture(GLuint t) { + renderer()->BindTexture(GL_TEXTURE_2D, t, kColorTexUnit); + } + + private: + auto GetName(int flags) -> std::string { + return std::string("BlurProgramGL"); + } + auto GetPFlags(int flags) -> int { + int pflags = PFLAG_USES_POSITION_ATTR | PFLAG_USES_UV_ATTR; + return pflags; + } + auto GetVertexCode(int flags) -> std::string { + std::string s; + s = "uniform mat4 modelViewProjectionMatrix;\n" + "attribute vec4 position;\n" + "attribute " MEDIUMP + "vec2 uv;\n" + "varying " MEDIUMP + "vec2 vUV1;\n" + "varying " MEDIUMP + "vec2 vUV2;\n" + "varying " MEDIUMP + "vec2 vUV3;\n" + "varying " MEDIUMP + "vec2 vUV4;\n" + "varying " MEDIUMP + "vec2 vUV5;\n" + "varying " MEDIUMP + "vec2 vUV6;\n" + "varying " MEDIUMP + "vec2 vUV7;\n" + "varying " MEDIUMP + "vec2 vUV8;\n" + "uniform " MEDIUMP + "vec2 pixelSize;\n" + "void main() {\n" + " gl_Position = modelViewProjectionMatrix*position;\n" + " vUV1 = uv+vec2(-0.5,0)*pixelSize;\n" + " vUV2 = uv+vec2(-1.5,0)*pixelSize;\n" + " vUV3 = uv+vec2(0.5,0)*pixelSize;\n" + " vUV4 = uv+vec2(1.5,0)*pixelSize;\n" + " vUV5 = uv+vec2(-0.5,1.0)*pixelSize;\n" + " vUV6 = uv+vec2(0.5,1.0)*pixelSize;\n" + " vUV7 = uv+vec2(-0.5,-1.0)*pixelSize;\n" + " vUV8 = uv+vec2(0.5,-1.0)*pixelSize;\n"; + s += "}"; + + if (flags & SHD_DEBUG_PRINT) + Log("\nVertex code for shader '" + GetName(flags) + "':\n\n" + s); + return s; + } + auto GetFragmentCode(int flags) -> std::string { + std::string s; + s = "uniform " MEDIUMP + "sampler2D colorTex;\n" + "varying " MEDIUMP + "vec2 vUV1;\n" + "varying " MEDIUMP + "vec2 vUV2;\n" + "varying " MEDIUMP + "vec2 vUV3;\n" + "varying " MEDIUMP + "vec2 vUV4;\n" + "varying " MEDIUMP + "vec2 vUV5;\n" + "varying " MEDIUMP + "vec2 vUV6;\n" + "varying " MEDIUMP + "vec2 vUV7;\n" + "varying " MEDIUMP + "vec2 vUV8;\n" + "void main() {\n" + " gl_FragColor = 0.125*(texture2D(colorTex,vUV1)\n" + " + texture2D(colorTex,vUV2)\n" + " + texture2D(colorTex,vUV3)\n" + " + texture2D(colorTex,vUV4)\n" + " + texture2D(colorTex,vUV5)\n" + " + texture2D(colorTex,vUV6)\n" + " + texture2D(colorTex,vUV7)\n" + " + texture2D(colorTex,vUV8));\n" + "}"; + if (flags & SHD_DEBUG_PRINT) { + Log("\nFragment code for shader '" + GetName(flags) + "':\n\n" + s); + } + return s; + } + + int flags_; + GLint pixel_size_location_; + float pixel_size_x_, pixel_size_y_; +}; // BlurProgramGL + +class RendererGL::ShieldProgramGL : public RendererGL::ProgramGL { + public: + enum TextureUnit { + kDepthTexUnit, + }; + + ShieldProgramGL(RendererGL* renderer, int flags) + : RendererGL::ProgramGL( + renderer, Object::New(GetVertexCode(flags)), + Object::New(GetFragmentCode(flags)), GetName(flags), + GetPFlags(flags)), + flags_(flags) { + SetTextureUnit("depthTex", kDepthTexUnit); + } + void SetDepthTexture(GLuint t) { + renderer()->BindTexture(GL_TEXTURE_2D, t, kDepthTexUnit); + } + + private: + auto GetName(int flags) -> std::string { + return std::string("ShieldProgramGL"); + } + auto GetPFlags(int flags) -> int { + int pflags = PFLAG_USES_POSITION_ATTR; + return pflags; + } + auto GetVertexCode(int flags) -> std::string { + std::string s; + s = "uniform mat4 modelViewProjectionMatrix;\n" + "attribute vec4 position;\n" + "varying " HIGHP + "vec4 vScreenCoord;\n" + "void main() {\n" + " gl_Position = modelViewProjectionMatrix*position;\n" + " vScreenCoord = vec4(gl_Position.xy/gl_Position.w,gl_Position.zw);\n" + " vScreenCoord.xy += vec2(1.0);\n" + " vScreenCoord.xy *= vec2(0.5*vScreenCoord.w);\n"; + s += "}"; + + if (flags & SHD_DEBUG_PRINT) + Log("\nVertex code for shader '" + GetName(flags) + "':\n\n" + s); + return s; + } + auto GetFragmentCode(int flags) -> std::string { + std::string s; + s = "uniform " HIGHP + "sampler2D depthTex;\n" + "varying " HIGHP + "vec4 vScreenCoord;\n" + "void main() {\n" + " " HIGHP "float depth = texture2DProj(depthTex,vScreenCoord).r;\n"; + + // adreno bug where depth is returned as 0..1 instead of glDepthRange() + if (GetFunkyDepthIssue()) { + s += " depth = " + std::to_string(kBackingDepth3) + "+depth*(" + + std::to_string(kBackingDepth4) + "-" + + std::to_string(kBackingDepth3) + ");\n"; + } + // s+= " depth = + // "+std::to_string(kBackingDepth3)+"0.15+depth*(0.9-0.15);\n"; " depth + // *= + // 0.936;\n" " depth = 1.0/(65535.0*((1.0/depth)/16777216.0));\n" " depth + //= 1.0/((1.0/depth)+0.08);\n" " depth += 0.1f;\n" + s += " " HIGHP + "float d = abs(depth - gl_FragCoord.z);\n" + " d = 1.0 - smoothstep(0.0,0.0006,d);\n" + " d = 0.2*smoothstep(0.96,1.0,d)+0.2*d+0.4*d*d*d;\n"; + + // some mali chips seem to have no high precision and thus this looks + // terrible.. + // ..in those cases lets done down the intersection effect significantly + if (GetDrawsShieldsFunny()) { + s += " gl_FragColor = vec4(d*0.13,d*0.1,d,0);\n"; + } else { + s += " gl_FragColor = vec4(d*0.5,d*0.4,d,0);\n"; + } + s += "}"; + + // this shows msaa depth error on bridgit + //" gl_FragColor = vec4(smoothstep(0.73,0.77,depth),0.0,0.0,0.5);\n" + + // " d = 1.0 - smoothstep(0.0,0.0006,d);\n" + // " d = 0.2*smoothstep(0.96,1.0,d)+0.2*d+0.4*d*d*d;\n" + //" if (d < 0.01) gl_FragColor = vec4(0.0,1.0,0.0,0.5);\n" + + //" gl_FragColor = vec4(vec3(10.0*abs(depth-gl_FragCoord.z)),1);\n" + // " gl_FragColor = vec4(0,10.0*abs(depth-gl_FragCoord.z),0,0.1);\n" + // " if (depth < gl_FragCoord.z) gl_FragColor = + // vec4(1.0-10.0*(gl_FragCoord.z-depth),0,0,1);\n" " else gl_FragColor = + // vec4(0,1.0-10.0*(depth-gl_FragCoord.z),0,1);\n" + //" gl_FragColor = vec4(vec3(depth),1);\n" + + if (flags & SHD_DEBUG_PRINT) + Log("\nFragment code for shader '" + GetName(flags) + "':\n\n" + s); + return s; + } + + int flags_; +}; + +class RendererGL::PostProcessProgramGL : public RendererGL::ProgramGL { + public: + enum TextureUnit { + kColorTexUnit, + kDepthTexUnit, + kColorSlightBlurredTexUnit, + kColorBlurredTexUnit, + kColorBlurredMoreTexUnit + }; + + PostProcessProgramGL(RendererGL* renderer, int flags) + : RendererGL::ProgramGL( + renderer, Object::New(GetVertexCode(flags)), + Object::New(GetFragmentCode(flags)), GetName(flags), + GetPFlags(flags)), + flags_(flags), + dof_near_min_(0), + dof_near_max_(0), + dof_far_min_(0), + dof_far_max_(0), + distort_(0.0f) { + SetTextureUnit("colorTex", kColorTexUnit); + + if (UsesSlightBlurredTex()) + SetTextureUnit("colorSlightBlurredTex", kColorSlightBlurredTexUnit); + if (UsesBlurredTexture()) + SetTextureUnit("colorBlurredTex", kColorBlurredTexUnit); + SetTextureUnit("colorBlurredMoreTex", kColorBlurredMoreTexUnit); + SetTextureUnit("depthTex", kDepthTexUnit); + + dof_location_ = glGetUniformLocation(program(), "dofRange"); +#if !MSAA_ERROR_TEST + assert(dof_location_ != -1); +#endif + + if (flags & SHD_DISTORT) { + distort_location_ = glGetUniformLocation(program(), "distort"); + assert(distort_location_ != -1); + } + } + + auto UsesSlightBlurredTex() -> bool { + return static_cast(flags_ & SHD_EYES); + } + auto UsesBlurredTexture() -> bool { + return static_cast(flags_ & (SHD_HIGHER_QUALITY | SHD_EYES)); + } + void SetColorTexture(GLuint t) { + renderer()->BindTexture(GL_TEXTURE_2D, t, kColorTexUnit); + } + void SetColorSlightBlurredTexture(GLuint t) { + renderer()->BindTexture(GL_TEXTURE_2D, t, kColorSlightBlurredTexUnit); + } + void SetColorBlurredMoreTexture(GLuint t) { + renderer()->BindTexture(GL_TEXTURE_2D, t, kColorBlurredMoreTexUnit); + } + void SetColorBlurredTexture(GLuint t) { + renderer()->BindTexture(GL_TEXTURE_2D, t, kColorBlurredTexUnit); + } + void SetDepthTexture(GLuint t) { + renderer()->BindTexture(GL_TEXTURE_2D, t, kDepthTexUnit); + } + + void SetDepthOfFieldRanges(float near_min, float near_max, float far_min, + float far_max) { + assert(IsBound()); + if (near_min != dof_near_min_ || near_max != dof_near_max_ + || far_min != dof_far_min_ || far_max != dof_far_max_) { + DEBUG_CHECK_GL_ERROR; + dof_near_min_ = near_min; + dof_near_max_ = near_max; + dof_far_min_ = far_min; + dof_far_max_ = far_max; + float vals[4] = {dof_near_min_, dof_near_max_, dof_far_min_, + dof_far_max_}; + glUniform1fv(dof_location_, 4, vals); + DEBUG_CHECK_GL_ERROR; + } + } + void SetDistort(float distort) { + assert(IsBound()); + assert(flags_ & SHD_DISTORT); + if (distort != distort_) { + DEBUG_CHECK_GL_ERROR; + distort_ = distort; + glUniform1f(distort_location_, distort_); + DEBUG_CHECK_GL_ERROR; + } + } + + private: + auto GetName(int flags) -> std::string { + return std::string("PostProcessProgramGL"); + } + auto GetPFlags(int flags) -> int { + int pflags = PFLAG_USES_POSITION_ATTR; + if (flags & SHD_DISTORT) { + pflags |= (PFLAG_USES_NORMAL_ATTR | PFLAG_USES_MODEL_VIEW_MATRIX); + } + return pflags; + } + + // testing MSAA BUG +#if MSAA_ERROR_TEST + string GetVertexCode(int flags) { + string s; + s = "uniform mat4 modelViewProjectionMatrix;\n" + "attribute vec4 position;\n"; + if (flags & SHD_DISTORT) + s += "attribute " LOWP + "vec3 normal;\n" + "uniform mat4 modelViewMatrix;\n" + "uniform float distort;\n"; + if (flags & SHD_EYES) s += "varying " HIGHP "float calcedDepth;\n"; + + s += "varying " HIGHP + "vec4 vScreenCoord;\n" + "void main() {\n" + " gl_Position = modelViewProjectionMatrix*position;\n"; + if (flags & SHD_DISTORT) + s += " float eyeDot = " + "abs(normalize(modelViewMatrix*vec4(normal,0.0))).z;\n" + " vec4 posDistorted = " + "modelViewProjectionMatrix*(position-eyeDot*distort*vec4(normal,0));" + "\n" + " vScreenCoord = " + "vec4(posDistorted.xy/posDistorted.w,posDistorted.zw);\n" + " vScreenCoord.xy += vec2(1.0);\n" + " vScreenCoord.xy *= vec2(0.5*vScreenCoord.w);\n"; + else + s += " vScreenCoord = " + "vec4(gl_Position.xy/gl_Position.w,gl_Position.zw);\n" + " vScreenCoord.xy += vec2(1.0);\n" + " vScreenCoord.xy *= vec2(0.5*vScreenCoord.w);\n"; + if (flags & SHD_EYES) + s += " calcedDepth = " + std::to_string(kBackingDepth3) + "+" + + std::to_string(kBackingDepth4 - kBackingDepth3) + + "*(0.5*(gl_Position.z/gl_Position.w)+0.5);\n"; + s += "}"; + + if (flags & SHD_DEBUG_PRINT) + Log("\nVertex code for shader '" + GetName(flags) + "':\n\n" + s); + return s; + } + string GetFragmentCode(int flags) { + string s; + s = "uniform " HIGHP + "sampler2D depthTex;\n" + "varying " HIGHP "vec4 vScreenCoord;\n"; + s += "void main() {\n" + " " HIGHP "float depth = texture2DProj(depthTex,vScreenCoord).r;\n"; + s += " gl_FragColor = vec4(vec3(14.0*(depth-0.76)),1);\n"; + s += "}"; + + if (flags & SHD_DEBUG_PRINT) + Log("\nFragment code for shader '" + GetName(flags) + "':\n\n" + s); + return s; + } + +#else // msaa bug test + + auto GetVertexCode(int flags) -> std::string { + std::string s; + s = "uniform mat4 modelViewProjectionMatrix;\n" + "attribute vec4 position;\n"; + if (flags & SHD_DISTORT) + s += "attribute " LOWP + "vec3 normal;\n" + "uniform mat4 modelViewMatrix;\n" + "uniform float distort;\n"; + if (flags & SHD_EYES) { + s += "varying " HIGHP "float calcedDepth;\n"; + } + + s += "varying " MEDIUMP + "vec4 vScreenCoord;\n" + "void main() {\n" + " gl_Position = modelViewProjectionMatrix*position;\n"; + if (flags & SHD_DISTORT) { + s += " float eyeDot = " + "abs(normalize(modelViewMatrix*vec4(normal,0.0))).z;\n" + " vec4 posDistorted = " + "modelViewProjectionMatrix*(position-eyeDot*distort*vec4(normal,0));" + "\n" + " vScreenCoord = " + "vec4(posDistorted.xy/posDistorted.w,posDistorted.zw);\n" + " vScreenCoord.xy += vec2(1.0);\n" + " vScreenCoord.xy *= vec2(0.5*vScreenCoord.w);\n"; + } else { + s += " vScreenCoord = " + "vec4(gl_Position.xy/gl_Position.w,gl_Position.zw);\n" + " vScreenCoord.xy += vec2(1.0);\n" + " vScreenCoord.xy *= vec2(0.5*vScreenCoord.w);\n"; + } + if (flags & SHD_EYES) { + s += " calcedDepth = " + std::to_string(kBackingDepth3) + "+" + + std::to_string(kBackingDepth4 - kBackingDepth3) + + "*(0.5*(gl_Position.z/gl_Position.w)+0.5);\n"; + } + s += "}"; + + if (flags & SHD_DEBUG_PRINT) + Log("\nVertex code for shader '" + GetName(flags) + "':\n\n" + s); + return s; + } + auto GetFragmentCode(int flags) -> std::string { + std::string s; + s = "uniform " LOWP + "sampler2D colorTex;\n" + "uniform " LOWP + "sampler2D colorBlurredMoreTex;\n" + "uniform " HIGHP + "sampler2D depthTex;\n" + "varying " MEDIUMP + "vec4 vScreenCoord;\n" + "uniform " LOWP "float dofRange[4];\n"; + if (flags & (SHD_HIGHER_QUALITY | SHD_EYES)) { + s += "uniform " LOWP "sampler2D colorBlurredTex;\n"; + } + if (flags & SHD_EYES) + s += "uniform " LOWP + "sampler2D colorSlightBlurredTex;\n" + "varying " HIGHP "float calcedDepth;\n"; + + s += + "void main() {\n" + " " MEDIUMP "float depth = texture2DProj(depthTex,vScreenCoord).r;\n"; + + bool doConditional = ((flags & SHD_CONDITIONAL) && !(flags & (SHD_EYES))); + + if (doConditional) { + // special-case completely out of focus areas and completely in-focus + // areas.. + s += " if (depth > dofRange[1] && depth < dofRange[2]) {\n"; + if (flags & SHD_HIGHER_QUALITY) { + s += + " " LOWP + "vec4 color = texture2DProj(colorTex,vScreenCoord);\n" + " " LOWP + "vec4 colorBlurred = texture2DProj(colorBlurredTex,vScreenCoord);\n" + " " LOWP + "vec4 colorBlurredMore = " + "0.4*texture2DProj(colorBlurredMoreTex,vScreenCoord);\n" + " " MEDIUMP + "vec4 diff = colorBlurred-color;\n" + " diff = sign(diff) * max(vec4(0.0),abs(diff)-0.12);\n" + " gl_FragColor = (0.55*colorBlurredMore) + " + "(0.62+colorBlurredMore)*(color-diff);\n\n"; + } else { + s += " gl_FragColor = texture2DProj(colorTex,vScreenCoord);\n"; + } + s += " }\n" + " else if (depth < dofRange[0] || depth > dofRange[3]) {\n"; + if (flags & SHD_HIGHER_QUALITY) { + s += + " " LOWP + "vec4 colorBlurred = texture2DProj(colorBlurredTex,vScreenCoord);\n" + " " LOWP + "vec4 colorBlurredMore = " + "0.4*texture2DProj(colorBlurredMoreTex,vScreenCoord);\n" + " gl_FragColor = (0.55*colorBlurredMore) + " + "(0.62+colorBlurredMore)*colorBlurred;\n\n"; + } else { + s += " gl_FragColor = " + "texture2DProj(colorBlurredMoreTex,vScreenCoord);\n"; + } + s += " }\n" + " else{\n"; + } + + // transition areas.. + s += " " LOWP "vec4 color = texture2DProj(colorTex,vScreenCoord);\n"; + if (flags & SHD_EYES) + s += " " LOWP + "vec4 colorSlightBlurred = " + "texture2DProj(colorSlightBlurredTex,vScreenCoord);\n"; + + if (flags & (SHD_HIGHER_QUALITY | SHD_EYES)) { + s += " " LOWP + "vec4 colorBlurred = texture2DProj(colorBlurredTex,vScreenCoord);\n" + " " LOWP + "vec4 colorBlurredMore = " + "0.4*texture2DProj(colorBlurredMoreTex,vScreenCoord);\n" + " " LOWP "float blur = " BLURSCALE + " (smoothstep(dofRange[2],dofRange[3],depth)\n" + " + 1.0 - " + "smoothstep(dofRange[0],dofRange[1],depth));\n" + " " MEDIUMP + "vec4 diff = colorBlurred-color;\n" + " diff = sign(diff) * max(vec4(0.0),abs(diff)-0.12);\n" + " gl_FragColor = (0.55*colorBlurredMore) + " + "(0.62+colorBlurredMore)*mix(color-diff,colorBlurred,blur);\n\n"; + } else { + s += " " LOWP + "vec4 colorBlurredMore = " + "texture2DProj(colorBlurredMoreTex,vScreenCoord);\n" + " " LOWP "float blur = " BLURSCALE + " (smoothstep(dofRange[2],dofRange[3],depth)\n" + " + 1.0 - " + "smoothstep(dofRange[0],dofRange[1],depth));\n" + " gl_FragColor = mix(color,colorBlurredMore,blur);\n\n"; + } + + if (flags & SHD_EYES) { + s += " " MEDIUMP "vec4 diffEye = colorBlurred-color;\n"; + s += " diffEye = sign(diffEye) * max(vec4(0.0),abs(diffEye)-0.06);\n"; + s += " " LOWP + "vec4 baseColorEye = " + "mix(color-10.0*(diffEye),colorSlightBlurred,0.83);\n"; + s += " " LOWP + "vec4 eyeColor = (0.55*colorBlurredMore) + " + "(0.62+colorBlurredMore)*mix(baseColorEye,colorBlurred,blur);\n\n"; + s += " " LOWP + "float dBlend = smoothstep(-0.0004,-0.0001,depth-calcedDepth);\n" + " gl_FragColor = mix(gl_FragColor,eyeColor,dBlend);\n"; + } + if (doConditional) { + s += " }\n"; + } + + // demonstrates MSAA striation issue: + // s += " gl_FragColor = + // mix(gl_FragColor,vec4(vec3(14.0*(depth-0.76)),1),0.999);\n"; + // s += " gl_FragColor = + // vec4(vec3(14.0*(texture2DProj(depthTex,vScreenCoord).r-0.76)),1);\n"; + s += "}"; + + if (flags & SHD_DEBUG_PRINT) + Log("\nFragment code for shader '" + GetName(flags) + "':\n\n" + s); + return s; + } +#endif // msaa bug test + + int flags_; + float dof_near_min_; + float dof_near_max_; + float dof_far_min_; + float dof_far_max_; + GLint dof_location_; + float distort_; + GLint distort_location_; +}; + +class RendererGL::SpriteProgramGL : public RendererGL::ProgramGL { + public: + enum TextureUnit { kColorTexUnit, kDepthTexUnit }; + + SpriteProgramGL(RendererGL* renderer, int flags) + : RendererGL::ProgramGL( + renderer, Object::New(GetVertexCode(flags)), + Object::New(GetFragmentCode(flags)), GetName(flags), + GetPFlags(flags)), + flags_(flags), + r_(0), + g_(0), + b_(0), + a_(0) { + SetTextureUnit("colorTex", kColorTexUnit); + + if (flags & SHD_OVERLAY) { + SetTextureUnit("depthTex", kDepthTexUnit); + } + + if (flags & SHD_COLOR) { + color_location_ = glGetUniformLocation(program(), "colorU"); + assert(color_location_ != -1); + } + DEBUG_CHECK_GL_ERROR; + } + void SetColorTexture(const TextureData* t) { + renderer()->BindTexture(GL_TEXTURE_2D, t, kColorTexUnit); + } + void SetDepthTexture(GLuint t) { + assert(flags_ & SHD_OVERLAY); + renderer()->BindTexture(GL_TEXTURE_2D, t, kDepthTexUnit); + } + void SetColor(float r, float g, float b, float a = 1.0f) { + assert(flags_ & SHD_COLOR); + assert(IsBound()); + if (r != r_ || g != g_ || b != b_ || a != a_) { + r_ = r; + g_ = g; + b_ = b; + a_ = a; + glUniform4f(color_location_, r_, g_, b_, a_); + } + } + + private: + auto GetName(int flags) -> std::string { + return std::string("SpriteProgramGL"); + } + auto GetPFlags(int flags) -> int { + int pflags = PFLAG_USES_POSITION_ATTR | PFLAG_USES_SIZE_ATTR + | PFLAG_USES_COLOR_ATTR | PFLAG_USES_UV_ATTR; + if (flags & SHD_CAMERA_ALIGNED) pflags |= PFLAG_USES_CAM_ORIENT_MATRIX; + return pflags; + } + auto GetVertexCode(int flags) -> std::string { + std::string s; + s += "uniform mat4 modelViewProjectionMatrix;\n" + "attribute vec4 position;\n" + "attribute " MEDIUMP + "vec2 uv;\n" + "attribute " MEDIUMP + "float size;\n" + "varying " MEDIUMP "vec2 vUV;\n"; + + if (flags & SHD_COLOR) s += "uniform " LOWP "vec4 colorU;\n"; + + if (flags & SHD_CAMERA_ALIGNED) s += "uniform mat4 camOrientMatrix;\n"; + + if (flags & SHD_OVERLAY) s += "varying " LOWP "vec4 vScreenCoord;\n"; + + s += "attribute " LOWP + "vec4 color;\n" + "varying " LOWP + "vec4 vColor;\n" + "void main() {\n"; + + if (flags & SHD_CAMERA_ALIGNED) + s += " " HIGHP + "vec4 pLocal = " + "(position+camOrientMatrix*vec4((uv.s-0.5)*size,0,(uv.t-0.5)*size,0)" + ");\n"; + else + s += " " HIGHP + "vec4 pLocal = " + "(position+vec4((uv.s-0.5)*size,0,(uv.t-0.5)*size,0));\n"; + s += " gl_Position = modelViewProjectionMatrix*pLocal;\n" + " vUV = uv;\n"; + if (flags & SHD_COLOR) + s += " vColor = color*colorU;\n"; + else + s += " vColor = color;\n"; + if (flags & SHD_OVERLAY) + s += " vScreenCoord = " + "vec4(gl_Position.xy/gl_Position.w,gl_Position.zw);\n" + " vScreenCoord.xy += vec2(1.0);\n" + " vScreenCoord.xy *= vec2(0.5*vScreenCoord.w);\n"; + s += "}"; + + if (flags & SHD_DEBUG_PRINT) + Log("\nVertex code for shader '" + GetName(flags) + "':\n\n" + s); + return s; + } + auto GetFragmentCode(int flags) -> std::string { + std::string s; + + s += "uniform " LOWP + "sampler2D colorTex;\n" + "varying " MEDIUMP + "vec2 vUV;\n" + "varying " LOWP "vec4 vColor;\n"; + if (flags & SHD_OVERLAY) + s += "varying " MEDIUMP + "vec4 vScreenCoord;\n" + "uniform " MEDIUMP "sampler2D depthTex;\n"; + + s += "void main() {\n" + " gl_FragColor = vColor*vec4(texture2D(colorTex,vUV).r);\n"; + if (flags & SHD_EXP2) + s += " gl_FragColor = vec4(vUV,0,0) + " + "vec4(gl_FragColor.rgb*gl_FragColor.rgb,gl_FragColor.a);\n"; + if (flags & SHD_OVERLAY) { + s += " " MEDIUMP + "float depth = texture2DProj(depthTex,vScreenCoord).r;\n"; + // adreno 320 bug where depth is returned as 0..1 instead of + // glDepthRange() + if (GetFunkyDepthIssue()) { + s += " depth = " + std::to_string(kBackingDepth3) + "+depth*(" + + std::to_string(kBackingDepth4) + "-" + + std::to_string(kBackingDepth3) + ");\n"; + } + s += " gl_FragColor *= " + "(1.0-smoothstep(0.0,0.001,gl_FragCoord.z-depth));\n"; + } + s += "}"; + if (flags & SHD_DEBUG_PRINT) + Log("\nFragment code for shader '" + GetName(flags) + "':\n\n" + s); + return s; + } + float r_, g_, b_, a_; + GLint color_location_; + + int flags_; +}; + +class RendererGL::TextureDataGL : public TextureRendererData { + public: + TextureDataGL(const TextureData& texture_in, RendererGL* renderer_in) + : tex_media_(&texture_in), texture_(0), renderer_(renderer_in) { + assert(InGraphicsThread()); + DEBUG_CHECK_GL_ERROR; + glGenTextures(1, &texture_); + DEBUG_CHECK_GL_ERROR; + } + + ~TextureDataGL() override { + if (!InGraphicsThread()) { + Log("Error: TextureDataGL dying outside of graphics thread."); + } else { + // if we're currently bound as anything, clear that out + // (otherwise a new texture with that same ID won't be bindable) + for (int i = 0; i < kMaxGLTexUnitsUsed; i++) { + if ((renderer_->bound_textures_2d_[i]) == texture_) { + renderer_->bound_textures_2d_[i] = -1; + } + if ((renderer_->bound_textures_cube_map_[i]) == texture_) { + renderer_->bound_textures_cube_map_[i] = -1; + } + } + if (!g_graphics_server->renderer_context_lost()) { + glDeleteTextures(1, &texture_); + DEBUG_CHECK_GL_ERROR; + } + } + } + + auto GetTexture() const -> GLuint { return texture_; } + + void Load() override { + assert(InGraphicsThread()); + DEBUG_CHECK_GL_ERROR; + + if (tex_media_->texture_type() == TextureType::k2D) { + renderer_->BindTexture(GL_TEXTURE_2D, texture_); + const TexturePreloadData* preload_data = &tex_media_->preload_datas()[0]; + int base_src_level = preload_data->base_level; + assert(preload_data->buffers[base_src_level]); + GraphicsQuality q = g_graphics_server->quality(); + + // Determine whether to use anisotropic sampling on this texture: + // basically all the UI stuff that is only ever seen from straight on + // doesn't need it. + bool allow_ani = true; + + // FIXME - filtering by filename.. once we get this stuff on a server we + // should include this as metadata instead. + const char* n = tex_media_->file_name().c_str(); + + // The following exceptions should *never* need aniso-sampling. + { + if (!strcmp(n, "fontBig")) { // NOLINT(bugprone-branch-clone) + allow_ani = false; + + // Lets splurge on this for higher but not high. + // (names over characters might benefit, though most text doesnt) + } else if (strstr(n, "Icon")) { + allow_ani = false; + } else if (strstr(n, "characterIconMask")) { + allow_ani = false; + } else if (!strcmp(n, "bg")) { + allow_ani = false; + } else if (strstr(n, "light")) { + allow_ani = false; + } else if (strstr(n, "shadow")) { + allow_ani = false; + } else if (!strcmp(n, "sparks")) { + allow_ani = false; + } else if (!strcmp(n, "smoke")) { + allow_ani = false; + } else if (!strcmp(n, "scorch")) { + allow_ani = false; + } else if (!strcmp(n, "scorchBig")) { + allow_ani = false; + } else if (!strcmp(n, "white")) { + allow_ani = false; + } else if (!strcmp(n, "buttonBomb")) { + allow_ani = false; + } else if (!strcmp(n, "buttonJump")) { + allow_ani = false; + } else if (!strcmp(n, "buttonPickUp")) { + allow_ani = false; + } else if (!strcmp(n, "buttonPunch")) { + allow_ani = false; + } else if (strstr(n, "touchArrows")) { + allow_ani = false; + } else if (!strcmp(n, "actionButtons")) { + allow_ani = false; + } + } + // The following are considered 'nice to have' - we turn aniso. off for + // them in anything less than 'higher' mode. + if (allow_ani && (q < GraphicsQuality::kHigher)) { + if (strstr(n, "ColorMask")) { // NOLINT(bugprone-branch-clone) + allow_ani = false; // character color-masks + } else if (strstr(n, "softRect")) { + allow_ani = false; + } else if (strstr(n, "BG")) { + allow_ani = false; // level backgrounds + } else if (!strcmp(n, "explosion")) { + allow_ani = false; + } else if (!strcmp(n, "bar")) { + allow_ani = false; + } + } + + // In higher quality we do anisotropic trilinear mipmap. + if (q >= GraphicsQuality::kHigher) { + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, + GL_LINEAR_MIPMAP_LINEAR); + if (g_anisotropic_support && allow_ani) { + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, + std::min(16.0f, g_max_anisotropy)); + } + } else if (q >= GraphicsQuality::kHigh) { + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, + GL_LINEAR_MIPMAP_LINEAR); + if (g_anisotropic_support && allow_ani) + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, + std::min(16.0f, g_max_anisotropy)); + } else if (q >= GraphicsQuality::kMedium) { + // In medium quality we don't do anisotropy but do trilinear. + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, + GL_LINEAR_MIPMAP_LINEAR); + } else { + // in low quality we do bilinear + assert(q == GraphicsQuality::kLow); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, + GL_LINEAR_MIPMAP_NEAREST); + } + + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + + int src_level = base_src_level; + int level = 0; + bool all_levels_handled = false; + while (preload_data->buffers[src_level] != nullptr + && !all_levels_handled) { + switch (preload_data->formats[src_level]) { + case TextureFormat::kRGBA_8888: { + glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA, + preload_data->widths[src_level], + preload_data->heights[src_level], 0, GL_RGBA, + GL_UNSIGNED_BYTE, preload_data->buffers[src_level]); + + // At the moment we always just let GL generate mipmaps + // for uncompressed textures; is there any reason not to? + glGenerateMipmap(GL_TEXTURE_2D); + all_levels_handled = true; + break; + } + case TextureFormat::kRGBA_4444: { + glTexImage2D( + GL_TEXTURE_2D, level, GL_RGBA, preload_data->widths[src_level], + preload_data->heights[src_level], 0, GL_RGBA, + GL_UNSIGNED_SHORT_4_4_4_4, preload_data->buffers[src_level]); + + // At the moment we always just let GL generate mipmaps + // for uncompressed textures; is there any reason not to? + glGenerateMipmap(GL_TEXTURE_2D); + all_levels_handled = true; + break; + } + case TextureFormat::kRGB_565: { + glTexImage2D( + GL_TEXTURE_2D, level, GL_RGB, preload_data->widths[src_level], + preload_data->heights[src_level], 0, GL_RGB, + GL_UNSIGNED_SHORT_5_6_5, preload_data->buffers[src_level]); + + // At the moment we always just let GL generate mipmaps + // for uncompressed textures; is there any reason not to? + glGenerateMipmap(GL_TEXTURE_2D); + all_levels_handled = true; + break; + } + case TextureFormat::kRGB_888: { + glTexImage2D(GL_TEXTURE_2D, level, GL_RGB, + preload_data->widths[src_level], + preload_data->heights[src_level], 0, GL_RGB, + GL_UNSIGNED_BYTE, preload_data->buffers[src_level]); + + // At the moment we always just let GL generate mipmaps + // for uncompressed textures; is there any reason not to? + glGenerateMipmap(GL_TEXTURE_2D); + all_levels_handled = true; + break; + } + default: { + glCompressedTexImage2D( + GL_TEXTURE_2D, level, + GetGLTextureFormat(preload_data->formats[src_level]), + preload_data->widths[src_level], + preload_data->heights[src_level], 0, + static_cast_check_fit(preload_data->sizes[src_level]), + preload_data->buffers[src_level]); + break; + } + } + src_level++; + level++; + DEBUG_CHECK_GL_ERROR; + } + GL_LABEL_OBJECT(GL_TEXTURE, texture_, tex_media_->GetName().c_str()); + } else if (tex_media_->texture_type() == TextureType::kCubeMap) { + // Cube map. + renderer_->BindTexture(GL_TEXTURE_CUBE_MAP, texture_); + + bool do_generate_mips = false; + for (uint32_t i = 0; i < 6; i++) { + const TexturePreloadData* preload_data = + &tex_media_->preload_datas()[i]; + int base_src_level = preload_data->base_level; + assert(preload_data->buffers[base_src_level]); + + GraphicsQuality q = g_graphics_server->quality(); + + // do trilinear in higher quality; otherwise bilinear is good enough.. + if (q >= GraphicsQuality::kHigher) { + glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, + GL_LINEAR_MIPMAP_LINEAR); + } else { + glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, + GL_LINEAR_MIPMAP_NEAREST); + } + + glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, + GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, + GL_CLAMP_TO_EDGE); + + int src_level = base_src_level; + int level = 0; + bool generating_remaining_mips = false; + while (preload_data->buffers[src_level] != nullptr + && !generating_remaining_mips) { + switch (preload_data->formats[src_level]) { + case TextureFormat::kRGBA_8888: + glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, level, GL_RGBA, + preload_data->widths[src_level], + preload_data->heights[src_level], 0, GL_RGBA, + GL_UNSIGNED_BYTE, preload_data->buffers[src_level]); + generating_remaining_mips = do_generate_mips = true; + break; + case TextureFormat::kRGBA_4444: + glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, level, GL_RGBA, + preload_data->widths[src_level], + preload_data->heights[src_level], 0, GL_RGBA, + GL_UNSIGNED_SHORT_4_4_4_4, + preload_data->buffers[src_level]); + generating_remaining_mips = do_generate_mips = true; + break; + case TextureFormat::kRGB_565: + glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, level, GL_RGB, + preload_data->widths[src_level], + preload_data->heights[src_level], 0, GL_RGB, + GL_UNSIGNED_SHORT_5_6_5, + preload_data->buffers[src_level]); + generating_remaining_mips = do_generate_mips = true; + break; + case TextureFormat::kRGB_888: + glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, level, GL_RGB, + preload_data->widths[src_level], + preload_data->heights[src_level], 0, GL_RGB, + GL_UNSIGNED_BYTE, preload_data->buffers[src_level]); + generating_remaining_mips = do_generate_mips = true; + break; + default: + glCompressedTexImage2D( + GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, level, + GetGLTextureFormat(preload_data->formats[src_level]), + preload_data->widths[src_level], + preload_data->heights[src_level], 0, + static_cast_check_fit( + preload_data->sizes[src_level]), + preload_data->buffers[src_level]); + break; + } + src_level++; + level++; + DEBUG_CHECK_GL_ERROR; + } + } + + // If we're generating remaining mips on the gpu, do so. + if (do_generate_mips) { + glGenerateMipmap(GL_TEXTURE_CUBE_MAP); + } + + GL_LABEL_OBJECT(GL_TEXTURE, texture_, tex_media_->GetName().c_str()); + } else { + throw Exception(); + } + DEBUG_CHECK_GL_ERROR; + } + + private: + const TextureData* tex_media_; + RendererGL* renderer_; + GLuint texture_; +}; // TextureDataGL + +void RendererGL::SetViewport(GLint x, GLint y, GLsizei width, GLsizei height) { + if (x != viewport_x_ || y != viewport_y_ || width != viewport_width_ + || height != viewport_height_) { + viewport_x_ = x; + viewport_y_ = y; + viewport_width_ = width; + viewport_height_ = height; + glViewport(viewport_x_, viewport_y_, viewport_width_, viewport_height_); + } +} + +void RendererGL::SetVertexAttribArrayEnabled(GLuint i, bool enabled) { + assert(!g_vao_support); + assert(i < kVertexAttrCount); + if (enabled != vertex_attrib_arrays_enabled_[i]) { + if (enabled) { + glEnableVertexAttribArray(i); + } else { + glDisableVertexAttribArray(i); + } + vertex_attrib_arrays_enabled_[i] = enabled; + } +} + +void RendererGL::BindTextureUnit(uint32_t tex_unit) { + assert(tex_unit >= 0 && tex_unit < kMaxGLTexUnitsUsed); + if (active_tex_unit_ != tex_unit) { + glActiveTexture(GL_TEXTURE0 + tex_unit); + active_tex_unit_ = tex_unit; + } +} + +void RendererGL::BindFramebuffer(GLuint fb) { + if (active_framebuffer_ != fb) { + glBindFramebuffer(GL_FRAMEBUFFER, fb); + active_framebuffer_ = fb; + } +} + +void RendererGL::BindArrayBuffer(GLuint b) { + if (active_array_buffer_ != b) { + glBindBuffer(GL_ARRAY_BUFFER, b); + active_array_buffer_ = b; + } +} + +void RendererGL::BindTexture(GLuint type, const TextureData* t, + GLuint tex_unit) { + if (t) { + auto data = static_cast_check_type(t->renderer_data()); + BindTexture(type, data->GetTexture(), tex_unit); + } else { + // Fallback to noise. + BindTexture(type, random_tex_, tex_unit); + } +} + +void RendererGL::BindTexture(GLuint type, GLuint tex, GLuint tex_unit) { + switch (type) { + case GL_TEXTURE_2D: { + if (tex != bound_textures_2d_[tex_unit]) { + BindTextureUnit(tex_unit); + glBindTexture(type, tex); + bound_textures_2d_[tex_unit] = tex; + } + break; + } + case GL_TEXTURE_CUBE_MAP: { + if (tex != bound_textures_cube_map_[tex_unit]) { + BindTextureUnit(tex_unit); + glBindTexture(type, tex); + bound_textures_cube_map_[tex_unit] = tex; + } + break; + } + default: + throw Exception(); + } +} + +class RendererGL::ModelDataGL : public ModelRendererData { + public: + enum BufferType { kVertices, kIndices, kBufferCount }; + + ModelDataGL(const ModelData& model, RendererGL* renderer) + : renderer_(renderer), fake_vao_(nullptr) { +#if BA_DEBUG_BUILD + name_ = model.GetName(); +#endif // BA_DEBUG_BUILD + + assert(InGraphicsThread()); + DEBUG_CHECK_GL_ERROR; + + // Create our vertex array to hold all this state (if supported). + if (g_vao_support) { + glGenVertexArrays(1, &vao_); + DEBUG_CHECK_GL_ERROR; + renderer->BindVertexArray(vao_); + DEBUG_CHECK_GL_ERROR; + } else { + fake_vao_ = new FakeVertexArrayObject(renderer_); + } + + glGenBuffers(kBufferCount, vbos_); + + DEBUG_CHECK_GL_ERROR; + + // Fill our vertex data buffer. + renderer_->BindArrayBuffer(vbos_[kVertices]); + DEBUG_CHECK_GL_ERROR; + glBufferData(GL_ARRAY_BUFFER, + static_cast_check_fit(model.vertices().size() + * sizeof(VertexObjectFull)), + &(model.vertices()[0]), GL_STATIC_DRAW); + DEBUG_CHECK_GL_ERROR; + + // ..and point our array at its members. + if (fake_vao_) { + fake_vao_->SetAttribBuffer(vbos_[kVertices], kVertexAttrPosition, 3, + GL_FLOAT, GL_FALSE, sizeof(VertexObjectFull), + offsetof(VertexObjectFull, position)); + fake_vao_->SetAttribBuffer( + vbos_[kVertices], kVertexAttrUV, 2, GL_UNSIGNED_SHORT, GL_TRUE, + sizeof(VertexObjectFull), offsetof(VertexObjectFull, uv)); + fake_vao_->SetAttribBuffer(vbos_[kVertices], kVertexAttrNormal, 3, + GL_SHORT, GL_TRUE, sizeof(VertexObjectFull), + offsetof(VertexObjectFull, normal)); + DEBUG_CHECK_GL_ERROR; + } else { + glVertexAttribPointer( + kVertexAttrPosition, 3, GL_FLOAT, GL_FALSE, sizeof(VertexObjectFull), + reinterpret_cast(offsetof(VertexObjectFull, position))); + glEnableVertexAttribArray(kVertexAttrPosition); + glVertexAttribPointer( + kVertexAttrUV, 2, GL_UNSIGNED_SHORT, GL_TRUE, + sizeof(VertexObjectFull), + reinterpret_cast(offsetof(VertexObjectFull, uv))); + glEnableVertexAttribArray(kVertexAttrUV); + glVertexAttribPointer( + kVertexAttrNormal, 3, GL_SHORT, GL_TRUE, sizeof(VertexObjectFull), + reinterpret_cast(offsetof(VertexObjectFull, normal))); + glEnableVertexAttribArray(kVertexAttrNormal); + DEBUG_CHECK_GL_ERROR; + } + + // fill our index data buffer + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbos_[kIndices]); + if (!g_vao_support) { + assert(fake_vao_); + fake_vao_->SetElementBuffer(vbos_[kIndices]); + } + + const GLvoid* index_data; + switch (model.GetIndexSize()) { + case 1: { + elem_count_ = static_cast(model.indices8().size()); + index_type_ = GL_UNSIGNED_BYTE; + index_data = static_cast(model.indices8().data()); + break; + } + case 2: { + elem_count_ = static_cast(model.indices16().size()); + index_type_ = GL_UNSIGNED_SHORT; + index_data = static_cast(model.indices16().data()); + break; + } + case 4: { + BA_LOG_ONCE( + "GL WARNING - USING 32 BIT INDICES WHICH WONT WORK IN ES2!!"); + elem_count_ = static_cast(model.indices32().size()); + index_type_ = GL_UNSIGNED_INT; + index_data = static_cast(model.indices32().data()); + break; + } + default: + throw Exception(); + } + glBufferData( + GL_ELEMENT_ARRAY_BUFFER, + static_cast_check_fit(elem_count_ * model.GetIndexSize()), + index_data, GL_STATIC_DRAW); + + DEBUG_CHECK_GL_ERROR; + } // ModelDataGL + + ~ModelDataGL() override { + assert(InGraphicsThread()); + DEBUG_CHECK_GL_ERROR; + + // Unbind if we're bound; otherwise if a new vao pops up with our same ID + // it'd be prevented from binding + if (g_vao_support) { + if (vao_ == renderer_->current_vertex_array_) { + renderer_->BindVertexArray(0); + } + if (!g_graphics_server->renderer_context_lost()) { + glDeleteVertexArrays(1, &vao_); + } + } else { + assert(fake_vao_); + delete fake_vao_; + fake_vao_ = nullptr; + } + // make sure our dying buffer isn't current.. + // (don't wanna prevent binding to a new buffer with a recycled id) + for (unsigned int vbo : vbos_) { + if (vbo == renderer_->active_array_buffer_) { + renderer_->active_array_buffer_ = -1; + } + } + if (!g_graphics_server->renderer_context_lost()) { + glDeleteBuffers(kBufferCount, vbos_); + DEBUG_CHECK_GL_ERROR; + } + } + + void Bind() { + if (g_vao_support) { + renderer_->BindVertexArray(vao_); + DEBUG_CHECK_GL_ERROR; + } else { + assert(fake_vao_); + fake_vao_->Bind(); + DEBUG_CHECK_GL_ERROR; + } + } + void Draw() { + DEBUG_CHECK_GL_ERROR; + if (elem_count_ > 0) { + glDrawElements(GL_TRIANGLES, elem_count_, index_type_, nullptr); + } + DEBUG_CHECK_GL_ERROR; + } + +#if BA_DEBUG_BUILD + auto name() const -> const std::string& { return name_; } +#endif + + private: +#if BA_DEBUG_BUILD + std::string name_; +#endif + + RendererGL* renderer_{}; + uint32_t elem_count_{}; + GLuint index_type_{}; + GLuint vao_{}; + GLuint vbos_[kBufferCount]{}; + FakeVertexArrayObject* fake_vao_{}; +}; // ModelDataGL + +class RendererGL::MeshDataGL : public MeshRendererData { + public: + enum BufferType { + kVertexBufferPrimary, + kIndexBuffer, + kVertexBufferSecondary + }; + enum Flags { + kUsesIndexBuffer = 1u, + kUsesSecondaryBuffer = 1u << 1u, + kUsesDynamicDraw = 1u << 2u + }; + MeshDataGL(RendererGL* renderer, uint32_t flags) + : renderer_(renderer), + uses_secondary_data_(static_cast(flags & kUsesSecondaryBuffer)), + uses_index_data_(static_cast(flags & kUsesIndexBuffer)) { + assert(InGraphicsThread()); + + // Create our vertex array to hold all this state. + if (g_vao_support) { + glGenVertexArrays(1, &vao_); + renderer->BindVertexArray(vao_); + } else { + fake_vao_ = new FakeVertexArrayObject(renderer_); + } + glGenBuffers(GetBufferCount(), vbos_); + } + auto uses_index_data() const -> bool { return uses_index_data_; } + + // Set us up to be recycled. + void Reset() { + index_state_ = primary_state_ = secondary_state_ = 0; + have_index_data_ = have_secondary_data_ = have_primary_data_ = false; + } + + void Bind() { + if (g_vao_support) { + renderer_->BindVertexArray(vao_); + DEBUG_CHECK_GL_ERROR; + } else { + assert(fake_vao_); + fake_vao_->Bind(); + DEBUG_CHECK_GL_ERROR; + } + } + + void Draw(DrawType draw_type) { + DEBUG_CHECK_GL_ERROR; + assert(have_primary_data_); + assert(have_index_data_ || !uses_index_data_); + assert(have_secondary_data_ || !uses_secondary_data_); + GLuint gl_draw_type; + switch (draw_type) { + case DrawType::kTriangles: + gl_draw_type = GL_TRIANGLES; + break; + case DrawType::kPoints: + gl_draw_type = GL_POINTS; + break; + default: + throw Exception(); + } + if (uses_index_data_) { + glDrawElements(gl_draw_type, elem_count_, index_type_, nullptr); + } else { + glDrawArrays(gl_draw_type, 0, elem_count_); + } + DEBUG_CHECK_GL_ERROR; + } + + ~MeshDataGL() override { + assert(InGraphicsThread()); + // unbind if we're bound .. otherwise we might prevent a new with our ID + // from binding + if (g_vao_support) { + if (vao_ == renderer_->current_vertex_array_) { + renderer_->BindVertexArray(0); + } + if (!g_graphics_server->renderer_context_lost()) { + glDeleteVertexArrays(1, &vao_); + } + } else { + assert(fake_vao_); + delete fake_vao_; + fake_vao_ = nullptr; + } + // make sure our dying buffer isn't current.. + // (don't wanna prevent binding to a new buffer with a recycled id) + for (int i = 0; i < GetBufferCount(); i++) { + if (vbos_[i] == renderer_->active_array_buffer_) { + renderer_->active_array_buffer_ = -1; + } + } + if (!g_graphics_server->renderer_context_lost()) { + glDeleteBuffers(GetBufferCount(), vbos_); + DEBUG_CHECK_GL_ERROR; + } + } + + void SetIndexData(MeshIndexBuffer32* data) { + assert(uses_index_data_); + if (data->state != index_state_) { + if (g_vao_support) { + renderer_->BindVertexArray(vao_); + } else { + assert(fake_vao_); + fake_vao_->SetElementBuffer(vbos_[kIndexBuffer]); + } + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbos_[kIndexBuffer]); + elem_count_ = static_cast(data->elements.size()); + assert(elem_count_ > 0); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, + static_cast_check_fit( + data->elements.size() * sizeof(data->elements[0])), + &data->elements[0], + dynamic_draw_ ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW); + index_state_ = data->state; + have_index_data_ = true; + BA_LOG_ONCE("GL WARNING - USING 32 BIT INDICES WHICH WONT WORK IN ES2!!"); + index_type_ = GL_UNSIGNED_INT; + } + } + void SetIndexData(MeshIndexBuffer16* data) { + assert(uses_index_data_); + if (data->state != index_state_) { + if (g_vao_support) { + renderer_->BindVertexArray(vao_); + } else { + assert(fake_vao_); + fake_vao_->SetElementBuffer(vbos_[kIndexBuffer]); + } + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbos_[kIndexBuffer]); + elem_count_ = static_cast(data->elements.size()); + assert(elem_count_ > 0); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, + static_cast_check_fit( + data->elements.size() * sizeof(data->elements[0])), + &data->elements[0], + dynamic_draw_ ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW); + index_state_ = data->state; + have_index_data_ = true; + index_type_ = GL_UNSIGNED_SHORT; + } + } + + // When dynamic-draw is on, it means *all* buffers should be flagged as + // dynamic. + void set_dynamic_draw(bool enable) { dynamic_draw_ = enable; } + + auto vao() const -> GLuint { return vao_; } + + protected: + template + void UpdateBufferData(BufferType buffer_type, MeshBuffer* data, + uint32_t* state, bool* have, GLuint draw_type) { + assert(state && have); + if (data->state != *state) { + DEBUG_CHECK_GL_ERROR; + + // Hmmm didnt think we had to have vao bound here but causes problems on + // qualcomm if not. +#if BA_OSTYPE_ANDROID + if (g_vao_support && renderer_->is_adreno_) { + renderer_->BindVertexArray(vao_); + } +#endif + renderer_->BindArrayBuffer(vbos_[buffer_type]); + assert(!data->elements.empty()); + if (!uses_index_data_ && buffer_type == kVertexBufferPrimary) { + elem_count_ = static_cast(data->elements.size()); + } + glBufferData(GL_ARRAY_BUFFER, + static_cast(data->elements.size() + * sizeof(data->elements[0])), + &(data->elements[0]), draw_type); + DEBUG_CHECK_GL_ERROR; + *state = data->state; + *have = true; + } else { + assert(*have); + } + } + + // FIXME - we should do some sort of ring-buffer system. + GLuint vbos_[3]{}; + GLuint vao_{}; + auto GetBufferCount() const -> int { + return uses_secondary_data_ ? 3 : (uses_index_data_ ? 2 : 1); + } + bool uses_index_data_{}; + bool uses_secondary_data_{}; + uint32_t index_state_{}; + uint32_t primary_state_{}; + uint32_t secondary_state_{}; + bool dynamic_draw_{}; + bool have_index_data_{}; + bool have_primary_data_{}; + bool have_secondary_data_{}; + RendererGL* renderer_{}; + uint32_t elem_count_{}; + GLuint index_type_{GL_UNSIGNED_SHORT}; + FakeVertexArrayObject* fake_vao_{}; +}; // MeshDataGL + +class RendererGL::MeshDataSimpleSplitGL : public RendererGL::MeshDataGL { + public: + explicit MeshDataSimpleSplitGL(RendererGL* renderer) + : MeshDataGL(renderer, kUsesSecondaryBuffer | kUsesIndexBuffer) { + // Set up our static vertex data. + if (fake_vao_) { + fake_vao_->SetAttribBuffer(vbos_[kVertexBufferPrimary], kVertexAttrUV, 2, + GL_UNSIGNED_SHORT, GL_TRUE, + sizeof(VertexSimpleSplitStatic), + offsetof(VertexSimpleSplitStatic, uv)); + } else { + renderer_->BindArrayBuffer(vbos_[kVertexBufferPrimary]); + glVertexAttribPointer( + kVertexAttrUV, 2, GL_UNSIGNED_SHORT, GL_TRUE, + sizeof(VertexSimpleSplitStatic), + reinterpret_cast(offsetof(VertexSimpleSplitStatic, uv))); + glEnableVertexAttribArray(kVertexAttrUV); + } + + // ..and our dynamic vertex data. + if (fake_vao_) { + fake_vao_->SetAttribBuffer(vbos_[kVertexBufferSecondary], + kVertexAttrPosition, 3, GL_FLOAT, GL_FALSE, + sizeof(VertexSimpleSplitDynamic), + offsetof(VertexSimpleSplitDynamic, position)); + } else { + renderer_->BindArrayBuffer(vbos_[kVertexBufferSecondary]); + glVertexAttribPointer(kVertexAttrPosition, 3, GL_FLOAT, GL_FALSE, + sizeof(VertexSimpleSplitDynamic), + reinterpret_cast( + offsetof(VertexSimpleSplitDynamic, position))); + glEnableVertexAttribArray(kVertexAttrPosition); + } + } + void SetStaticData(MeshBuffer* data) { + UpdateBufferData(kVertexBufferPrimary, data, &primary_state_, + &have_primary_data_, GL_STATIC_DRAW); + } + void SetDynamicData(MeshBuffer* data) { + assert(uses_secondary_data_); + UpdateBufferData(kVertexBufferSecondary, data, &secondary_state_, + &have_secondary_data_, + GL_DYNAMIC_DRAW); // this is *always* dynamic + } +}; + +class RendererGL::MeshDataObjectSplitGL : public RendererGL::MeshDataGL { + public: + explicit MeshDataObjectSplitGL(RendererGL* renderer) + : MeshDataGL(renderer, kUsesSecondaryBuffer | kUsesIndexBuffer) { + // Set up our static vertex data. + if (fake_vao_) { + fake_vao_->SetAttribBuffer(vbos_[kVertexBufferPrimary], kVertexAttrUV, 2, + GL_UNSIGNED_SHORT, GL_TRUE, + sizeof(VertexObjectSplitStatic), + offsetof(VertexObjectSplitStatic, uv)); + } else { + renderer_->BindArrayBuffer(vbos_[kVertexBufferPrimary]); + glVertexAttribPointer( + kVertexAttrUV, 2, GL_UNSIGNED_SHORT, GL_TRUE, + sizeof(VertexObjectSplitStatic), + reinterpret_cast(offsetof(VertexObjectSplitStatic, uv))); + glEnableVertexAttribArray(kVertexAttrUV); + } + + // ..and our dynamic vertex data. + if (fake_vao_) { + fake_vao_->SetAttribBuffer(vbos_[kVertexBufferSecondary], + kVertexAttrPosition, 3, GL_FLOAT, GL_FALSE, + sizeof(VertexObjectSplitDynamic), + offsetof(VertexObjectSplitDynamic, position)); + fake_vao_->SetAttribBuffer(vbos_[kVertexBufferSecondary], + kVertexAttrNormal, 3, GL_SHORT, GL_TRUE, + sizeof(VertexObjectSplitDynamic), + offsetof(VertexObjectSplitDynamic, normal)); + } else { + renderer_->BindArrayBuffer(vbos_[kVertexBufferSecondary]); + glVertexAttribPointer(kVertexAttrPosition, 3, GL_FLOAT, GL_FALSE, + sizeof(VertexObjectSplitDynamic), + reinterpret_cast( + offsetof(VertexObjectSplitDynamic, position))); + glEnableVertexAttribArray(kVertexAttrPosition); + glVertexAttribPointer( + kVertexAttrNormal, 3, GL_SHORT, GL_TRUE, + sizeof(VertexObjectSplitDynamic), + reinterpret_cast(offsetof(VertexObjectSplitDynamic, normal))); + glEnableVertexAttribArray(kVertexAttrNormal); + } + } + void SetStaticData(MeshBuffer* data) { + UpdateBufferData(kVertexBufferPrimary, data, &primary_state_, + &have_primary_data_, GL_STATIC_DRAW); + } + void SetDynamicData(MeshBuffer* data) { + assert(uses_secondary_data_); + UpdateBufferData(kVertexBufferSecondary, data, &secondary_state_, + &have_secondary_data_, + GL_DYNAMIC_DRAW); // this is *always* dynamic + } +}; + +class RendererGL::MeshDataSimpleFullGL : public RendererGL::MeshDataGL { + public: + explicit MeshDataSimpleFullGL(RendererGL* renderer) + : MeshDataGL(renderer, kUsesIndexBuffer) { + // Set up our vertex data. + if (fake_vao_) { + fake_vao_->SetAttribBuffer( + vbos_[kVertexBufferPrimary], kVertexAttrUV, 2, GL_UNSIGNED_SHORT, + GL_TRUE, sizeof(VertexSimpleFull), offsetof(VertexSimpleFull, uv)); + fake_vao_->SetAttribBuffer(vbos_[kVertexBufferPrimary], + kVertexAttrPosition, 3, GL_FLOAT, GL_FALSE, + sizeof(VertexSimpleFull), + offsetof(VertexSimpleFull, position)); + } else { + renderer_->BindArrayBuffer(vbos_[kVertexBufferPrimary]); + glVertexAttribPointer( + kVertexAttrUV, 2, GL_UNSIGNED_SHORT, GL_TRUE, + sizeof(VertexSimpleFull), + reinterpret_cast(offsetof(VertexSimpleFull, uv))); + glEnableVertexAttribArray(kVertexAttrUV); + glVertexAttribPointer( + kVertexAttrPosition, 3, GL_FLOAT, GL_FALSE, sizeof(VertexSimpleFull), + reinterpret_cast(offsetof(VertexSimpleFull, position))); + glEnableVertexAttribArray(kVertexAttrPosition); + } + } + void SetData(MeshBuffer* data) { + UpdateBufferData(kVertexBufferPrimary, data, &primary_state_, + &have_primary_data_, + dynamic_draw_ ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW); + } +}; + +class RendererGL::MeshDataDualTextureFullGL : public RendererGL::MeshDataGL { + public: + explicit MeshDataDualTextureFullGL(RendererGL* renderer) + : MeshDataGL(renderer, kUsesIndexBuffer) { + // Set up our vertex data. + if (fake_vao_) { + fake_vao_->SetAttribBuffer(vbos_[kVertexBufferPrimary], kVertexAttrUV, 2, + GL_UNSIGNED_SHORT, GL_TRUE, + sizeof(VertexDualTextureFull), + offsetof(VertexDualTextureFull, uv)); + fake_vao_->SetAttribBuffer(vbos_[kVertexBufferPrimary], kVertexAttrUV2, 2, + GL_UNSIGNED_SHORT, GL_TRUE, + sizeof(VertexDualTextureFull), + offsetof(VertexDualTextureFull, uv2)); + fake_vao_->SetAttribBuffer(vbos_[kVertexBufferPrimary], + kVertexAttrPosition, 3, GL_FLOAT, GL_FALSE, + sizeof(VertexDualTextureFull), + offsetof(VertexDualTextureFull, position)); + } else { + renderer_->BindArrayBuffer(vbos_[kVertexBufferPrimary]); + glVertexAttribPointer( + kVertexAttrUV, 2, GL_UNSIGNED_SHORT, GL_TRUE, + sizeof(VertexDualTextureFull), + reinterpret_cast(offsetof(VertexDualTextureFull, uv))); + glEnableVertexAttribArray(kVertexAttrUV); + glVertexAttribPointer( + kVertexAttrUV2, 2, GL_UNSIGNED_SHORT, GL_TRUE, + sizeof(VertexDualTextureFull), + reinterpret_cast(offsetof(VertexDualTextureFull, uv2))); + glEnableVertexAttribArray(kVertexAttrUV2); + glVertexAttribPointer( + kVertexAttrPosition, 3, GL_FLOAT, GL_FALSE, + sizeof(VertexDualTextureFull), + reinterpret_cast(offsetof(VertexDualTextureFull, position))); + glEnableVertexAttribArray(kVertexAttrPosition); + } + } + void SetData(MeshBuffer* data) { + UpdateBufferData(kVertexBufferPrimary, data, &primary_state_, + &have_primary_data_, + dynamic_draw_ ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW); + } +}; + +class RendererGL::MeshDataSmokeFullGL : public RendererGL::MeshDataGL { + public: + explicit MeshDataSmokeFullGL(RendererGL* renderer) + : MeshDataGL(renderer, kUsesIndexBuffer) { + // Set up our vertex data. + if (fake_vao_) { + fake_vao_->SetAttribBuffer(vbos_[kVertexBufferPrimary], kVertexAttrUV, 2, + GL_FLOAT, GL_FALSE, sizeof(VertexSmokeFull), + offsetof(VertexSmokeFull, uv)); + fake_vao_->SetAttribBuffer(vbos_[kVertexBufferPrimary], + kVertexAttrPosition, 3, GL_FLOAT, GL_FALSE, + sizeof(VertexSmokeFull), + offsetof(VertexSmokeFull, position)); + fake_vao_->SetAttribBuffer( + vbos_[kVertexBufferPrimary], kVertexAttrErode, 1, GL_UNSIGNED_BYTE, + GL_TRUE, sizeof(VertexSmokeFull), offsetof(VertexSmokeFull, erode)); + fake_vao_->SetAttribBuffer( + vbos_[kVertexBufferPrimary], kVertexAttrDiffuse, 1, GL_UNSIGNED_BYTE, + GL_TRUE, sizeof(VertexSmokeFull), offsetof(VertexSmokeFull, diffuse)); + fake_vao_->SetAttribBuffer( + vbos_[kVertexBufferPrimary], kVertexAttrColor, 4, GL_UNSIGNED_BYTE, + GL_TRUE, sizeof(VertexSmokeFull), offsetof(VertexSmokeFull, color)); + } else { + renderer_->BindArrayBuffer(vbos_[kVertexBufferPrimary]); + glVertexAttribPointer( + kVertexAttrUV, 2, GL_FLOAT, GL_FALSE, sizeof(VertexSmokeFull), + reinterpret_cast(offsetof(VertexSmokeFull, uv))); + glEnableVertexAttribArray(kVertexAttrUV); + glVertexAttribPointer( + kVertexAttrPosition, 3, GL_FLOAT, GL_FALSE, sizeof(VertexSmokeFull), + reinterpret_cast(offsetof(VertexSmokeFull, position))); + glEnableVertexAttribArray(kVertexAttrPosition); + glVertexAttribPointer( + kVertexAttrErode, 1, GL_UNSIGNED_BYTE, GL_TRUE, + sizeof(VertexSmokeFull), + reinterpret_cast(offsetof(VertexSmokeFull, erode))); + glEnableVertexAttribArray(kVertexAttrErode); + glVertexAttribPointer( + kVertexAttrDiffuse, 1, GL_UNSIGNED_BYTE, GL_TRUE, + sizeof(VertexSmokeFull), + reinterpret_cast(offsetof(VertexSmokeFull, diffuse))); + glEnableVertexAttribArray(kVertexAttrDiffuse); + glVertexAttribPointer( + kVertexAttrColor, 4, GL_UNSIGNED_BYTE, GL_TRUE, + sizeof(VertexSmokeFull), + reinterpret_cast(offsetof(VertexSmokeFull, color))); + glEnableVertexAttribArray(kVertexAttrColor); + } + } + void SetData(MeshBuffer* data) { + UpdateBufferData(kVertexBufferPrimary, data, &primary_state_, + &have_primary_data_, + dynamic_draw_ ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW); + } +}; + +class RendererGL::MeshDataSpriteGL : public RendererGL::MeshDataGL { + public: + explicit MeshDataSpriteGL(RendererGL* renderer) + : MeshDataGL(renderer, kUsesIndexBuffer) { + // Set up our vertex data. + if (fake_vao_) { + fake_vao_->SetAttribBuffer( + vbos_[kVertexBufferPrimary], kVertexAttrPosition, 3, GL_FLOAT, + GL_FALSE, sizeof(VertexSprite), offsetof(VertexSprite, position)); + fake_vao_->SetAttribBuffer( + vbos_[kVertexBufferPrimary], kVertexAttrUV, 2, GL_UNSIGNED_SHORT, + GL_TRUE, sizeof(VertexSprite), offsetof(VertexSprite, uv)); + fake_vao_->SetAttribBuffer(vbos_[kVertexBufferPrimary], kVertexAttrSize, + 1, GL_FLOAT, GL_FALSE, sizeof(VertexSprite), + offsetof(VertexSprite, size)); + fake_vao_->SetAttribBuffer(vbos_[kVertexBufferPrimary], kVertexAttrColor, + 4, GL_FLOAT, GL_FALSE, sizeof(VertexSprite), + offsetof(VertexSprite, color)); + } else { + renderer_->BindArrayBuffer(vbos_[kVertexBufferPrimary]); + glVertexAttribPointer( + kVertexAttrPosition, 3, GL_FLOAT, GL_FALSE, sizeof(VertexSprite), + reinterpret_cast(offsetof(VertexSprite, position))); + glEnableVertexAttribArray(kVertexAttrPosition); + glVertexAttribPointer( + kVertexAttrUV, 2, GL_UNSIGNED_SHORT, GL_TRUE, sizeof(VertexSprite), + reinterpret_cast(offsetof(VertexSprite, uv))); + glEnableVertexAttribArray(kVertexAttrUV); + glVertexAttribPointer( + kVertexAttrSize, 1, GL_FLOAT, GL_FALSE, sizeof(VertexSprite), + reinterpret_cast(offsetof(VertexSprite, size))); + glEnableVertexAttribArray(kVertexAttrSize); + glVertexAttribPointer( + kVertexAttrColor, 4, GL_FLOAT, GL_FALSE, sizeof(VertexSprite), + reinterpret_cast(offsetof(VertexSprite, color))); + glEnableVertexAttribArray(kVertexAttrColor); + } + } + void SetData(MeshBuffer* data) { + UpdateBufferData(kVertexBufferPrimary, data, &primary_state_, + &have_primary_data_, + dynamic_draw_ ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW); + } +}; + +class RendererGL::RenderTargetGL : public RenderTarget { + public: + void Bind() { + if (type_ == Type::kFramebuffer) { + assert(framebuffer_.exists()); + framebuffer_->Bind(); + } else { + assert(type_ == Type::kScreen); + renderer_->BindFramebuffer(renderer_->screen_framebuffer_); + } + } + + void DrawBegin(bool must_clear_color, float clear_r, float clear_g, + float clear_b, float clear_a) override { + assert(InGraphicsThread()); + DEBUG_CHECK_GL_ERROR; + + Bind(); + +#if BA_CARDBOARD_BUILD + int x, y; + // viewport offsets only apply to the screen render-target + if (type_ == Type::kScreen) { + x = renderer_->VRGetViewportX(); + y = renderer_->VRGetViewportY(); + } else { + x = 0; + y = 0; + } + renderer_->SetViewport(x, y, physical_width_, physical_height_); +#else + renderer_->SetViewport(0, 0, static_cast(physical_width_), + static_cast(physical_height_)); +#endif + + { + // Clear depth, color, etc. + GLuint clear_mask = 0; + + // If they *requested* a clear for color, do so. Otherwise invalidate. + if (must_clear_color) { + clear_mask |= GL_COLOR_BUFFER_BIT; + } else { + renderer_->InvalidateFramebuffer(true, false, false); + } + + if (depth_) { + // FIXME make sure depth writing is turned on at this point. + // this needs to be on for glClear to work on depth. + if (!renderer_->depth_writing_enabled_) { + BA_LOG_ONCE( + "RendererGL: depth-writing not enabled when clearing depth"); + } + clear_mask |= GL_DEPTH_BUFFER_BIT; + } + + if (clear_mask != 0) { + if (clear_mask & GL_COLOR_BUFFER_BIT) { + glClearColor(clear_r, clear_g, clear_b, clear_a); + DEBUG_CHECK_GL_ERROR; + } + glClear(clear_mask); + DEBUG_CHECK_GL_ERROR; + } + } + } + + auto GetFramebufferID() -> GLuint { + if (type_ == Type::kFramebuffer) { + assert(framebuffer_.exists()); + return framebuffer_->id(); + } else { + return 0; // screen + } + } + auto framebuffer() -> FramebufferObjectGL* { + assert(type_ == Type::kFramebuffer); + return framebuffer_.get(); + } + // Screen. + explicit RenderTargetGL(RendererGL* renderer) + : RenderTarget(Type::kScreen), renderer_(renderer) { + assert(InGraphicsThread()); + depth_ = true; + + // This will update our width/height values. + ScreenSizeChanged(); + } + + // Framebuffer. + RenderTargetGL(RendererGL* renderer, int width, int height, + bool linear_interp, bool depth, bool texture, + bool depth_texture, bool high_quality, bool msaa, bool alpha) + : RenderTarget(Type::kFramebuffer), renderer_(renderer) { + assert(InGraphicsThread()); + DEBUG_CHECK_GL_ERROR; + framebuffer_ = Object::New( + renderer, width, height, linear_interp, depth, texture, depth_texture, + high_quality, msaa, alpha); + physical_width_ = static_cast(width); + physical_height_ = static_cast(height); + depth_ = depth; + DEBUG_CHECK_GL_ERROR; + } + + private: + Object::Ref framebuffer_; + RendererGL* renderer_{}; + friend class RenderPass; +}; // RenderTargetGL + +RendererGL::RendererGL() { + if (explicit_bool(FORCE_CHECK_GL_ERRORS)) { + ScreenMessage("GL ERROR CHECKS ENABLED"); + } + + // For some reason we're getting an immediately + // GL_INVALID_FRAMEBUFFER_OPERATION on EL-CAPITAN, though we shouldn't have + // run any gl code yet. might be worth looking into at some point, but gonna + // ignore for now. +#if BA_OSTYPE_MACOS + glGetError(); +#endif // BA_OSTYPE_MACOS + + assert(InGraphicsThread()); + DEBUG_CHECK_GL_ERROR; + + SyncGLState(); + DEBUG_CHECK_GL_ERROR; +} + +void RendererGL::CheckFunkyDepthIssue() { + if (funky_depth_issue_set_) { + return; + } + + // Note: this test fails for some reason on some Broadcom VideoCore and older + // NVidia chips (tegra 2?) + // ...so lets limit testing to adreno chips since that's the only place the + // problem is known to happen. + if (!is_adreno_ || !supports_depth_textures_) { + funky_depth_issue_set_ = true; + funky_depth_issue_ = false; + return; + } + + // on some adreno chips, depth buffer values are always returned + // in a 0-1 range in shaders even if a depth range is set; everywhere + // else they return that depth range. + // to test for this we can create a temp buffer, clear it, set a depth range, + // Log("RUNNING DEPTH TEST"); + + Object::Ref test_rt1; + Object::Ref test_rt2; + + test_rt1 = Object::New(this, 32, 32, true, true, true, true, + false, false, false); + DEBUG_CHECK_GL_ERROR; + test_rt2 = Object::New(this, 32, 32, true, false, true, false, + false, false, false); + DEBUG_CHECK_GL_ERROR; + + // this screws up some qualcomm chips.. + SetDepthRange(0.0f, 0.5f); + + // draw a flat color plane into our first render target + SetDepthWriting(true); + SetDepthTesting(true); + SetBlend(false); + SetDoubleSided(false); + test_rt1->DrawBegin(true, 1.0f, 1.0f, 1.0f, 1.0f); + SimpleProgramGL* p = simple_color_prog_; + p->Bind(); + p->SetColor(1, 0, 1); + g_graphics_server->ModelViewReset(); + g_graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1); + GetActiveProgram()->PrepareToDraw(); + screen_mesh_->Bind(); + screen_mesh_->Draw(DrawType::kTriangles); + DEBUG_CHECK_GL_ERROR; + + // now draw into a second buffer the difference between the + // depth tex lookup and the gl frag depth. + SetDepthWriting(false); + SetDepthTesting(false); + SetBlend(false); + SetDoubleSided(false); + test_rt2->DrawBegin(false, 1.0f, 1.0f, 1.0f, 1.0f); + p = simple_tex_dtest_prog_; + p->Bind(); + g_graphics_server->ModelViewReset(); + g_graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1); + p->SetColorTexture(test_rt1->framebuffer()->depth_texture()); + GetActiveProgram()->PrepareToDraw(); + screen_mesh_->Bind(); + screen_mesh_->Draw(DrawType::kTriangles); + DEBUG_CHECK_GL_ERROR; + + // now sample a pixel from our render-target + // if the depths matched, the value will be 0; otherwise it'll be 30 or so + // (allow a bit of leeway to account for dithering/etc..) + uint8_t buffer[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + glReadPixels(0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, buffer); + + // sample 4 pixels to reduce effects of dithering.. + funky_depth_issue_ = + ((buffer[0] + buffer[4] + buffer[8] + buffer[12]) / 4 >= 15); + funky_depth_issue_set_ = true; + + DEBUG_CHECK_GL_ERROR; +} + +void RendererGL::PushGroupMarker(const char* label) { + GL_PUSH_GROUP_MARKER(label); +} +void RendererGL::PopGroupMarker() { GL_POP_GROUP_MARKER(); } + +void RendererGL::InvalidateFramebuffer(bool color, bool depth, + bool target_read_framebuffer) { + DEBUG_CHECK_GL_ERROR; + + // currently discard is mobile only +#if BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID + + if (g_discard_framebuffer_support || g_invalidate_framebuffer_support) { + GLenum attachments[5]; + // need to use different flags for the main framebuffer.. + int count = 0; + if (active_framebuffer_ == 0 && !target_read_framebuffer) { + if (color) { + attachments[count++] = GL_COLOR_EXT; + } + if (depth) { + attachments[count++] = GL_DEPTH_EXT; + } + } else { + if (color) { + attachments[count++] = GL_COLOR_ATTACHMENT0; + } + if (depth) { + attachments[count++] = GL_DEPTH_ATTACHMENT; + } + } + // apparently the oculus docs say glInvalidateFramebuffer errors + // on a mali es3 implementation so they always use glDiscard when present... + if (g_invalidate_framebuffer_support) { +#if BA_OSTYPE_IOS_TVOS + throw Exception(); // shouldnt happen +#else + glInvalidateFramebuffer( + target_read_framebuffer ? GL_READ_FRAMEBUFFER : GL_FRAMEBUFFER, count, + attachments); +#endif + } else { + // if we've got a read-framebuffer we should have invalidate too.. + assert(!target_read_framebuffer); + glDiscardFramebufferEXT(GL_FRAMEBUFFER, count, attachments); + } + DEBUG_CHECK_GL_ERROR; + } +#endif // BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID +} + +RendererGL::~RendererGL() { + assert(InGraphicsThread()); + printf("FIXME: need to unload renderer on destroy.\n"); + // Unload(); + DEBUG_CHECK_GL_ERROR; +} + +void RendererGL::UseProgram(ProgramGL* p) { + if (p != current_program_) { + glUseProgram(p->program()); + current_program_ = p; + } +} + +void RendererGL::SyncGLState() { +#if BA_RIFT_BUILD + if (IsVRMode()) { + glFrontFace(GL_CCW); + } + + // if (time(nullptr)%2 == 0) { + // glEnable(GL_FRAMEBUFFER_SRGB); + // } else { + // glDisable(GL_FRAMEBUFFER_SRGB); + // } +#endif // BA_RIFT_BUILD + + active_tex_unit_ = -1; // force a set next time + active_framebuffer_ = -1; // ditto + active_array_buffer_ = -1; // ditto + for (int i = 0; i < kMaxGLTexUnitsUsed; i++) { + bound_textures_2d_[i] = -1; // ditto + bound_textures_cube_map_[i] = -1; // ditto + } + glUseProgram(0); + current_program_ = nullptr; + current_vertex_array_ = 0; + + if (g_vao_support) { + glBindVertexArray(0); + } else { + for (GLuint i = 0; i < kVertexAttrCount; i++) { + glDisableVertexAttribArray(i); + vertex_attrib_arrays_enabled_[i] = false; + } + } + + // wack these out so the next call will definitely call glViewport + viewport_x_ = -9999; + viewport_y_ = -9999; + viewport_width_ = -9999; + viewport_height_ = -9999; + + glDisable(GL_BLEND); + blend_ = false; + + // currently we only ever write to an alpha buffer for our vr flat overlay + // texture, and in that case we need alpha to accumulate; not get overwritten. + // could probably enable this everywhere but I don't know if it's supported on + // all hardware or slower.. + if (IsVRMode()) { +#if BA_OSTYPE_WINDOWS + if (glBlendFuncSeparate == nullptr) { + throw Exception( + "VR mode is not supported by your GPU (no glBlendFuncSeparate); Try " + "updating your drivers?..."); + } +#endif // BA_WINDOWS_BUILD + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, + GL_ONE_MINUS_SRC_ALPHA); + } else { + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } + blend_premult_ = false; + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + double_sided_ = false; + draw_front_ = true; + glDisable(GL_DEPTH_TEST); + depth_testing_enabled_ = false; + glDepthMask(static_cast(true)); + depth_writing_enabled_ = true; + draw_at_equal_depth_ = false; + glDepthFunc(GL_LESS); + depth_range_min_ = 0.0f; + depth_range_max_ = 1.0f; + glDepthRange(depth_range_min_, depth_range_max_); +} + +#define GET_MESH_DATA(TYPE, VAR) \ + auto* VAR = static_cast(mesh_data->renderer_data()); \ + assert(VAR&& VAR == dynamic_cast(mesh_data->renderer_data())) + +#define GET_INDEX_BUFFER() \ + assert(buffer != buffers.end()); \ + assert(index_size != index_sizes.end()); \ + MeshIndexBuffer16* indices16{nullptr}; \ + MeshIndexBuffer32* indices32{nullptr}; \ + assert(*index_size == 4 || *index_size == 2); \ + bool use_indices32 = (*index_size == 4); \ + if (use_indices32) { \ + indices32 = static_cast(buffer->get()); \ + assert(indices32&& indices32 \ + == dynamic_cast(buffer->get())); \ + } else { \ + indices16 = static_cast(buffer->get()); \ + assert(indices16&& indices16 \ + == dynamic_cast(buffer->get())); \ + } \ + index_size++; \ + buffer++ + +#define GET_BUFFER(TYPE, VAR) \ + assert(buffer != buffers.end()); \ + auto* VAR = static_cast(buffer->get()); \ + assert(VAR&& VAR == dynamic_cast(buffer->get())); \ + buffer++ + +// Takes all latest mesh data from the client side and applies it +// to our gl implementations. +void RendererGL::UpdateMeshes( + const std::vector >& meshes, + const std::vector& index_sizes, + const std::vector >& buffers) { + auto index_size = index_sizes.begin(); + auto buffer = buffers.begin(); + for (auto&& mesh : meshes) { + // For each mesh, plug in the latest and greatest buffers it + // should be using. + MeshData* mesh_data = mesh->mesh_data; + switch (mesh_data->type()) { + case MeshDataType::kIndexedSimpleSplit: { + GET_MESH_DATA(MeshDataSimpleSplitGL, m); + GET_INDEX_BUFFER(); + GET_BUFFER(MeshBuffer, static_data); + GET_BUFFER(MeshBuffer, dynamic_data); + if (use_indices32) { + m->SetIndexData(indices32); + } else { + m->SetIndexData(indices16); + } + m->SetStaticData(static_data); + m->SetDynamicData(dynamic_data); + break; + } + case MeshDataType::kIndexedObjectSplit: { + GET_MESH_DATA(MeshDataObjectSplitGL, m); + GET_INDEX_BUFFER(); + GET_BUFFER(MeshBuffer, static_data); + GET_BUFFER(MeshBuffer, dynamic_data); + if (use_indices32) { + m->SetIndexData(indices32); + } else { + m->SetIndexData(indices16); + } + m->SetStaticData(static_data); + m->SetDynamicData(dynamic_data); + break; + } + case MeshDataType::kIndexedSimpleFull: { + GET_MESH_DATA(MeshDataSimpleFullGL, m); + GET_INDEX_BUFFER(); + GET_BUFFER(MeshBuffer, data); + if (use_indices32) { + m->SetIndexData(indices32); + } else { + m->SetIndexData(indices16); + } + m->SetData(data); + break; + } + case MeshDataType::kIndexedDualTextureFull: { + GET_MESH_DATA(MeshDataDualTextureFullGL, m); + GET_INDEX_BUFFER(); + GET_BUFFER(MeshBuffer, data); + if (use_indices32) { + m->SetIndexData(indices32); + } else { + m->SetIndexData(indices16); + } + m->SetData(data); + break; + } + case MeshDataType::kIndexedSmokeFull: { + GET_MESH_DATA(MeshDataSmokeFullGL, m); + GET_INDEX_BUFFER(); + GET_BUFFER(MeshBuffer, data); + if (use_indices32) { + m->SetIndexData(indices32); + } else { + m->SetIndexData(indices16); + } + m->SetData(data); + break; + } + case MeshDataType::kSprite: { + GET_MESH_DATA(MeshDataSpriteGL, m); + GET_INDEX_BUFFER(); + GET_BUFFER(MeshBuffer, data); + if (use_indices32) { + m->SetIndexData(indices32); + } else { + m->SetIndexData(indices16); + } + m->SetData(data); + break; + } + default: + throw Exception("Invalid meshdata type: " + + std::to_string(static_cast(mesh_data->type()))); + } + } + // We should have gone through all lists exactly.. + assert(index_size == index_sizes.end()); + assert(buffer == buffers.end()); +} +#undef GET_MESH_DATA +#undef GET_BUFFER +#undef GET_INDEX_BUFFER + +void RendererGL::StandardPostProcessSetup(PostProcessProgramGL* p, + const RenderPass& pass) { + auto* cam_target = static_cast(camera_render_target()); + assert(cam_target + && dynamic_cast(camera_render_target()) + == cam_target); + RenderPass* beauty_pass = pass.frame_def()->beauty_pass(); + assert(beauty_pass); + SetDoubleSided(false); + SetBlend(false); + p->Bind(); + p->SetColorTexture(cam_target->framebuffer()->texture()); + if (p->UsesSlightBlurredTex()) { + p->SetColorSlightBlurredTexture(blur_buffers_[0]->texture()); + } + if (blur_buffers_.size() > 1) { + if (p->UsesBlurredTexture()) { + p->SetColorBlurredTexture(blur_buffers_[1]->texture()); + } + p->SetColorBlurredMoreTexture( + blur_buffers_[blur_buffers_.size() - 1]->texture()); + } else { + if (p->UsesBlurredTexture()) { + p->SetColorBlurredTexture(blur_buffers_[0]->texture()); + } + p->SetColorBlurredMoreTexture(blur_buffers_[0]->texture()); + } + p->SetDepthTexture(cam_target->framebuffer()->depth_texture()); + float dof_near_smoothed = this->dof_near_smoothed(); + float dof_far_smoothed = this->dof_far_smoothed(); + if (pass.frame_def()->orbiting()) { + p->SetDepthOfFieldRanges( + GetZBufferValue(beauty_pass, 0.80f * dof_near_smoothed), + GetZBufferValue(beauty_pass, 0.91f * dof_near_smoothed), + GetZBufferValue(beauty_pass, 1.01f * dof_far_smoothed), + GetZBufferValue(beauty_pass, 1.10f * dof_far_smoothed)); + } else { + p->SetDepthOfFieldRanges( + GetZBufferValue(beauty_pass, 0.93f * dof_near_smoothed), + GetZBufferValue(beauty_pass, 0.99f * dof_near_smoothed), + GetZBufferValue(beauty_pass, 1.03f * dof_far_smoothed), + GetZBufferValue(beauty_pass, 1.09f * dof_far_smoothed)); + } +} + +void RendererGL::ProcessRenderCommandBuffer(RenderCommandBuffer* buffer, + const RenderPass& pass, + RenderTarget* render_target) { + buffer->ReadBegin(); + RenderCommandBuffer::Command cmd; + while ((cmd = buffer->GetCommand()) != RenderCommandBuffer::Command::kEnd) { + switch (cmd) { + case RenderCommandBuffer::Command::kEnd: + break; + case RenderCommandBuffer::Command::kShader: { + auto shader = static_cast(buffer->GetInt()); + switch (shader) { + case ShadingType::kSimpleColor: { + SetDoubleSided(false); + SetBlend(false); + SimpleProgramGL* p = simple_color_prog_; + p->Bind(); + float r, g, b; + buffer->GetFloats(&r, &g, &b); + p->SetColor(r, g, b); + break; + } + case ShadingType::kSimpleColorTransparent: { + SetDoubleSided(false); + bool premult = static_cast(buffer->GetInt()); + SetBlend(true); + SetBlendPremult(premult); + SimpleProgramGL* p = simple_color_prog_; + p->Bind(); + float r, g, b, a; + buffer->GetFloats(&r, &g, &b, &a); + p->SetColor(r, g, b, a); + break; + } + case ShadingType::kSimpleColorTransparentDoubleSided: { + SetDoubleSided(true); + bool premult = static_cast(buffer->GetInt()); + SetBlend(true); + SetBlendPremult(premult); + SimpleProgramGL* p = simple_color_prog_; + p->Bind(); + float r, g, b, a; + buffer->GetFloats(&r, &g, &b, &a); + p->SetColor(r, g, b, a); + break; + } + case ShadingType::kSimpleTexture: { + SetDoubleSided(false); + SetBlend(false); + SimpleProgramGL* p = simple_tex_prog_; + p->Bind(); + p->SetColorTexture(buffer->GetTexture()); + break; + } + case ShadingType::kSimpleTextureModulatedTransparent: { + SetDoubleSided(false); + bool premult = static_cast(buffer->GetInt()); + SetBlend(true); + SetBlendPremult(premult); + float r, g, b, a; + buffer->GetFloats(&r, &g, &b, &a); + SimpleProgramGL* p = simple_tex_mod_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + p->SetColorTexture(buffer->GetTexture()); + break; + } + case ShadingType::kSimpleTextureModulatedTransFlatness: { + SetDoubleSided(false); + bool premult = static_cast(buffer->GetInt()); + SetBlend(true); + SetBlendPremult(premult); + float r, g, b, a, flatness; + buffer->GetFloats(&r, &g, &b, &a, &flatness); + SimpleProgramGL* p = simple_tex_mod_flatness_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + p->SetColorTexture(buffer->GetTexture()); + p->SetFlatness(flatness); + break; + } + case ShadingType::kSimpleTextureModulatedTransparentShadow: { + SetDoubleSided(false); + bool premult = static_cast(buffer->GetInt()); + SetBlend(true); + SetBlendPremult(premult); + float r, g, b, a, shadow_offset_x, shadow_offset_y, shadow_blur, + shadow_opacity; + buffer->GetFloats(&r, &g, &b, &a, &shadow_offset_x, + &shadow_offset_y, &shadow_blur, &shadow_opacity); + SimpleProgramGL* p = simple_tex_mod_shadow_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + const TextureData* t = buffer->GetTexture(); + const TextureData* t_mask = buffer->GetTexture(); + p->SetColorTexture(t); + // If this isn't a full-res texture, ramp down the blurring we do. + p->SetShadow(shadow_offset_x, shadow_offset_y, + std::max(0.0f, shadow_blur), shadow_opacity); + p->SetMaskUV2Texture(t_mask); + break; + } + case ShadingType::kSimpleTexModulatedTransShadowFlatness: { + SetDoubleSided(false); + bool premult = static_cast(buffer->GetInt()); + SetBlend(true); + SetBlendPremult(premult); + float r, g, b, a, shadow_offset_x, shadow_offset_y, shadow_blur, + shadow_opacity, flatness; + buffer->GetFloats(&r, &g, &b, &a, &shadow_offset_x, + &shadow_offset_y, &shadow_blur, &shadow_opacity, + &flatness); + SimpleProgramGL* p = simple_tex_mod_shadow_flatness_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + const TextureData* t = buffer->GetTexture(); + const TextureData* t_mask = buffer->GetTexture(); + p->SetColorTexture(t); + // If this isn't a full-res texture, ramp down the blurring we do. + p->SetShadow(shadow_offset_x, shadow_offset_y, + std::max(0.0f, shadow_blur), shadow_opacity); + p->SetMaskUV2Texture(t_mask); + p->SetFlatness(flatness); + break; + } + case ShadingType::kSimpleTextureModulatedTransparentGlow: { + SetDoubleSided(false); + bool premult = static_cast(buffer->GetInt()); + SetBlend(true); + SetBlendPremult(premult); + float r, g, b, a, glow_amount, glow_blur; + buffer->GetFloats(&r, &g, &b, &a, &glow_amount, &glow_blur); + SimpleProgramGL* p = simple_tex_mod_glow_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + const TextureData* t = buffer->GetTexture(); + p->SetColorTexture(t); + + // Glow. + p->setGlow(glow_amount, std::max(0.0f, glow_blur)); + break; + } + case ShadingType::kSimpleTextureModulatedTransparentGlowMaskUV2: { + SetDoubleSided(false); + bool premult = static_cast(buffer->GetInt()); + SetBlend(true); + SetBlendPremult(premult); + float r, g, b, a, glow_amount, glow_blur; + buffer->GetFloats(&r, &g, &b, &a, &glow_amount, &glow_blur); + SimpleProgramGL* p = simple_tex_mod_glow_maskuv2_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + const TextureData* t = buffer->GetTexture(); + p->SetColorTexture(t); + const TextureData* t_mask = buffer->GetTexture(); + p->SetMaskUV2Texture(t_mask); + // Glow. + p->setGlow(glow_amount, std::max(0.0f, glow_blur)); + break; + } + case ShadingType::kSimpleTextureModulatedTransparentDoubleSided: { + SetDoubleSided(true); + bool premult = static_cast(buffer->GetInt()); + SetBlend(true); + SetBlendPremult(premult); + float r, g, b, a; + buffer->GetFloats(&r, &g, &b, &a); + SimpleProgramGL* p = simple_tex_mod_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + p->SetColorTexture(buffer->GetTexture()); + break; + } + case ShadingType::kSimpleTextureModulated: { + SetDoubleSided(false); + SetBlend(false); + float r, g, b; + buffer->GetFloats(&r, &g, &b); + SimpleProgramGL* p = simple_tex_mod_prog_; + p->Bind(); + p->SetColor(r, g, b); + p->SetColorTexture(buffer->GetTexture()); + break; + } + case ShadingType::kSimpleTextureModulatedColorized: { + SetDoubleSided(false); + SetBlend(false); + float r, g, b, colorize_r, colorize_g, colorize_b; + buffer->GetFloats(&r, &g, &b, &colorize_r, &colorize_g, + &colorize_b); + SimpleProgramGL* p = simple_tex_mod_colorized_prog_; + p->Bind(); + p->SetColor(r, g, b); + p->SetColorTexture(buffer->GetTexture()); + p->SetColorizeColor(colorize_r, colorize_g, colorize_b); + p->SetColorizeTexture(buffer->GetTexture()); + break; + } + case ShadingType::kSimpleTextureModulatedColorized2: { + SetDoubleSided(false); + SetBlend(false); + float r, g, b, colorize_r, colorize_g, colorize_b, colorize2_r, + colorize2_g, colorize2_b; + buffer->GetFloats(&r, &g, &b, &colorize_r, &colorize_g, &colorize_b, + &colorize2_r, &colorize2_g, &colorize2_b); + SimpleProgramGL* p = simple_tex_mod_colorized2_prog_; + p->Bind(); + p->SetColor(r, g, b); + p->SetColorTexture(buffer->GetTexture()); + p->SetColorizeTexture(buffer->GetTexture()); + p->SetColorizeColor(colorize_r, colorize_g, colorize_b); + p->SetColorize2Color(colorize2_r, colorize2_g, colorize2_b); + break; + } + case ShadingType::kSimpleTextureModulatedColorized2Masked: { + SetDoubleSided(false); + SetBlend(false); + float r, g, b, a, colorize_r, colorize_g, colorize_b, colorize2_r, + colorize2_g, colorize2_b; + buffer->GetFloats(&r, &g, &b, &a, &colorize_r, &colorize_g, + &colorize_b, &colorize2_r, &colorize2_g, + &colorize2_b); + SimpleProgramGL* p = simple_tex_mod_colorized2_masked_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + p->SetColorTexture(buffer->GetTexture()); + p->SetColorizeColor(colorize_r, colorize_g, colorize_b); + p->SetColorize2Color(colorize2_r, colorize2_g, colorize2_b); + p->SetColorizeTexture(buffer->GetTexture()); + p->SetMaskTexture(buffer->GetTexture()); + break; + } + case ShadingType::kSimpleTextureModulatedTransparentColorized: { + SetDoubleSided(false); + bool premult = static_cast(buffer->GetInt()); + SetBlend(true); + SetBlendPremult(premult); + float r, g, b, a, colorize_r, colorize_g, colorize_b; + buffer->GetFloats(&r, &g, &b, &a, &colorize_r, &colorize_g, + &colorize_b); + SimpleProgramGL* p = simple_tex_mod_colorized_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + p->SetColorTexture(buffer->GetTexture()); + p->SetColorizeColor(colorize_r, colorize_g, colorize_b); + p->SetColorizeTexture(buffer->GetTexture()); + break; + } + case ShadingType::kSimpleTextureModulatedTransparentColorized2: { + SetDoubleSided(false); + bool premult = static_cast(buffer->GetInt()); + SetBlend(true); + SetBlendPremult(premult); + float r, g, b, a, colorize_r, colorize_g, colorize_b, colorize2_r, + colorize2_g, colorize2_b; + buffer->GetFloats(&r, &g, &b, &a, &colorize_r, &colorize_g, + &colorize_b, &colorize2_r, &colorize2_g, + &colorize2_b); + SimpleProgramGL* p = simple_tex_mod_colorized2_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + p->SetColorTexture(buffer->GetTexture()); + p->SetColorizeColor(colorize_r, colorize_g, colorize_b); + p->SetColorize2Color(colorize2_r, colorize2_g, colorize2_b); + p->SetColorizeTexture(buffer->GetTexture()); + break; + } + case ShadingType:: + kSimpleTextureModulatedTransparentColorized2Masked: { + SetDoubleSided(false); + bool premult = static_cast(buffer->GetInt()); + SetBlend(true); + SetBlendPremult(premult); + float r, g, b, a, colorize_r, colorize_g, colorize_b, colorize2_r, + colorize2_g, colorize2_b; + buffer->GetFloats(&r, &g, &b, &a, &colorize_r, &colorize_g, + &colorize_b, &colorize2_r, &colorize2_g, + &colorize2_b); + SimpleProgramGL* p = simple_tex_mod_colorized2_masked_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + p->SetColorTexture(buffer->GetTexture()); + p->SetColorizeColor(colorize_r, colorize_g, colorize_b); + p->SetColorize2Color(colorize2_r, colorize2_g, colorize2_b); + p->SetColorizeTexture(buffer->GetTexture()); + p->SetMaskTexture(buffer->GetTexture()); + break; + } + case ShadingType::kObject: { + SetDoubleSided(false); + SetBlend(false); + float r, g, b; + buffer->GetFloats(&r, &g, &b); + ObjectProgramGL* p = obj_prog_; + p->Bind(); + p->SetColor(r, g, b); + p->SetColorTexture(buffer->GetTexture()); + p->SetVignetteTexture(vignette_tex_); + break; + } + case ShadingType::kSmoke: { + SetDoubleSided(true); + SetBlend(true); + SetBlendPremult(true); + float r, g, b, a; + buffer->GetFloats(&r, &g, &b, &a); + SmokeProgramGL* p = smoke_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + p->SetColorTexture(buffer->GetTexture()); + break; + } + case ShadingType::kSmokeOverlay: { + SetDoubleSided(true); + SetBlend(true); + SetBlendPremult(true); + float r, g, b, a; + buffer->GetFloats(&r, &g, &b, &a); + SmokeProgramGL* p = smoke_overlay_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + p->SetColorTexture(buffer->GetTexture()); + p->SetDepthTexture( + static_cast(camera_render_target()) + ->framebuffer() + ->depth_texture()); + p->SetBlurTexture( + blur_buffers_[blur_buffers_.size() - 1]->texture()); + break; + } + case ShadingType::kPostProcessNormalDistort: { + float distort = buffer->GetFloat(); + PostProcessProgramGL* p = postprocess_distort_prog_; + StandardPostProcessSetup(p, pass); + p->SetDistort(distort); + break; + } + case ShadingType::kPostProcess: { + PostProcessProgramGL* p = postprocess_prog_; + StandardPostProcessSetup(p, pass); + break; + } + case ShadingType::kPostProcessEyes: { + assert(postprocess_eyes_prog_); + PostProcessProgramGL* p = postprocess_eyes_prog_; + StandardPostProcessSetup(p, pass); + break; + } + case ShadingType::kSprite: { + SetDoubleSided(false); + SetBlend(true); + SetBlendPremult(true); + + float r, g, b, a; + buffer->GetFloats(&r, &g, &b, &a); + bool overlay = static_cast(buffer->GetInt()); + bool cam_aligned = static_cast(buffer->GetInt()); + + SpriteProgramGL* p; + if (cam_aligned) { + if (overlay) { + p = sprite_camalign_overlay_prog_; + } else { + p = sprite_camalign_prog_; + } + } else { + assert(!overlay); // Unsupported combo. + p = sprite_prog_; + } + p->Bind(); + if (overlay) { + p->SetDepthTexture( + static_cast(camera_render_target()) + ->framebuffer() + ->depth_texture()); + } + p->SetColor(r, g, b, a); + p->SetColorTexture(buffer->GetTexture()); + break; + } + case ShadingType::kObjectTransparent: { + SetDoubleSided(false); + bool premult = static_cast(buffer->GetInt()); + + SetBlend(true); + SetBlendPremult(premult); + + float r, g, b, a; + buffer->GetFloats(&r, &g, &b, &a); + ObjectProgramGL* p = obj_transparent_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + p->SetColorTexture(buffer->GetTexture()); + p->SetVignetteTexture(vignette_tex_); + break; + } + case ShadingType::kObjectLightShadow: { + SetDoubleSided(false); + SetBlend(false); + auto light_shadow = static_cast(buffer->GetInt()); + int world_space = buffer->GetInt(); + float r, g, b; + buffer->GetFloats(&r, &g, &b); + ObjectProgramGL* p = world_space ? obj_lightshad_worldspace_prog_ + : obj_lightshad_prog_; + p->Bind(); + p->SetColor(r, g, b); + p->SetColorTexture(buffer->GetTexture()); + p->SetVignetteTexture(vignette_tex_); + GLuint light_shadow_tex; + switch (light_shadow) { + case LightShadowType::kTerrain: + light_shadow_tex = + static_cast(light_shadow_render_target()) + ->framebuffer() + ->texture(); + break; + case LightShadowType::kObject: + light_shadow_tex = + static_cast(light_render_target()) + ->framebuffer() + ->texture(); + break; + default: + throw Exception(); + } + p->SetLightShadowTexture(light_shadow_tex); + break; + } + case ShadingType::kObjectLightShadowTransparent: { + SetDoubleSided(false); + bool premult = static_cast(buffer->GetInt()); + SetBlend(true); + SetBlendPremult(premult); + auto light_shadow = static_cast(buffer->GetInt()); + float r, g, b, a; + buffer->GetFloats(&r, &g, &b, &a); + ObjectProgramGL* p = obj_lightshad_transparent_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + p->SetColorTexture(buffer->GetTexture()); + p->SetVignetteTexture(vignette_tex_); + GLuint light_shadow_tex; + switch (light_shadow) { + case LightShadowType::kTerrain: + light_shadow_tex = + static_cast(light_shadow_render_target()) + ->framebuffer() + ->texture(); + break; + case LightShadowType::kObject: + light_shadow_tex = + static_cast(light_render_target()) + ->framebuffer() + ->texture(); + break; + default: + throw Exception(); + } + p->SetLightShadowTexture(light_shadow_tex); + + break; + } + case ShadingType::kObjectReflectLightShadow: { + SetDoubleSided(false); + SetBlend(false); + auto light_shadow = static_cast(buffer->GetInt()); + int world_space = buffer->GetInt(); + float r, g, b, reflect_r, reflect_g, reflect_b; + buffer->GetFloats(&r, &g, &b, &reflect_r, &reflect_g, &reflect_b); + ObjectProgramGL* p = world_space + ? obj_refl_lightshad_worldspace_prog_ + : obj_refl_lightshad_prog_; + p->Bind(); + p->SetColor(r, g, b); + p->SetColorTexture(buffer->GetTexture()); + p->SetReflectionTexture(buffer->GetTexture()); + p->SetReflectionMult(reflect_r, reflect_g, reflect_b); + p->SetVignetteTexture(vignette_tex_); + GLuint light_shadow_tex; + switch (light_shadow) { + case LightShadowType::kTerrain: + light_shadow_tex = + static_cast(light_shadow_render_target()) + ->framebuffer() + ->texture(); + break; + case LightShadowType::kObject: + light_shadow_tex = + static_cast(light_render_target()) + ->framebuffer() + ->texture(); + break; + default: + throw Exception(); + } + p->SetLightShadowTexture(light_shadow_tex); + break; + } + case ShadingType::kObjectReflectLightShadowDoubleSided: { + // FIXME: This shader isn't actually flipping the normal for the + // back side of the face.. for now we don't care though. + SetDoubleSided(true); + SetBlend(false); + auto light_shadow = static_cast(buffer->GetInt()); + int world_space = buffer->GetInt(); + + // Verified. + float r, g, b, reflect_r, reflect_g, reflect_b; + buffer->GetFloats(&r, &g, &b, &reflect_r, &reflect_g, &reflect_b); + ObjectProgramGL* p; + + // Testing why reflection is wonky.. + if (explicit_bool(false)) { + p = world_space ? obj_lightshad_worldspace_prog_ + : obj_lightshad_prog_; + p->Bind(); + p->SetColor(r, g, b); + p->SetColorTexture(buffer->GetTexture()); + buffer->GetTexture(); + } else { + p = world_space ? obj_refl_lightshad_worldspace_prog_ + : obj_refl_lightshad_prog_; + p->Bind(); + p->SetColor(r, g, b); + p->SetColorTexture(buffer->GetTexture()); + p->SetReflectionTexture(buffer->GetTexture()); + p->SetReflectionMult(reflect_r, reflect_g, reflect_b); + } + p->SetVignetteTexture(vignette_tex_); + GLuint light_shadow_tex; + switch (light_shadow) { + case LightShadowType::kTerrain: + light_shadow_tex = + static_cast(light_shadow_render_target()) + ->framebuffer() + ->texture(); + break; + case LightShadowType::kObject: + light_shadow_tex = + static_cast(light_render_target()) + ->framebuffer() + ->texture(); + break; + default: + throw Exception(); + } + p->SetLightShadowTexture(light_shadow_tex); + break; + } + case ShadingType::kObjectReflectLightShadowColorized: { + SetDoubleSided(false); + SetBlend(false); + auto light_shadow = static_cast(buffer->GetInt()); + float r, g, b, reflect_r, reflect_g, reflect_b, colorize_r, + colorize_g, colorize_b; + buffer->GetFloats(&r, &g, &b, &reflect_r, &reflect_g, &reflect_b, + &colorize_r, &colorize_g, &colorize_b); + ObjectProgramGL* p = obj_refl_lightshad_colorize_prog_; + p->Bind(); + p->SetColor(r, g, b); + p->SetColorTexture(buffer->GetTexture()); + p->SetColorizeTexture(buffer->GetTexture()); + p->SetColorizeColor(colorize_r, colorize_g, colorize_b); + p->SetReflectionTexture(buffer->GetTexture()); + p->SetReflectionMult(reflect_r, reflect_g, reflect_b); + p->SetVignetteTexture(vignette_tex_); + GLuint light_shadow_tex; + switch (light_shadow) { + case LightShadowType::kTerrain: + light_shadow_tex = + static_cast(light_shadow_render_target()) + ->framebuffer() + ->texture(); + break; + case LightShadowType::kObject: + light_shadow_tex = + static_cast(light_render_target()) + ->framebuffer() + ->texture(); + break; + default: + throw Exception(); + } + p->SetLightShadowTexture(light_shadow_tex); + break; + } + case ShadingType::kObjectReflectLightShadowColorized2: { + SetDoubleSided(false); + SetBlend(false); + auto light_shadow = static_cast(buffer->GetInt()); + + float r, g, b, reflect_r, reflect_g, reflect_b, colorize_r, + colorize_g, colorize_b, colorize2_r, colorize2_g, colorize2_b; + buffer->GetFloats(&r, &g, &b, &reflect_r, &reflect_g, &reflect_b, + &colorize_r, &colorize_g, &colorize_b, + &colorize2_r, &colorize2_g, &colorize2_b); + ObjectProgramGL* p = obj_refl_lightshad_colorize2_prog_; + p->Bind(); + p->SetColor(r, g, b); + p->SetColorTexture(buffer->GetTexture()); + + p->SetColorizeTexture(buffer->GetTexture()); + p->SetColorizeColor(colorize_r, colorize_g, colorize_b); + p->SetColorize2Color(colorize2_r, colorize2_g, colorize2_b); + + p->SetReflectionTexture(buffer->GetTexture()); + p->SetReflectionMult(reflect_r, reflect_g, reflect_b); + + p->SetVignetteTexture(vignette_tex_); + GLuint light_shadow_tex; + switch (light_shadow) { + case LightShadowType::kTerrain: + light_shadow_tex = + static_cast(light_shadow_render_target()) + ->framebuffer() + ->texture(); + break; + case LightShadowType::kObject: + light_shadow_tex = + static_cast(light_render_target()) + ->framebuffer() + ->texture(); + break; + default: + throw Exception(); + } + p->SetLightShadowTexture(light_shadow_tex); + break; + } + case ShadingType::kObjectReflectLightShadowAdd: { + SetDoubleSided(false); + SetBlend(false); + auto light_shadow = static_cast(buffer->GetInt()); + float r, g, b, add_r, add_g, add_b, reflect_r, reflect_g, reflect_b; + buffer->GetFloats(&r, &g, &b, &add_r, &add_g, &add_b, &reflect_r, + &reflect_g, &reflect_b); + ObjectProgramGL* p = obj_refl_lightshad_add_prog_; + p->Bind(); + p->SetColor(r, g, b); + p->SetColorTexture(buffer->GetTexture()); + p->SetAddColor(add_r, add_g, add_b); + p->SetReflectionTexture(buffer->GetTexture()); + p->SetReflectionMult(reflect_r, reflect_g, reflect_b); + + p->SetVignetteTexture(vignette_tex_); + GLuint light_shadow_tex; + switch (light_shadow) { + case LightShadowType::kTerrain: + light_shadow_tex = + static_cast(light_shadow_render_target()) + ->framebuffer() + ->texture(); + break; + case LightShadowType::kObject: + light_shadow_tex = + static_cast(light_render_target()) + ->framebuffer() + ->texture(); + break; + default: + throw Exception(); + } + p->SetLightShadowTexture(light_shadow_tex); + break; + } + case ShadingType::kObjectReflectLightShadowAddColorized: { + SetDoubleSided(false); + SetBlend(false); + auto light_shadow = static_cast(buffer->GetInt()); + + float r, g, b, add_r, add_g, add_b, reflect_r, reflect_g, reflect_b, + colorize_r, colorize_g, colorize_b; + buffer->GetFloats(&r, &g, &b, &add_r, &add_g, &add_b, &reflect_r, + &reflect_g, &reflect_b, &colorize_r, &colorize_g, + &colorize_b); + ObjectProgramGL* p = obj_refl_lightshad_add_colorize_prog_; + p->Bind(); + p->SetColor(r, g, b); + p->SetColorTexture(buffer->GetTexture()); + p->SetAddColor(add_r, add_g, add_b); + + p->SetColorizeTexture(buffer->GetTexture()); + p->SetColorizeColor(colorize_r, colorize_g, colorize_b); + + p->SetReflectionTexture(buffer->GetTexture()); + p->SetReflectionMult(reflect_r, reflect_g, reflect_b); + + p->SetVignetteTexture(vignette_tex_); + GLuint light_shadow_tex; + switch (light_shadow) { + case LightShadowType::kTerrain: + light_shadow_tex = + static_cast(light_shadow_render_target()) + ->framebuffer() + ->texture(); + break; + case LightShadowType::kObject: + light_shadow_tex = + static_cast(light_render_target()) + ->framebuffer() + ->texture(); + break; + default: + throw Exception(); + } + p->SetLightShadowTexture(light_shadow_tex); + break; + } + case ShadingType::kObjectReflectLightShadowAddColorized2: { + SetDoubleSided(false); + SetBlend(false); + auto light_shadow = static_cast(buffer->GetInt()); + + float r, g, b, add_r, add_g, add_b, reflect_r, reflect_g, reflect_b, + colorize_r, colorize_g, colorize_b, colorize2_r, colorize2_g, + colorize2_b; + buffer->GetFloats(&r, &g, &b, &add_r, &add_g, &add_b, &reflect_r, + &reflect_g, &reflect_b, &colorize_r, &colorize_g, + &colorize_b, &colorize2_r, &colorize2_g, + &colorize2_b); + ObjectProgramGL* p = obj_refl_lightshad_add_colorize2_prog_; + p->Bind(); + p->SetColor(r, g, b); + p->SetColorTexture(buffer->GetTexture()); + p->SetAddColor(add_r, add_g, add_b); + + p->SetColorizeTexture(buffer->GetTexture()); + p->SetColorizeColor(colorize_r, colorize_g, colorize_b); + p->SetColorize2Color(colorize2_r, colorize2_g, colorize2_b); + + p->SetReflectionTexture(buffer->GetTexture()); + p->SetReflectionMult(reflect_r, reflect_g, reflect_b); + + p->SetVignetteTexture(vignette_tex_); + GLuint light_shadow_tex; + switch (light_shadow) { + case LightShadowType::kTerrain: + light_shadow_tex = + static_cast(light_shadow_render_target()) + ->framebuffer() + ->texture(); + break; + case LightShadowType::kObject: + light_shadow_tex = + static_cast(light_render_target()) + ->framebuffer() + ->texture(); + break; + default: + throw Exception(); + } + p->SetLightShadowTexture(light_shadow_tex); + break; + } + case ShadingType::kObjectReflect: { + SetDoubleSided(false); + SetBlend(false); + int world_space = buffer->GetInt(); + // verified + float r, g, b, reflect_r, reflect_g, reflect_b; + buffer->GetFloats(&r, &g, &b, &reflect_r, &reflect_g, &reflect_b); + ObjectProgramGL* p = + world_space ? obj_refl_worldspace_prog_ : obj_refl_prog_; + p->Bind(); + p->SetColor(r, g, b); + p->SetColorTexture(buffer->GetTexture()); + p->SetReflectionTexture(buffer->GetTexture()); // reflection + p->SetReflectionMult(reflect_r, reflect_g, reflect_b); + break; + } + case ShadingType::kObjectReflectTransparent: { + SetDoubleSided(false); + + bool premult = static_cast(buffer->GetInt()); + SetBlend(true); + SetBlendPremult(premult); + float r, g, b, a, reflect_r, reflect_g, reflect_b; + buffer->GetFloats(&r, &g, &b, &a, &reflect_r, &reflect_g, + &reflect_b); + ObjectProgramGL* p = obj_refl_transparent_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + p->SetColorTexture(buffer->GetTexture()); + p->SetReflectionTexture(buffer->GetTexture()); // reflection + p->SetReflectionMult(reflect_r, reflect_g, reflect_b); + break; + } + case ShadingType::kObjectReflectAddTransparent: { + SetDoubleSided(false); + + bool premult = static_cast(buffer->GetInt()); + SetBlend(true); + SetBlendPremult(premult); + float r, g, b, a, add_r, add_g, add_b, reflect_r, reflect_g, + reflect_b; + buffer->GetFloats(&r, &g, &b, &a, &add_r, &add_g, &add_b, + &reflect_r, &reflect_g, &reflect_b); + ObjectProgramGL* p = obj_refl_add_transparent_prog_; + p->Bind(); + p->SetColor(r, g, b, a); + p->SetColorTexture(buffer->GetTexture()); + p->SetAddColor(add_r, add_g, add_b); + p->SetReflectionTexture(buffer->GetTexture()); // reflection + p->SetReflectionMult(reflect_r, reflect_g, reflect_b); + break; + } + case ShadingType::kShield: { + SetDoubleSided(true); + SetBlend(true); + SetBlendPremult(true); + ShieldProgramGL* p = shield_prog_; + p->Bind(); + p->SetDepthTexture( + static_cast(camera_render_target()) + ->framebuffer() + ->depth_texture()); + break; + } + case ShadingType::kSpecial: { + SetDoubleSided(false); + + // if we ever need to use non-blend version + // of this in real renders, we should split off a non-blend version + SetBlend(true); + SetBlendPremult(true); + auto source = (SpecialComponent::Source)buffer->GetInt(); + SimpleProgramGL* p = simple_tex_mod_prog_; + p->Bind(); + switch (source) { + case SpecialComponent::Source::kLightBuffer: + p->SetColorTexture( + static_cast(light_render_target()) + ->framebuffer() + ->texture()); + break; + case SpecialComponent::Source::kLightShadowBuffer: + p->SetColorTexture( + static_cast(light_shadow_render_target()) + ->framebuffer() + ->texture()); + break; + case SpecialComponent::Source::kVROverlayBuffer: { + p->SetColorTexture(static_cast( + vr_overlay_flat_render_target()) + ->framebuffer() + ->texture()); + p->SetColor(1, 1, 1, 0.95f); + break; + } + default: + throw Exception(); + break; + } + break; + } + default: + throw Exception(); + } + break; + } + case RenderCommandBuffer::Command::kSimpleComponentInlineColor: { + float r, g, b, a; + buffer->GetFloats(&r, &g, &b, &a); + auto* p = static_cast(GetActiveProgram()); + assert(p != nullptr + && p == dynamic_cast(GetActiveProgram())); + p->SetColor(r, g, b, a); + break; + } + case RenderCommandBuffer::Command::kObjectComponentInlineColor: { + float r, g, b, a; + buffer->GetFloats(&r, &g, &b, &a); + auto* p = static_cast(GetActiveProgram()); + assert(p != nullptr + && p == dynamic_cast(GetActiveProgram())); + p->SetColor(r, g, b, a); + break; + } + case RenderCommandBuffer::Command::kObjectComponentInlineAddColor: { + float r, g, b; + buffer->GetFloats(&r, &g, &b); + auto* p = static_cast(GetActiveProgram()); + assert(p != nullptr + && p == dynamic_cast(GetActiveProgram())); + p->SetAddColor(r, g, b); + break; + } + case RenderCommandBuffer::Command::kDrawModel: { + int flags = buffer->GetInt(); + const ModelData* m = buffer->GetModel(); + assert(m); + auto model = static_cast_check_type(m->renderer_data()); + assert(model); + + // if they don't wanna draw in reflections... + if ((flags & kModelDrawFlagNoReflection) && drawing_reflection()) { + break; + } + GetActiveProgram()->PrepareToDraw(); + model->Bind(); + model->Draw(); + break; + } + case RenderCommandBuffer::Command::kDrawModelInstanced: { + int flags = buffer->GetInt(); + const ModelData* m = buffer->GetModel(); + assert(m); + auto model = static_cast_check_type(m->renderer_data()); + assert(model); + Matrix44f* mats; + int count; + mats = buffer->GetMatrices(&count); + // if they don't wanna draw in reflections... + if ((flags & kModelDrawFlagNoReflection) && drawing_reflection()) { + break; + } + model->Bind(); + for (int i = 0; i < count; i++) { + g_graphics_server->PushTransform(); + g_graphics_server->MultMatrix(mats[i]); + GetActiveProgram()->PrepareToDraw(); + model->Draw(); + g_graphics_server->PopTransform(); + } + break; + } + // NOLINTNEXTLINE(bugprone-branch-clone) + case RenderCommandBuffer::Command::kBeginDebugDrawTriangles: { + GetActiveProgram()->PrepareToDraw(); +#if ENABLE_DEBUG_DRAWING + glBegin(GL_TRIANGLES); +#endif + break; + } + case RenderCommandBuffer::Command::kBeginDebugDrawLines: { + GetActiveProgram()->PrepareToDraw(); +#if ENABLE_DEBUG_DRAWING + glBegin(GL_LINES); +#endif + break; + } + case RenderCommandBuffer::Command::kEndDebugDraw: { +#if ENABLE_DEBUG_DRAWING + glEnd(); +#endif // ENABLE_DEBUG_DRAWING + break; + } + case RenderCommandBuffer::Command::kDebugDrawVertex3: { + float x, y, z; + buffer->GetFloats(&x, &y, &z); +#if ENABLE_DEBUG_DRAWING + glVertex3f(x, y, z); +#endif // ENABLE_DEBUG_DRAWING + break; + } + case RenderCommandBuffer::Command::kDrawMesh: { + int flags = buffer->GetInt(); + auto* mesh = buffer->GetMeshRendererData(); + assert(mesh); + if ((flags & kModelDrawFlagNoReflection) && drawing_reflection()) { + break; + } + GetActiveProgram()->PrepareToDraw(); + mesh->Bind(); + mesh->Draw(DrawType::kTriangles); + break; + } + case RenderCommandBuffer::Command::kDrawScreenQuad: { + // Save proj/mv matrices, set up to draw a simple screen quad at the + // back of our depth range, draw, and restore + Matrix44f old_model_view_matrix = + g_graphics_server->model_view_matrix(); + Matrix44f old_projection_matrix = + g_graphics_server->projection_matrix(); + g_graphics_server->SetModelViewMatrix(kMatrix44fIdentity); + g_graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 0.01f); + GetActiveProgram()->PrepareToDraw(); + screen_mesh_->Bind(); + screen_mesh_->Draw(DrawType::kTriangles); + g_graphics_server->SetModelViewMatrix(old_model_view_matrix); + g_graphics_server->SetProjectionMatrix(old_projection_matrix); + break; + } + case RenderCommandBuffer::Command::kScissorPush: { + Rect r; + buffer->GetFloats(&r.l, &r.b, &r.r, &r.t); + + // Convert scissor-values from model space to view space. + // this of course assumes there's no rotations and whatnot.. + Vector3f bot_left_pt = + g_graphics_server->model_view_matrix() * Vector3f(r.l, r.b, 0); + Vector3f top_right_pt = + g_graphics_server->model_view_matrix() * Vector3f(r.r, r.t, 0); + r.l = bot_left_pt.x; + r.b = bot_left_pt.y; + r.r = top_right_pt.x; + r.t = top_right_pt.y; + ScissorPush(r, render_target); + break; + } + case RenderCommandBuffer::Command::kScissorPop: { + ScissorPop(render_target); + break; + } + case RenderCommandBuffer::Command::kPushTransform: { + g_graphics_server->PushTransform(); + break; + } + case RenderCommandBuffer::Command::kTranslate2: { + float x, y; + buffer->GetFloats(&x, &y); + g_graphics_server->Translate(Vector3f(x, y, 0)); + break; + } + case RenderCommandBuffer::Command::kTranslate3: { + float x, y, z; + buffer->GetFloats(&x, &y, &z); + g_graphics_server->Translate(Vector3f(x, y, z)); + break; + } + case RenderCommandBuffer::Command::kCursorTranslate: { + float x, y; + g_platform->GetCursorPosition(&x, &y); + g_graphics_server->Translate(Vector3f(x, y, 0)); + break; + } + case RenderCommandBuffer::Command::kScale2: { + float x, y; + buffer->GetFloats(&x, &y); + g_graphics_server->scale(Vector3f(x, y, 1.0f)); + break; + } + case RenderCommandBuffer::Command::kScale3: { + float x, y, z; + buffer->GetFloats(&x, &y, &z); + g_graphics_server->scale(Vector3f(x, y, z)); + break; + } + case RenderCommandBuffer::Command::kScaleUniform: { + float s = buffer->GetFloat(); + g_graphics_server->scale(Vector3f(s, s, s)); + break; + } +#if BA_VR_BUILD + case RenderCommandBuffer::Command::kTransformToRightHand: { + VRTransformToRightHand(); + break; + } + case RenderCommandBuffer::Command::kTransformToLeftHand: { + VRTransformToLeftHand(); + break; + } + case RenderCommandBuffer::Command::kTransformToHead: { + VRTransformToHead(); + break; + } +#endif // BA_VR_BUILD + case RenderCommandBuffer::Command::kTranslateToProjectedPoint: { + float x, y, z; + buffer->GetFloats(&x, &y, &z); + Vector3f t = pass.frame_def()->beauty_pass()->tex_project_matrix() + * Vector3f(x, y, z); + g_graphics_server->Translate( + Vector3f(t.x * g_graphics_server->screen_virtual_width(), + t.y * g_graphics_server->screen_virtual_height(), 0)); + break; + } + case RenderCommandBuffer::Command::kRotate: { + float angle, x, y, z; + buffer->GetFloats(&angle, &x, &y, &z); + g_graphics_server->Rotate(angle, Vector3f(x, y, z)); + break; + } + case RenderCommandBuffer::Command::kMultMatrix: { + g_graphics_server->MultMatrix(*(buffer->GetMatrix())); + break; + } + case RenderCommandBuffer::Command::kPopTransform: { + g_graphics_server->PopTransform(); + break; + } + case RenderCommandBuffer::Command::kFlipCullFace: { + FlipCullFace(); + break; + } + default: + throw Exception("Invalid command in render-command-buffer"); + } + } + assert(buffer->IsEmpty()); +} // NOLINT (yes this is too long) + +void RendererGL::BlitBuffer(RenderTarget* src_in, RenderTarget* dst_in, + bool depth, bool linear_interpolation, + bool force_shader_mode, bool invalidate_source) { + DEBUG_CHECK_GL_ERROR; + auto* src = static_cast(src_in); + assert(src && src == dynamic_cast(src_in)); + auto* dst = static_cast(dst_in); +#if BA_DEBUG_BUILD + assert(dst && dst == dynamic_cast(dst_in)); +#endif + bool do_shader_blit{true}; + + // If they want depth we *MUST* use glBlitFramebuffer and can't have linear + // interp.. + if (depth) { + assert(g_blit_framebuffer_support && !force_shader_mode); + linear_interpolation = false; + } + // Use glBlitFramebuffer when its available. + // FIXME: This should be available in ES3. +#if !BA_OSTYPE_IOS_TVOS + if (g_blit_framebuffer_support && !force_shader_mode) { + do_shader_blit = false; + DEBUG_CHECK_GL_ERROR; + glBindFramebuffer(GL_READ_FRAMEBUFFER, src->GetFramebufferID()); + DEBUG_CHECK_GL_ERROR; + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, dst->GetFramebufferID()); + DEBUG_CHECK_GL_ERROR; + + glBlitFramebuffer(0, 0, static_cast(src->physical_width()), + static_cast(src->physical_height()), 0, 0, + static_cast(dst->physical_width()), + static_cast(dst->physical_height()), + depth ? (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + : GL_COLOR_BUFFER_BIT, + linear_interpolation ? GL_LINEAR : GL_NEAREST); + DEBUG_CHECK_GL_ERROR; + if (invalidate_source) { + InvalidateFramebuffer(true, depth, true); + } + } else { + do_shader_blit = true; + } +#endif + if (do_shader_blit) { + SetDepthWriting(false); + SetDepthTesting(false); + dst_in->DrawBegin(false); + g_graphics_server->ModelViewReset(); + g_graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1); + + // Copied from ShadingType::kSimpleColor + SetDoubleSided(false); + SetBlend(false); + SimpleProgramGL* p = simple_tex_prog_; + p->Bind(); + p->SetColorTexture(src->framebuffer()->texture()); + GetActiveProgram()->PrepareToDraw(); + screen_mesh_->Bind(); + screen_mesh_->Draw(DrawType::kTriangles); + DEBUG_CHECK_GL_ERROR; + } +} + +void RendererGL::ScissorPush(const Rect& r_in, RenderTarget* render_target) { + if (scissor_rects_.empty()) { + glEnable(GL_SCISSOR_TEST); + scissor_rects_.push_back(r_in); + } else { + Rect r; + Rect rp = scissor_rects_.back(); + r.l = r_in.l > rp.l ? r_in.l : rp.l; + r.r = r_in.r < rp.r ? r_in.r : rp.r; + r.b = r_in.b > rp.b ? r_in.b : rp.b; + r.t = r_in.t < rp.t ? r_in.t : rp.t; + scissor_rects_.push_back(r); + } + Rect clip = scissor_rects_.back(); + if (clip.l > clip.r) clip.l = clip.r; + if (clip.b > clip.t) clip.b = clip.t; + auto* glt = static_cast(render_target); + float scissor_scale_x = + static_cast(render_target)->GetScissorScaleX(); + float scissor_scale_y = + static_cast(render_target)->GetScissorScaleY(); + glScissor(static_cast(glt->GetScissorX(clip.l)), + static_cast(glt->GetScissorY(clip.b)), + static_cast(scissor_scale_x * (clip.r - clip.l)), + static_cast(scissor_scale_y * (clip.t - clip.b))); + DEBUG_CHECK_GL_ERROR; +} + +void RendererGL::ScissorPop(RenderTarget* render_target) { + BA_PRECONDITION(!scissor_rects_.empty()); + scissor_rects_.pop_back(); + if (scissor_rects_.empty()) { + glDisable(GL_SCISSOR_TEST); + } else { + Rect clip = scissor_rects_.back(); + if (clip.l > clip.r) clip.l = clip.r; + if (clip.b > clip.t) clip.b = clip.t; + auto* glt = static_cast(render_target); + float scissor_scale_x = + static_cast(render_target)->GetScissorScaleX(); + float scissor_scale_y = + static_cast(render_target)->GetScissorScaleY(); + glScissor(static_cast(glt->GetScissorX(clip.l)), + static_cast(glt->GetScissorY(clip.b)), + static_cast(scissor_scale_x * (clip.r - clip.l)), + static_cast(scissor_scale_y * (clip.t - clip.b))); + } + DEBUG_CHECK_GL_ERROR; +} + +// fixme filter our redundant sets.. +void RendererGL::SetDepthWriting(bool enable) { + if (enable != depth_writing_enabled_) { + depth_writing_enabled_ = enable; + glDepthMask(static_cast(enable)); + } +} + +void RendererGL::SetDrawAtEqualDepth(bool enable) { + if (enable != draw_at_equal_depth_) { + draw_at_equal_depth_ = enable; + if (enable) { + glDepthFunc(GL_LEQUAL); + } else { + glDepthFunc(GL_LESS); + } + } +} + +// FIXME FIXME FIXME FIXME +// turning off GL_DEPTH_TEST also disables +// depth writing which we may not want. +// It sounds like the proper thing +// to do in that case is leave GL_DEPTH_TEST on +// and set glDepthFunc(GL_ALWAYS) + +void RendererGL::SetDepthTesting(bool enable) { + if (enable != depth_testing_enabled_) { + depth_testing_enabled_ = enable; + if (enable) { + glEnable(GL_DEPTH_TEST); + } else { + glDisable(GL_DEPTH_TEST); + } + } +} + +void RendererGL::SetDepthRange(float min, float max) { + if (min != depth_range_min_ || max != depth_range_max_) { + depth_range_min_ = min; + depth_range_max_ = max; + glDepthRange(min, max); + } +} + +void RendererGL::FlipCullFace() { + draw_front_ = !draw_front_; + if (draw_front_) { + glCullFace(GL_BACK); + } else { + glCullFace(GL_FRONT); + } +} + +void RendererGL::SetBlend(bool b) { +#if !ENABLE_BLEND + b = false; +#endif + if (blend_ != b) { + blend_ = b; + if (blend_) { + glEnable(GL_BLEND); + } else { + glDisable(GL_BLEND); + } + } +} +void RendererGL::SetBlendPremult(bool b) { + if (blend_premult_ != b) { + blend_premult_ = b; + if (blend_premult_) { + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + } else { + // currently we only ever write to an alpha buffer for our vr overlay + // texture, and in that case we need alpha to accumulate; not get + // overwritten. could probably enable this everywhere but I don't know if + // it's supported on all hardware or is slower or whatnot.. + if (IsVRMode()) { + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, + GL_ONE_MINUS_SRC_ALPHA); + } else { + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } + } + } +} + +void RendererGL::BindVertexArray(GLuint v) { + assert(g_vao_support); + if (v != current_vertex_array_) { + glBindVertexArray(v); + current_vertex_array_ = v; + } +} +void RendererGL::SetDoubleSided(bool d) { + if (double_sided_ != d) { + double_sided_ = d; + if (double_sided_) { + glDisable(GL_CULL_FACE); + } else { + glEnable(GL_CULL_FACE); + } + } +} + +void RendererGL::UpdateVignetteTex(bool force) { + if (force || vignette_quality_ != g_graphics_server->quality() + || vignette_tex_outer_r_ != vignette_outer().x + || vignette_tex_outer_g_ != vignette_outer().y + || vignette_tex_outer_b_ != vignette_outer().z + || vignette_tex_inner_r_ != vignette_inner().x + || vignette_tex_inner_g_ != vignette_inner().y + || vignette_tex_inner_b_ != vignette_inner().z) { + vignette_tex_outer_r_ = vignette_outer().x; + vignette_tex_outer_g_ = vignette_outer().y; + vignette_tex_outer_b_ = vignette_outer().z; + vignette_tex_inner_r_ = vignette_inner().x; + vignette_tex_inner_g_ = vignette_inner().y; + vignette_tex_inner_b_ = vignette_inner().z; + vignette_quality_ = g_graphics_server->quality(); + + const int width = 64; + const int height = 64; + const size_t tex_buffer_size = width * height * 4; + std::vector datavec(tex_buffer_size); + uint8_t* data{datavec.data()}; + float max_c = 0.5f * 0.5f * 0.5f * 0.5f; + uint8_t* b = data; + + float out_r = std::min( + 255.0f, std::max(0.0f, 255.0f * (1.0f - vignette_tex_outer_r_))); + float out_g = std::min( + 255.0f, std::max(0.0f, 255.0f * (1.0f - vignette_tex_outer_g_))); + float out_b = std::min( + 255.0f, std::max(0.0f, 255.0f * (1.0f - vignette_tex_outer_b_))); + float in_r = std::min( + 255.0f, std::max(0.0f, 255.0f * (1.0f - vignette_tex_inner_r_))); + float in_g = std::min( + 255.0f, std::max(0.0f, 255.0f * (1.0f - vignette_tex_inner_g_))); + float in_b = std::min( + 255.0f, std::max(0.0f, 255.0f * (1.0f - vignette_tex_inner_b_))); + + for (int y = 0; y < height; y++) { + float d3 = static_cast(y) / (height - 1); + float d4 = 1.0f - d3; + for (int x = 0; x < width; x++) { + float d1 = static_cast(x) / (width - 1); + float d2 = 1.0f - d1; + float c = 1.0f * (1.0f - ((d1 * d2 * d3 * d4) / max_c)); + c = 0.5f * (c * c) + 0.5f * c; + c = std::min(1.0f, std::max(0.0f, c)); + + b[0] = static_cast(c * out_r + (1.0f - c) * in_r); + b[1] = static_cast(c * out_g + (1.0f - c) * in_g); + b[2] = static_cast(c * out_b + (1.0f - c) * in_b); + b[3] = 255; // alpha + b += 4; + } + } + + glGetError(); // clear error + BindTexture(GL_TEXTURE_2D, vignette_tex_); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, + GL_UNSIGNED_BYTE, data); + + // If 32 bit failed for some reason, attempt 16. + GLenum err = glGetError(); + if (err != GL_NO_ERROR) { + static bool reported = false; + if (!reported) { + Log("Error: 32-bit vignette creation failed; falling back to 16."); + reported = true; + } + const int kVignetteTexWidth = 64; + const int kVignetteTexHeight = 32; + const int kVignetteTexBufferSize = kVignetteTexWidth * kVignetteTexHeight; + uint16_t data2[kVignetteTexBufferSize]; + float max_c2 = 0.5f * 0.5f * 0.5f * 0.5f; + uint16_t* b2 = data2; + + float out_r2 = std::min( + 32.0f, std::max(0.0f, 32.0f * (1.0f - vignette_tex_outer_r_))); + float out_g2 = std::min( + 64.0f, std::max(0.0f, 64.0f * (1.0f - vignette_tex_outer_g_))); + float out_b2 = std::min( + 32.0f, std::max(0.0f, 32.0f * (1.0f - vignette_tex_outer_b_))); + float in_r2 = std::min( + 32.0f, std::max(0.0f, 32.0f * (1.0f - vignette_tex_inner_r_))); + float in_g2 = std::min( + 64.0f, std::max(0.0f, 64.0f * (1.0f - vignette_tex_inner_g_))); + float in_b2 = std::min( + 32.0f, std::max(0.0f, 32.0f * (1.0f - vignette_tex_inner_b_))); + + // IMPORTANT - if we tweak anything here we need to tweak vertex + // shaders that calc this on the fly as well.. + for (int y = 0; y < height; y++) { + float d3 = static_cast(y) / (height - 1); + float d4 = 1.0f - d3; + for (int x = 0; x < width; x++) { + float d1 = static_cast(x) / (width - 1); + float d2 = 1.0f - d1; + float c = 1.0f * (1.0f - ((d1 * d2 * d3 * d4) / max_c2)); + c = 0.5f * (c * c) + 0.5f * c; + c = std::min(1.0f, std::max(0.0f, c)); + int red = + std::min(31, static_cast(c * out_r2 + (1.0f - c) * in_r2)); + int green = + std::min(63, static_cast(c * out_g2 + (1.0f - c) * in_g2)); + int blue = + std::min(31, static_cast(c * out_b2 + (1.0f - c) * in_b2)); + *b2 = static_cast(red << 11 | green << 5 | blue); + b2 += 1; + } + } + BindTexture(GL_TEXTURE_2D, vignette_tex_); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, + GL_UNSIGNED_SHORT_5_6_5, data2); + DEBUG_CHECK_GL_ERROR; + } + if (force) { + GL_LABEL_OBJECT(GL_TEXTURE, vignette_tex_, "vignetteTex"); + } + } +} + +auto RendererGL::GetFunkyDepthIssue() -> bool { + if (!funky_depth_issue_set_) { + BA_LOG_ONCE("fetching funky depth issue but not set"); + } + return funky_depth_issue_; +} + +auto RendererGL::GetDrawsShieldsFunny() -> bool { + if (!draws_shields_funny_set_) { + BA_LOG_ONCE("fetching draws-shields-funny value but not set"); + } + return draws_shields_funny_; +} + +void RendererGL::CheckCapabilities() { CheckGLExtensions(); } + +#if BA_OSTYPE_ANDROID +std::string RendererGL::GetAutoAndroidRes() { + assert(InMainThread()); + + const char* renderer = (const char*)glGetString(GL_RENDERER); + + // on the adreno 4xxx or 5xxx series we should be able to do anything... + if (strstr(renderer, "Adreno (TM) 4") || strstr(renderer, "Adreno (TM) 5")) { + // for phones lets go with 1080p (phones most likely have 1920x1080-ish + // aspect ratios) + if (GetInterfaceType() == UIScale::kSmall) { + return "1080p"; + } else { + // tablets are more likely to have 1920x1200 so lets inch a bit higher + return "1200p"; + } + } + + // on extra-speedy devices we should be able to do 1920x1200 + if (is_extra_speedy_android_device_) { + // for phones lets go with 1080p (phones most likely have 1920x1080-ish + // aspect ratios) + if (GetInterfaceType() == UIScale::kSmall) { + return "1080p"; + } else { + // tablets are more likely to have 1920x1200 so lets inch a bit higher + return "1200p"; + } + } + + // Amazon Fire tablet (as of jan '18) needs REAL low res to feel smooth.. + if (g_platform->GetDeviceName() == "Amazon KFAUWI") { + return "480p"; + } + + // fall back to the old 'Auto' values elsewhere + // - this is generally 720p (but varies in a few cases) + return "Auto"; +} +#endif // BA_OSTYPE_ANDROID + +auto RendererGL::GetAutoTextureQuality() -> TextureQuality { + assert(InMainThread()); + + TextureQuality qual{TextureQuality::kHigh}; + +#if BA_OSTYPE_ANDROID + { + // lets be cheaper in VR mode since we have to draw twice.. + if (IsVRMode()) { + qual = TextureQuality::kMedium; + } else { + // ouya is a special case since we have dds textures there; default to + // high +#if BA_OUYA_BUILD + qual = TextureQuality::kHigh; +#else // BA_OUYA_BUILD + // on android we default to high quality mode if we support ETC2; + // otherwise go with medium + if (g_graphics_server->SupportsTextureCompressionType( + TextureCompressionType::kETC2) + || is_speedy_android_device_) { + qual = TextureQuality::kHigh; + } else { + qual = TextureQuality::kMedium; + } +#endif // BA_OUYA_BUILD + } + } +#elif BA_OSTYPE_IOS_TVOS + { + if (AppleUtils::IsSlowIOSDevice()) { + qual = TextureQuality::kMedium; + } else { + qual = TextureQuality::kHigh; + } + } +#else // BA_OSTYPE_ANDROID + { + // On other platforms (mac,pc,etc) just default to high. + qual = TextureQuality::kHigh; + } +#endif // BA_OSTYPE_ANDROID + + return qual; +} + +auto RendererGL::GetAutoGraphicsQuality() -> GraphicsQuality { + assert(InMainThread()); + GraphicsQuality q{GraphicsQuality::kMedium}; +#if BA_OSTYPE_ANDROID + // lets be cheaper in VR mode since we draw twice.. + if (IsVRMode()) { + q = GraphicsQuality::kMedium; + } else { + if (is_extra_speedy_android_device_) { + q = GraphicsQuality::kHigher; + } else if (g_running_es3 || is_speedy_android_device_) { + q = GraphicsQuality::kHigh; + } else { + q = GraphicsQuality::kMedium; + } + } +#elif BA_OSTYPE_IOS_TVOS + // on IOS we default to low-quality for slow devices (iphone-4, etc) + // medium for recent-ish ones (ipad2, iphone4s, etc), high for newer-ish + // (iPhone5, iPad4), and higher for anything beyond that + if (AppleUtils::IsSlowIOSDevice()) { + q = GraphicsQuality::kLow; + } else if (AppleUtils::IsMediumIOSDevice()) { + q = GraphicsQuality::kMedium; + } else if (AppleUtils::IsHighIOSDevice()) { + q = GraphicsQuality::kHigh; + } else { + q = GraphicsQuality::kHigher; + } +#else + // Elsewhere (desktops and such) we default to higher. + q = GraphicsQuality::kHigher; +#endif + return q; +} + +void RendererGL::RetainShader(ProgramGL* p) { shaders_.emplace_back(p); } + +void RendererGL::Load() { + assert(InGraphicsThread()); + assert(!data_loaded_); + assert(g_graphics_server->graphics_quality_set()); + if (!got_screen_framebuffer_) { + got_screen_framebuffer_ = true; + + // Grab the current framebuffer and consider that to be our 'screen' + // framebuffer. + // This can be 0 for the main framebuffer or something else. + glGetIntegerv(GL_FRAMEBUFFER_BINDING, + reinterpret_cast(&screen_framebuffer_)); + } + Renderer::Load(); + int high_qual_pp_flag = + g_graphics_server->quality() >= GraphicsQuality::kHigher + ? SHD_HIGHER_QUALITY + : 0; + screen_mesh_ = std::make_unique(this); + VertexSimpleFull v[] = {{{-1, -1, 0}, {0, 0}}, + {{1, -1, 0}, {65535, 0}}, + {{1, 1, 0}, {65535, 65535}}, + {{-1, 1, 0}, {0, 65535}}}; + const uint16_t indices[] = {0, 1, 2, 0, 2, 3}; + MeshBuffer buffer(4, v); + buffer.state = 1; // Necessary for this to set properly. + MeshIndexBuffer16 i_buffer(6, indices); + i_buffer.state = 1; // Necessary for this to set properly. + screen_mesh_->SetData(&buffer); + screen_mesh_->SetIndexData(&i_buffer); + assert(shaders_.empty()); + ProgramGL* p; + p = simple_color_prog_ = new SimpleProgramGL(this, SHD_MODULATE); + RetainShader(p); + p = simple_tex_prog_ = new SimpleProgramGL(this, SHD_TEXTURE); + RetainShader(p); + p = simple_tex_dtest_prog_ = + new SimpleProgramGL(this, SHD_TEXTURE | SHD_DEPTH_BUG_TEST); + RetainShader(p); + + // Have to run this after we've created the shader to be able to test it. + CheckFunkyDepthIssue(); + p = simple_tex_mod_prog_ = + new SimpleProgramGL(this, SHD_TEXTURE | SHD_MODULATE); + RetainShader(p); + p = simple_tex_mod_flatness_prog_ = + new SimpleProgramGL(this, SHD_TEXTURE | SHD_MODULATE | SHD_FLATNESS); + RetainShader(p); + p = simple_tex_mod_shadow_prog_ = new SimpleProgramGL( + this, SHD_TEXTURE | SHD_MODULATE | SHD_SHADOW | SHD_MASK_UV2); + RetainShader(p); + p = simple_tex_mod_shadow_flatness_prog_ = + new SimpleProgramGL(this, SHD_TEXTURE | SHD_MODULATE | SHD_SHADOW + | SHD_MASK_UV2 | SHD_FLATNESS); + RetainShader(p); + p = simple_tex_mod_glow_prog_ = + new SimpleProgramGL(this, SHD_TEXTURE | SHD_MODULATE | SHD_GLOW); + RetainShader(p); + p = simple_tex_mod_glow_maskuv2_prog_ = new SimpleProgramGL( + this, SHD_TEXTURE | SHD_MODULATE | SHD_GLOW | SHD_MASK_UV2); + RetainShader(p); + p = simple_tex_mod_colorized_prog_ = + new SimpleProgramGL(this, SHD_TEXTURE | SHD_MODULATE | SHD_COLORIZE); + RetainShader(p); + p = simple_tex_mod_colorized2_prog_ = new SimpleProgramGL( + this, SHD_TEXTURE | SHD_MODULATE | SHD_COLORIZE | SHD_COLORIZE2); + RetainShader(p); + p = simple_tex_mod_colorized2_masked_prog_ = + new SimpleProgramGL(this, SHD_TEXTURE | SHD_MODULATE | SHD_COLORIZE + | SHD_COLORIZE2 | SHD_MASKED); + RetainShader(p); + p = obj_prog_ = new ObjectProgramGL(this, 0); + RetainShader(p); + p = obj_transparent_prog_ = new ObjectProgramGL(this, SHD_OBJ_TRANSPARENT); + RetainShader(p); + p = obj_lightshad_transparent_prog_ = + new ObjectProgramGL(this, SHD_OBJ_TRANSPARENT | SHD_LIGHT_SHADOW); + RetainShader(p); + p = obj_refl_prog_ = new ObjectProgramGL(this, SHD_REFLECTION); + RetainShader(p); + p = obj_refl_worldspace_prog_ = + new ObjectProgramGL(this, SHD_REFLECTION | SHD_WORLD_SPACE_PTS); + RetainShader(p); + p = obj_refl_transparent_prog_ = + new ObjectProgramGL(this, SHD_REFLECTION | SHD_OBJ_TRANSPARENT); + RetainShader(p); + p = obj_refl_add_transparent_prog_ = + new ObjectProgramGL(this, SHD_REFLECTION | SHD_ADD | SHD_OBJ_TRANSPARENT); + RetainShader(p); + p = obj_lightshad_prog_ = new ObjectProgramGL(this, SHD_LIGHT_SHADOW); + RetainShader(p); + p = obj_lightshad_worldspace_prog_ = + new ObjectProgramGL(this, SHD_LIGHT_SHADOW | SHD_WORLD_SPACE_PTS); + RetainShader(p); + p = obj_refl_lightshad_prog_ = + new ObjectProgramGL(this, SHD_LIGHT_SHADOW | SHD_REFLECTION); + RetainShader(p); + p = obj_refl_lightshad_worldspace_prog_ = new ObjectProgramGL( + this, SHD_LIGHT_SHADOW | SHD_REFLECTION | SHD_WORLD_SPACE_PTS); + RetainShader(p); + p = obj_refl_lightshad_colorize_prog_ = new ObjectProgramGL( + this, SHD_LIGHT_SHADOW | SHD_REFLECTION | SHD_COLORIZE); + RetainShader(p); + p = obj_refl_lightshad_colorize2_prog_ = new ObjectProgramGL( + this, SHD_LIGHT_SHADOW | SHD_REFLECTION | SHD_COLORIZE | SHD_COLORIZE2); + RetainShader(p); + p = obj_refl_lightshad_add_prog_ = + new ObjectProgramGL(this, SHD_LIGHT_SHADOW | SHD_REFLECTION | SHD_ADD); + RetainShader(p); + p = obj_refl_lightshad_add_colorize_prog_ = new ObjectProgramGL( + this, SHD_LIGHT_SHADOW | SHD_REFLECTION | SHD_ADD | SHD_COLORIZE); + RetainShader(p); + p = obj_refl_lightshad_add_colorize2_prog_ = + new ObjectProgramGL(this, SHD_LIGHT_SHADOW | SHD_REFLECTION | SHD_ADD + | SHD_COLORIZE | SHD_COLORIZE2); + RetainShader(p); + p = smoke_prog_ = + new SmokeProgramGL(this, SHD_OBJ_TRANSPARENT | SHD_WORLD_SPACE_PTS); + RetainShader(p); + p = smoke_overlay_prog_ = new SmokeProgramGL( + this, SHD_OBJ_TRANSPARENT | SHD_WORLD_SPACE_PTS | SHD_OVERLAY); + RetainShader(p); + p = sprite_prog_ = new SpriteProgramGL(this, SHD_COLOR); + RetainShader(p); + p = sprite_camalign_prog_ = + new SpriteProgramGL(this, SHD_CAMERA_ALIGNED | SHD_COLOR); + RetainShader(p); + p = sprite_camalign_overlay_prog_ = + new SpriteProgramGL(this, SHD_CAMERA_ALIGNED | SHD_OVERLAY | SHD_COLOR); + RetainShader(p); + p = blur_prog_ = new BlurProgramGL(this, 0); + RetainShader(p); + p = shield_prog_ = new ShieldProgramGL(this, 0); + RetainShader(p); + + // Conditional seems to be a *very* slight win on some architectures (A7), a + // loss on some (A5) and a wash on some (Adreno 320). + // Gonna wait before a clean win before turning it on. + p = postprocess_prog_ = new PostProcessProgramGL(this, high_qual_pp_flag); + RetainShader(p); + if (g_graphics_server->quality() >= GraphicsQuality::kHigher) { + p = postprocess_eyes_prog_ = new PostProcessProgramGL(this, SHD_EYES); + RetainShader(p); + } else { + postprocess_eyes_prog_ = nullptr; + } + p = postprocess_distort_prog_ = + new PostProcessProgramGL(this, SHD_DISTORT | high_qual_pp_flag); + RetainShader(p); + + // Generate our random value texture. + { + glGenTextures(1, &random_tex_); + BindTexture(GL_TEXTURE_2D, random_tex_); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + const int tex_buffer_size = 128 * 128 * 3; + unsigned char data[tex_buffer_size]; + for (unsigned char& i : data) { + i = static_cast(rand()); // NOLINT + } + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 128, 128, 0, GL_RGB, + GL_UNSIGNED_BYTE, data); + GL_LABEL_OBJECT(GL_TEXTURE, random_tex_, "randomTex"); + } + + // Generate our vignette tex. + { + glGenTextures(1, &vignette_tex_); + BindTexture(GL_TEXTURE_2D, vignette_tex_); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + UpdateVignetteTex(true); + } + + // Let's pre-fill our recyclable mesh-datas list to reduce the need to make + // more which could cause hitches. + assert(recycle_mesh_datas_simple_split_.empty()); + for (int i = 0; i < 10; i++) { + recycle_mesh_datas_simple_split_.push_back(new MeshDataSimpleSplitGL(this)); + } + assert(recycle_mesh_datas_object_split_.empty()); + for (int i = 0; i < 10; i++) { + recycle_mesh_datas_object_split_.push_back(new MeshDataObjectSplitGL(this)); + } + assert(recycle_mesh_datas_simple_full_.empty()); + for (int i = 0; i < 10; i++) { + recycle_mesh_datas_simple_full_.push_back(new MeshDataSimpleFullGL(this)); + } + assert(recycle_mesh_datas_dual_texture_full_.empty()); + for (int i = 0; i < 10; i++) { + recycle_mesh_datas_dual_texture_full_.push_back( + new MeshDataDualTextureFullGL(this)); + } + assert(recycle_mesh_datas_smoke_full_.empty()); + for (int i = 0; i < 2; i++) { + recycle_mesh_datas_smoke_full_.push_back(new MeshDataSmokeFullGL(this)); + } + assert(recycle_mesh_datas_sprite_.empty()); + for (int i = 0; i < 2; i++) { + recycle_mesh_datas_sprite_.push_back(new MeshDataSpriteGL(this)); + } + + // Re-sync with the GL state since we might be dealing with a new context/etc. + SyncGLState(); + DEBUG_CHECK_GL_ERROR; + data_loaded_ = true; +} + +// in +void RendererGL::PostLoad() { + Renderer::PostLoad(); + // control may pass back to cardboard after we've finished loading + // but before we render, (in cases such as graphics settings switches) + // ...and it seems they can screw up our VAOs if we leave them bound... + // so lets be defensive. +#if BA_CARDBOARD_BUILD + SyncGLState(); +#endif +} + +void RendererGL::Unload() { + assert(InGraphicsThread()); + DEBUG_CHECK_GL_ERROR; + assert(data_loaded_); + Renderer::Unload(); + // clear out recycle-mesh-datas + for (auto&& i : recycle_mesh_datas_simple_split_) { + delete i; + } + recycle_mesh_datas_simple_split_.clear(); + for (auto&& i : recycle_mesh_datas_object_split_) { + delete i; + } + recycle_mesh_datas_object_split_.clear(); + for (auto&& i : recycle_mesh_datas_simple_full_) { + delete i; + } + recycle_mesh_datas_simple_full_.clear(); + for (auto&& i : recycle_mesh_datas_dual_texture_full_) { + delete i; + } + recycle_mesh_datas_dual_texture_full_.clear(); + for (auto&& i : recycle_mesh_datas_smoke_full_) { + delete i; + } + recycle_mesh_datas_smoke_full_.clear(); + for (auto&& i : recycle_mesh_datas_sprite_) { + delete i; + } + recycle_mesh_datas_sprite_.clear(); + screen_mesh_.reset(); + if (!g_graphics_server->renderer_context_lost()) { + glDeleteTextures(1, &random_tex_); + glDeleteTextures(1, &vignette_tex_); + } + blur_buffers_.clear(); + shaders_.clear(); + simple_color_prog_ = nullptr; + simple_tex_prog_ = nullptr; + simple_tex_dtest_prog_ = nullptr; + simple_tex_mod_prog_ = nullptr; + simple_tex_mod_flatness_prog_ = nullptr; + simple_tex_mod_shadow_prog_ = nullptr; + simple_tex_mod_shadow_flatness_prog_ = nullptr; + simple_tex_mod_glow_prog_ = nullptr; + simple_tex_mod_glow_maskuv2_prog_ = nullptr; + simple_tex_mod_colorized_prog_ = nullptr; + simple_tex_mod_colorized2_prog_ = nullptr; + simple_tex_mod_colorized2_masked_prog_ = nullptr; + obj_prog_ = nullptr; + obj_transparent_prog_ = nullptr; + obj_refl_prog_ = nullptr; + obj_refl_worldspace_prog_ = nullptr; + obj_refl_transparent_prog_ = nullptr; + obj_refl_add_transparent_prog_ = nullptr; + obj_lightshad_prog_ = nullptr; + obj_lightshad_worldspace_prog_ = nullptr; + obj_refl_lightshad_prog_ = nullptr; + obj_refl_lightshad_worldspace_prog_ = nullptr; + obj_refl_lightshad_colorize_prog_ = nullptr; + obj_refl_lightshad_colorize2_prog_ = nullptr; + obj_refl_lightshad_add_prog_ = nullptr; + obj_refl_lightshad_add_colorize_prog_ = nullptr; + obj_refl_lightshad_add_colorize2_prog_ = nullptr; + smoke_prog_ = nullptr; + smoke_overlay_prog_ = nullptr; + sprite_prog_ = nullptr; + sprite_camalign_prog_ = nullptr; + sprite_camalign_overlay_prog_ = nullptr; + obj_lightshad_transparent_prog_ = nullptr; + blur_prog_ = nullptr; + shield_prog_ = nullptr; + postprocess_prog_ = nullptr; + postprocess_eyes_prog_ = nullptr; + postprocess_distort_prog_ = nullptr; + data_loaded_ = false; + DEBUG_CHECK_GL_ERROR; +} + +auto RendererGL::NewModelData(const ModelData& model) -> ModelRendererData* { + return Object::NewDeferred(model, this); +} +auto RendererGL::NewTextureData(const TextureData& texture) + -> TextureRendererData* { + return Object::NewDeferred(texture, this); +} +auto RendererGL::NewScreenRenderTarget() -> RenderTarget* { + return Object::NewDeferred(this); +} +auto RendererGL::NewFramebufferRenderTarget(int width, int height, + bool linear_interp, bool depth, + bool texture, bool depth_texture, + bool high_quality, bool msaa, + bool alpha) -> RenderTarget* { + return Object::NewDeferred(this, width, height, linear_interp, + depth, texture, depth_texture, + high_quality, msaa, alpha); +} + +auto RendererGL::NewMeshData(MeshDataType mesh_type, MeshDrawType draw_type) + -> MeshRendererData* { + switch (mesh_type) { + case MeshDataType::kIndexedSimpleSplit: { + MeshDataSimpleSplitGL* data; + // use a recycled one if we've got one.. otherwise create a new one + auto i = recycle_mesh_datas_simple_split_.rbegin(); + if (i != recycle_mesh_datas_simple_split_.rend()) { + data = *i; + recycle_mesh_datas_simple_split_.pop_back(); + } else { + data = new MeshDataSimpleSplitGL(this); + } + return data; + break; + } + case MeshDataType::kIndexedObjectSplit: { + MeshDataObjectSplitGL* data; + // use a recycled one if we've got one.. otherwise create a new one + auto i = recycle_mesh_datas_object_split_.rbegin(); + if (i != recycle_mesh_datas_object_split_.rend()) { + data = *i; + recycle_mesh_datas_object_split_.pop_back(); + } else { + data = new MeshDataObjectSplitGL(this); + } + return data; + break; + } + case MeshDataType::kIndexedSimpleFull: { + MeshDataSimpleFullGL* data; + // use a recycled one if we've got one.. otherwise create a new one + auto i = recycle_mesh_datas_simple_full_.rbegin(); + if (i != recycle_mesh_datas_simple_full_.rend()) { + data = *i; + recycle_mesh_datas_simple_full_.pop_back(); + } else { + data = new MeshDataSimpleFullGL(this); + } + data->set_dynamic_draw(draw_type == MeshDrawType::kDynamic); + return data; + break; + } + case MeshDataType::kIndexedDualTextureFull: { + MeshDataDualTextureFullGL* data; + // use a recycled one if we've got one.. otherwise create a new one + auto i = recycle_mesh_datas_dual_texture_full_.rbegin(); + if (i != recycle_mesh_datas_dual_texture_full_.rend()) { + data = *i; + recycle_mesh_datas_dual_texture_full_.pop_back(); + } else { + data = new MeshDataDualTextureFullGL(this); + } + data->set_dynamic_draw(draw_type == MeshDrawType::kDynamic); + return data; + break; + } + case MeshDataType::kIndexedSmokeFull: { + MeshDataSmokeFullGL* data; + // use a recycled one if we've got one.. otherwise create a new one + auto i = recycle_mesh_datas_smoke_full_.rbegin(); + if (i != recycle_mesh_datas_smoke_full_.rend()) { + data = *i; + recycle_mesh_datas_smoke_full_.pop_back(); + } else { + data = new MeshDataSmokeFullGL(this); + } + data->set_dynamic_draw(draw_type == MeshDrawType::kDynamic); + return data; + break; + } + case MeshDataType::kSprite: { + MeshDataSpriteGL* data; + // use a recycled one if we've got one.. otherwise create a new one + auto i = recycle_mesh_datas_sprite_.rbegin(); + if (i != recycle_mesh_datas_sprite_.rend()) { + data = *i; + recycle_mesh_datas_sprite_.pop_back(); + } else { + data = new MeshDataSpriteGL(this); + } + data->set_dynamic_draw(draw_type == MeshDrawType::kDynamic); + return data; + break; + } + default: + throw Exception(); + break; + } +} +void RendererGL::DeleteMeshData(MeshRendererData* source_in, + MeshDataType mesh_type) { + // when we're done with mesh-data we keep it around for recycling... + // it seems that killing off VAO/VBOs can be hitchy (on mac at least) + // hmmm should we have some sort of threshold at which point we kill off + // some?.. + + switch (mesh_type) { + case MeshDataType::kIndexedSimpleSplit: { + auto source = static_cast(source_in); + assert(source + && source == dynamic_cast(source_in)); + source->Reset(); + recycle_mesh_datas_simple_split_.push_back(source); + break; + } + case MeshDataType::kIndexedObjectSplit: { + auto source = static_cast(source_in); + assert(source + && source == dynamic_cast(source_in)); + source->Reset(); + recycle_mesh_datas_object_split_.push_back(source); + break; + } + case MeshDataType::kIndexedSimpleFull: { + auto source = static_cast(source_in); + assert(source + && source == dynamic_cast(source_in)); + source->Reset(); + recycle_mesh_datas_simple_full_.push_back(source); + break; + } + case MeshDataType::kIndexedDualTextureFull: { + auto source = static_cast(source_in); + assert(source + && source == dynamic_cast(source_in)); + source->Reset(); + recycle_mesh_datas_dual_texture_full_.push_back(source); + break; + } + case MeshDataType::kIndexedSmokeFull: { + auto source = static_cast(source_in); + assert(source && source == dynamic_cast(source_in)); + source->Reset(); + recycle_mesh_datas_smoke_full_.push_back(source); + break; + } + case MeshDataType::kSprite: { + auto source = static_cast(source_in); + assert(source && source == dynamic_cast(source_in)); + source->Reset(); + recycle_mesh_datas_sprite_.push_back(source); + break; + } + default: + throw Exception(); + break; + } +} + +void RendererGL::CheckForErrors() { + // lets only check periodically.. i doubt it hurts to run this all the time + // but just in case... + error_check_counter_++; + if (error_check_counter_ > 120) { + error_check_counter_ = 0; + CHECK_GL_ERROR; + } +} + +void RendererGL::DrawDebug() { + if (explicit_bool(false)) { + // Draw our cam buffer if we have it. + if (has_camera_render_target()) { + SetDepthWriting(false); + SetDepthTesting(false); + SetDoubleSided(false); + SetBlend(false); + SimpleProgramGL* p = simple_tex_prog_; + p->Bind(); + + g_graphics_server->ModelViewReset(); + g_graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1); + + float tx = -0.6f; + float ty = 0.6f; + + g_graphics_server->PushTransform(); + g_graphics_server->scale(Vector3f(0.4f, 0.4f, 0.4f)); + g_graphics_server->Translate(Vector3f(-1.3f, -0.7f, 0)); + + // Draw cam buffer. + g_graphics_server->PushTransform(); + g_graphics_server->Translate(Vector3f(tx, ty, 0)); + tx += 0.2f; + ty -= 0.25f; + g_graphics_server->scale(Vector3f(0.5f, 0.5f, 1.0f)); + p->SetColorTexture(static_cast(camera_render_target()) + ->framebuffer() + ->texture()); + GetActiveProgram()->PrepareToDraw(); + screen_mesh_->Bind(); + screen_mesh_->Draw(DrawType::kTriangles); + g_graphics_server->PopTransform(); + + // Draw blur buffers. + if (explicit_bool(false)) { + for (auto&& i : blur_buffers_) { + g_graphics_server->PushTransform(); + g_graphics_server->Translate(Vector3f(tx, ty, 0)); + tx += 0.2f; + ty -= 0.25f; + g_graphics_server->scale(Vector3f(0.5f, 0.5f, 1.0f)); + p->SetColorTexture(i->texture()); + GetActiveProgram()->PrepareToDraw(); + screen_mesh_->Bind(); + screen_mesh_->Draw(DrawType::kTriangles); + g_graphics_server->PopTransform(); + } + } + g_graphics_server->PopTransform(); + } + } +} + +void RendererGL::GenerateCameraBufferBlurPasses() { + // If our cam-buffer res has changed since last time, regenerate our blur + // buffers. + auto* cam_buffer = static_cast(camera_render_target()); + assert(cam_buffer != nullptr + && dynamic_cast(camera_render_target()) + == cam_buffer); + + if (cam_buffer->physical_width() != last_cam_buffer_width_ + || cam_buffer->physical_height() != last_cam_buffer_height_ + || blur_res_count() != last_blur_res_count_ || blur_buffers_.empty()) { + blur_buffers_.clear(); + last_cam_buffer_width_ = cam_buffer->physical_width(); + last_cam_buffer_height_ = cam_buffer->physical_height(); + last_blur_res_count_ = blur_res_count(); + int w = static_cast(last_cam_buffer_width_); + int h = static_cast(last_cam_buffer_height_); + + // In higher-quality we do multiple levels and 16-bit dithering is kinda + // noticeable and ugly then. + bool high_quality_fbos = + (g_graphics_server->quality() >= GraphicsQuality::kHigher); + for (int i = 0; i < blur_res_count(); i++) { + assert(w % 2 == 0); + assert(h % 2 == 0); + w /= 2; + h /= 2; + blur_buffers_.push_back(Object::New( + this, w, h, + true, // linear_interp + false, // depth + true, // tex + false, // depthTex + high_quality_fbos, // highQuality + false, // msaa + false // alpha + )); // NOLINT(whitespace/parens) + } + + // Final redundant one (we run an extra blur without down-rezing). + if (g_graphics_server->quality() >= GraphicsQuality::kHigher) + blur_buffers_.push_back(Object::New( + this, w, h, + true, // linear_interp + false, // depth + true, // tex + false, // depthTex + false, // highQuality + false, // msaa + false // alpha + )); // NOLINT(whitespace/parens) + } + + // Ok now go through and do the blurring. + SetDepthWriting(false); + SetDepthTesting(false); + g_graphics_server->ModelViewReset(); + g_graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1); + SetDoubleSided(false); + SetBlend(false); + + BlurProgramGL* p = blur_prog_; + p->Bind(); + + FramebufferObjectGL* src_fb = + static_cast(camera_render_target())->framebuffer(); + for (auto&& i : blur_buffers_) { + FramebufferObjectGL* fb = i.get(); + assert(fb); + fb->Bind(); + SetViewport(0, 0, fb->width(), fb->height()); + InvalidateFramebuffer(true, false, false); + p->SetColorTexture(src_fb->texture()); + if (fb->width() == src_fb->width()) { // Our last one is equal res. + p->SetPixelSize(2.0f / static_cast(fb->width()), + 2.0f / static_cast(fb->height())); + } else { + p->SetPixelSize(1.0f / static_cast(fb->width()), + 1.0f / static_cast(fb->height())); + } + GetActiveProgram()->PrepareToDraw(); + screen_mesh_->Bind(); + screen_mesh_->Draw(DrawType::kTriangles); + src_fb = fb; + } +} + +void RendererGL::CardboardDisableScissor() { glDisable(GL_SCISSOR_TEST); } + +void RendererGL::CardboardEnableScissor() { glEnable(GL_SCISSOR_TEST); } + +void RendererGL::VREyeRenderBegin() { + assert(IsVRMode()); + + // On rift we need to turn off srgb conversion for each eye render + // so we can dump our linear data into oculus' srgb buffer as-is. + // (we really should add proper srgb support to the engine at some point) +#if BA_RIFT_BUILD + glDisable(GL_FRAMEBUFFER_SRGB); +#endif // BA_RIFT_BUILD + + GLuint fb; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, reinterpret_cast(&fb)); + screen_framebuffer_ = fb; +} + +#if BA_VR_BUILD +void RendererGL::VRSyncRenderStates() { + // GL state has been mucked with outside of our code; let's resync stuff.. + SyncGLState(); +} +#endif // BA_VR_BUILD + +void RendererGL::RenderFrameDefEnd() { + // Need to set some states to keep cardboard happy. +#if BA_CARDBOARD_BUILD + if (IsVRMode()) { + SyncGLState(); + glEnable(GL_SCISSOR_TEST); + } +#endif // BA_CARDBOARD_BUILD +} + +#pragma clang diagnostic pop + +} // namespace ballistica + +#endif // BA_ENABLE_OPENGL diff --git a/src/ballistica/graphics/gl/renderer_gl.h b/src/ballistica/graphics/gl/renderer_gl.h new file mode 100644 index 00000000..864cdce7 --- /dev/null +++ b/src/ballistica/graphics/gl/renderer_gl.h @@ -0,0 +1,263 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_GL_RENDERER_GL_H_ +#define BALLISTICA_GRAPHICS_GL_RENDERER_GL_H_ + +#include +#include +#include + +#include "ballistica/ballistica.h" + +#if BA_ENABLE_OPENGL + +#include "ballistica/core/object.h" +#include "ballistica/graphics/gl/gl_sys.h" +#include "ballistica/graphics/renderer.h" + +namespace ballistica { + +// for now lets not go above 8 since that's what the iPhone 3gs has.. +// ...haha perhaps should revisit this +constexpr int kMaxGLTexUnitsUsed = 5; + +class RendererGL : public Renderer { + class FakeVertexArrayObject; + class TextureDataGL; + class ModelDataGL; + class MeshDataGL; + class MeshDataSimpleSplitGL; + class MeshDataObjectSplitGL; + class MeshDataSimpleFullGL; + class MeshDataDualTextureFullGL; + class MeshDataSmokeFullGL; + class MeshDataSpriteGL; + class RenderTargetGL; + class FramebufferObjectGL; + class ShaderGL; + class FragmentShaderGL; + class VertexShaderGL; + class ProgramGL; + class SimpleProgramGL; + class ObjectProgramGL; + class SmokeProgramGL; + class BlurProgramGL; + class ShieldProgramGL; + class PostProcessProgramGL; + class SpriteProgramGL; + + public: + RendererGL(); + ~RendererGL() override; + void Unload() override; + void Load() override; + void PostLoad() override; + + // our vertex attrs + enum VertexAttr { + kVertexAttrPosition, + kVertexAttrUV, + kVertexAttrNormal, + kVertexAttrErode, + kVertexAttrColor, + kVertexAttrSize, + kVertexAttrDiffuse, + kVertexAttrUV2, + kVertexAttrCount + }; + + void CheckCapabilities() override; + auto GetAutoGraphicsQuality() -> GraphicsQuality override; + auto GetAutoTextureQuality() -> TextureQuality override; +#if BA_OSTYPE_ANDROID + std::string GetAutoAndroidRes() override; +#endif // BA_OSTYPE_ANDROID + + protected: + void DrawDebug() override; + void CheckForErrors() override; + void GenerateCameraBufferBlurPasses() override; + void FlipCullFace() override; + void SetDepthRange(float min, float max) override; + void SetDepthWriting(bool enable) override; + void SetDepthTesting(bool enable) override; + void SetDrawAtEqualDepth(bool enable) override; + auto NewScreenRenderTarget() -> RenderTarget* override; + auto NewFramebufferRenderTarget(int width, int height, bool linear_interp, + bool depth, bool texture, + bool depth_is_texture, bool high_quality, + bool msaa, bool alpha) + -> RenderTarget* override; + auto NewModelData(const ModelData& model) -> ModelRendererData* override; + auto NewTextureData(const TextureData& texture) + -> TextureRendererData* override; + auto NewMeshData(MeshDataType type, MeshDrawType drawType) + -> MeshRendererData* override; + void DeleteMeshData(MeshRendererData* data, MeshDataType type) override; + void ProcessRenderCommandBuffer(RenderCommandBuffer* buffer, + const RenderPass& pass, + RenderTarget* render_target) override; + void BlitBuffer(RenderTarget* src, RenderTarget* dst, bool depth, + bool linear_interpolation, bool force_shader_mode, + bool invalidate_source) override; + void UpdateMeshes( + const std::vector >& meshes, + const std::vector& index_sizes, + const std::vector >& buffers) override; + void PushGroupMarker(const char* label) override; + void PopGroupMarker() override; + auto IsMSAAEnabled() const -> bool override; + void InvalidateFramebuffer(bool color, bool depth, + bool target_read_framebuffer) override; + void VREyeRenderBegin() override; + void CardboardDisableScissor() override; + void CardboardEnableScissor() override; + void RenderFrameDefEnd() override; + +#if BA_VR_BUILD + void VRSyncRenderStates() override; +#endif // BA_VR_BUILD + + // TEMP + auto current_vertex_array() const -> GLuint { return current_vertex_array_; } + + private: + void CheckFunkyDepthIssue(); + auto GetMSAASamplesForFramebuffer(int width, int height) -> int; + void UpdateMSAAEnabled() override; + void CheckGLExtensions(); + void UpdateVignetteTex(bool force) override; + void StandardPostProcessSetup(PostProcessProgramGL* p, + const RenderPass& pass); + void SyncGLState(); + void RetainShader(ProgramGL* p); + void SetViewport(GLint x, GLint y, GLsizei width, GLsizei height); + void UseProgram(ProgramGL* p); + auto GetActiveProgram() const -> ProgramGL* { + assert(current_program_); + return current_program_; + } + void SetDoubleSided(bool d); + void ScissorPush(const Rect& rIn, RenderTarget* render_target); + void ScissorPop(RenderTarget* render_target); + void BindVertexArray(GLuint v); + + // Note: This is only for use when VAOs aren't supported. + void SetVertexAttribArrayEnabled(GLuint i, bool enabled); + void BindTexture(GLuint type, const TextureData* t, GLuint tex_unit = 0); + void BindTexture(GLuint type, GLuint tex, GLuint tex_unit = 0); + void BindTextureUnit(uint32_t tex_unit); + void BindFramebuffer(GLuint fb); + void BindArrayBuffer(GLuint b); + void SetBlend(bool b); + void SetBlendPremult(bool b); + millisecs_t dof_update_time_{}; + std::vector > blur_buffers_; + bool supports_depth_textures_{}; + bool first_extension_check_{true}; + bool is_tegra_4_{}; + bool is_tegra_k1_{}; + bool is_recent_adreno_{}; + bool is_adreno_{}; + bool enable_msaa_{}; + float last_cam_buffer_width_{}; + float last_cam_buffer_height_{}; + int last_blur_res_count_{}; + float vignette_tex_outer_r_{}; + float vignette_tex_outer_g_{}; + float vignette_tex_outer_b_{}; + float vignette_tex_inner_r_{}; + float vignette_tex_inner_g_{}; + float vignette_tex_inner_b_{}; + float depth_range_min_{}; + float depth_range_max_{}; + bool draw_at_equal_depth_{}; + bool depth_writing_enabled_{}; + bool depth_testing_enabled_{}; + bool data_loaded_{}; + bool draw_front_{}; + GLuint screen_framebuffer_{}; + bool got_screen_framebuffer_{}; + GLuint random_tex_{}; + GLuint vignette_tex_{}; + GraphicsQuality vignette_quality_{}; + std::vector > shaders_; + GLint viewport_x_{}; + GLint viewport_y_{}; + GLint viewport_width_{}; + GLint viewport_height_{}; + SimpleProgramGL* simple_color_prog_{}; + SimpleProgramGL* simple_tex_prog_{}; + SimpleProgramGL* simple_tex_dtest_prog_{}; + SimpleProgramGL* simple_tex_mod_prog_{}; + SimpleProgramGL* simple_tex_mod_flatness_prog_{}; + SimpleProgramGL* simple_tex_mod_shadow_prog_{}; + SimpleProgramGL* simple_tex_mod_shadow_flatness_prog_{}; + SimpleProgramGL* simple_tex_mod_glow_prog_{}; + SimpleProgramGL* simple_tex_mod_glow_maskuv2_prog_{}; + SimpleProgramGL* simple_tex_mod_colorized_prog_{}; + SimpleProgramGL* simple_tex_mod_colorized2_prog_{}; + SimpleProgramGL* simple_tex_mod_colorized2_masked_prog_{}; + ObjectProgramGL* obj_prog_{}; + ObjectProgramGL* obj_transparent_prog_{}; + ObjectProgramGL* obj_lightshad_transparent_prog_{}; + ObjectProgramGL* obj_refl_prog_{}; + ObjectProgramGL* obj_refl_worldspace_prog_{}; + ObjectProgramGL* obj_refl_transparent_prog_{}; + ObjectProgramGL* obj_refl_add_transparent_prog_{}; + ObjectProgramGL* obj_lightshad_prog_{}; + ObjectProgramGL* obj_lightshad_worldspace_prog_{}; + ObjectProgramGL* obj_refl_lightshad_prog_{}; + ObjectProgramGL* obj_refl_lightshad_worldspace_prog_{}; + ObjectProgramGL* obj_refl_lightshad_colorize_prog_{}; + ObjectProgramGL* obj_refl_lightshad_colorize2_prog_{}; + ObjectProgramGL* obj_refl_lightshad_add_prog_{}; + ObjectProgramGL* obj_refl_lightshad_add_colorize_prog_{}; + ObjectProgramGL* obj_refl_lightshad_add_colorize2_prog_{}; + SmokeProgramGL* smoke_prog_{}; + SmokeProgramGL* smoke_overlay_prog_{}; + SpriteProgramGL* sprite_prog_{}; + SpriteProgramGL* sprite_camalign_prog_{}; + SpriteProgramGL* sprite_camalign_overlay_prog_{}; + BlurProgramGL* blur_prog_{}; + ShieldProgramGL* shield_prog_{}; + PostProcessProgramGL* postprocess_prog_{}; + PostProcessProgramGL* postprocess_eyes_prog_{}; + PostProcessProgramGL* postprocess_distort_prog_{}; + static auto GetFunkyDepthIssue() -> bool; + static auto GetDrawsShieldsFunny() -> bool; + static bool funky_depth_issue_set_; + static bool funky_depth_issue_; + static bool draws_shields_funny_set_; + static bool draws_shields_funny_; +#if BA_OSTYPE_ANDROID + static bool is_speedy_android_device_; + static bool is_extra_speedy_android_device_; +#endif // BA_OSTYPE_ANDROID + ProgramGL* current_program_{}; + bool double_sided_{}; + std::vector scissor_rects_; + GLuint current_vertex_array_{}; + bool vertex_attrib_arrays_enabled_[kVertexAttrCount]{}; + int active_tex_unit_{}; + int active_framebuffer_{}; + int active_array_buffer_{}; + int bound_textures_2d_[kMaxGLTexUnitsUsed]{}; + int bound_textures_cube_map_[kMaxGLTexUnitsUsed]{}; + bool blend_{}; + bool blend_premult_{}; + std::unique_ptr screen_mesh_; + std::vector recycle_mesh_datas_simple_split_; + std::vector recycle_mesh_datas_object_split_; + std::vector recycle_mesh_datas_simple_full_; + std::vector recycle_mesh_datas_dual_texture_full_; + std::vector recycle_mesh_datas_smoke_full_; + std::vector recycle_mesh_datas_sprite_; + int error_check_counter_{}; +}; + +} // namespace ballistica + +#endif // BA_ENABLE_OPENGL + +#endif // BALLISTICA_GRAPHICS_GL_RENDERER_GL_H_ diff --git a/src/ballistica/graphics/graphics.cc b/src/ballistica/graphics/graphics.cc new file mode 100644 index 00000000..8e9a077d --- /dev/null +++ b/src/ballistica/graphics/graphics.cc @@ -0,0 +1,1868 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/graphics.h" + +#include +#include +#include + +#include "ballistica/app/app.h" +#include "ballistica/app/app_globals.h" +#include "ballistica/dynamics/bg/bg_dynamics.h" +#include "ballistica/game/connection/connection_to_client.h" +#include "ballistica/game/connection/connection_to_host.h" +#include "ballistica/game/session/session.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/camera.h" +#include "ballistica/graphics/component/empty_component.h" +#include "ballistica/graphics/component/object_component.h" +#include "ballistica/graphics/component/post_process_component.h" +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/graphics/component/special_component.h" +#include "ballistica/graphics/component/sprite_component.h" +#include "ballistica/graphics/gl/renderer_gl.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/graphics/net_graph.h" +#include "ballistica/graphics/text/text_graphics.h" +#include "ballistica/input/input.h" +#include "ballistica/platform/platform.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_context_call.h" +#include "ballistica/scene/scene.h" +#include "ballistica/ui/console.h" +#include "ballistica/ui/root_ui.h" +#include "ballistica/ui/ui.h" +#include "ballistica/ui/widget/root_widget.h" + +namespace ballistica { + +const float kScreenMessageZDepth = -0.06f; +const float kScreenMeshZDepth = -0.05f; +const float kProgressBarZDepth = 0.0f; +const int kProgressBarFadeTime = 500; +const float kDebugImgZDepth = -0.04f; +const float kCursorZDepth = -0.1f; + +auto Graphics::IsShaderTransparent(ShadingType c) -> bool { + switch (c) { + case ShadingType::kSimpleColorTransparent: + case ShadingType::kSimpleColorTransparentDoubleSided: + case ShadingType::kObjectTransparent: + case ShadingType::kObjectLightShadowTransparent: + case ShadingType::kObjectReflectTransparent: + case ShadingType::kObjectReflectAddTransparent: + case ShadingType::kSimpleTextureModulatedTransparent: + case ShadingType::kSimpleTextureModulatedTransFlatness: + case ShadingType::kSimpleTextureModulatedTransparentDoubleSided: + case ShadingType::kSimpleTextureModulatedTransparentColorized: + case ShadingType::kSimpleTextureModulatedTransparentColorized2: + case ShadingType::kSimpleTextureModulatedTransparentColorized2Masked: + case ShadingType::kSimpleTextureModulatedTransparentShadow: + case ShadingType::kSimpleTexModulatedTransShadowFlatness: + case ShadingType::kSimpleTextureModulatedTransparentGlow: + case ShadingType::kSimpleTextureModulatedTransparentGlowMaskUV2: + case ShadingType::kSpecial: + case ShadingType::kShield: + case ShadingType::kSmoke: + case ShadingType::kSmokeOverlay: + case ShadingType::kSprite: + return true; + case ShadingType::kSimpleColor: + case ShadingType::kSimpleTextureModulated: + case ShadingType::kSimpleTextureModulatedColorized: + case ShadingType::kSimpleTextureModulatedColorized2: + case ShadingType::kSimpleTextureModulatedColorized2Masked: + case ShadingType::kSimpleTexture: + case ShadingType::kObject: + case ShadingType::kObjectReflect: + case ShadingType::kObjectLightShadow: + case ShadingType::kObjectReflectLightShadow: + case ShadingType::kObjectReflectLightShadowDoubleSided: + case ShadingType::kObjectReflectLightShadowColorized: + case ShadingType::kObjectReflectLightShadowColorized2: + case ShadingType::kObjectReflectLightShadowAdd: + case ShadingType::kObjectReflectLightShadowAddColorized: + case ShadingType::kObjectReflectLightShadowAddColorized2: + case ShadingType::kPostProcess: + case ShadingType::kPostProcessEyes: + case ShadingType::kPostProcessNormalDistort: + return false; + default: + throw Exception(); // in case we forget to add new ones here... + } +} + +Graphics::Graphics() = default; +Graphics::~Graphics() = default; + +void Graphics::SetGyroEnabled(bool enable) { + // If we're turning back on, suppress gyro updates for a bit. + if (enable && !gyro_enabled_) { + last_suppress_gyro_time_ = GetRealTime(); + } + gyro_enabled_ = enable; +} + +void Graphics::UpdateProgressBarProgress(float target) { + millisecs_t real_time = GetRealTime(); + float p = target; + if (p < 0) { + p = 0; + } + if (real_time - last_progress_bar_draw_time_ > 400) { + last_progress_bar_draw_time_ = real_time - 400; + } + while (last_progress_bar_draw_time_ < real_time) { + last_progress_bar_draw_time_++; + progress_bar_progress_ += (p - progress_bar_progress_) * 0.02f; + } +} + +void Graphics::DrawProgressBar(RenderPass* pass, float opacity) { + millisecs_t real_time = GetRealTime(); + float amount = progress_bar_progress_; + if (amount < 0) amount = 0; + + SimpleComponent c(pass); + c.SetTransparent(true); + float o = opacity; + float delay = 0; + + // Fade in for the first 2 seconds if desired. + if (progress_bar_fade_in_) { + millisecs_t since_start = real_time - last_progress_bar_start_time_; + if (since_start < delay) { + o = 0.0f; + } else if (since_start < 2000 + delay) { + o *= (since_start - delay) / 2000.0f; + } + } + + // Fade out at the end. + if (amount > 0.75f) { + o *= (1.0f - amount) * 4.0f; + } + + float b = pass->virtual_height() / 2.0f - 20.0f; + float t = pass->virtual_height() / 2.0f + 20.0f; + float l = 100.0f; + float r = pass->virtual_width() - 100.0f; + float p = 1.0f - amount; + if (p < 0) { + p = 0; + } else if (p > 1.0f) { + p = 1.0f; + } + p = l + (1.0f - p) * (r - l); + + progress_bar_bottom_mesh_->SetPositionAndSize(l, b, kProgressBarZDepth, + (r - l), (t - b)); + progress_bar_top_mesh_->SetPositionAndSize(l, b, kProgressBarZDepth, (p - l), + (t - b)); + + c.SetColor(0.0f, 0.07f, 0.0f, 1 * o); + c.DrawMesh(progress_bar_bottom_mesh_.get()); + c.Submit(); + + c.SetColor(0.23f, 0.17f, 0.35f, 1 * o); + c.DrawMesh(progress_bar_top_mesh_.get()); + c.Submit(); +} + +void Graphics::SetShadowRange(float lower_bottom, float lower_top, + float upper_bottom, float upper_top) { + assert(lower_top >= lower_bottom && upper_bottom >= lower_top + && upper_top >= upper_bottom); + shadow_lower_bottom_ = lower_bottom; + shadow_lower_top_ = lower_top; + shadow_upper_bottom_ = upper_bottom; + shadow_upper_top_ = upper_top; +} + +auto Graphics::GetShadowDensity(float x, float y, float z) -> float { + if (y < shadow_lower_bottom_) { // NOLINT(bugprone-branch-clone) + return 0.0f; + } else if (y < shadow_lower_top_) { + float amt = + (y - shadow_lower_bottom_) / (shadow_lower_top_ - shadow_lower_bottom_); + return amt; + } else if (y < shadow_upper_bottom_) { + return 1.0f; + } else if (y < shadow_upper_top_) { + float amt = + (y - shadow_upper_bottom_) / (shadow_upper_top_ - shadow_upper_bottom_); + return 1.0f - amt; + } else { + return 0.0f; + } +} + +class Graphics::ScreenMessageEntry { + public: + ScreenMessageEntry(std::string s_in, bool align_left_in, uint32_t c, + const Vector3f& color_in, Texture* texture_in, + Texture* tint_texture_in, const Vector3f& tint_in, + const Vector3f& tint2_in) + : align_left(align_left_in), + creation_time(c), + s_raw(std::move(s_in)), + color(color_in), + texture(texture_in), + tint_texture(tint_texture_in), + tint(tint_in), + tint2(tint2_in), + v_smoothed(0.0f), + translation_dirty(true), + mesh_dirty(true) {} + auto GetText() -> TextGroup&; + void UpdateTranslation(); + bool align_left; + uint32_t creation_time; + Vector3f color; + Vector3f tint; + Vector3f tint2; + std::string s_raw; + std::string s_translated; + Object::Ref texture; + Object::Ref tint_texture; + float v_smoothed; + bool translation_dirty; + bool mesh_dirty; + + private: + Object::Ref s_mesh_; +}; + +// Draw controls and things that lie on top of the action. +void Graphics::DrawMiscOverlays(RenderPass* pass) { + // Every now and then, update our stats. + while (GetRealTime() >= next_stat_update_time_) { + if (GetRealTime() - next_stat_update_time_ > 1000) { + next_stat_update_time_ = GetRealTime() + 1000; + } else { + next_stat_update_time_ += 1000; + } + int total_frames_rendered = + g_graphics_server->renderer()->total_frames_rendered(); + last_fps_ = total_frames_rendered - last_total_frames_rendered_; + last_total_frames_rendered_ = total_frames_rendered; + } + float v{}; + + if (show_fps_) { + char fps_str[32]; + snprintf(fps_str, sizeof(fps_str), "%d", last_fps_); + if (fps_str != fps_string_) { + fps_string_ = fps_str; + if (!fps_text_group_.exists()) { + fps_text_group_ = Object::New(); + } + fps_text_group_->SetText(fps_string_); + } + SimpleComponent c(pass); + c.SetTransparent(true); + if (IsVRMode()) { + c.SetColor(1, 1, 1, 1); + } else { + c.SetColor(0.8f, 0.8f, 0.8f, 1.0f); + } + int text_elem_count = fps_text_group_->GetElementCount(); + for (int e = 0; e < text_elem_count; e++) { + c.SetTexture(fps_text_group_->GetElementTexture(e)); + if (IsVRMode()) { + c.SetShadow(-0.003f * fps_text_group_->GetElementUScale(e), + -0.003f * fps_text_group_->GetElementVScale(e), 0.0f, 1.0f); + c.SetMaskUV2Texture(fps_text_group_->GetElementMaskUV2Texture(e)); + } + c.SetFlatness(1.0f); + c.DrawMesh(fps_text_group_->GetElementMesh(e)); + } + c.Submit(); + } + + if (show_net_info_) { + char net_info_str[128]; + int64_t in_count = 0; + int64_t in_size = 0; + int64_t in_size_compressed = 0; + int64_t outCount = 0; + int64_t out_size = 0; + int64_t out_size_compressed = 0; + int64_t resends = 0; + int64_t resends_size = 0; + bool do_ping = false; + float ping{}; + bool show = false; + + // Add in/out data for any host connection. + if (ConnectionToHost* connection_to_host = g_game->connection_to_host()) { + if (connection_to_host->can_communicate()) show = true; + in_size += connection_to_host->GetBytesInPerSecond(); + in_size_compressed += connection_to_host->GetBytesInPerSecondCompressed(); + in_count += connection_to_host->GetMessagesInPerSecond(); + out_size += connection_to_host->GetBytesOutPerSecond(); + out_size_compressed += + connection_to_host->GetBytesOutPerSecondCompressed(); + outCount += connection_to_host->GetMessagesOutPerSecond(); + resends += connection_to_host->GetMessageResendsPerSecond(); + resends_size += connection_to_host->GetBytesResentPerSecond(); + ping = connection_to_host->average_ping(); + } else { + int connected_count = 0; + for (auto&& i : g_game->connections_to_clients()) { + ConnectionToClient* client = i.second.get(); + if (client->can_communicate()) { + show = true; + connected_count += 1; + } + in_size += client->GetBytesInPerSecond(); + in_size_compressed += client->GetBytesInPerSecondCompressed(); + in_count += client->GetMessagesInPerSecond(); + out_size += client->GetBytesOutPerSecond(); + out_size_compressed += client->GetBytesOutPerSecondCompressed(); + outCount += client->GetMessagesOutPerSecond(); + resends += client->GetMessageResendsPerSecond(); + resends_size += client->GetBytesResentPerSecond(); + ping += client->average_ping(); + } + + // We want an average for ping. + if (connected_count > 0) { + ping /= static_cast(connected_count); + } + } + + if (show) { + if (do_ping) { + snprintf(net_info_str, sizeof(net_info_str), + "ping: %f\nin: %d/%d/%d\nout: %d/%d/%d\nrpt: " + "%d/%d", + ping, static_cast_check_fit(in_size), + static_cast_check_fit(in_size_compressed), + static_cast_check_fit(in_count), + static_cast_check_fit(out_size), + static_cast_check_fit(out_size_compressed), + static_cast_check_fit(outCount), + static_cast_check_fit(resends_size), + static_cast_check_fit(resends)); + } else { + snprintf(net_info_str, sizeof(net_info_str), + "in: %d/%d/%d\nout: %d/%d/%d\nrpt: %d/%d", + static_cast_check_fit(in_size), + static_cast_check_fit(in_size_compressed), + static_cast_check_fit(in_count), + static_cast_check_fit(out_size), + static_cast_check_fit(out_size_compressed), + static_cast_check_fit(outCount), + static_cast_check_fit(resends_size), + static_cast_check_fit(resends)); + } + net_info_str[sizeof(net_info_str) - 1] = 0; // in case we overran.. + if (net_info_str != net_info_string_) { + net_info_string_ = net_info_str; + if (!net_info_text_group_.exists()) { + net_info_text_group_ = Object::New(); + } + net_info_text_group_->SetText(net_info_string_); + } + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetColor(0.8f, 0.8f, 0.8f, 1.0f); + int text_elem_count = net_info_text_group_->GetElementCount(); + for (int e = 0; e < text_elem_count; e++) { + c.SetTexture(net_info_text_group_->GetElementTexture(e)); + c.SetFlatness(1.0f); + c.PushTransform(); + c.Translate(4.0f, + (show_fps_ ? 66.0f : 40.0f) + (do_ping ? 17.0f : 0.0f), + kScreenMessageZDepth); + c.Scale(0.7f, 0.7f); + c.DrawMesh(net_info_text_group_->GetElementMesh(e)); + c.PopTransform(); + } + c.Submit(); + } + } + + // Draw debug graphs. + if (explicit_bool(false)) { + if (!debug_graph_1_.exists()) { + debug_graph_1_ = Object::New(); + } + debug_graph_1_->Draw(pass, GetRealTime(), 50.0f, 50.0f, 500.0f, 100.0f); + if (!debug_graph_2_.exists()) { + debug_graph_2_ = Object::New(); + } + debug_graph_2_->Draw(pass, GetRealTime(), 50.0f, 160.0f, 500.0f, 100.0f); + } + + // Screen messages (bottom). + { + // Delete old ones. + if (!screen_messages_.empty()) { + millisecs_t cutoff; + if (GetRealTime() > 5000) { + cutoff = GetRealTime() - 5000; + for (auto i = screen_messages_.begin(); i != screen_messages_.end();) { + if (i->creation_time < cutoff) { + auto next = i; + next++; + screen_messages_.erase(i); + i = next; + } else { + i++; + } + } + } + } + + // Delete if we have too many. + while ((screen_messages_.size()) > 4) { + screen_messages_.erase(screen_messages_.begin()); + } + + // Draw all existing. + if (!screen_messages_.empty()) { + bool vr = IsVRMode(); + + // These are less disruptive in the middle for menus but at the bottom + // during gameplay. + float start_v = g_graphics->screen_virtual_height() * 0.05f; + float scale; + switch (GetInterfaceType()) { + case UIScale::kSmall: + scale = 1.5f; + break; + case UIScale::kMedium: + scale = 1.2f; + break; + default: + scale = 1.0f; + break; + } + + // Shadows. + { + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetTexture(g_media->GetTexture(SystemTextureID::kSoftRectVertical)); + + float screen_width = g_graphics->screen_virtual_width(); + + v = start_v; + + millisecs_t youngest_age = 9999; + + for (auto i = screen_messages_.rbegin(); i != screen_messages_.rend(); + i++) { + // Update the translation if need be. + i->UpdateTranslation(); + + millisecs_t age = GetRealTime() - i->creation_time; + youngest_age = std::min(youngest_age, age); + float s_extra = 1.0f; + if (age < 100) { + s_extra = std::min(1.2f, 1.2f * (age / 100.0f)); + } else if (age < 150) { + s_extra = 1.2f - 0.2f * ((150.0f - age) / 50.0f); + } + + float a; + if (age > 3000) { + a = 1.0f - static_cast(age - 3000) / 2000; + } else { + a = 1; + } + a *= 0.8f; + + if (vr) { + a *= 0.8f; + } + + assert(!i->translation_dirty); + float str_height = + g_text_graphics->GetStringHeight(i->s_translated.c_str()); + float str_width = + g_text_graphics->GetStringWidth(i->s_translated.c_str()); + + if ((str_width * scale) > (screen_width - 40)) { + s_extra *= ((screen_width - 40) / (str_width * scale)); + } + + float r = i->color.x; + float g = i->color.y; + float b = i->color.z; + GetSafeColor(&r, &g, &b); + + float v_extra = scale * (youngest_age * 0.01f); + + float fade; + if (age < 100) { + fade = 1.0f; + } else { + fade = std::max(0.0f, (200.0f - age) / 100.0f); + } + c.SetColor(r * fade, g * fade, b * fade, a); + + c.PushTransform(); + if (i->v_smoothed == 0.0f) { + i->v_smoothed = v + v_extra; + } else { + float smoothing = 0.8f; + i->v_smoothed = + smoothing * i->v_smoothed + (1.0f - smoothing) * (v + v_extra); + } + c.Translate(screen_width * 0.5f, i->v_smoothed, + vr ? 60 : kScreenMessageZDepth); + if (vr) { + // Let's drop down a bit in vr mode. + c.Translate(0, -10.0f, 0); + c.Scale((str_width + 60) * scale * s_extra, + (str_height + 20) * scale * s_extra); + + // Align our bottom with where we just scaled from. + c.Translate(0, 0.5f, 0); + } else { + c.Scale((str_width + 110) * scale * s_extra, + (str_height + 40) * scale * s_extra); + + // Align our bottom with where we just scaled from. + c.Translate(0, 0.5f, 0); + } + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + + v += scale * (36 + str_height); + if (v > g_graphics->screen_virtual_height() + 30) { + break; + } + } + c.Submit(); + } + + // Now the strings themselves. + { + SimpleComponent c(pass); + c.SetTransparent(true); + + float screen_width = g_graphics->screen_virtual_width(); + v = start_v; + millisecs_t youngest_age = 9999; + + for (auto i = screen_messages_.rbegin(); i != screen_messages_.rend(); + i++) { + millisecs_t age = GetRealTime() - i->creation_time; + youngest_age = std::min(youngest_age, age); + float s_extra = 1.0f; + if (age < 100) { + s_extra = std::min(1.2f, 1.2f * (age / 100.0f)); + } else if (age < 150) { + s_extra = 1.2f - 0.2f * ((150.0f - age) / 50.0f); + } + float a; + if (age > 3000) { + a = 1.0f - static_cast(age - 3000) / 2000; + } else { + a = 1; + } + assert(!i->translation_dirty); + float str_height = + g_text_graphics->GetStringHeight(i->s_translated.c_str()); + float str_width = + g_text_graphics->GetStringWidth(i->s_translated.c_str()); + + if ((str_width * scale) > (screen_width - 40)) { + s_extra *= ((screen_width - 40) / (str_width * scale)); + } + float r = i->color.x; + float g = i->color.y; + float b = i->color.z; + GetSafeColor(&r, &g, &b, 0.85f); + + int elem_count = i->GetText().GetElementCount(); + for (int e = 0; e < elem_count; e++) { + // Gracefully skip unloaded textures. + TextureData* t = i->GetText().GetElementTexture(e); + if (!t->preloaded()) continue; + c.SetTexture(t); + if (i->GetText().GetElementCanColor(e)) { + c.SetColor(r, g, b, a); + } else { + c.SetColor(1, 1, 1, a); + } + c.SetFlatness(i->GetText().GetElementMaxFlatness(e)); + c.PushTransform(); + c.Translate(screen_width * 0.5f, i->v_smoothed, + vr ? 150 : kScreenMessageZDepth); + c.Scale(scale * s_extra, scale * s_extra); + c.Translate(0, 20); + c.DrawMesh(i->GetText().GetElementMesh(e)); + c.PopTransform(); + } + + v += scale * (36 + str_height); + if (v > g_graphics->screen_virtual_height() + 30) break; + } + c.Submit(); + } + } + } + + // Screen messages (top). + { + // Delete old ones. + if (!screen_messages_top_.empty()) { + millisecs_t cutoff; + if (GetRealTime() > 5000) { + cutoff = GetRealTime() - 5000; + for (auto i = screen_messages_top_.begin(); + i != screen_messages_top_.end();) { + if (i->creation_time < cutoff) { + auto next = i; + next++; + screen_messages_top_.erase(i); + i = next; + } else { + i++; + } + } + } + } + + // Delete if we have too many. + while ((screen_messages_top_.size()) > 6) { + screen_messages_top_.erase(screen_messages_top_.begin()); + } + + if (!screen_messages_top_.empty()) { + SimpleComponent c(pass); + c.SetTransparent(true); + + // Draw all existing. + float h = pass->virtual_width() - 300.0f; + v = g_graphics->screen_virtual_height() - 50.0f; + + float v_base = g_graphics->screen_virtual_height(); + float last_v = -999.0f; + + float min_spacing = 25.0f; + + for (auto i = screen_messages_top_.rbegin(); + i != screen_messages_top_.rend(); i++) { + // Update the translation if need be. + i->UpdateTranslation(); + + millisecs_t age = GetRealTime() - i->creation_time; + float s_extra = 1.0f; + if (age < 100) { + s_extra = std::min(1.1f, 1.1f * (age / 100.0f)); + } else if (age < 150) { + s_extra = 1.1f - 0.1f * ((150.0f - age) / 50.0f); + } + + float a; + if (age > 3000) { + a = 1.0f - static_cast(age - 3000) / 2000; + } else { + a = 1; + } + + i->v_smoothed += 0.1f; + if (i->v_smoothed - last_v < min_spacing) { + i->v_smoothed += + 8.0f * (1.0f - ((i->v_smoothed - last_v) / min_spacing)); + } + last_v = i->v_smoothed; + + // Draw the image if they provided one. + if (i->texture.exists()) { + c.Submit(); + + SimpleComponent c2(pass); + c2.SetTransparent(true); + c2.SetTexture(i->texture); + if (i->tint_texture.exists()) { + c2.SetColorizeTexture(i->tint_texture); + c2.SetColorizeColor(i->tint.x, i->tint.y, i->tint.z); + c2.SetColorizeColor2(i->tint2.x, i->tint2.y, i->tint2.z); + c2.SetMaskTexture( + g_media->GetTexture(SystemTextureID::kCharacterIconMask)); + } + c2.SetColor(1, 1, 1, a); + c2.PushTransform(); + c2.Translate(h - 14, v_base + 10 + i->v_smoothed, + kScreenMessageZDepth); + c2.Scale(22.0f * s_extra, 22.0f * s_extra); + c2.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c2.PopTransform(); + c2.Submit(); + } + + float r = i->color.x; + float g = i->color.y; + float b = i->color.z; + GetSafeColor(&r, &g, &b); + + int elem_count = i->GetText().GetElementCount(); + for (int e = 0; e < elem_count; e++) { + // Gracefully skip unloaded textures. + TextureData* t = i->GetText().GetElementTexture(e); + if (!t->preloaded()) continue; + c.SetTexture(t); + if (i->GetText().GetElementCanColor(e)) { + c.SetColor(r, g, b, a); + } else { + c.SetColor(1, 1, 1, a); + } + c.SetShadow(-0.003f * i->GetText().GetElementUScale(e), + -0.003f * i->GetText().GetElementVScale(e), 0.0f, + 1.0f * a); + c.SetFlatness(i->GetText().GetElementMaxFlatness(e)); + c.SetMaskUV2Texture(i->GetText().GetElementMaskUV2Texture(e)); + c.PushTransform(); + c.Translate(h, v_base + 2 + i->v_smoothed, kScreenMessageZDepth); + c.Scale(0.6f * s_extra, 0.6f * s_extra); + c.DrawMesh(i->GetText().GetElementMesh(e)); + c.PopTransform(); + } + assert(!i->translation_dirty); + v -= g_text_graphics->GetStringHeight(i->s_translated.c_str()) * 0.6f + + 8.0f; + } + c.Submit(); + } + } +} + +void Graphics::GetSafeColor(float* red, float* green, float* blue, + float target_intensity) { + assert(red && green && blue); + + // Mult our color up to try and hit the target intensity. + float intensity = 0.2989f * (*red) + 0.5870f * (*green) + 0.1140f * (*blue); + if (intensity < target_intensity) { + float s = target_intensity / std::max(0.001f, intensity); + *red = std::min(1.0f, (*red) * s); + *green = std::min(1.0f, (*green) * s); + *blue = std::min(1.0f, (*blue) * s); + } + + // We may still be short of our target intensity due to clamping (ie: (10,0,0) + // will not look any brighter than (1,0,0)) if that's the case, just convert + // the difference to a grey value and add that to all channels... this *still* + // might not get us there so lets do it a few times if need be. (i'm sure + // there's a less bone-headed way to do this) + for (int i = 0; i < 4; i++) { + float remaining = + (0.2989f * (*red) + 0.5870f * (*green) + 0.1140f * (*blue)) - 1.0f; + if (remaining > 0.0f) { + *red = std::min(1.0f, (*red) + 0.2989f * remaining); + *green = std::min(1.0f, (*green) + 0.5870f * remaining); + *blue = std::min(1.0f, (*blue) + 0.1140f * remaining); + } else { + break; + } + } +} + +void Graphics::AddScreenMessage(const std::string& msg, const Vector3f& color, + bool top, Texture* texture, + Texture* tint_texture, const Vector3f& tint, + const Vector3f& tint2) { + // So we know we're always dealing with valid utf8. + std::string m = Utils::GetValidUTF8(msg.c_str(), "ga9msg"); + + assert(InGameThread()); + if (top) { + float start_v = -40.0f; + if (!screen_messages_top_.empty()) { + start_v = std::min( + start_v, + std::max(-100.0f, screen_messages_top_.back().v_smoothed - 25.0f)); + } + screen_messages_top_.emplace_back(m, true, GetRealTime(), color, texture, + tint_texture, tint, tint2); + screen_messages_top_.back().v_smoothed = start_v; + } else { + screen_messages_.emplace_back(m, false, GetRealTime(), color, texture, + tint_texture, tint, tint2); + } +} + +void Graphics::Reset() { + fade_ = 0; + fade_start_ = 0; + + if (!camera_.exists()) { + camera_ = Object::New(); + } + + // Wipe out top screen messages since they might be using textures that are + // being reset. Bottom ones are ok since they have no textures. + screen_messages_top_.clear(); +} + +void Graphics::InitInternalComponents(FrameDef* frame_def) { + RenderPass* pass = frame_def->GetOverlayFlatPass(); + + screen_mesh_ = Object::New(); + + // Let's draw a bit bigger than screen to account for tv-border-mode. + float w = pass->virtual_width(); + float h = pass->virtual_height(); + if (IsVRMode()) { + screen_mesh_->SetPositionAndSize( + -(0.5f * kVRBorder) * w, (-0.5f * kVRBorder) * h, kScreenMeshZDepth, + (1.0f + kVRBorder) * w, (1.0f + kVRBorder) * h); + } else { + screen_mesh_->SetPositionAndSize( + -(0.5f * kTVBorder) * w, (-0.5f * kTVBorder) * h, kScreenMeshZDepth, + (1.0f + kTVBorder) * w, (1.0f + kTVBorder) * h); + } + progress_bar_top_mesh_ = Object::New(); + progress_bar_bottom_mesh_ = Object::New(); + load_dot_mesh_ = Object::New(); + load_dot_mesh_->SetPositionAndSize(0, 0, 0, 2, 2); +} + +auto Graphics::GetEmptyFrameDef() -> FrameDef* { + assert(InGameThread()); + FrameDef* frame_def; + + // Grab a ready-to-use recycled one if available. + if (!recycle_frame_defs_.empty()) { + frame_def = recycle_frame_defs_.back(); + recycle_frame_defs_.pop_back(); + } else { + frame_def = new FrameDef(); + } + frame_def->Reset(); + return frame_def; +} + +void Graphics::ClearFrameDefDeleteList() { + assert(InGameThread()); + std::lock_guard lock(frame_def_delete_list_mutex_); + + for (auto& i : frame_def_delete_list_) { + // We recycle our frame_defs so we don't have to reallocate all those + // buffers. + if (recycle_frame_defs_.size() < 5) { + recycle_frame_defs_.push_back(i); + } else { + delete i; + } + } + frame_def_delete_list_.clear(); +} + +void Graphics::FadeScreen(bool to, millisecs_t time, PyObject* endcall) { + // If there's an ourstanding fade-end command, go ahead and run it. + // (otherwise, overlapping fades can cause things to get lost) + if (fade_end_call_.exists()) { + if (g_buildconfig.debug_build()) { + Log("WARNING: 2 fades overlapping; running first fade-end-call early"); + } + g_game->PushPythonCall(fade_end_call_); + fade_end_call_.Clear(); + } + set_fade_start_on_next_draw_ = true; + fade_time_ = time; + fade_out_ = !to; + if (endcall) { + fade_end_call_ = Object::New(endcall); + } + fade_ = 1.0f; +} + +void Graphics::DrawLoadDot(RenderPass* pass) { + // Draw a little bugger in the corner if we're loading something. + SimpleComponent c(pass); + c.SetTransparent(true); + + // Draw red if we've got graphics stuff loading. Green if only other stuff + // left. + if (g_media->GetGraphicalPendingLoadCount() > 0) { + c.SetColor(0.2f, 0, 0, 1); + } else { + c.SetColor(0, 0.2f, 0, 1); + } + c.DrawMesh(load_dot_mesh_.get()); + c.Submit(); +} + +void Graphics::UpdateGyro(millisecs_t real_time, millisecs_t elapsed) { + Vector3f tilt = gyro_vals_; + + // Our gyro vals get set from another thread and we don't use a lock, + // so perhaps there's a chance we get corrupted float values here?.. + // Let's watch out for crazy vals just in case. + for (float& i : tilt.v) { + // Check for NaN and Inf: + if (!std::isfinite(i)) { + i = 0.0f; + } + + // Clamp crazy big values: + i = std::min(100.0f, std::max(-100.0f, i)); + } + + // Our math was calibrated for 60hz (16ms per frame); + // adjust for other framerates... + float timescale = static_cast(elapsed) / 16.0f; + + // If we've recently been told to suppress the gyro, zero these. + // (prevents hitches when being restored, etc) + if (!gyro_enabled_ || camera_gyro_explicitly_disabled_ + || (real_time - last_suppress_gyro_time_ < 1000)) { + tilt = Vector3f{0.0, 0.0, 0.0}; + } + + float tilt_smoothing = 0.0f; + tilt_smoothed_ = + tilt_smoothing * tilt_smoothed_ + (1.0f - tilt_smoothing) * tilt; + + tilt_vel_ = tilt_smoothed_ * 3.0f; + tilt_pos_ += tilt_vel_ * timescale; + + // Technically this will behave slightly differently at different time scales, + // but it should be close to correct.. + // tilt_pos_ *= 0.991f; + tilt_pos_ *= std::max(0.0, 1.0f - 0.01 * timescale); + + // Some gyros seem wonky and either give us crazy big values or consistently + // offset ones. Let's keep a running tally of magnitude that slowly drops over + // time, and if it reaches a certain value lets just kill gyro input. + if (gyro_broken_) { + tilt_pos_ *= 0.0f; + } else { + gyro_mag_test_ += tilt_vel_.Length() * 0.01f * timescale; + gyro_mag_test_ = std::max(0.0f, gyro_mag_test_ - 0.02f * timescale); + if (gyro_mag_test_ > 100.0f) { + ScreenMessage("Wonky gyro; disabling tilt.", {1, 0, 0}); + gyro_broken_ = true; + } + } +} + +void Graphics::ApplyCamera(FrameDef* frame_def) { + camera_->Update(frame_def->base_time_elapsed()); + camera_->UpdatePosition(); + camera_->ApplyToFrameDef(frame_def); +} + +void Graphics::DrawWorld(Session* session, FrameDef* frame_def) { + // Draw all session contents (nodes, etc.) + overlay_node_z_depth_ = -0.95f; + if (session) { + session->Draw(frame_def); + frame_def->set_benchmark_type(session->benchmark_type()); + } + if (!HeadlessMode()) { + g_bg_dynamics->Draw(frame_def); + } + + // Lastly draw any blotches that have been building up. + DrawBlotches(frame_def); + + // Add a few explicit things to a few passes. + DrawBoxingGlovesTest(frame_def); +} + +void Graphics::BuildAndPushFrameDef() { + assert(InGameThread()); + assert(camera_.exists()); + + // We should not be building/pushing any frames until after + // app-launch-commands have been run.. + BA_PRECONDITION_FATAL(g_game->ran_app_launch_commands()); + + // This should no longer be necessary.. + WaitForRendererToExist(); + + Session* session = g_game->GetForegroundSession(); + bool session_fills_screen = session ? session->DoesFillScreen() : false; + millisecs_t real_time = GetRealTime(); + + // Store how much time this frame_def represents. + millisecs_t net_time = g_game->master_time(); + millisecs_t elapsed = + std::min(millisecs_t{50}, net_time - last_create_frame_def_time_); + last_create_frame_def_time_ = net_time; + + UpdateGyro(real_time, elapsed); + + FrameDef* frame_def = GetEmptyFrameDef(); + frame_def->set_real_time(real_time); + frame_def->set_base_time(g_game->master_time()); + frame_def->set_base_time_elapsed(elapsed); + frame_def->set_frame_number(frame_def_count_++); + + if (!internal_components_inited_) { + InitInternalComponents(frame_def); + internal_components_inited_ = true; + } + + ApplyCamera(frame_def); + + // Clear to black for either progress bar or when we've got no meaningful + // session to draw. + frame_def->set_needs_clear(progress_bar_ || !session_fills_screen); + + if (progress_bar_) { + UpdateAndDrawProgressBar(frame_def, real_time); + } else { + // Ok, we're drawing a real frame. + + DrawWorld(session, frame_def); + + // Now some overlay stuff. + RenderPass* overlay_pass = frame_def->overlay_pass(); + + DrawUI(frame_def); + + // Let input draw anything it needs to. (touch input graphics, etc) + g_input->Draw(frame_def); + + DrawMiscOverlays(overlay_pass); + + // Draw console. + if (!HeadlessMode() && g_app_globals->console) { + g_app_globals->console->Draw(overlay_pass); + } + + DrawCursor(overlay_pass, real_time); + + // Draw our light/shadow images to the screen if desired. + DrawDebugBuffers(overlay_pass); + + // In high-quality modes we draw a screen-quad as a catch-all for blitting + // the world buffer to the screen (other nodes can add their own blitters + // such as distortion shapes which will have priority). + if (frame_def->quality() >= GraphicsQuality::kHigh) { + PostProcessComponent c(frame_def->blit_pass()); + c.DrawScreenQuad(); + c.Submit(); + } + + DrawFades(frame_def, real_time); + + // Sanity test: If we're in VR, the only reason we should have stuff in the + // flat overlay pass is if there's windows present (we want to avoid + // drawing/blitting the 2d UI buffer during gameplay for efficiency). + if (IsVRMode()) { + if (frame_def->GetOverlayFlatPass()->HasDrawCommands()) { + if (!g_ui->IsWindowPresent()) { + BA_LOG_ONCE( + "Drawing in overlay pass in VR mode without UI; shouldn't " + "happen!"); + } + } + } + + if (g_media->GetPendingLoadCount() > 0) { + DrawLoadDot(overlay_pass); + } + + // Lastly, if we had anything waiting to run until the progress bar was + // gone, run it. + g_python->RunCleanFrameCommands(); + } + + frame_def->Finalize(); + + // Include all mesh-data loads and unloads that have accumulated up to this + // point the graphics thread will have to handle these before rendering the + // frame_def. + frame_def->set_mesh_data_creates(mesh_data_creates_); + mesh_data_creates_.clear(); + frame_def->set_mesh_data_destroys(mesh_data_destroys_); + mesh_data_destroys_.clear(); + + g_graphics_server->SetFrameDef(frame_def); + + // Clean up frame_defs awaiting deletion. + ClearFrameDefDeleteList(); + + // Clear our blotches out regardless of whether we rendered them. + blotch_indices_.clear(); + blotch_verts_.clear(); + blotch_soft_indices_.clear(); + blotch_soft_verts_.clear(); + blotch_soft_obj_indices_.clear(); + blotch_soft_obj_verts_.clear(); +} + +auto Graphics::DrawUI(FrameDef* frame_def) -> void { g_ui->Draw(frame_def); } + +void Graphics::DrawBoxingGlovesTest(FrameDef* frame_def) { + // Test: boxing glove. + if (explicit_bool(false)) { + float a = 0; + + // Blit. + if (explicit_bool(true)) { + PostProcessComponent c(frame_def->blit_pass()); + c.setNormalDistort(0.07f); + c.PushTransform(); + c.Translate(0, 7, -3.3f); + c.Scale(10, 10, 10); + c.Rotate(a, 0, 0, 1); + c.DrawModel(g_media->GetModel(SystemModelID::kBoxingGlove)); + c.PopTransform(); + c.Submit(); + } + + // Beauty. + if (explicit_bool(false)) { + ObjectComponent c(frame_def->beauty_pass()); + c.SetTexture(g_media->GetTexture(SystemTextureID::kBoxingGlove)); + c.SetReflection(ReflectionType::kSoft); + c.SetReflectionScale(0.4f, 0.4f, 0.4f); + c.PushTransform(); + c.Translate(0.0f, 3.7f, -3.3f); + c.Scale(10.0f, 10.0f, 10.0f); + c.Rotate(a, 0.0f, 0.0f, 1.0f); + c.DrawModel(g_media->GetModel(SystemModelID::kBoxingGlove)); + c.PopTransform(); + c.Submit(); + } + + // Light. + if (explicit_bool(true)) { + SimpleComponent c(frame_def->light_shadow_pass()); + c.SetColor(0.16f, 0.11f, 0.1f, 1.0f); + c.SetTransparent(true); + c.PushTransform(); + c.Translate(0.0f, 3.7f, -3.3f); + c.Scale(10.0f, 10.0f, 10.0f); + c.Rotate(a, 0.0f, 0.0f, 1.0f); + c.DrawModel(g_media->GetModel(SystemModelID::kBoxingGlove)); + c.PopTransform(); + c.Submit(); + } + } +} + +void Graphics::DrawDebugBuffers(RenderPass* pass) { + if (explicit_bool(false)) { + { + SpecialComponent c(pass, SpecialComponent::Source::kLightBuffer); + float csize = 100; + c.PushTransform(); + c.Translate(70, 400, kDebugImgZDepth); + c.Scale(csize, csize); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + c.Submit(); + } + { + SpecialComponent c(pass, SpecialComponent::Source::kLightShadowBuffer); + float csize = 100; + c.PushTransform(); + c.Translate(70, 250, kDebugImgZDepth); + c.Scale(csize, csize); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + c.Submit(); + } + } +} + +void Graphics::UpdateAndDrawProgressBar(FrameDef* frame_def, + millisecs_t real_time) { + RenderPass* pass = frame_def->overlay_pass(); + UpdateProgressBarProgress( + 1.0f + - static_cast(g_media->GetGraphicalPendingLoadCount()) + / static_cast(progress_bar_loads_)); + DrawProgressBar(pass, 1.0f); + + // If we were drawing a progress bar, see if everything is now loaded.. if + // so, start rendering normally next frame. + int count = g_media->GetGraphicalPendingLoadCount(); + if (count <= 0) { + progress_bar_ = false; + progress_bar_end_time_ = real_time; + } + if (g_media->GetPendingLoadCount() > 0) { + DrawLoadDot(pass); + } +} + +void Graphics::DrawFades(FrameDef* frame_def, millisecs_t real_time) { + RenderPass* overlay_pass = frame_def->overlay_pass(); + + // Guard against accidental fades that never fade back in. + if (fade_ <= 0.0f && fade_out_) { + millisecs_t faded_time = real_time - (fade_start_ + fade_time_); + if (faded_time > 15000) { + Log("FORCE-ENDING STUCK FADE"); + fade_out_ = false; + fade_ = 1.0f; + fade_time_ = 1000; + fade_start_ = real_time; + } + } + + // Update fade values. + if (fade_ > 0) { + if (set_fade_start_on_next_draw_) { + set_fade_start_on_next_draw_ = false; + fade_start_ = real_time; + } + bool was_done = fade_ <= 0; + if (real_time <= fade_start_) { + fade_ = 1; + } else if ((real_time - fade_start_) < fade_time_) { + fade_ = 1.0f + - (static_cast(real_time - fade_start_) + / static_cast(fade_time_)); + if (fade_ <= 0) fade_ = 0.00001f; + } else { + fade_ = 0; + if (!was_done && fade_end_call_.exists()) { + g_game->PushPythonCall(fade_end_call_); + fade_end_call_.Clear(); + } + } + } + + // Draw a fade if we're either in a fade or fading back in from a + // progress-bar screen. + if (fade_ > 0.00001f || fade_out_ + || (real_time - progress_bar_end_time_ < kProgressBarFadeTime)) { + float a = fade_out_ ? 1 - fade_ : fade_; + if (real_time - progress_bar_end_time_ < kProgressBarFadeTime) { + a = 1.0f * a + + (1.0f + - static_cast(real_time - progress_bar_end_time_) + / static_cast(kProgressBarFadeTime)) + * (1.0f - a); + } + if (IsVRMode()) { +#if BA_VR_BUILD + SimpleComponent c(frame_def->vr_cover_pass()); + c.SetTransparent(false); + Vector3f cam_pt = {0.0f, 0.0f, 0.0f}; + Vector3f cam_target_pt = {0.0f, 0.0f, 0.0f}; + cam_pt = + Vector3f(frame_def->cam_original().x, frame_def->cam_original().y, + frame_def->cam_original().z); + // in vr follow-mode the cam point gets tweaked.. (fixme should probably + // just do this on the camera end) + if (frame_def->camera_mode() == CameraMode::kOrbit) { + // fudge this one up a bit; looks better that way.. + cam_target_pt = Vector3f(frame_def->cam_target_original().x, + frame_def->cam_target_original().y + 6.0f, + frame_def->cam_target_original().z); + } else { + cam_target_pt = Vector3f(frame_def->cam_target_original().x, + frame_def->cam_target_original().y, + frame_def->cam_target_original().z); + } + Vector3f diff = cam_target_pt - cam_pt; + diff.Normalize(); + Vector3f side = Vector3f::Cross(diff, Vector3f(0.0f, 1.0f, 0.0f)); + Vector3f up = Vector3f::Cross(diff, side); + c.SetColor(0, 0, 0); + c.PushTransform(); + // we start in vr-overlay screen space; get back to world.. + c.Translate(cam_pt.x, cam_pt.y, cam_pt.z); + c.MultMatrix(Matrix44fOrient(diff, up).m); + // at the very end we stay turned around so we get 100% black + if (a < 0.98f) { + c.Translate(0, 0, 40.0f * a); + c.Rotate(180, 1, 0, 0); + } + float inv_a = 1.0f - a; + float s = 100.0f * inv_a + 5.0f * a; + c.Scale(s, s, s); + c.DrawModel(g_media->GetModel(SystemModelID::kVRFade)); + c.PopTransform(); + c.Submit(); +#else // BA_VR_BUILD + throw Exception(); +#endif // BA_VR_BUILD + } else { + SimpleComponent c(overlay_pass); + c.SetTransparent(a < 1.0f); + c.SetColor(0, 0, 0, a); + c.DrawMesh(screen_mesh_.get()); + c.Submit(); + } + + // If we're doing a progress-bar fade, throw in the fading progress bar. + if (real_time - progress_bar_end_time_ < kProgressBarFadeTime / 2) { + float o = (1.0f + - static_cast(real_time - progress_bar_end_time_) + / (static_cast(kProgressBarFadeTime) * 0.5f)); + UpdateProgressBarProgress(1.0f); + DrawProgressBar(overlay_pass, o); + } + } +} + +void Graphics::DrawCursor(RenderPass* pass, millisecs_t real_time) { + assert(InGameThread()); + + bool can_show_cursor = g_platform->IsRunningOnDesktop(); + bool should_show_cursor = camera_->manual() || g_input->IsCursorVisible(); + + if (g_buildconfig.hardware_cursor()) { + // If we're using a hardware cursor, ship hardware cursor visibility + // updates to the app thread periodically. + bool new_cursor_visibility = false; + if (can_show_cursor && should_show_cursor) { + new_cursor_visibility = true; + } + + // 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_ + || real_time - last_cursor_visibility_event_time_ > 2000) { + hardware_cursor_visible_ = new_cursor_visibility; + last_cursor_visibility_event_time_ = real_time; + g_app->PushCursorUpdate(hardware_cursor_visible_); + } + } else { + // Draw software cursor. + if (can_show_cursor && should_show_cursor) { + SimpleComponent c(pass); + c.SetTransparent(true); + float csize = 50.0f; + c.SetTexture(g_media->GetTexture(SystemTextureID::kCursor)); + c.PushTransform(); + + // Note: we don't plug in known cursor position values here; we tell the + // renderer to insert the latest values on its end; this lessens cursor + // lag substantially. + c.CursorTranslate(); + c.Translate(csize * 0.44f, csize * -0.44f, kCursorZDepth); + c.Scale(csize, csize); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + c.Submit(); + } + } +} + +void Graphics::DrawBlotches(FrameDef* frame_def) { + if (!this->blotch_verts_.empty()) { + if (!this->shadow_blotch_mesh_.exists()) + this->shadow_blotch_mesh_ = Object::New(); + this->shadow_blotch_mesh_->SetIndexData(Object::New( + this->blotch_indices_.size(), &this->blotch_indices_[0])); + this->shadow_blotch_mesh_->SetData(Object::New>( + this->blotch_verts_.size(), &this->blotch_verts_[0])); + SpriteComponent c(frame_def->light_shadow_pass()); + c.SetTexture(g_media->GetTexture(SystemTextureID::kLight)); + c.DrawMesh(this->shadow_blotch_mesh_.get()); + c.Submit(); + } + if (!this->blotch_soft_verts_.empty()) { + if (!this->shadow_blotch_soft_mesh_.exists()) + this->shadow_blotch_soft_mesh_ = Object::New(); + this->shadow_blotch_soft_mesh_->SetIndexData(Object::New( + this->blotch_soft_indices_.size(), &this->blotch_soft_indices_[0])); + this->shadow_blotch_soft_mesh_->SetData( + Object::New>(this->blotch_soft_verts_.size(), + &this->blotch_soft_verts_[0])); + SpriteComponent c(frame_def->light_shadow_pass()); + c.SetTexture(g_media->GetTexture(SystemTextureID::kLightSoft)); + c.DrawMesh(this->shadow_blotch_soft_mesh_.get()); + c.Submit(); + } + if (!this->blotch_soft_obj_verts_.empty()) { + if (!this->shadow_blotch_soft_obj_mesh_.exists()) { + this->shadow_blotch_soft_obj_mesh_ = Object::New(); + } + this->shadow_blotch_soft_obj_mesh_->SetIndexData( + Object::New(this->blotch_soft_obj_indices_.size(), + &this->blotch_soft_obj_indices_[0])); + this->shadow_blotch_soft_obj_mesh_->SetData( + Object::New>( + this->blotch_soft_obj_verts_.size(), + &this->blotch_soft_obj_verts_[0])); + SpriteComponent c(frame_def->light_pass()); + c.SetTexture(g_media->GetTexture(SystemTextureID::kLightSoft)); + c.DrawMesh(this->shadow_blotch_soft_obj_mesh_.get()); + c.Submit(); + } +} + +void Graphics::SetSupportsHighQualityGraphics(bool s) { + supports_high_quality_graphics_ = s; + has_supports_high_quality_graphics_value_ = true; +} + +void Graphics::ClearScreenMessageTranslations() { + for (auto&& i : screen_messages_) { + i.translation_dirty = true; + } + for (auto&& i : screen_messages_top_) { + i.translation_dirty = true; + } +} + +void Graphics::ReturnCompletedFrameDef(FrameDef* frame_def) { + std::lock_guard lock(frame_def_delete_list_mutex_); + g_graphics->frame_def_delete_list_.push_back(frame_def); +} + +void Graphics::AddMeshDataCreate(MeshData* d) { + assert(InGameThread()); + assert(g_graphics); + + // Add this to our list of new-mesh-datas. We'll include this with our + // next frame_def to have the graphics thread load before it processes + // the frame_def. + mesh_data_creates_.push_back(d); +} + +void Graphics::AddMeshDataDestroy(MeshData* d) { + assert(InGameThread()); + assert(g_graphics); + + // Add this to our list of delete-mesh-datas; we'll include this with our + // next frame_def to have the graphics thread kill before it processes + // the frame_def. + mesh_data_destroys_.push_back(d); +} + +void Graphics::EnableProgressBar(bool fade_in) { + assert(InGameThread()); + progress_bar_loads_ = g_media->GetGraphicalPendingLoadCount(); + assert(progress_bar_loads_ >= 0); + if (progress_bar_loads_ > 0) { + progress_bar_ = true; + progress_bar_fade_in_ = fade_in; + last_progress_bar_draw_time_ = GetRealTime(); + last_progress_bar_start_time_ = last_progress_bar_draw_time_; + progress_bar_progress_ = 0.0f; + } +} + +void Graphics::ToggleManualCamera() { + assert(InGameThread()); + camera_->SetManual(!camera_->manual()); + if (camera_->manual()) { + ScreenMessage("Manual Camera On"); + } else { + ScreenMessage("Manual Camera Off"); + } +} + +void Graphics::LocalCameraShake(float mag) { + assert(InGameThread()); + if (camera_.exists()) { + camera_->Shake(mag); + } +} + +void Graphics::ToggleDebugInfoDisplay() { + assert(InGameThread()); + debug_info_display_ = !debug_info_display_; + if (debug_info_display_) { + ScreenMessage("debug info on\n"); + } else { + ScreenMessage("debug info off\n"); + } +} + +void Graphics::ToggleDebugDraw() { + assert(InGameThread()); + debug_draw_ = !debug_draw_; + if (g_graphics_server->renderer()) { + g_graphics_server->renderer()->set_debug_draw_mode(debug_draw_); + } +} + +void Graphics::ReleaseFadeEndCommand() { fade_end_call_.Clear(); } + +void Graphics::WaitForRendererToExist() { + // Conceivably we could hit this point before our graphics thread has created + // the renderer. In that case lets wait a moment. + int sleep_count = 0; + while (g_graphics_server == nullptr + || g_graphics_server->renderer() == nullptr) { + BA_LOG_ONCE( + "BuildAndPushFrameDef() called before renderer is up; spinning..."); + Platform::SleepMS(100); + sleep_count++; + if (sleep_count > 100) { + throw Exception( + "Aborting waiting for renderer to come up in BuildAndPushFrameDef()"); + } + } +} + +auto Graphics::ValueTest(const std::string& arg, double* absval, + double* deltaval, double* outval) -> bool { + return false; +} + +void Graphics::DoDrawBlotch(std::vector* indices, + std::vector* verts, + const Vector3f& pos, float size, float r, float g, + float b, float a) { + assert(InGameThread()); + assert(indices && verts); + + // Add verts. + assert((*verts).size() < 65536); + auto count = static_cast((*verts).size()); + (*verts).resize(count + 4); + { + VertexSprite& p((*verts)[count]); + p.position[0] = pos.x; + p.position[1] = pos.y; + p.position[2] = pos.z; + p.uv[0] = 0; + p.uv[1] = 0; + p.size = size; + p.color[0] = r; + p.color[1] = g; + p.color[2] = b; + p.color[3] = a; + } + { + VertexSprite& p((*verts)[count + 1]); + p.position[0] = pos.x; + p.position[1] = pos.y; + p.position[2] = pos.z; + p.uv[0] = 0; + p.uv[1] = 65535; + p.size = size; + p.color[0] = r; + p.color[1] = g; + p.color[2] = b; + p.color[3] = a; + } + { + VertexSprite& p((*verts)[count + 2]); + p.position[0] = pos.x; + p.position[1] = pos.y; + p.position[2] = pos.z; + p.uv[0] = 65535; + p.uv[1] = 0; + p.size = size; + p.color[0] = r; + p.color[1] = g; + p.color[2] = b; + p.color[3] = a; + } + { + VertexSprite& p((*verts)[count + 3]); + p.position[0] = pos.x; + p.position[1] = pos.y; + p.position[2] = pos.z; + p.uv[0] = 65535; + p.uv[1] = 65535; + p.size = size; + p.color[0] = r; + p.color[1] = g; + p.color[2] = b; + p.color[3] = a; + } + + // Add indices. + { + size_t i_count = (*indices).size(); + (*indices).resize(i_count + 6); + uint16_t* i = &(*indices)[i_count]; + i[0] = count; + i[1] = static_cast(count + 1); + i[2] = static_cast(count + 2); + i[3] = static_cast(count + 1); + i[4] = static_cast(count + 3); + i[5] = static_cast(count + 2); + } +} + +void Graphics::DrawRadialMeter(MeshIndexedSimpleFull* m, float amt) { + // FIXME - we're updating this every frame so we should use pure dynamic data; + // not a mix of static and dynamic. + + if (amt >= 0.999f) { + // clang-format off + uint16_t indices[] = {0, 1, 2, 1, 3, 2}; + VertexSimpleFull vertices[] = { + {-1, -1, 0, 0, 65535}, + {1, -1, 0, 65535, 65535}, + {-1, 1, 0, 0, 0}, + {1, 1, 0, 65535, 0, + } + }; + // clang-format on + m->SetIndexData(Object::New(6, indices)); + m->SetData(Object::New>(4, vertices)); + + } else { + bool flipped = true; + uint16_t indices[15]; + VertexSimpleFull v[15]; + float x = -tanf(amt * (3.141592f * 2.0f)); + uint16_t i = 0; + + // First 45 degrees past 12:00. + if (amt > 0.875f) { + if (flipped) { + v[i].uv[0] = 0; + v[i].uv[1] = 0; + v[i].position[0] = -1; + v[i].position[1] = 1; + v[i].position[2] = 0; + indices[i] = i; + i++; + v[i].uv[0] = static_cast(65535 - 65535 * 0.5f); + v[i].uv[1] = static_cast(65535 * 0.5f); + v[i].position[0] = 0; + v[i].position[1] = 0; + v[i].position[2] = 0; + indices[i] = i; + i++; + v[i].uv[0] = static_cast(65535 - 65535 * (0.5f + x * 0.5f)); + v[i].uv[1] = 0; + v[i].position[0] = -x; + v[i].position[1] = 1; + v[i].position[2] = 0; + indices[i] = i; + i++; + } + } + + // Top right down to bot-right. + if (amt > 0.625f) { + float y = (amt > 0.875f ? -1.0f : 1.0f / tanf(amt * (3.141592f * 2.0f))); + if (flipped) { + v[i].uv[0] = 0; + v[i].uv[1] = static_cast(65535 * (0.5f + y * 0.5f)); + v[i].position[0] = -1; + v[i].position[1] = -y; + v[i].position[2] = 0; + indices[i] = i; + i++; + v[i].uv[0] = 0; + v[i].uv[1] = 65535; + v[i].position[0] = -1; + v[i].position[1] = -1; + v[i].position[2] = 0; + indices[i] = i; + i++; + v[i].uv[0] = static_cast(65535 - 65535 * 0.5f); + v[i].uv[1] = static_cast(65535 * 0.5f); + v[i].position[0] = 0; + v[i].position[1] = 0; + v[i].position[2] = 0; + indices[i] = i; + i++; + } + } + + // Bot right to bot left. + if (amt > 0.375f) { + float x2 = (amt > 0.625f ? 1.0f : tanf(amt * (3.141592f * 2.0f))); + if (flipped) { + v[i].uv[0] = static_cast(65535 - 65535 * (0.5f + x2 * 0.5f)); + v[i].uv[1] = 65535; + v[i].position[0] = -x2; + v[i].position[1] = -1; + v[i].position[2] = 0; + indices[i] = i; + i++; + + v[i].uv[0] = 65535; + v[i].uv[1] = 65535; + v[i].position[0] = 1; + v[i].position[1] = -1; + v[i].position[2] = 0; + indices[i] = i; + i++; + + v[i].uv[0] = static_cast(65535 - 65535 * 0.5f); + v[i].uv[1] = static_cast(65535 * 0.5f); + v[i].position[0] = 0; + v[i].position[1] = 0; + v[i].position[2] = 0; + indices[i] = i; + i++; + } + } + + // Bot left to top left. + if (amt > 0.125f) { + float y = (amt > 0.375f ? -1.0f : 1.0f / tanf(amt * (3.141592f * 2.0f))); + + if (flipped) { + v[i].uv[0] = static_cast(65535 - 65535 * 0.5f); + v[i].uv[1] = static_cast(65535 * 0.5f); + v[i].position[0] = 0; + v[i].position[1] = 0; + v[i].position[2] = 0; + indices[i] = i; + i++; + + v[i].uv[0] = 65535; + v[i].uv[1] = static_cast(65535 * (0.5f - 0.5f * y)); + v[i].position[0] = 1; + v[i].position[1] = y; + v[i].position[2] = 0; + indices[i] = i; + i++; + + v[i].uv[0] = 65535; + v[i].uv[1] = 0; + v[i].position[0] = 1; + v[i].position[1] = 1; + v[i].position[2] = 0; + indices[i] = i; + i++; + } + } + + // Top left to top mid. + { + float x2 = (amt > 0.125f ? 1.0f : tanf(amt * (3.141592f * 2.0f))); + if (flipped) { + v[i].uv[0] = static_cast(65535 - 65535 * 0.5f); + v[i].uv[1] = static_cast(65535 * 0.5f); + v[i].position[0] = 0; + v[i].position[1] = 0; + v[i].position[2] = 0; + indices[i] = i; + i++; + + v[i].uv[0] = static_cast(65535 - 65535 * (0.5f - x2 * 0.5f)); + v[i].uv[1] = 0; + v[i].position[0] = x2; + v[i].position[1] = 1; + v[i].position[2] = 0; + indices[i] = i; + i++; + + v[i].uv[0] = static_cast(65535 - 65535 * 0.5f); + v[i].uv[1] = 0; + v[i].position[0] = 0; + v[i].position[1] = 1; + v[i].position[2] = 0; + indices[i] = i; + i++; + } + } + m->SetIndexData(Object::New(i, indices)); + m->SetData(Object::New>(i, v)); + } +} + +auto Graphics::ScreenMessageEntry::GetText() -> TextGroup& { + assert(!translation_dirty); + if (!s_mesh_.exists()) { + s_mesh_ = Object::New(); + mesh_dirty = true; + } + if (mesh_dirty) { + s_mesh_->SetText( + s_translated, + align_left ? TextMesh::HAlign::kLeft : TextMesh::HAlign::kCenter, + TextMesh::VAlign::kBottom); + mesh_dirty = false; + } + return *s_mesh_; +} + +void Graphics::ScreenResize(float virtual_width, float virtual_height, + float pixel_width, float pixel_height) { + assert(InGameThread()); + res_x_virtual_ = virtual_width; + res_y_virtual_ = virtual_height; + res_x_ = pixel_width; + res_y_ = pixel_height; + + // Need to rebuild internal components (some are sized to the screen). + internal_components_inited_ = false; +} + +void Graphics::ScreenMessageEntry::UpdateTranslation() { + if (translation_dirty) { + s_translated = g_game->CompileResourceString( + s_raw, "Graphics::ScreenMessageEntry::UpdateTranslation"); + translation_dirty = false; + mesh_dirty = true; + } +} + +auto Graphics::CubeMapFromReflectionType(ReflectionType reflection_type) + -> SystemCubeMapTextureID { + switch (reflection_type) { + case ReflectionType::kChar: + return SystemCubeMapTextureID::kReflectionChar; + case ReflectionType::kPowerup: + return SystemCubeMapTextureID::kReflectionPowerup; + case ReflectionType::kSoft: + return SystemCubeMapTextureID::kReflectionSoft; + case ReflectionType::kSharp: + return SystemCubeMapTextureID::kReflectionSharp; + case ReflectionType::kSharper: + return SystemCubeMapTextureID::kReflectionSharper; + case ReflectionType::kSharpest: + return SystemCubeMapTextureID::kReflectionSharpest; + default: + throw Exception(); + } +} + +auto Graphics::StringFromReflectionType(ReflectionType r) -> std::string { + switch (r) { + case ReflectionType::kSoft: + return "soft"; + break; + case ReflectionType::kChar: + return "char"; + break; + case ReflectionType::kPowerup: + return "powerup"; + break; + case ReflectionType::kSharp: + return "sharp"; + break; + case ReflectionType::kSharper: + return "sharper"; + break; + case ReflectionType::kSharpest: + return "sharpest"; + break; + case ReflectionType::kNone: + return "none"; + break; + default: + throw Exception("Invalid reflection value: " + + std::to_string(static_cast(r))); + break; + } +} + +auto Graphics::ReflectionTypeFromString(const std::string& s) + -> ReflectionType { + ReflectionType r; + if (s == "soft") { + r = ReflectionType::kSoft; + } else if (s == "char") { + r = ReflectionType::kChar; + } else if (s == "powerup") { + r = ReflectionType::kPowerup; + } else if (s == "sharp") { + r = ReflectionType::kSharp; + } else if (s == "sharper") { + r = ReflectionType::kSharper; + } else if (s == "sharpest") { + r = ReflectionType::kSharpest; + } else if (s.empty() || s == "none") { + r = ReflectionType::kNone; + } else { + throw Exception("invalid reflection type: '" + s + "'"); + } + return r; +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/graphics.h b/src/ballistica/graphics/graphics.h new file mode 100644 index 00000000..5a842041 --- /dev/null +++ b/src/ballistica/graphics/graphics.h @@ -0,0 +1,424 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_GRAPHICS_H_ +#define BALLISTICA_GRAPHICS_GRAPHICS_H_ + +#include +#include +#include +#include +#include +#include + +#include "ballistica/core/object.h" +#include "ballistica/math/matrix44f.h" +#include "ballistica/math/rect.h" +#include "ballistica/math/vector2f.h" + +namespace ballistica { + +// Light/shadow res is divided by this to get pure light res. +const int kLightResDiv = 4; + +// How we divide up our z depth spectrum: +const float kBackingDepth5 = 1.0f; + +// Background +// blit-shapes (with cam buffer) +const float kBackingDepth4 = 0.9f; + +// World (without cam buffer) or overlay-3d (with cam buffer) +const float kBackingDepth3C = 0.65f; +const float kBackingDepth3B = 0.4f; +const float kBackingDepth3 = 0.15f; + +// Overlay-3d (without cam buffer) / overlay(vr) +const float kBackingDepth2C = 0.147f; +const float kBackingDepth2B = 0.143f; +const float kBackingDepth2 = 0.14f; + +// Overlay(non-vr) // cover (vr) +const float kBackingDepth1B = 0.01f; +const float kBackingDepth1 = 0.0f; + +const float kShadowNeutral = 0.5f; + +// Client class for graphics operations (used from the game thread). +class Graphics { + public: + Graphics(); + virtual ~Graphics(); + + static auto IsShaderTransparent(ShadingType c) -> bool; + static auto CubeMapFromReflectionType(ReflectionType reflection_type) + -> SystemCubeMapTextureID; + + // Given a string, return a reflection type. + static auto ReflectionTypeFromString(const std::string& s) -> ReflectionType; + + // ..and the opposite. + static auto StringFromReflectionType(ReflectionType reflectionType) + -> std::string; + + auto Reset() -> void; + auto BuildAndPushFrameDef() -> void; + + virtual auto ApplyCamera(FrameDef* frame_def) -> void; + + // Called when the GraphicsServer's screen configuration changes. + auto ScreenResize(float virtual_width, float virtual_height, + float physical_width, float physical_height) -> void; + + // Called when the GraphicsServer has sent us a frame-def for deletion. + auto ReturnCompletedFrameDef(FrameDef* frame_def) -> void; + + auto screen_pixel_width() const -> float { return res_x_; } + auto screen_pixel_height() const -> float { return res_y_; } + + // Return the size of the virtual screen. This value should always + // be used for interface positioning, etc. + auto screen_virtual_width() const -> float { return res_x_virtual_; } + auto screen_virtual_height() const -> float { return res_y_virtual_; } + + auto ClearScreenMessageTranslations() -> void; + + // Given a point in space, returns the shadow density that should be drawn + // into the shadow pass. Does this belong somewhere else? + auto GetShadowDensity(float x, float y, float z) -> float; + + static auto GetSafeColor(float* r, float* g, float* b, + float target_intensity = 0.6f) -> void; + + // Print a message to the on-screen list. + auto AddScreenMessage(const std::string& msg, + const Vector3f& color = Vector3f{1, 1, 1}, + bool top = false, Texture* texture = nullptr, + Texture* tint_texture = nullptr, + const Vector3f& tint = Vector3f{1, 1, 1}, + const Vector3f& tint2 = Vector3f{1, 1, 1}) -> void; + + // Fade the local screen in or out over the given time period. + auto FadeScreen(bool to, millisecs_t time, PyObject* endcall) -> void; + + static auto DrawRadialMeter(MeshIndexedSimpleFull* m, float amt) -> void; + + // Ways to add a few simple component types quickly. + // (uses particle rendering for efficient batches). + auto DrawBlotch(const Vector3f& pos, float size, float r, float g, float b, + float a) -> void { + DoDrawBlotch(&blotch_indices_, &blotch_verts_, pos, size, r, g, b, a); + } + + auto DrawBlotchSoft(const Vector3f& pos, float size, float r, float g, + float b, float a) -> void { + DoDrawBlotch(&blotch_soft_indices_, &blotch_soft_verts_, pos, size, r, g, b, + a); + } + + // Draw a soft blotch on objects; not terrain. + auto DrawBlotchSoftObj(const Vector3f& pos, float size, float r, float g, + float b, float a) -> void { + DoDrawBlotch(&blotch_soft_obj_indices_, &blotch_soft_obj_verts_, pos, size, + r, g, b, a); + } + + // Enable progress bar drawing locally. + auto EnableProgressBar(bool fade_in) -> void; + + auto camera() -> Camera* { return camera_.get(); } + auto ToggleManualCamera() -> void; + auto LocalCameraShake(float intensity) -> void; + auto ToggleDebugDraw() -> void; + auto debug_info_display() const -> bool { return debug_info_display_; } + auto ToggleDebugInfoDisplay() -> void; + auto SetGyroEnabled(bool enable) -> void; + auto floor_reflection() const -> bool { + assert(InGameThread()); + return floor_reflection_; + } + auto set_floor_reflection(bool val) -> void { + assert(InGameThread()); + floor_reflection_ = val; + } + auto set_shadow_offset(const Vector3f& val) -> void { + assert(InGameThread()); + shadow_offset_ = val; + } + auto set_shadow_scale(float x, float y) -> void { + assert(InGameThread()); + shadow_scale_.x = x; + shadow_scale_.y = y; + } + auto set_shadow_ortho(bool o) -> void { + assert(InGameThread()); + shadow_ortho_ = o; + } + auto tint() -> const Vector3f& { return tint_; } + auto set_tint(const Vector3f& val) -> void { + assert(InGameThread()); + tint_ = val; + } + + auto set_ambient_color(const Vector3f& val) -> void { + assert(InGameThread()); + ambient_color_ = val; + } + auto set_vignette_outer(const Vector3f& val) -> void { + assert(InGameThread()); + vignette_outer_ = val; + } + auto set_vignette_inner(const Vector3f& val) -> void { + assert(InGameThread()); + vignette_inner_ = val; + } + auto shadow_offset() const -> const Vector3f& { + assert(InGameThread()); + return shadow_offset_; + } + auto shadow_scale() const -> const Vector2f& { + assert(InGameThread()); + return shadow_scale_; + } + auto tint() const -> const Vector3f& { + assert(InGameThread()); + return tint_; + } + auto ambient_color() const -> const Vector3f& { + assert(InGameThread()); + return ambient_color_; + } + auto vignette_outer() const -> const Vector3f& { + assert(InGameThread()); + return vignette_outer_; + } + auto vignette_inner() const -> const Vector3f& { + assert(InGameThread()); + return vignette_inner_; + } + auto shadow_ortho() const -> bool { + assert(InGameThread()); + return shadow_ortho_; + } + auto SetShadowRange(float lower_bottom, float lower_top, float upper_bottom, + float upper_top) -> void; + auto ReleaseFadeEndCommand() -> void; + auto set_show_fps(bool val) -> void { show_fps_ = val; } + + // FIXME - move to graphics_server + auto set_tv_border(bool val) -> void { + assert(InGameThread()); + tv_border_ = val; + } + auto tv_border() const -> bool { + assert(InGameThread()); + return tv_border_; + } + + // Nodes that draw flat stuff into the overlay pass should query this z value + // for where to draw in z. + auto overlay_node_z_depth() -> float { + fetched_overlay_node_z_depth_ = true; + return overlay_node_z_depth_; + } + + // This should be called before/after drawing each node to keep the value + // incrementing. + auto PreNodeDraw() -> void { fetched_overlay_node_z_depth_ = false; } + auto PostNodeDraw() -> void { + if (fetched_overlay_node_z_depth_) { + overlay_node_z_depth_ *= 0.99f; + } + } + + auto accel() const -> const Vector3f& { return accel_pos_; } + auto tilt() const -> const Vector3f& { return tilt_pos_; } + + auto PixelToVirtualX(float x) const -> float { + if (tv_border_) { + // In this case, 0 to 1 in physical coords maps to -0.05f to 1.05f in + // virtual. + return (-0.5f * kTVBorder) * res_x_virtual_ + + (1.0f + kTVBorder) * res_x_virtual_ * (x / res_x_); + } + return x * (res_x_virtual_ / res_x_); + } + auto PixelToVirtualY(float y) const -> float { + if (tv_border_) { + // In this case, 0 to 1 in physical coords maps to -0.05f to 1.05f in + // virtual. + return (-0.5f * kTVBorder) * res_y_virtual_ + + (1.0f + kTVBorder) * res_y_virtual_ * (y / res_y_); + } + return y * (res_y_virtual_ / res_y_); + } + auto supports_high_quality_graphics() const -> bool { + assert(has_supports_high_quality_graphics_value_); + return supports_high_quality_graphics_; + } + auto SetSupportsHighQualityGraphics(bool s) -> void; + auto has_supports_high_quality_graphics_value() const -> bool { + return has_supports_high_quality_graphics_value_; + } + auto set_internal_components_inited(bool val) -> void { + internal_components_inited_ = val; + } + auto set_gyro_vals(const Vector3f& vals) -> void { gyro_vals_ = vals; } + // auto draw_overlay_bounds() const -> bool { return draw_overlay_bounds_; } + // auto set_draw_overlay_bounds(bool val) -> void { draw_overlay_bounds_ = + // val; } + auto show_net_info() const -> bool { return show_net_info_; } + auto set_show_net_info(bool val) -> void { show_net_info_ = val; } + auto debug_graph_1() const -> NetGraph* { return debug_graph_1_.get(); } + auto debug_graph_2() const -> NetGraph* { return debug_graph_2_.get(); } + + // Used by meshes. + auto AddMeshDataCreate(MeshData* d) -> void; + auto AddMeshDataDestroy(MeshData* d) -> void; + + // For debugging: ensures that only transparent or opaque components + // are submitted while enabled. + auto drawing_transparent_only() const -> bool { + return drawing_transparent_only_; + } + auto set_drawing_transparent_only(bool val) -> void { + drawing_transparent_only_ = val; + } + + auto drawing_opaque_only() const -> bool { return drawing_opaque_only_; } + auto set_drawing_opaque_only(bool val) -> void { drawing_opaque_only_ = val; } + + // Handle testing values from _ba.value_test() + virtual auto ValueTest(const std::string& arg, double* absval, + double* deltaval, double* outval) -> bool; + virtual auto DrawUI(FrameDef* frame_def) -> void; + virtual auto DrawWorld(Session* session, FrameDef* frame_def) -> void; + + auto set_camera_shake_disabled(bool disabled) -> void { + camera_shake_disabled_ = disabled; + } + auto camera_shake_disabled() const { return camera_shake_disabled_; } + auto set_camera_gyro_explicitly_disabled(bool disabled) -> void { + camera_gyro_explicitly_disabled_ = disabled; + } + + private: + class ScreenMessageEntry; + auto DrawBoxingGlovesTest(FrameDef* frame_def) -> void; + auto DrawBlotches(FrameDef* frame_def) -> void; + auto DrawCursor(RenderPass* pass, millisecs_t real_time) -> void; + auto DrawFades(FrameDef* frame_def, millisecs_t real_time) -> void; + auto DrawDebugBuffers(RenderPass* pass) -> void; + auto WaitForRendererToExist() -> void; + + auto UpdateAndDrawProgressBar(FrameDef* frame_def, millisecs_t real_time) + -> void; + auto DoDrawBlotch(std::vector* indices, + std::vector* verts, const Vector3f& pos, + float size, float r, float g, float b, float a) -> void; + auto GetEmptyFrameDef() -> FrameDef*; + auto InitInternalComponents(FrameDef* frame_def) -> void; + auto DrawMiscOverlays(RenderPass* pass) -> void; + auto DrawLoadDot(RenderPass* pass) -> void; + auto ClearFrameDefDeleteList() -> void; + auto DrawProgressBar(RenderPass* pass, float opacity) -> void; + auto UpdateProgressBarProgress(float target) -> void; + auto UpdateGyro(millisecs_t real_time, millisecs_t elapsed) -> void; + + bool drawing_transparent_only_{}; + bool drawing_opaque_only_{}; + std::vector mesh_data_creates_; + std::vector mesh_data_destroys_; + bool has_supports_high_quality_graphics_value_{}; + bool supports_high_quality_graphics_ = false; + millisecs_t last_create_frame_def_time_{}; + Vector3f shadow_offset_{0.0f, 0.0f, 0.0f}; + Vector2f shadow_scale_{1.0f, 1.0f}; + bool shadow_ortho_ = false; + Vector3f tint_{1.0f, 1.0f, 1.0f}; + Vector3f ambient_color_{1.0f, 1.0f, 1.0f}; + Vector3f vignette_outer_{0.0f, 0.0f, 0.0f}; + Vector3f vignette_inner_{1.0f, 1.0f, 1.0f}; + std::vector recycle_frame_defs_; + millisecs_t last_jitter_update_time_ = 0; + Vector3f jitter_{0.0f, 0.0f, 0.0f}; + Vector3f accel_smoothed_{0.0f, 0.0f, 0.0f}; + Vector3f accel_smoothed2_{0.0f, 0.0f, 0.0f}; + Vector3f accel_hi_pass_{0.0f, 0.0f, 0.0f}; + Vector3f accel_vel_{0.0f, 0.0f, 0.0f}; + Vector3f accel_pos_{0.0f, 0.0f, 0.0f}; + Vector3f tilt_smoothed_ = {0.0f, 0.0f, 0.0f}; + Vector3f tilt_vel_{0.0f, 0.0f, 0.0f}; + Vector3f tilt_pos_{0.0f, 0.0f, 0.0f}; + bool gyro_broken_{}; + float gyro_mag_test_{}; + bool fetched_overlay_node_z_depth_{}; + float overlay_node_z_depth_{}; + bool internal_components_inited_{}; + Object::Ref screen_mesh_; + Object::Ref progress_bar_bottom_mesh_; + Object::Ref progress_bar_top_mesh_; + Object::Ref load_dot_mesh_; + Object::Ref fps_text_group_; + Object::Ref net_info_text_group_; + Object::Ref shadow_blotch_mesh_; + Object::Ref shadow_blotch_soft_mesh_; + Object::Ref shadow_blotch_soft_obj_mesh_; + std::string fps_string_; + std::string net_info_string_; + std::vector blotch_indices_; + std::vector blotch_verts_; + std::vector blotch_soft_indices_; + std::vector blotch_soft_verts_; + std::vector blotch_soft_obj_indices_; + std::vector blotch_soft_obj_verts_; + bool show_fps_{}; + bool show_net_info_{}; + bool tv_border_{}; + bool floor_reflection_{}; + Object::Ref debug_graph_1_; + Object::Ref debug_graph_2_; + std::mutex frame_def_delete_list_mutex_; + std::vector frame_def_delete_list_; + bool debug_draw_{}; + bool debug_info_display_{}; + Object::Ref camera_; + millisecs_t next_stat_update_time_{}; + int last_total_frames_rendered_{}; + int last_fps_{}; + std::list screen_messages_; + std::list screen_messages_top_; + bool set_fade_start_on_next_draw_{}; + millisecs_t fade_start_{}; + millisecs_t fade_time_{}; + bool fade_out_{true}; + Object::Ref fade_end_call_; + float fade_{}; + Vector3f gyro_vals_{0.0f, 0.0, 0.0f}; + float res_x_{100}; + float res_y_{100}; + float res_x_virtual_{100}; + float res_y_virtual_{100}; + int progress_bar_loads_{}; + bool progress_bar_{}; + bool progress_bar_fade_in_{}; + millisecs_t progress_bar_end_time_{-9999}; + float progress_bar_progress_{}; + millisecs_t last_progress_bar_draw_time_{}; + millisecs_t last_progress_bar_start_time_{}; + float screen_gamma_{1.0f}; + float shadow_lower_bottom_{-4.0f}; + float shadow_lower_top_{4.0f}; + float shadow_upper_bottom_{30.0f}; + float shadow_upper_top_{40.0f}; + bool hardware_cursor_visible_{}; + bool camera_shake_disabled_{}; + bool camera_gyro_explicitly_disabled_{}; + millisecs_t last_cursor_visibility_event_time_{}; + int64_t frame_def_count_{1}; + bool gyro_enabled_{true}; + millisecs_t last_suppress_gyro_time_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_GRAPHICS_H_ diff --git a/src/ballistica/graphics/graphics_server.cc b/src/ballistica/graphics/graphics_server.cc new file mode 100644 index 00000000..af77a907 --- /dev/null +++ b/src/ballistica/graphics/graphics_server.cc @@ -0,0 +1,765 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/graphics_server.h" + +#include +#include + +#include "ballistica/core/thread.h" +#include "ballistica/generic/lambda_runnable.h" +#include "ballistica/graphics/gl/renderer_gl.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/platform/platform.h" +#include "ballistica/scene/scene.h" + +// FIXME: clear out this conditional stuff. +#if BA_SDL_BUILD +#include "ballistica/platform/sdl/sdl_app.h" +#else +#include "ballistica/app/app.h" +#endif + +namespace ballistica { + +#if BA_OSTYPE_MACOS && BA_XCODE_BUILD +void GraphicsServer::FullscreenCheck() { + if (!fullscreen_enabled()) { +#if BA_ENABLE_OPENGL + SDL_WM_ToggleFullScreen(gl_context_->sdl_screen_surface()); +#endif + } +} +#endif + +GraphicsServer::GraphicsServer(Thread* thread) : Module("graphics", thread) { + // We're a singleton. + assert(g_graphics_server == nullptr); + g_graphics_server = this; + + // For janky old non-event-push mode, just fall back on a timer for rendering. + if (!g_platform->IsEventPushMode()) { + render_timer_ = NewThreadTimer(1000 / 60, true, + NewLambdaRunnable([this] { TryRender(); })); + } +} + +GraphicsServer::~GraphicsServer() { assert(g_graphics); } + +void GraphicsServer::SetRenderHold() { + assert(InGraphicsThread()); + render_hold_++; +} + +void GraphicsServer::SetFrameDef(FrameDef* framedef) { + // Note: we're just setting the framedef directly here + // even though this gets called from the game thread. + // Ideally it would seem we should push these to our thread + // event list, but currently we spin-lock waiting for new + // frames to appear which would prevent that from working; + // we would need to change that code. + assert(frame_def_ == nullptr); + frame_def_ = framedef; +} + +auto GraphicsServer::GetRenderFrameDef() -> FrameDef* { + assert(InGraphicsThread()); + millisecs_t real_time = GetRealTime(); + + if (!renderer_) { + return nullptr; + } + + // If the app says it's minimized minimized, don't do anything. + // (on iOS we'll get shut down if we make GL calls in this state, etc) + if (g_app->paused()) { + return nullptr; + } + + // Do some incremental loading every time we try to render. + g_media->RunPendingGraphicsLoads(); + + // Spin and wait for a short bit for a frame_def to appear. If it does, we + // grab it, render it, and also message the game thread to start generating + // another one. + while (true) { + if (frame_def_) { + FrameDef* frame_def = frame_def_; + frame_def_ = nullptr; + + // Tell the game thread we're ready for the next frame_def so it can start + // building it while we render this one. + g_game->PushFrameDefRequest(); + return frame_def; + } + + // If there's no frame_def for us, sleep for a bit and wait for it. + // But if we've been waiting for too long, give up. + // On some platforms such as android, this frame will still get flipped + // whether we draw in it or not, so we really dont want to not draw if we + // can help it. + millisecs_t t = GetRealTime() - real_time; + if (t >= 1000) { + break; // Fail. + } + Platform::SleepMS(2); + } + return nullptr; +} + +// Runs any mesh updates contained in the frame-def. +void GraphicsServer::RunFrameDefMeshUpdates(FrameDef* frame_def) { + assert(InGraphicsThread()); + + // Run any mesh-data creates/destroys included with this frame_def. + for (auto&& i : frame_def->mesh_data_creates()) { + assert(i != nullptr); + i->iterator_ = mesh_datas_.insert(mesh_datas_.end(), i); + i->Load(renderer_); + } + for (auto&& i : frame_def->mesh_data_destroys()) { + assert(i != nullptr); + i->Unload(renderer_); + mesh_datas_.erase(i->iterator_); + } +} + +// Renders shadow passes and other common parts of a frame_def. +void GraphicsServer::PreprocessRenderFrameDef(FrameDef* frame_def) { + assert(InGraphicsThread()); + + // Now let the renderer do any preprocess passes (shadows, etc). + renderer_->PreprocessFrameDef(frame_def); +} + +// Does the default drawing to the screen, either from the left or right stereo +// eye or in mono. +void GraphicsServer::DrawRenderFrameDef(FrameDef* frame_def, int eye) { + renderer_->RenderFrameDef(frame_def); +} + +// Clean up the frame_def once done drawing it. +void GraphicsServer::FinishRenderFrameDef(FrameDef* frame_def) { + renderer_->FinishFrameDef(frame_def); + + // Let the app know a frame render is complete (it may need to do a swap/etc). + g_app->DidFinishRenderingFrame(frame_def); +} + +void GraphicsServer::TryRender() { + assert(InGraphicsThread()); + + if (FrameDef* frame_def = GetRenderFrameDef()) { + // Note: we always run mesh updates contained in the framedef + // even if we don't actually render it. + // (Hmm this seems flaky; will TryRender always get called + // for each FrameDef?... perhaps we should separate mesh updates + // from FrameDefs? Or change our logic so that frame-defs *always* get + // rendered. + RunFrameDefMeshUpdates(frame_def); + + // Only actually render if we have a screen and aren't in a hold. + auto target = renderer()->screen_render_target(); + if (target != nullptr && render_hold_ == 0) { + PreprocessRenderFrameDef(frame_def); + DrawRenderFrameDef(frame_def); + FinishRenderFrameDef(frame_def); + } + + // Send this frame_def back to the game thread for deletion. + g_graphics->ReturnCompletedFrameDef(frame_def); + } +} + +// Reload all media (for debugging/benchmarking purposes). +void GraphicsServer::ReloadMedia() { + assert(InMainThread()); + + // Immediately unload all renderer data here in this thread. + if (renderer_) { + g_media->UnloadRendererBits(true, true); + } + + // Set a render-hold so we ignore all frame_defs up until the point at which + // we receive the corresponding remove-hold. + // (At which point subsequent frame-defs will be be progress-bar frame_defs so + // we won't hitch if we actually render them.) + assert(g_graphics_server); + SetRenderHold(); + + // Now tell the game thread to kick off loads for everything, flip on + // progress bar drawing, and then tell the graphics thread to stop ignoring + // frame-defs. + g_game->PushCall([this] { + g_media->MarkAllMediaForLoad(); + g_graphics->EnableProgressBar(false); + PushRemoveRenderHoldCall(); + }); +} + +// Call when renderer context has been lost. +void GraphicsServer::RebuildLostContext() { + assert(InGraphicsThread()); + + if (!renderer_) { + Log("Error: No renderer on GraphicsServer::_rebuildContext."); + return; + } + + // Mark our context as lost so the renderer knows to not try and tear things + // down itself. + set_renderer_context_lost(true); + + // Unload all texture and model data here in the render thread. + g_media->UnloadRendererBits(true, true); + + // Also unload dynamic meshes. + for (auto&& i : mesh_datas_) { + i->Unload(renderer_); + } + + // And other internal renderer stuff. + renderer_->Unload(); + + set_renderer_context_lost(false); + + // Now reload. + renderer_->Load(); + + // Also (re)load all dynamic meshes. + for (auto&& i : mesh_datas_) { + i->Load(renderer_); + } + + renderer_->ScreenSizeChanged(); + + // Set a render-hold so we ignore all frame_defs up until the point at which + // we receive the corresponding remove-hold. + // (At which point subsequent frame-defs will be be progress-bar frame_defs so + // we won't hitch if we actually render them.) + SetRenderHold(); + + // Now tell the game thread to kick off loads for everything, flip on progress + // bar drawing, and then tell the graphics thread to stop ignoring frame-defs. + g_game->PushCall([this] { + g_media->MarkAllMediaForLoad(); + g_graphics->EnableProgressBar(false); + PushRemoveRenderHoldCall(); + }); +} + +void GraphicsServer::SetScreen(bool fullscreen, int width, int height, + TextureQuality texture_quality_requested, + GraphicsQuality graphics_quality_requested, + const std::string& android_res) { + assert(InGraphicsThread()); + + // If we know what we support, filter out requests we don't support + // (will keep us from rebuilding contexts due to our requested and actual + // values not lining up). + if (g_graphics->has_supports_high_quality_graphics_value()) { + if (!g_graphics->supports_high_quality_graphics() + && (graphics_quality_requested == GraphicsQuality::kHigh + || graphics_quality_requested == GraphicsQuality::kHigher)) { + graphics_quality_requested = GraphicsQuality::kMedium; + } + } + +#if BA_OSTYPE_MACOS && BA_XCODE_BUILD && BA_SDL_BUILD + bool create_fullscreen_check_timer = false; +#endif + + bool do_toggle_fs = false; + bool do_set_existing_fs = false; + + if (HeadlessMode()) { + // We don't actually make or update a renderer in headless, but we + // still need to set our list of supported textures types/etc. to avoid + // complaints. + std::list c_types; + SetTextureCompressionTypes(c_types); + quality_requested_ = quality_actual_ = GraphicsQuality::kLow; + graphics_quality_set_ = true; + texture_quality_requested_ = texture_quality_actual_ = TextureQuality::kLow; + texture_quality_set_ = true; + } else { + // OK - starting in SDL2 we never pass in specific resolution requests.. + // we request fullscreen-windows for full-screen situations and that's it. + // (otherwise we may wind up with huge windows due to passing in desktop + // resolutions and retina wonkiness) + width = 800; + height = 600; + + // We should never have to recreate the context after the initial time on + // our modern builds. + bool need_full_context_rebuild = (!renderer_); + bool need_renderer_reload; + + // We need a full renderer reload if quality values have changed. + need_renderer_reload = + ((texture_quality_requested_ != texture_quality_requested) + || (quality_requested_ != graphics_quality_requested) + || !texture_quality_set() || !graphics_quality_set()); + + // This stuff requires a full context rebuild. + if (need_full_context_rebuild || need_renderer_reload) { + HandleFullContextScreenRebuild(need_full_context_rebuild, fullscreen, + width, height, graphics_quality_requested, + texture_quality_requested); + // On mac, we let window save/restore handle our fullscreen restoring for + // us. However if document restore is turned off we'll start windowed on + // every launch. So if we're trying to make a fullscreen setup, lets + // check after a short delay to make sure we have it, and run a + // full-screen-toggle ourself if not. +#if BA_OSTYPE_MACOS && BA_XCODE_BUILD && BA_SDL_BUILD + if (fullscreen) { + create_fullscreen_check_timer = true; + } +#endif // BA_OSTYPE_MACOS + + } else { + // on SDL2 builds we can just set fullscreen on the existing window; no + // need for a context rebuild +#if BA_SDL2_BUILD + do_set_existing_fs = true; +#else + // On our old custom SDL1.2 mac build, fullscreen toggling winds up here. + // this doesn't require a context rebuild either. + if (fullscreen != fullscreen_enabled()) { + do_toggle_fs = true; + } +#endif // BA_SDL2_BUILD + } + + HandlePushAndroidRes(android_res); + +#if BA_OSTYPE_MACOS && BA_XCODE_BUILD && BA_SDL_BUILD + if (create_fullscreen_check_timer) { + NewThreadTimer(1000, false, + NewLambdaRunnable([this] { FullscreenCheck(); })); + } +#endif // BA_OSTYPE_MACOS + + HandleFullscreenToggling(do_set_existing_fs, do_toggle_fs, fullscreen); + } + + // The first time we complete setting up our screen, we send a message + // back to the game thread to complete the init process.. (they can't start + // loading graphics and things until we have our context set up so we know + // what types of textures to load, etc) + if (!initial_screen_created_) { + initial_screen_created_ = true; + g_game->PushInitialScreenCreatedCall(); + } +} + +void GraphicsServer::HandleFullContextScreenRebuild( + bool need_full_context_rebuild, bool fullscreen, int width, int height, + GraphicsQuality graphics_quality_requested, + TextureQuality texture_quality_requested) { + // Unload renderer-specific data (display-lists, internal textures, etc) + if (renderer_) { + // Unload all textures and models.. these will be reloaded as-needed + // automatically for the new context.. + g_media->UnloadRendererBits(true, true); + + // Also unload all dynamic meshes. + for (auto&& i : mesh_datas_) { + i->Unload(renderer_); + } + + // And all internal renderer stuff. + renderer_->Unload(); + } + + // Handle screen/context recreation. + if (need_full_context_rebuild) { + // On mac we store the values we *want* separate from those we get.. (so + // we know when our request has changed; not our result). +#if !(BA_OSTYPE_MACOS && BA_XCODE_BUILD) + fullscreen_enabled_ = fullscreen; +#endif + + target_res_x_ = static_cast(width); + target_res_y_ = static_cast(height); + +#if BA_ENABLE_OPENGL + gl_context_ = std::make_unique(width, height, fullscreen); + res_x_ = static_cast(gl_context_->res_x()); + res_y_ = static_cast(gl_context_->res_y()); +#endif + + UpdateVirtualScreenRes(); + + // Inform the game thread of the latest values. + g_game->PushScreenResizeCall(res_x_virtual_, res_y_virtual_, res_x_, + res_y_); + } + + if (!renderer_) { +#if BA_ENABLE_OPENGL + renderer_ = new RendererGL(); +#endif + } + + // Make sure we've done this first so we can properly set auto values and + // whatnot. + renderer_->CheckCapabilities(); + + // Update graphics quality. + quality_requested_ = graphics_quality_requested; + if (quality_requested_ == GraphicsQuality::kAuto) { + quality_actual_ = renderer_->GetAutoGraphicsQuality(); + } else { + quality_actual_ = quality_requested_; + } + + // If we don't support high quality graphics, make sure we're no higher than + // medium. + BA_PRECONDITION(g_graphics->has_supports_high_quality_graphics_value()); + if (!g_graphics->supports_high_quality_graphics() + && quality_actual_ >= GraphicsQuality::kHigh) { + quality_actual_ = GraphicsQuality::kMedium; + } + graphics_quality_set_ = true; + + // Update texture quality. + texture_quality_requested_ = texture_quality_requested; + if (texture_quality_requested_ == TextureQuality::kAuto) { + texture_quality_actual_ = renderer_->GetAutoTextureQuality(); + } else { + texture_quality_actual_ = texture_quality_requested_; + } + texture_quality_set_ = true; + + // Ok we've got our qualities figured out; now load/update the renderer. + renderer_->Load(); + + // Also (re)load all existing dynamic meshes. + for (auto&& i : mesh_datas_) { + i->Load(renderer_); + } + renderer_->ScreenSizeChanged(); + renderer_->PostLoad(); + + // Set a render-hold so we ignore all frame_defs up until the point at which + // we receive the corresponding remove-hold. + // (At which point subsequent frame-defs will be be progress-bar frame_defs + // so we won't hitch if we actually render them.) + SetRenderHold(); + + // Now tell the game thread to kick off loads for everything, flip on + // progress bar drawing, and then tell the graphics thread to stop ignoring + // frame-defs. + g_game->PushCall([this] { + g_media->MarkAllMediaForLoad(); + g_graphics->set_internal_components_inited(false); + g_graphics->EnableProgressBar(false); + PushRemoveRenderHoldCall(); + }); +} + +// Given physical res, calculate virtual res. +void GraphicsServer::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 GraphicsServer::UpdateVirtualScreenRes() { + assert(InGraphicsThread()); + // In vr mode our virtual res is independent of our screen size. + // (since it gets drawn to an overlay) + if (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_); + } +} + +void GraphicsServer::VideoResize(float h, float v) { + assert(InGraphicsThread()); + + if (target_res_x_ == h && target_res_y_ == v) { + return; + } + + target_res_x_ = h; + target_res_y_ = v; + res_x_ = h; + res_y_ = v; + UpdateVirtualScreenRes(); + + // Inform the game thread of the latest values. + g_game->PushScreenResizeCall(res_x_virtual_, res_y_virtual_, res_x_, res_y_); + if (renderer_) { + renderer_->ScreenSizeChanged(); + } +} + +// FIXME: Shouldn't have android-specific code in here. +void GraphicsServer::HandlePushAndroidRes(const std::string& android_res) { + if (g_buildconfig.ostype_android()) { + // We push android res to the java layer here. We don't actually worry + // about screen-size-changed callbacks and whatnot, since those will happen + // automatically once things actually change. We just want to be sure that + // we have a renderer so we can calc what our auto res should be. + assert(renderer_); + std::string fin_res; + if (android_res == "Auto") { + fin_res = renderer_->GetAutoAndroidRes(); + } else { + fin_res = android_res; + } + g_platform->AndroidSetResString(fin_res); + } +} + +void GraphicsServer::HandleFullscreenToggling(bool do_set_existing_fs, + bool do_toggle_fs, + bool fullscreen) { + if (do_set_existing_fs) { +#if BA_SDL2_BUILD + bool rift_vr_mode = false; +#if BA_RIFT_BUILD + if (IsVRMode()) { + rift_vr_mode = true; + } +#endif // BA_RIFT_BUILD + if (explicit_bool(!rift_vr_mode)) { +#if BA_OSTYPE_IOS_TVOS + set_fullscreen_enabled(true); + +#else // BA_OSTYPE_IOS_TVOS + uint32_t fullscreen_flag = SDL_WINDOW_FULLSCREEN_DESKTOP; + SDL_SetWindowFullscreen(gl_context_->sdl_window(), + fullscreen ? fullscreen_flag : 0); + + // Ideally this should be driven by OS events and not just explicitly by + // us (so, for instance, if someone presses fullscreen on mac we'd know + // we've gone into fullscreen). But this works for now. + set_fullscreen_enabled(fullscreen); +#endif // BA_OSTYPE_IOS_TVOS + } +#endif // BA_SDL2_BUILD + } else if (do_toggle_fs) { + // If we're doing a fullscreen-toggle, we need to do it after coming out of + // sync mode (because the toggle triggers sync-mode itself). +#if BA_OSTYPE_MACOS && BA_XCODE_BUILD +#if BA_ENABLE_OPENGL + SDL_WM_ToggleFullScreen(gl_context_->sdl_screen_surface()); +#endif +#endif // macos && xcode_build + } +} + +void GraphicsServer::SetTextureCompressionTypes( + const std::list& types) { + texture_compression_types_ = 0; // Reset. + for (auto&& i : types) { + texture_compression_types_ |= (0x01u << (static_cast(i))); + } + texture_compression_types_set_ = true; +} + +void GraphicsServer::SetOrthoProjection(float left, float right, float bottom, + float top, float nearval, + float farval) { + float tx = -((right + left) / (right - left)); + float ty = -((top + bottom) / (top - bottom)); + float tz = -((farval + nearval) / (farval - nearval)); + + projection_matrix_.m[0] = 2.0f / (right - left); + projection_matrix_.m[4] = 0.0f; + projection_matrix_.m[8] = 0.0f; + projection_matrix_.m[12] = tx; + + projection_matrix_.m[1] = 0.0f; + projection_matrix_.m[5] = 2.0f / (top - bottom); + projection_matrix_.m[9] = 0.0f; + projection_matrix_.m[13] = ty; + + projection_matrix_.m[2] = 0.0f; + projection_matrix_.m[6] = 0.0f; + projection_matrix_.m[10] = -2.0f / (farval - nearval); + projection_matrix_.m[14] = tz; + + projection_matrix_.m[3] = 0.0f; + projection_matrix_.m[7] = 0.0f; + projection_matrix_.m[11] = 0.0f; + projection_matrix_.m[15] = 1.0f; + + model_view_projection_matrix_dirty_ = true; + projection_matrix_state_++; +} + +void GraphicsServer::SetCamera(const Vector3f& eye, const Vector3f& target, + const Vector3f& up_vector) { + assert(InGraphicsThread()); + + // Reset the modelview stack. + model_view_stack_.clear(); + + auto forward = (target - eye).Normalized(); + auto side = Vector3f::Cross(forward, up_vector).Normalized(); + Vector3f up = Vector3f::Cross(side, forward); + + //------------------ + model_view_matrix_.m[0] = side.x; + model_view_matrix_.m[4] = side.y; + model_view_matrix_.m[8] = side.z; + model_view_matrix_.m[12] = 0.0f; + //------------------ + model_view_matrix_.m[1] = up.x; + model_view_matrix_.m[5] = up.y; + model_view_matrix_.m[9] = up.z; + model_view_matrix_.m[13] = 0.0f; + //------------------ + model_view_matrix_.m[2] = -forward.x; + model_view_matrix_.m[6] = -forward.y; + model_view_matrix_.m[10] = -forward.z; + model_view_matrix_.m[14] = 0.0f; + //------------------ + model_view_matrix_.m[3] = model_view_matrix_.m[7] = model_view_matrix_.m[11] = + 0.0f; + model_view_matrix_.m[15] = 1.0f; + //------------------ + model_view_matrix_ = + Matrix44fTranslate(-eye.x, -eye.y, -eye.z) * model_view_matrix_; + view_world_matrix_ = model_view_matrix_.Inverse(); + + model_view_projection_matrix_dirty_ = true; + model_world_matrix_dirty_ = true; + + cam_pos_ = eye; + cam_target_ = target; + cam_pos_state_++; + cam_orient_matrix_dirty_ = true; +} + +void GraphicsServer::UpdateCamOrientMatrix() { + assert(InGraphicsThread()); + if (cam_orient_matrix_dirty_) { + cam_orient_matrix_ = kMatrix44fIdentity; + Vector3f to_cam = cam_pos_ - cam_target_; + to_cam.Normalize(); + Vector3f world_up(0, 1, 0); + Vector3f side = Vector3f::Cross(world_up, to_cam); + side.Normalize(); + Vector3f up = Vector3f::Cross(side, to_cam); + cam_orient_matrix_.m[0] = side.x; + cam_orient_matrix_.m[1] = side.y; + cam_orient_matrix_.m[2] = side.z; + cam_orient_matrix_.m[4] = to_cam.x; + cam_orient_matrix_.m[5] = to_cam.y; + cam_orient_matrix_.m[6] = to_cam.z; + cam_orient_matrix_.m[8] = up.x; + cam_orient_matrix_.m[9] = up.y; + cam_orient_matrix_.m[10] = up.z; + cam_orient_matrix_.m[3] = cam_orient_matrix_.m[7] = + cam_orient_matrix_.m[11] = cam_orient_matrix_.m[12] = + cam_orient_matrix_.m[13] = cam_orient_matrix_.m[14] = 0.0f; + cam_orient_matrix_.m[15] = 1.0f; + cam_orient_matrix_state_++; + } +} + +#pragma mark PushCalls + +void GraphicsServer::PushSetScreenCall(bool fullscreen, int width, int height, + TextureQuality texture_quality, + GraphicsQuality graphics_quality, + const std::string& android_res) { + PushCall([=] { + SetScreen(fullscreen, width, height, texture_quality, graphics_quality, + android_res); + }); +} + +void GraphicsServer::PushReloadMediaCall() { + PushCall([this] { ReloadMedia(); }); +} + +void GraphicsServer::PushSetScreenGammaCall(float gamma) { + PushCall([this, gamma] { + assert(InGraphicsThread()); + if (!renderer_) { + return; + } + renderer_->set_screen_gamma(gamma); + }); +} + +void GraphicsServer::PushSetScreenPixelScaleCall(float pixel_scale) { + PushCall([this, pixel_scale] { + assert(InGraphicsThread()); + if (!renderer_) { + return; + } + renderer_->set_pixel_scale(pixel_scale); + }); +} + +void GraphicsServer::PushSetVSyncCall(bool sync, bool auto_sync) { + PushCall([this, sync, auto_sync] { + assert(InGraphicsThread()); + +#if BA_SDL_BUILD + + // Currently only supported for SDLApp. + // May want to revisit this later. + if (g_buildconfig.sdl_build()) { + // Even if we were built with SDL, we may not be running in sdl-app-mode + // (for instance, Rift in VR mode). Only do this if we're an sdl app. + if (auto app = dynamic_cast(g_app)) { + v_sync_ = sync; + auto_vsync_ = auto_sync; + if (gl_context_) { + app->SetAutoVSync(auto_vsync_); + // Set it directly if not auto... + if (!auto_vsync_) { + gl_context_->SetVSync(v_sync_); + } + } else { + Log("Error: Got SetVSyncCall with no gl context."); + } + } + } +#endif // BA_HEADLESS_BUILD + }); +} + +void GraphicsServer::PushComponentUnloadCall( + const std::vector*>& components) { + PushCall([this, components] { + // Unload all components we were passed. + for (auto&& i : components) { + (**i).Unload(); + } + // ..and then ship these pointers back to the game thread so it can free the + // references. + g_game->PushFreeMediaComponentRefsCall(components); + }); +} + +void GraphicsServer::PushRemoveRenderHoldCall() { + PushCall([this] { + assert(render_hold_); + render_hold_--; + if (render_hold_ < 0) { + Log("Error: RenderHold < 0"); + render_hold_ = 0; + } + }); +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/graphics_server.h b/src/ballistica/graphics/graphics_server.h new file mode 100644 index 00000000..589bf1d4 --- /dev/null +++ b/src/ballistica/graphics/graphics_server.h @@ -0,0 +1,334 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_GRAPHICS_SERVER_H_ +#define BALLISTICA_GRAPHICS_GRAPHICS_SERVER_H_ + +#include +#include +#include +#include + +#include "ballistica/ballistica.h" +#include "ballistica/core/module.h" +#include "ballistica/core/object.h" +#include "ballistica/math/matrix44f.h" + +namespace ballistica { + +// Runs in the main thread and renders frame_defs shipped to it by the +// Graphics +class GraphicsServer : public Module { + public: + explicit GraphicsServer(Thread* thread); + auto PushSetScreenGammaCall(float gamma) -> void; + auto PushSetScreenPixelScaleCall(float pixel_scale) -> void; + auto PushSetVSyncCall(bool sync, bool auto_sync) -> void; + auto PushSetScreenCall(bool fullscreen, int width, int height, + TextureQuality texture_quality, + GraphicsQuality graphics_quality, + const std::string& android_res) -> void; + auto PushReloadMediaCall() -> void; + auto PushRemoveRenderHoldCall() -> void; + auto PushComponentUnloadCall( + const std::vector*>& components) -> void; + auto SetRenderHold() -> void; + + // Used by the game thread to pass frame-defs to the graphics server + // for rendering. + auto SetFrameDef(FrameDef* framedef) -> void; + + // returns the next frame_def needing to be rendered, waiting for it to arrive + // if necessary. this can return nullptr if no frame_defs come in within a + // reasonable amount of time. a frame_def here *must* be rendered and disposed + // of using the RenderFrameDef* calls + auto GetRenderFrameDef() -> FrameDef*; + + auto RunFrameDefMeshUpdates(FrameDef* frame_def) -> void; + + // renders shadow passes and other common parts of a frame_def + auto PreprocessRenderFrameDef(FrameDef* frame_def) -> void; + + // Does the default drawing to the screen, either from the left or right + // stereo eye or in mono. + auto DrawRenderFrameDef(FrameDef* frame_def, int eye = -1) -> void; + + // Clean up the frame_def once done drawing it. + auto FinishRenderFrameDef(FrameDef* frame_def) -> void; + + // Equivalent to calling GetRenderFrameDef() and then preprocess, draw (in + // mono), and finish. + auto TryRender() -> void; + + // init the modelview matrix to look here + auto SetCamera(const Vector3f& eye, const Vector3f& target, + const Vector3f& up) -> void; + auto SetOrthoProjection(float left, float right, float bottom, float top, + float near, float far) -> void; + auto ModelViewReset() -> void { + model_view_matrix_ = kMatrix44fIdentity; + model_view_projection_matrix_dirty_ = model_world_matrix_dirty_ = true; + model_view_stack_.clear(); + } + auto SetProjectionMatrix(const Matrix44f& p) -> void { + projection_matrix_ = p; + model_view_projection_matrix_dirty_ = true; + projection_matrix_state_++; + } + auto projection_matrix_state() -> uint32_t { + return projection_matrix_state_; + } + + auto SetLightShadowProjectionMatrix(const Matrix44f& p) -> void { + // This will generally get repeatedly set to the same value + // so we can do nothing most of the time. + if (p != light_shadow_projection_matrix_) { + light_shadow_projection_matrix_ = p; + light_shadow_projection_matrix_state_++; + } + } + auto light_shadow_projection_matrix_state() const -> uint32_t { + return light_shadow_projection_matrix_state_; + } + auto light_shadow_projection_matrix() const -> const Matrix44f& { + return light_shadow_projection_matrix_; + } + + // Returns the modelview * projection matrix. + auto GetModelViewProjectionMatrix() -> const Matrix44f& { + UpdateModelViewProjectionMatrix(); + return model_view_projection_matrix_; + } + + auto GetModelViewProjectionMatrixState() -> uint32_t { + UpdateModelViewProjectionMatrix(); + return model_view_projection_matrix_state_; + } + + auto GetModelWorldMatrix() -> const Matrix44f& { + UpdateModelWorldMatrix(); + return model_world_matrix_; + } + + auto GetModelWorldMatrixState() -> uint32_t { + UpdateModelWorldMatrix(); + return model_world_matrix_state_; + } + + auto cam_pos() -> const Vector3f& { return cam_pos_; } + auto cam_pos_state() -> uint32_t { return cam_pos_state_; } + auto GetCamOrientMatrix() -> const Matrix44f& { + UpdateCamOrientMatrix(); + return cam_orient_matrix_; + } + + auto GetCamOrientMatrixState() -> uint32_t { + UpdateCamOrientMatrix(); + return cam_orient_matrix_state_; + } + + auto model_view_matrix() const -> const Matrix44f& { + return model_view_matrix_; + } + auto SetModelViewMatrix(const Matrix44f& m) -> void { + model_view_matrix_ = m; + model_view_projection_matrix_dirty_ = model_world_matrix_dirty_ = true; + } + + auto projection_matrix() const -> const Matrix44f& { + return projection_matrix_; + } + auto PushTransform() -> void { + model_view_stack_.push_back(model_view_matrix_); + assert(model_view_stack_.size() < 20); + } + + auto PopTransform() -> void { + assert(!model_view_stack_.empty()); + model_view_matrix_ = model_view_stack_.back(); + model_view_stack_.pop_back(); + model_view_projection_matrix_dirty_ = model_world_matrix_dirty_ = true; + } + + auto Translate(const Vector3f& t) -> void { + model_view_matrix_ = Matrix44fTranslate(t) * model_view_matrix_; + model_view_projection_matrix_dirty_ = model_world_matrix_dirty_ = true; + } + + auto Rotate(float angle, const Vector3f& axis) -> void { + model_view_matrix_ = Matrix44fRotate(axis, angle) * model_view_matrix_; + model_view_projection_matrix_dirty_ = model_world_matrix_dirty_ = true; + } + + auto MultMatrix(const Matrix44f& m) -> void { + model_view_matrix_ = m * model_view_matrix_; + model_view_projection_matrix_dirty_ = model_world_matrix_dirty_ = true; + } + + auto scale(const Vector3f& s) -> void { + model_view_matrix_ = Matrix44fScale(s) * model_view_matrix_; + model_view_projection_matrix_dirty_ = model_world_matrix_dirty_ = true; + } + + auto RebuildLostContext() -> void; + ~GraphicsServer() override; + + auto renderer() { return renderer_; } + auto quality() const -> GraphicsQuality { + assert(graphics_quality_set_); + return quality_actual_; + } + + auto texture_quality() const -> TextureQuality { + assert(texture_quality_set_); + return texture_quality_actual_; + } + + auto screen_pixel_width() const -> float { + assert(InMainThread()); + return res_x_; + } + auto screen_pixel_height() const -> float { + assert(InMainThread()); + return res_y_; + } + + auto screen_virtual_width() const -> float { + assert(InMainThread()); + return res_x_virtual_; + } + auto screen_virtual_height() const -> float { + assert(InMainThread()); + return res_y_virtual_; + } + auto set_tv_border(bool val) -> void { + assert(InMainThread()); + tv_border_ = val; + } + auto tv_border() const { + assert(InMainThread()); + return tv_border_; + } + + auto graphics_quality_set() const { return graphics_quality_set_; } + auto texture_quality_set() const { return texture_quality_set_; } + + auto SupportsTextureCompressionType(TextureCompressionType t) const -> bool { + assert(texture_compression_types_set_); + return ((texture_compression_types_ & (0x01u << static_cast(t))) + != 0u); + } + auto SetTextureCompressionTypes( + const std::list& types) -> void; + + auto texture_compression_types_are_set() const { + return texture_compression_types_set_; + } + auto set_renderer_context_lost(bool lost) -> auto { + renderer_context_lost_ = lost; + } + auto renderer_context_lost() const { return renderer_context_lost_; } + auto fullscreen_enabled() const { return fullscreen_enabled_; } + + // This doesn't actually toggle fullscreen. It is used to inform the game + // when fullscreen changes under it. + auto set_fullscreen_enabled(bool fs) -> void { fullscreen_enabled_ = fs; } + auto VideoResize(float h, float v) -> void; + +#if BA_ENABLE_OPENGL + auto gl_context() const -> GLContext* { return gl_context_.get(); } +#endif + + auto graphics_quality_requested() const { return quality_requested_; } + auto texture_quality_requested() const { return texture_quality_requested_; } + auto renderer() const { return renderer_; } + auto initial_screen_created() const { return initial_screen_created_; } + + private: + auto HandleFullscreenToggling(bool do_set_existing_fs, bool do_toggle_fs, + bool fullscreen) -> void; + auto HandlePushAndroidRes(const std::string& android_res) -> void; + auto HandleFullContextScreenRebuild( + bool need_full_context_rebuild, bool fullscreen, int width, int height, + GraphicsQuality graphics_quality_requested, + TextureQuality texture_quality_requested) -> void; + + // Update virtual screen dimensions based on the current physical ones. + static auto CalcVirtualRes(float* x, float* y) -> void; + + auto UpdateVirtualScreenRes() -> void; + auto UpdateCamOrientMatrix() -> void; + auto ReloadMedia() -> void; + auto UpdateModelViewProjectionMatrix() -> void { + if (model_view_projection_matrix_dirty_) { + model_view_projection_matrix_ = model_view_matrix_ * projection_matrix_; + model_view_projection_matrix_state_++; + model_view_projection_matrix_dirty_ = false; + } + } + auto UpdateModelWorldMatrix() -> void { + if (model_world_matrix_dirty_) { + model_world_matrix_ = model_view_matrix_ * view_world_matrix_; + model_world_matrix_state_++; + model_world_matrix_dirty_ = false; + } + } +#if BA_ENABLE_OPENGL + std::unique_ptr gl_context_; +#endif + float res_x_{}; + float res_y_{}; + float res_x_virtual_{0.0f}; + float res_y_virtual_{0.0f}; + bool tv_border_{}; + bool renderer_context_lost_{}; + uint32_t texture_compression_types_{}; + bool texture_compression_types_set_{}; + TextureQuality texture_quality_requested_{TextureQuality::kLow}; + TextureQuality texture_quality_actual_{TextureQuality::kLow}; + GraphicsQuality quality_requested_{GraphicsQuality::kLow}; + GraphicsQuality quality_actual_{GraphicsQuality::kLow}; + bool graphics_quality_set_{}; + bool texture_quality_set_{}; + bool fullscreen_enabled_{}; + float target_res_x_{800.0f}; + float target_res_y_{600.0f}; + + Matrix44f model_view_matrix_{kMatrix44fIdentity}; + Matrix44f view_world_matrix_{kMatrix44fIdentity}; + Matrix44f projection_matrix_{kMatrix44fIdentity}; + Matrix44f model_view_projection_matrix_{kMatrix44fIdentity}; + Matrix44f model_world_matrix_{kMatrix44fIdentity}; + std::vector model_view_stack_; + uint32_t projection_matrix_state_{1}; + uint32_t model_view_projection_matrix_state_{1}; + uint32_t model_world_matrix_state_{1}; + bool model_view_projection_matrix_dirty_{true}; + bool model_world_matrix_dirty_{true}; + Matrix44f light_shadow_projection_matrix_{kMatrix44fIdentity}; + uint32_t light_shadow_projection_matrix_state_{1}; + Vector3f cam_pos_{0.0f, 0.0f, 0.0f}; + Vector3f cam_target_{0.0f, 0.0f, 0.0f}; + uint32_t cam_pos_state_{1}; + Matrix44f cam_orient_matrix_ = kMatrix44fIdentity; + uint32_t cam_orient_matrix_state_{1}; + bool cam_orient_matrix_dirty_{true}; + std::list mesh_datas_; + bool v_sync_{}; + bool auto_vsync_{}; + auto SetScreen(bool fullscreen, int width, int height, + TextureQuality texture_quality, + GraphicsQuality graphics_quality, + const std::string& android_res) -> void; + Timer* render_timer_{}; + Renderer* renderer_{}; + FrameDef* frame_def_{}; + bool initial_screen_created_{}; + int render_hold_{}; +#if BA_OSTYPE_MACOS && BA_XCODE_BUILD + void FullscreenCheck(); +#endif +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_GRAPHICS_SERVER_H_ diff --git a/src/ballistica/graphics/mesh/image_mesh.cc b/src/ballistica/graphics/mesh/image_mesh.cc new file mode 100644 index 00000000..1d915b5a --- /dev/null +++ b/src/ballistica/graphics/mesh/image_mesh.cc @@ -0,0 +1,17 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/mesh/image_mesh.h" + +namespace ballistica { + +const uint16_t kImageMeshIndices[] = {0, 1, 2, 1, 3, 2}; +const VertexSimpleSplitStatic kImageMeshVerticesStatic[] = { + {0, 65535}, {65535, 65535}, {0, 0}, {65535, 0}}; + +ImageMesh::ImageMesh() { + SetIndexData(Object::New(6, kImageMeshIndices)); + SetStaticData(Object::New >( + 4, kImageMeshVerticesStatic)); +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/mesh/image_mesh.h b/src/ballistica/graphics/mesh/image_mesh.h new file mode 100644 index 00000000..2044ddc6 --- /dev/null +++ b/src/ballistica/graphics/mesh/image_mesh.h @@ -0,0 +1,27 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_IMAGE_MESH_H_ +#define BALLISTICA_GRAPHICS_MESH_IMAGE_MESH_H_ + +#include "ballistica/graphics/mesh/mesh_indexed_simple_split.h" + +namespace ballistica { + +// a mesh set up to draw images +class ImageMesh : public MeshIndexedSimpleSplit { + public: + ImageMesh(); + void SetPositionAndSize(float x, float y, float z, float width, + float height) { + VertexSimpleSplitDynamic vdynamic[] = {{x, y, z}, + {x + width, y, z}, + {x, y + height, z}, + {x + width, y + height, z}}; + SetDynamicData( + Object::New>(4, vdynamic)); + } +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_IMAGE_MESH_H_ diff --git a/src/ballistica/graphics/mesh/mesh.h b/src/ballistica/graphics/mesh/mesh.h new file mode 100644 index 00000000..185bcc82 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh.h @@ -0,0 +1,46 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_H_ + +#include "ballistica/core/object.h" +#include "ballistica/graphics/mesh/mesh_data.h" +#include "ballistica/graphics/mesh/mesh_data_client_handle.h" + +namespace ballistica { + +// A user-defined dynamic mesh (unlike a model which is completely static) +class Mesh : public Object { + public: + auto type() const -> MeshDataType { return type_; } + auto mesh_data_client_handle() -> Object::Ref& { + return mesh_data_client_handle_; + } + + // Return whether it is safe to attempt drawing with present data. + virtual auto IsValid() const -> bool = 0; + auto last_frame_def_num() const -> int64_t { return last_frame_def_num_; } + void set_last_frame_def_num(int64_t f) { last_frame_def_num_ = f; } + + protected: + explicit Mesh(MeshDataType type, + MeshDrawType draw_type = MeshDrawType::kStatic) + : valid_(false), type_(type), last_frame_def_num_(0) { + mesh_data_client_handle_ = + Object::New(new MeshData(type, draw_type)); + } + + private: + int64_t last_frame_def_num_{}; + MeshDataType type_{}; + + // Renderer data for this mesh. We keep this as a shared pointer + // so that frame_defs or other things using this mesh can keep it alive + // even if we go away. + Object::Ref mesh_data_client_handle_; + bool valid_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_H_ diff --git a/src/ballistica/graphics/mesh/mesh_buffer.h b/src/ballistica/graphics/mesh/mesh_buffer.h new file mode 100644 index 00000000..009b5ae2 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_buffer.h @@ -0,0 +1,28 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_H_ + +#include +#include + +#include "ballistica/graphics/mesh/mesh_buffer_base.h" + +namespace ballistica { + +// Buffer for arbitrary mesh data. +template +class MeshBuffer : public MeshBufferBase { + public: + MeshBuffer() = default; + explicit MeshBuffer(size_t initial_size) : elements(initial_size) {} + MeshBuffer(size_t initial_size, const T* initial_data) + : elements(initial_size) { + memcpy(&elements[0], initial_data, initial_size * sizeof(T)); + } + std::vector elements; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_H_ diff --git a/src/ballistica/graphics/mesh/mesh_buffer_base.h b/src/ballistica/graphics/mesh/mesh_buffer_base.h new file mode 100644 index 00000000..0a498f49 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_buffer_base.h @@ -0,0 +1,21 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_BASE_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_BASE_H_ + +#include "ballistica/core/object.h" + +namespace ballistica { + +// Buffers used by the game thread to pass indices/vertices/etc. to meshes in +// the graphics thread. Note that it is safe to create these in other threads; +// you just need to turn off thread-checks until you pass ownership to the game +// thread. (or just avoid creating references outside of the game thread) +class MeshBufferBase : public Object { + public: + uint32_t state; // which dynamicState value on the mesh this corresponds to +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_BASE_H_ diff --git a/src/ballistica/graphics/mesh/mesh_buffer_vertex_simple_full.h b/src/ballistica/graphics/mesh/mesh_buffer_vertex_simple_full.h new file mode 100644 index 00000000..27f65892 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_buffer_vertex_simple_full.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SIMPLE_FULL_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SIMPLE_FULL_H_ + +#include "ballistica/graphics/mesh/mesh_buffer.h" + +namespace ballistica { + +// just make this a vanilla child class of our template +// (simply so we could predeclare this) +class MeshBufferVertexSimpleFull : public MeshBuffer { + using MeshBuffer::MeshBuffer; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SIMPLE_FULL_H_ diff --git a/src/ballistica/graphics/mesh/mesh_buffer_vertex_smoke_full.h b/src/ballistica/graphics/mesh/mesh_buffer_vertex_smoke_full.h new file mode 100644 index 00000000..31ef65ec --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_buffer_vertex_smoke_full.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SMOKE_FULL_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SMOKE_FULL_H_ + +#include "ballistica/graphics/mesh/mesh_buffer.h" + +namespace ballistica { + +// just make this a vanilla child class of our template +// (simply so we could predeclare this) +class MeshBufferVertexSmokeFull : public MeshBuffer { + using MeshBuffer::MeshBuffer; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SMOKE_FULL_H_ diff --git a/src/ballistica/graphics/mesh/mesh_buffer_vertex_sprite.h b/src/ballistica/graphics/mesh/mesh_buffer_vertex_sprite.h new file mode 100644 index 00000000..e2288f9e --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_buffer_vertex_sprite.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SPRITE_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SPRITE_H_ + +#include "ballistica/graphics/mesh/mesh_buffer.h" + +namespace ballistica { + +// just make this a vanilla child class of our template +// (simply so we could predeclare this) +class MeshBufferVertexSprite : public MeshBuffer { + using MeshBuffer::MeshBuffer; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SPRITE_H_ diff --git a/src/ballistica/graphics/mesh/mesh_data.cc b/src/ballistica/graphics/mesh/mesh_data.cc new file mode 100644 index 00000000..ba409272 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_data.cc @@ -0,0 +1,24 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/mesh/mesh_data.h" + +#include "ballistica/graphics/renderer.h" + +namespace ballistica { + +void MeshData::Load(Renderer* renderer) { + assert(InGraphicsThread()); + if (!renderer_data_) { + renderer_data_ = renderer->NewMeshData(type(), draw_type()); + } +} + +void MeshData::Unload(Renderer* renderer) { + assert(InGraphicsThread()); + if (renderer_data_) { + renderer->DeleteMeshData(renderer_data_, type()); + renderer_data_ = nullptr; + } +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/mesh/mesh_data.h b/src/ballistica/graphics/mesh/mesh_data.h new file mode 100644 index 00000000..1dd956c9 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_data.h @@ -0,0 +1,42 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_DATA_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_DATA_H_ + +#include + +#include "ballistica/ballistica.h" + +namespace ballistica { + +// The portion of a mesh that is owned by the graphics thread. +// This contains the renderer-specific data (GL buffers, etc) +class MeshData { + public: + MeshData(MeshDataType type, MeshDrawType draw_type) + : type_(type), draw_type_(draw_type) {} + virtual ~MeshData() { + if (renderer_data_) { + Log("Error: MeshData going down with rendererData intact!"); + } + } + std::list::iterator iterator_; + auto type() -> MeshDataType { return type_; } + auto draw_type() -> MeshDrawType { return draw_type_; } + void Load(Renderer* renderer); + void Unload(Renderer* renderer); + auto renderer_data() const -> MeshRendererData* { + assert(renderer_data_); + return renderer_data_; + } + + private: + MeshRendererData* renderer_data_{}; + MeshDataType type_{}; + MeshDrawType draw_type_{}; + BA_DISALLOW_CLASS_COPIES(MeshData); +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_DATA_H_ diff --git a/src/ballistica/graphics/mesh/mesh_data_client_handle.cc b/src/ballistica/graphics/mesh/mesh_data_client_handle.cc new file mode 100644 index 00000000..a0e26a78 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_data_client_handle.cc @@ -0,0 +1,17 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/mesh/mesh_data_client_handle.h" + +#include "ballistica/graphics/graphics.h" + +namespace ballistica { + +MeshDataClientHandle::MeshDataClientHandle(MeshData* d) : mesh_data(d) { + g_graphics->AddMeshDataCreate(mesh_data); +} + +MeshDataClientHandle::~MeshDataClientHandle() { + g_graphics->AddMeshDataDestroy(mesh_data); +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/mesh/mesh_data_client_handle.h b/src/ballistica/graphics/mesh/mesh_data_client_handle.h new file mode 100644 index 00000000..f0eae583 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_data_client_handle.h @@ -0,0 +1,22 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_DATA_CLIENT_HANDLE_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_DATA_CLIENT_HANDLE_H_ + +#include "ballistica/core/object.h" + +namespace ballistica { + +// Client-side (game-thread) handle to server-side (graphics-thread) mesh data. +// Server-side data will be created when this object is instantiated and +// cleared when this object goes down. +class MeshDataClientHandle : public Object { + public: + explicit MeshDataClientHandle(MeshData* d); + ~MeshDataClientHandle() override; + MeshData* mesh_data; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_DATA_CLIENT_HANDLE_H_ diff --git a/src/ballistica/graphics/mesh/mesh_index_buffer_16.h b/src/ballistica/graphics/mesh/mesh_index_buffer_16.h new file mode 100644 index 00000000..84e21a39 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_index_buffer_16.h @@ -0,0 +1,17 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEX_BUFFER_16_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_INDEX_BUFFER_16_H_ + +#include "ballistica/graphics/mesh/mesh_buffer.h" + +namespace ballistica { + +// standard buffer for indices +class MeshIndexBuffer16 : public MeshBuffer { + using MeshBuffer::MeshBuffer; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEX_BUFFER_16_H_ diff --git a/src/ballistica/graphics/mesh/mesh_index_buffer_32.h b/src/ballistica/graphics/mesh/mesh_index_buffer_32.h new file mode 100644 index 00000000..3cf64da5 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_index_buffer_32.h @@ -0,0 +1,17 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEX_BUFFER_32_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_INDEX_BUFFER_32_H_ + +#include "ballistica/graphics/mesh/mesh_buffer.h" + +namespace ballistica { + +// standard buffer for indices +class MeshIndexBuffer32 : public MeshBuffer { + using MeshBuffer::MeshBuffer; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEX_BUFFER_32_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed.h b/src/ballistica/graphics/mesh/mesh_indexed.h new file mode 100644 index 00000000..2841ea11 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_indexed.h @@ -0,0 +1,41 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_H_ + +#include "ballistica/graphics/mesh/mesh_indexed_base.h" + +namespace ballistica { + +// Mesh using indices and vertex data (all either static or dynamic). +// Supports both 16 and 32 bit indices. +template +class MeshIndexed : public MeshIndexedBase { + public: + explicit MeshIndexed(MeshDrawType draw_type = MeshDrawType::kDynamic) + : MeshIndexedBase(T, draw_type) {} + void SetData(const Object::Ref>& data) { + assert(!data->elements.empty()); + data_ = data; + data_->state = ++data_state_; + } + auto data() const -> const Object::Ref>& { return data_; } + + auto IsValid() const -> bool override { + if (!data_.exists() || data_->elements.empty() + || !MeshIndexedBase::IsValid()) { + return false; + } + + // Make sure our index size covers our element count. + return IndexSizeIsValid(data_->elements.size()); + } + + private: + Object::Ref> data_; + uint32_t data_state_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed_base.h b/src/ballistica/graphics/mesh/mesh_indexed_base.h new file mode 100644 index 00000000..e8daed0c --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_indexed_base.h @@ -0,0 +1,106 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_BASE_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_BASE_H_ + +#include "ballistica/graphics/mesh/mesh.h" +#include "ballistica/graphics/mesh/mesh_index_buffer_16.h" +#include "ballistica/graphics/mesh/mesh_index_buffer_32.h" + +namespace ballistica { + +// Mesh supporting index data. +class MeshIndexedBase : public Mesh { + public: + explicit MeshIndexedBase(MeshDataType type, + MeshDrawType draw_type = MeshDrawType::kDynamic) + : Mesh(type, draw_type) {} + + auto index_data_size() const -> int { + assert(index_data_size_ != 0); + return index_data_size_; + } + + void SetIndexData(const Object::Ref& data) { + assert(data.exists() && !data->elements.empty()); + // unlike vertex data, index data might often remain the same, so lets test + // for that and avoid some gl updates.. + if (index_data_32_.exists()) { + assert(data.exists() && index_data_32_.get()); + if (data->elements == index_data_32_->elements) { + return; // just keep our existing one + } + } + index_data_32_ = data; + index_data_32_->state = ++index_state_; + index_data_size_ = 4; + // kill any other index data we have + index_data_16_.Clear(); + } + + void SetIndexData(const Object::Ref& data) { + assert(data.exists() && !data->elements.empty()); + // unlike vertex data, index data might often remain the same, so lets test + // for that and avoid some gl updates.. + if (index_data_16_.exists()) { + assert(index_data_16_.get()); + if (data->elements == index_data_16_->elements) { + return; // just keep our existing one + } + } + // FIXME - we should probably just pass in a strong ref as an arg?... + index_data_16_ = data; + index_data_16_->state = ++index_state_; + index_data_size_ = 2; + // kill any other index data we have + index_data_32_.Clear(); + } + + // call this if you have nothing to draw + void SetEmpty() { + index_data_16_.Clear(); + index_data_32_.Clear(); + } + auto IsValid() const -> bool override { + switch (index_data_size()) { + case 4: + return (index_data_32_.exists() && !index_data_32_->elements.empty()); + case 2: + return (index_data_16_.exists() && !index_data_16_->elements.empty()); + default: + return false; + } + } + // Checks for valid index sizes given a data length. + // Will print a one-time warning and return false if invalid. + // For use by subclasses in their IsValid() overrides + auto IndexSizeIsValid(size_t data_size) const -> bool { + if (index_data_size() == 2 && data_size > 65535) { + BA_LOG_ONCE("ERROR: got mesh data with > 65535 elems and 16 bit indices: " + + GetObjectDescription() + + ". This case requires 32 bit indices."); + return false; + } + return true; + } + auto GetIndexData() const -> MeshBufferBase* { + switch (index_data_size()) { + case 4: + return index_data_32_.get(); + case 2: + return index_data_16_.get(); + default: + throw Exception(); + } + } + + private: + Object::Ref index_data_32_; + Object::Ref index_data_16_; + int index_data_size_ = 0; + uint32_t index_state_ = 0; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_BASE_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed_dual_texture_full.h b/src/ballistica/graphics/mesh/mesh_indexed_dual_texture_full.h new file mode 100644 index 00000000..8194061d --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_indexed_dual_texture_full.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_DUAL_TEXTURE_FULL_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_DUAL_TEXTURE_FULL_H_ + +#include "ballistica/graphics/mesh/mesh_indexed.h" + +namespace ballistica { + +class MeshIndexedDualTextureFull + : public MeshIndexed { + using MeshIndexed::MeshIndexed; // wheee c++11 magic +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_DUAL_TEXTURE_FULL_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed_object_split.h b/src/ballistica/graphics/mesh/mesh_indexed_object_split.h new file mode 100644 index 00000000..c1f57a3b --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_indexed_object_split.h @@ -0,0 +1,20 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_OBJECT_SPLIT_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_OBJECT_SPLIT_H_ + +#include "ballistica/graphics/mesh/mesh_indexed_static_dynamic.h" + +namespace ballistica { + +// a mesh with static indices and UVs and dynamic positions and normals +class MeshIndexedObjectSplit + : public MeshIndexedStaticDynamic { + using MeshIndexedStaticDynamic::MeshIndexedStaticDynamic; // c++11 magic! +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_OBJECT_SPLIT_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed_simple_full.h b/src/ballistica/graphics/mesh/mesh_indexed_simple_full.h new file mode 100644 index 00000000..9e7190c5 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_indexed_simple_full.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_FULL_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_FULL_H_ + +#include "ballistica/graphics/mesh/mesh_indexed.h" + +namespace ballistica { + +// a simple mesh with all data provided together (either static or dynamic) +class MeshIndexedSimpleFull + : public MeshIndexed { + using MeshIndexed::MeshIndexed; // wheee c++11 magic +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_FULL_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed_simple_split.h b/src/ballistica/graphics/mesh/mesh_indexed_simple_split.h new file mode 100644 index 00000000..d74f3095 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_indexed_simple_split.h @@ -0,0 +1,20 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_SPLIT_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_SPLIT_H_ + +#include "ballistica/graphics/mesh/mesh_indexed_static_dynamic.h" + +namespace ballistica { + +// a mesh with static indices and UVs and dynamic positions +class MeshIndexedSimpleSplit + : public MeshIndexedStaticDynamic { + using MeshIndexedStaticDynamic::MeshIndexedStaticDynamic; // c++11 magic! +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_SPLIT_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed_smoke_full.h b/src/ballistica/graphics/mesh/mesh_indexed_smoke_full.h new file mode 100644 index 00000000..a4e4bd26 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_indexed_smoke_full.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SMOKE_FULL_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SMOKE_FULL_H_ + +#include "ballistica/graphics/mesh/mesh_indexed.h" + +namespace ballistica { + +// a mesh with all data provided together (either static or dynamic) +class MeshIndexedSmokeFull + : public MeshIndexed { + using MeshIndexed::MeshIndexed; // wheee c++11 magic +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SMOKE_FULL_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed_static_dynamic.h b/src/ballistica/graphics/mesh/mesh_indexed_static_dynamic.h new file mode 100644 index 00000000..a1aeff0a --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_indexed_static_dynamic.h @@ -0,0 +1,58 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_STATIC_DYNAMIC_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_STATIC_DYNAMIC_H_ + +#include "ballistica/graphics/mesh/mesh_indexed_base.h" + +namespace ballistica { + +// mesh with static indices, some static vertex data, +// and some dynamic vertex data +template +class MeshIndexedStaticDynamic : public MeshIndexedBase { + public: + MeshIndexedStaticDynamic() : MeshIndexedBase(T) {} + void SetStaticData(const Object::Ref>& data) { + assert(data->elements.size() > 0); + static_data_ = data; + static_data_->state = ++static_state_; + } + void SetDynamicData(const Object::Ref>& data) { + assert(data->elements.size() > 0); + dynamic_data_ = data; + dynamic_data_->state = ++dynamic_state_; + } + auto IsValid() const -> bool override { + if (!static_data_.exists() || static_data_->elements.empty() + || !dynamic_data_.exists() || dynamic_data_->elements.empty() + || !MeshIndexedBase::IsValid()) { + return false; + } + + // Static and dynamic data sizes should always match, right? + if (static_data_->elements.size() != dynamic_data_->elements.size()) { + BA_LOG_ONCE("ERROR: mesh static and dynamic data sizes do not match"); + return false; + } + + // Make sure our index size covers our element count. + return IndexSizeIsValid(static_data_->elements.size()); + } + auto static_data() const -> const Object::Ref>& { + return static_data_; + } + auto dynamic_data() const -> const Object::Ref>& { + return dynamic_data_; + } + + private: + Object::Ref> static_data_; + Object::Ref> dynamic_data_; + uint32_t static_state_{}; + uint32_t dynamic_state_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_STATIC_DYNAMIC_H_ diff --git a/src/ballistica/graphics/mesh/mesh_non_indexed.h b/src/ballistica/graphics/mesh/mesh_non_indexed.h new file mode 100644 index 00000000..e0509379 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_non_indexed.h @@ -0,0 +1,42 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_NON_INDEXED_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_NON_INDEXED_H_ + +#include "ballistica/graphics/mesh/mesh.h" + +namespace ballistica { + +// Mesh using non-indexed vertex data. Good for situations where vertices +// are never shared between primitives (such as drawing points/sprites/etc). +template +class MeshNonIndexed : public Mesh { + public: + explicit MeshNonIndexed(MeshDrawType drawType = MeshDrawType::kDynamic) + : Mesh(T, drawType), data_state_(0) {} + // NOLINTNEXTLINE + void SetData(const Object::Ref>& data) { + data_ = data; + data_->state = ++data_state_; + } + + // Call this if you have nothing to draw. + void SetEmpty() { data_.clear(); } + auto IsValid() const -> bool override { +#if BA_DEBUG_BUILD + // Make extra sure that we're actually valid in debug mode. + if (data_.exists()) { + assert(data_->elements.size() > 0); + } +#endif + return (data_.exists()); + } + + private: + Object::Ref> data_{}; + uint32_t data_state_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_NON_INDEXED_H_ diff --git a/src/ballistica/graphics/mesh/mesh_renderer_data.h b/src/ballistica/graphics/mesh/mesh_renderer_data.h new file mode 100644 index 00000000..67b1eba7 --- /dev/null +++ b/src/ballistica/graphics/mesh/mesh_renderer_data.h @@ -0,0 +1,15 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_MESH_RENDERER_DATA_H_ +#define BALLISTICA_GRAPHICS_MESH_MESH_RENDERER_DATA_H_ + +namespace ballistica { + +class MeshRendererData { + public: + virtual ~MeshRendererData() = default; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_MESH_RENDERER_DATA_H_ diff --git a/src/ballistica/graphics/mesh/sprite_mesh.h b/src/ballistica/graphics/mesh/sprite_mesh.h new file mode 100644 index 00000000..e07bc502 --- /dev/null +++ b/src/ballistica/graphics/mesh/sprite_mesh.h @@ -0,0 +1,17 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_SPRITE_MESH_H_ +#define BALLISTICA_GRAPHICS_MESH_SPRITE_MESH_H_ + +#include "ballistica/graphics/mesh/mesh_indexed.h" + +namespace ballistica { + +// an indexed sprite-mesh +class SpriteMesh : public MeshIndexed { + using MeshIndexed::MeshIndexed; // wheeee c++11 magic +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_SPRITE_MESH_H_ diff --git a/src/ballistica/graphics/mesh/text_mesh.cc b/src/ballistica/graphics/mesh/text_mesh.cc new file mode 100644 index 00000000..c68e8649 --- /dev/null +++ b/src/ballistica/graphics/mesh/text_mesh.cc @@ -0,0 +1,574 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/mesh/text_mesh.h" + +#include +#include +#include + +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/text/text_graphics.h" +#include "ballistica/graphics/text/text_packer.h" + +namespace ballistica { + +TextMesh::TextMesh() : MeshIndexedDualTextureFull(MeshDrawType::kStatic) {} + +void TextMesh::SetText(const std::string& text_in, HAlign alignment_h, + VAlign alignment_v, bool big, uint32_t min_val, + uint32_t max_val, TextMeshEntryType entry_type, + TextPacker* packer) { + if (text_in == text_) { + // Covers corner case where we assign a new string to empty. + if (text_in.empty()) { + SetEmpty(); + } + return; + } + text_ = text_in; + + assert(Utils::IsValidUTF8(text_)); + + const char* txt = text_in.c_str(); + + // Quick-out for empty strings. + if (txt[0] == 0) { + SetEmpty(); + return; + } + + if (entry_type == TextMeshEntryType::kOSRendered) { + assert(packer != nullptr); + } + + // Start buffers big enough to handle the worst case + // (every char being a discrete letter). + int text_size = static_cast(text_in.size()); + assert(text_size > 0); + Object::Ref indices16; + Object::Ref indices32; + + // Go with 32 bit indices if there's any chance we'll have over 65535 pts; + // otherwise go with 16 bit. + // NOTE: disabling 32 bit indices for now; turns out they're + // not supported in OpenGL ES2 :-( + // It may be worth adding logic to split up meshes into multiple + // draw-calls. (or we can just wait until ES2 is dead). + if (explicit_bool(false) && 4 * text_size > 65535) { + indices32 = Object::New(6 * (text_size)); + } else { + indices16 = Object::New(6 * (text_size)); + } + auto vertices(Object::New>(4 * text_size)); + + uint16_t* index16 = indices16.exists() ? indices16->elements.data() : nullptr; + uint32_t* index32 = indices32.exists() ? indices32->elements.data() : nullptr; + + VertexDualTextureFull* v = &vertices->elements[0]; + uint32_t index_offset = 0; + float x = 0; + float x_offset, y_offset; + x_offset = x; + + float char_width; + float char_height; + float row_height; + float char_offset_h; + float char_offset_v; + + char_width = char_height = 32.0f; + row_height = kTextRowHeight; + char_offset_h = -3.0f; + char_offset_v = 7.0f; + uint32_t char_val; + float line_length; + + float l = 0; + float r = 0; + float b = 0; + float t = 0; + + float text_height; + + // Pre-calc the height of the text (if needed). + switch (alignment_v) { + case VAlign::kNone: + case VAlign::kTop: + text_height = 0; // Not used here. + break; + case VAlign::kCenter: + case VAlign::kBottom: { + int rows = 1; + for (const char* c = txt; *c != 0; c++) { + if (*c == '\n') rows++; + } + text_height = static_cast(rows) * row_height; + break; + } + default: + throw Exception(); + } + + switch (alignment_v) { + case VAlign::kNone: + y_offset = b + char_offset_v; + break; + case VAlign::kTop: + y_offset = b + char_offset_v + (t - b) - row_height; + break; + case VAlign::kCenter: + y_offset = + b + char_offset_v + ((t - b) / 2) + (text_height / 2) - row_height; + break; + case VAlign::kBottom: + y_offset = b + char_offset_v + text_height - row_height; + break; + default: + throw Exception(); + } + + const char* tc = txt; + bool first_char = true; + + std::vector os_span; + + while (*tc != 0) { + const char* tc_prev = tc; + + char_val = Utils::GetUTF8Value(tc); + + Utils::AdvanceUTF8(&tc); + + // Reset alignment on new lines. + if (first_char || char_val == '\n') { + // If we've been building an os-span, add it to the text-packer. + if (char_val == '\n' && !os_span.empty()) { + Rect r2; + float width; + std::string s = Utils::UTF8FromUnicode(os_span); + g_text_graphics->GetOSTextSpanBoundsAndWidth(s, &r2, &width); + if (packer) { + packer->AddSpan(s, x_offset, y_offset, r2); + } + os_span.clear(); + } + + switch (alignment_h) { + case HAlign::kLeft: + x_offset = l + char_offset_h; + break; + case HAlign::kCenter: + case HAlign::kRight: { + // For some alignments we need to pre-calc the length of the line. + line_length = 0; + const char* c; + + // If this was the first char, include it in this line tally. + // if it was a newline, don't. + if (first_char) { + c = tc_prev; + } else { + c = tc; + } + + // We have the OS render some chars, broken into single-line spans. + std::vector os_span_l; + + while (true) { + uint32_t val; + if (*c == 0) { // NOLINT(bugprone-branch-clone) + break; + } else if (*c == '\n') { + break; + } else { + val = Utils::GetUTF8Value(c); + Utils::AdvanceUTF8(&c); + + // Special case: if we're already doing an OS-span, tack certain + // chars onto it instead of switching back to glyph mode. + // (to reduce the number of times we switch back and forth) + if (TextGraphics::IsOSDrawableAscii(val) && !os_span_l.empty()) { + os_span_l.push_back(val); + } else if (TextGraphics::Glyph* g = + g_text_graphics->GetGlyph(val, big)) { + // Flipping back to glyphs; if we had been building an os_span, + // tally it. + if (!os_span_l.empty()) { + std::string s = Utils::UTF8FromUnicode(os_span_l); + line_length += g_text_graphics->GetOSTextSpanWidth(s); + os_span_l.clear(); + } + line_length += char_width * g->advance; + } else { + // Not a glyph char: add it to our current span to handle + // through the OS. + + if (g_buildconfig.enable_os_font_rendering()) { + os_span_l.push_back(val); + } + } + } + } + + // Add final os_span if there is one. + if (!os_span_l.empty()) { + std::string s = Utils::UTF8FromUnicode(os_span_l); + line_length += g_text_graphics->GetOSTextSpanWidth(s); + os_span_l.clear(); + } + if (alignment_h == HAlign::kCenter) { + x_offset = l + char_offset_h + ((r - l) / 2) - (line_length / 2); + } else { + x_offset = l + char_offset_h + (r - l) - line_length; + } + break; + } + default: + throw Exception(); + } + first_char = false; + } + + switch (char_val) { // NOLINT + case '\n': + y_offset -= row_height; + break; + + default: { + bool draw = true; + if (char_val < min_val || char_val > max_val) draw = false; + + // Only draw the private-use range when doing our extras sheets. + // (technically OS might be able to render these but don't allow that) + if (entry_type != TextMeshEntryType::kExtras + && (char_val >= 0xE000 && char_val <= 0xF8FF)) { + draw = false; + } + + // Special case: if we're already doing an OS-span, tack certain + // chars onto it instead of switching back to glyph mode. + // (to reduce the number of times we switch back and forth) + if (TextGraphics::IsOSDrawableAscii(char_val) && !os_span.empty()) { + os_span.push_back(char_val); + } else if (TextGraphics::Glyph* glyph = + g_text_graphics->GetGlyph(char_val, big)) { + // If we had been building up an OS-text span, + // commit it since we're flipping to glyphs now. + if (!os_span.empty()) { + Rect r2; + float width; + std::string s = Utils::UTF8FromUnicode(os_span); + g_text_graphics->GetOSTextSpanBoundsAndWidth(s, &r2, &width); + if (packer) packer->AddSpan(s, x_offset, y_offset, r2); + x_offset += width; + os_span.clear(); + } + + // Draw this glyph. + if (draw) { + float v_min = glyph->tex_min_y; + float v_max = glyph->tex_max_y; + float u_min = glyph->tex_min_x; + float u_max = glyph->tex_max_x; + auto v_max_i = static_cast(65535.0f * v_max); + auto v_min_i = static_cast(65535.0f * v_min); + auto u_max_i = static_cast(65535.0f * u_max); + auto u_min_i = static_cast(65535.0f * u_min); + + if (index16) { + *index16++ = static_cast_check_fit(index_offset + 0); + *index16++ = static_cast_check_fit(index_offset + 1); + *index16++ = static_cast_check_fit(index_offset + 2); + *index16++ = static_cast_check_fit(index_offset + 1); + *index16++ = static_cast_check_fit(index_offset + 3); + *index16++ = static_cast_check_fit(index_offset + 2); + } + if (index32) { + *index32++ = index_offset + 0; + *index32++ = index_offset + 1; + *index32++ = index_offset + 2; + *index32++ = index_offset + 1; + *index32++ = index_offset + 3; + *index32++ = index_offset + 2; + } + + // Bot left. + v->position[0] = x_offset + char_width * glyph->pen_offset_x; + v->position[1] = y_offset + char_height * glyph->pen_offset_y; + v->position[2] = 0; + v->uv[0] = static_cast_check_fit(u_min_i); + v->uv[1] = static_cast_check_fit(v_min_i); + v->uv2[0] = 0; + v->uv2[1] = 65535; + v++; + + // Bot right. + v->position[0] = + x_offset + char_width * (glyph->pen_offset_x + glyph->x_size); + v->position[1] = y_offset + char_height * glyph->pen_offset_y; + v->position[2] = 0; + v->uv[0] = static_cast_check_fit(u_max_i); + v->uv[1] = static_cast_check_fit(v_min_i); + v->uv2[0] = 65535; + v->uv2[1] = 65535; + v++; + + // Top left. + v->position[0] = x_offset + char_width * (glyph->pen_offset_x); + v->position[1] = + y_offset + char_height * (glyph->pen_offset_y + glyph->y_size); + v->position[2] = 0; + v->uv[0] = static_cast_check_fit(u_min_i); + v->uv[1] = static_cast_check_fit(v_max_i); + v->uv2[0] = 0; + v->uv2[1] = 0; + v++; + + // Top right. + v->position[0] = + x_offset + char_width * (glyph->pen_offset_x + glyph->x_size); + v->position[1] = + y_offset + char_height * (glyph->pen_offset_y + glyph->y_size); + v->position[2] = 0; + v->uv[0] = static_cast_check_fit(u_max_i); + v->uv[1] = static_cast_check_fit(v_max_i); + v->uv2[0] = 65535; + v->uv2[1] = 0; + v++; + index_offset += 4; + } + x_offset += char_width * glyph->advance; + } else { + // Add to the single-line span we'll ask the OS to render. + if (g_buildconfig.enable_os_font_rendering()) { + os_span.push_back(char_val); + } + } + break; + } + } + } + + // Commit any final OS-text span (can skip this if we're not + // the one drawing OS text). + if ((!os_span.empty()) && packer) { + Rect r2; + float width; + std::string s = Utils::UTF8FromUnicode(os_span); + g_text_graphics->GetOSTextSpanBoundsAndWidth(s, &r2, &width); + packer->AddSpan(s, x_offset, y_offset, r2); + os_span.clear(); + } + + // Now if we've been building a text-packer, + // compile it and add its final spans to our mesh. + if (packer) { + std::vector spans; + packer->compile(); + + // DEBUGGING - add a single quad above our first + // span showing the entire texture for debugging purposes + if (explicit_bool(false) && !packer->spans().empty()) { + int v_max_i = static_cast(65535 * 1.0f); + int v_min_i = static_cast(65535 * 0.0f); + int u_max_i = static_cast(65535 * 1.0f); + int u_min_i = static_cast(65535 * 0.0f); + + if (index16) { + *index16++ = static_cast_check_fit(index_offset + 0); + *index16++ = static_cast_check_fit(index_offset + 1); + *index16++ = static_cast_check_fit(index_offset + 2); + *index16++ = static_cast_check_fit(index_offset + 1); + *index16++ = static_cast_check_fit(index_offset + 3); + *index16++ = static_cast_check_fit(index_offset + 2); + } + if (index32) { + *index32++ = index_offset + 0; + *index32++ = index_offset + 1; + *index32++ = index_offset + 2; + *index32++ = index_offset + 1; + *index32++ = index_offset + 3; + *index32++ = index_offset + 2; + } + + x_offset = + packer->spans().front().bounds.l + packer->spans().front().x - 80.0f; + y_offset = + packer->spans().front().bounds.t + packer->spans().front().y + 90.0f; + + float width = static_cast(packer->texture_width()) * 0.7f; + float height = static_cast(packer->texture_height()) * 0.7f; + + // Bottom left. + v->position[0] = x_offset; + v->position[1] = y_offset; + v->position[2] = 0; + v->uv[0] = static_cast_check_fit(u_min_i); + v->uv[1] = static_cast_check_fit(v_max_i); + v->uv2[0] = 0; + v->uv2[1] = 65535; + v++; + + // Bottom right. + v->position[0] = x_offset + width; + v->position[1] = y_offset; + v->position[2] = 0; + v->uv[0] = static_cast_check_fit(u_max_i); + v->uv[1] = static_cast_check_fit(v_max_i); + v->uv2[0] = 65535; + v->uv2[1] = 65535; + v++; + + // Top left. + v->position[0] = x_offset; + v->position[1] = y_offset + height; + v->position[2] = 0; + v->uv[0] = static_cast_check_fit(u_min_i); + v->uv[1] = static_cast_check_fit(v_min_i); + v->uv2[0] = 0; + v->uv2[1] = 0; + v++; + + // Top right. + v->position[0] = x_offset + width; + v->position[1] = y_offset + height; + v->position[2] = 0; + v->uv[0] = static_cast_check_fit(u_max_i); + v->uv[1] = static_cast_check_fit(v_min_i); + v->uv2[0] = 65535; + v->uv2[1] = 0; + v++; + + index_offset += 4; + } + + for (auto&& i : packer->spans()) { + int v_max_i = + std::max(0, std::min(65535, static_cast(65535 * i.v_max))); + int v_min_i = + std::max(0, std::min(65535, static_cast(65535 * i.v_min))); + int u_max_i = + std::max(0, std::min(65535, static_cast(65535 * i.u_max))); + int u_min_i = + std::max(0, std::min(65535, static_cast(65535 * i.u_min))); + + if (index16) { + *index16++ = static_cast_check_fit(index_offset + 0); + *index16++ = static_cast_check_fit(index_offset + 1); + *index16++ = static_cast_check_fit(index_offset + 2); + *index16++ = static_cast_check_fit(index_offset + 1); + *index16++ = static_cast_check_fit(index_offset + 3); + *index16++ = static_cast_check_fit(index_offset + 2); + } + if (index32) { + *index32++ = index_offset + 0; + *index32++ = index_offset + 1; + *index32++ = index_offset + 2; + *index32++ = index_offset + 1; + *index32++ = index_offset + 3; + *index32++ = index_offset + 2; + } + + // Fudge-factor for lining OS-spans up with our stuff. + x_offset = i.x + 3.0f; + y_offset = i.y; + + // Bot left. + v->position[0] = x_offset + i.draw_bounds.l; + v->position[1] = y_offset + i.draw_bounds.b; + v->position[2] = 0; + v->uv[0] = static_cast_check_fit(u_min_i); + v->uv[1] = static_cast_check_fit(v_max_i); + v->uv2[0] = 0; + v->uv2[1] = 65535; + v++; + + // Bot right. + v->position[0] = x_offset + i.draw_bounds.r; + v->position[1] = y_offset + i.draw_bounds.b; + v->position[2] = 0; + v->uv[0] = static_cast_check_fit(u_max_i); + v->uv[1] = static_cast_check_fit(v_max_i); + v->uv2[0] = 65535; + v->uv2[1] = 65535; + v++; + + // Top left. + v->position[0] = x_offset + i.draw_bounds.l; + v->position[1] = y_offset + i.draw_bounds.t; + v->position[2] = 0; + v->uv[0] = static_cast_check_fit(u_min_i); + v->uv[1] = static_cast_check_fit(v_min_i); + v->uv2[0] = 0; + v->uv2[1] = 0; + v++; + + // Top right. + v->position[0] = x_offset + i.draw_bounds.r; + v->position[1] = y_offset + i.draw_bounds.t; + v->position[2] = 0; + v->uv[0] = static_cast_check_fit(u_max_i); + v->uv[1] = static_cast_check_fit(v_min_i); + v->uv2[0] = 65535; + v->uv2[1] = 0; + v++; + + index_offset += 4; + } + } + + // Make sure we didn't overshoot the end of our buffer. + if (index16) { + assert((index16 - indices16->elements.data()) + <= static_cast(indices16->elements.size())); + } + if (index32) { + assert((index32 - indices32->elements.data()) + <= static_cast(indices32->elements.size())); + } + assert((v - (&(vertices->elements[0]))) + <= static_cast(vertices->elements.size())); + + // clamp to what we actually used.. + if (index16) { + indices16->elements.resize(index16 - (indices16->elements.data())); + } + if (index32) { + indices32->elements.resize(index32 - (indices32->elements.data())); + } + vertices->elements.resize(v - (&(vertices->elements[0]))); + + // Either set data or abort if empty. + if (index16 && !indices16->elements.empty()) { + SetIndexData(indices16); + SetData(vertices); + } else if (index32 && !indices32->elements.empty()) { + // In a lot of cases we actually wind up with fewer than 65535 pts. + // (we theoretically could have needed more which is why we went 32bit). + // ...lets go ahead and downsize to 16 bit in this case to save a wee bit + // of gpu memory. + if (vertices->elements.size() < 65535) { + int size = static_cast(indices32->elements.size()); + indices16 = Object::NewDeferred(size); + uint16_t* i16 = indices16->elements.data(); + uint32_t* i32 = indices32->elements.data(); + for (int i = 0; i < size; i++) { + *i16++ = static_cast_check_fit(*i32++); + } + assert((i32 - indices32->elements.data()) + <= static_cast(indices32->elements.size())); + assert((i16 - indices16->elements.data()) + <= static_cast(indices16->elements.size())); + SetIndexData(indices16); + } else { + // we *actually* need 32 bit indices... + SetIndexData(indices32); + } + SetData(vertices); + } else { + SetEmpty(); + } +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/mesh/text_mesh.h b/src/ballistica/graphics/mesh/text_mesh.h new file mode 100644 index 00000000..ca73c141 --- /dev/null +++ b/src/ballistica/graphics/mesh/text_mesh.h @@ -0,0 +1,32 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_MESH_TEXT_MESH_H_ +#define BALLISTICA_GRAPHICS_MESH_TEXT_MESH_H_ + +#include + +#include "ballistica/graphics/mesh/mesh_indexed_dual_texture_full.h" + +namespace ballistica { + +// a mesh set up to draw text +// in general you should not use this directly; use TextGroup below, which will +// automatically handle switching meshes/textures in order to support the full +// unicode range +class TextMesh : public MeshIndexedDualTextureFull { + public: + enum class HAlign { kLeft, kCenter, kRight }; + enum class VAlign { kNone, kBottom, kCenter, kTop }; + TextMesh(); + void SetText(const std::string& text, HAlign alignment_h, VAlign alignment_v, + bool big, uint32_t min_val, uint32_t max_val, + TextMeshEntryType entry_type, TextPacker* packer); + auto text() const -> const std::string& { return text_; } + + private: + std::string text_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_MESH_TEXT_MESH_H_ diff --git a/src/ballistica/graphics/net_graph.cc b/src/ballistica/graphics/net_graph.cc new file mode 100644 index 00000000..86b4f427 --- /dev/null +++ b/src/ballistica/graphics/net_graph.cc @@ -0,0 +1,153 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/net_graph.h" + +#include +#include + +#include "ballistica/graphics/component/simple_component.h" + +namespace ballistica { + +class NetGraph::Impl { + public: + std::list> samples; + double duration = 2000.0; + double v_max_smoothed = 1.0; + ImageMesh bg_mesh; + MeshIndexedSimpleFull value_mesh; + TextGroup max_vel_text; +}; + +NetGraph::NetGraph() : impl_(new NetGraph::Impl()) {} + +NetGraph::~NetGraph() = default; + +void NetGraph::AddSample(double time, double value) { + impl_->samples.emplace_back(time, value); + double cutoffTime = time - impl_->duration; + + // Go ahead and prune old ones here so we don't grow out of control. + std::list>::iterator i; + for (i = impl_->samples.begin(); i != impl_->samples.end();) { + if (i->first < cutoffTime) { + auto i_next = i; + ++i_next; + impl_->samples.erase(i); + i = i_next; + } else { + break; + } + } +} + +void NetGraph::Draw(RenderPass* pass, double time, double x, double y, double w, + double h) { + impl_->bg_mesh.SetPositionAndSize( + static_cast(x), static_cast(y), 0.0f, static_cast(w), + static_cast(h)); + + int num_samples = static_cast(impl_->samples.size()); + + // Draw values (provided we have at least 2 samples) + bool draw_values = (num_samples >= 2); + if (draw_values) { + double t_left = time - impl_->duration; + double t_right = time; + double t_width = t_right - t_left; + double v_bottom = 0.0f; + + // Find the max y value we have and smoothly transition our bounds towards + // that. + double v_max = 0.0; + for (auto&& s : impl_->samples) { + if (s.second > v_max) { + v_max = s.second; + } + } + double smoothing = 0.95; + impl_->v_max_smoothed = + smoothing * impl_->v_max_smoothed + (1.0 - smoothing) * v_max * 1.1; + + double v_top = impl_->v_max_smoothed; + double v_height = v_top - v_bottom; + + // We need 2 verts per sample. + auto vertex_buffer( + Object::New>(num_samples * 2)); + VertexSimpleFull* v = vertex_buffer->elements.data(); + for (auto&& s : impl_->samples) { + double t = s.first; + double val = s.second; + double vx = x + w * ((t - t_left) / t_width); + double vy = y + h * ((val - v_bottom) / v_height); + v->position[0] = static_cast(vx); + v->position[1] = static_cast(y); + v->position[2] = 0.0f; + v->uv[0] = v->uv[1] = 0; + v++; + v->position[0] = static_cast(vx); + v->position[1] = static_cast(vy); + v->position[2] = 0.0f; + v->uv[0] = v->uv[1] = 0; + v++; + } + + // We need 2 tris per sample (minus the last). + auto index_buffer(Object::New((num_samples - 1) * 6)); + uint16_t* i = index_buffer->elements.data(); + auto s = impl_->samples.begin(); + int v_count = 0; + while (true) { + auto s_next = s; + ++s_next; + if (s_next == impl_->samples.end()) { + break; + } else { + *i++ = static_cast_check_fit(v_count); + *i++ = static_cast_check_fit(v_count + 2); + *i++ = static_cast_check_fit(v_count + 1); + *i++ = static_cast_check_fit(v_count + 2); + *i++ = static_cast_check_fit(v_count + 3); + *i++ = static_cast_check_fit(v_count + 1); + } + v_count += 2; + s = s_next; + } + impl_->value_mesh.SetIndexData(index_buffer); + impl_->value_mesh.SetData(vertex_buffer); + } + + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetColor(0.35f, 0.0f, 0.0f, 0.9f); + c.DrawMesh(&impl_->bg_mesh); + c.SetColor(0.0f, 1.0f, 0.0f, 0.85f); + if (draw_values) { + c.DrawMesh(&impl_->value_mesh); + } + c.Submit(); + + char val_str[32]; + snprintf(val_str, sizeof(val_str), "%.2f", impl_->v_max_smoothed); + impl_->max_vel_text.SetText(val_str, TextMesh::HAlign::kLeft, + TextMesh::VAlign::kTop); + + SimpleComponent c2(pass); + c2.SetTransparent(true); + c2.SetColor(1, 0, 0, 1); + c2.PushTransform(); + c2.Translate(static_cast(x), static_cast(y + h)); + float scale = static_cast(h) * 0.006f; + c2.Scale(scale, scale); + int text_elem_count = impl_->max_vel_text.GetElementCount(); + for (int e = 0; e < text_elem_count; e++) { + c2.SetTexture(impl_->max_vel_text.GetElementTexture(e)); + c2.SetFlatness(1.0f); + c2.DrawMesh(impl_->max_vel_text.GetElementMesh(e)); + } + c2.PopTransform(); + c2.Submit(); +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/net_graph.h b/src/ballistica/graphics/net_graph.h new file mode 100644 index 00000000..40b72227 --- /dev/null +++ b/src/ballistica/graphics/net_graph.h @@ -0,0 +1,27 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_NET_GRAPH_H_ +#define BALLISTICA_GRAPHICS_NET_GRAPH_H_ + +#include + +#include "ballistica/core/object.h" + +namespace ballistica { + +class NetGraph : public Object { + public: + NetGraph(); + ~NetGraph() override; + void AddSample(double time, double value); + void Draw(RenderPass* pass, double time, double x, double y, double w, + double h); + + private: + class Impl; + std::unique_ptr impl_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_NET_GRAPH_H_ diff --git a/src/ballistica/graphics/render_command_buffer.h b/src/ballistica/graphics/render_command_buffer.h new file mode 100644 index 00000000..27f8af98 --- /dev/null +++ b/src/ballistica/graphics/render_command_buffer.h @@ -0,0 +1,576 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_RENDER_COMMAND_BUFFER_H_ +#define BALLISTICA_GRAPHICS_RENDER_COMMAND_BUFFER_H_ + +#include + +#include "ballistica/ballistica.h" +#include "ballistica/graphics/frame_def.h" +#include "ballistica/graphics/mesh/mesh.h" +#include "ballistica/math/matrix44f.h" +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/texture.h" + +namespace ballistica { + +class RenderCommandBuffer { + public: + // IMPORTANT: make sure to update has_draw_commands with any new + // ones added here. + enum class Command { + kEnd, + kShader, + kDrawModel, + kDrawModelInstanced, + kDrawMesh, + kDrawScreenQuad, + kScissorPush, + kScissorPop, + kPushTransform, + kPopTransform, + kTranslate2, + kTranslate3, + kCursorTranslate, + kScaleUniform, + kTranslateToProjectedPoint, +#if BA_VR_BUILD + kTransformToRightHand, + kTransformToLeftHand, + kTransformToHead, +#endif + kScale2, + kScale3, + kRotate, + kMultMatrix, + kFlipCullFace, + kSimpleComponentInlineColor, + kObjectComponentInlineColor, + kObjectComponentInlineAddColor, + kBeginDebugDrawTriangles, + kBeginDebugDrawLines, + kEndDebugDraw, + kDebugDrawVertex3 + }; + + RenderCommandBuffer() = default; + void PutCommand(Command c) { + assert(!finalized_); + commands_.push_back(c); + } + + void PutFloat(float val) { + assert(!finalized_); + fvals_.push_back(val); + } + + void PutFloats(float f1, float f2) { + assert(!finalized_); + int s = static_cast(fvals_.size()); + fvals_.resize(fvals_.size() + 2); + float* f = &(fvals_[s]); + *f++ = f1; + *f = f2; + } + + void PutFloats(float f1, float f2, float f3) { + assert(!finalized_); + int s = static_cast(fvals_.size()); + fvals_.resize(fvals_.size() + 3); + float* f = &(fvals_[s]); + *f++ = f1; + *f++ = f2; + *f = f3; + } + + void PutFloats(float f1, float f2, float f3, float f4) { + assert(!finalized_); + int s = static_cast(fvals_.size()); + fvals_.resize(fvals_.size() + 4); + float* f = &(fvals_[s]); + *f++ = f1; + *f++ = f2; + *f++ = f3; + *f = f4; + } + + void PutFloats(float f1, float f2, float f3, float f4, float f5) { + assert(!finalized_); + int s = static_cast(fvals_.size()); + fvals_.resize(fvals_.size() + 5); + float* f = &(fvals_[s]); + *f++ = f1; + *f++ = f2; + *f++ = f3; + *f++ = f4; + *f = f5; + } + + void PutFloats(float f1, float f2, float f3, float f4, float f5, float f6) { + assert(!finalized_); + int s = static_cast(fvals_.size()); + fvals_.resize(fvals_.size() + 6); + float* f = &(fvals_[s]); + *f++ = f1; + *f++ = f2; + *f++ = f3; + *f++ = f4; + *f++ = f5; + *f = f6; + } + + void PutFloats(float f1, float f2, float f3, float f4, float f5, float f6, + float f7) { + assert(!finalized_); + int s = static_cast(fvals_.size()); + fvals_.resize(fvals_.size() + 7); + float* f = &(fvals_[s]); + *f++ = f1; + *f++ = f2; + *f++ = f3; + *f++ = f4; + *f++ = f5; + *f++ = f6; + *f = f7; + } + + void PutFloats(float f1, float f2, float f3, float f4, float f5, float f6, + float f7, float f8) { + assert(!finalized_); + int s = static_cast(fvals_.size()); + fvals_.resize(fvals_.size() + 8); + float* f = &(fvals_[s]); + *f++ = f1; + *f++ = f2; + *f++ = f3; + *f++ = f4; + *f++ = f5; + *f++ = f6; + *f++ = f7; + *f = f8; + } + + void PutFloats(float f1, float f2, float f3, float f4, float f5, float f6, + float f7, float f8, float f9) { + assert(!finalized_); + int s = static_cast(fvals_.size()); + fvals_.resize(fvals_.size() + 9); + float* f = &(fvals_[s]); + *f++ = f1; + *f++ = f2; + *f++ = f3; + *f++ = f4; + *f++ = f5; + *f++ = f6; + *f++ = f7; + *f++ = f8; + *f = f9; + } + + void PutFloats(float f1, float f2, float f3, float f4, float f5, float f6, + float f7, float f8, float f9, float f10) { + assert(!finalized_); + int s = static_cast(fvals_.size()); + fvals_.resize(fvals_.size() + 10); + float* f = &(fvals_[s]); + *f++ = f1; + *f++ = f2; + *f++ = f3; + *f++ = f4; + *f++ = f5; + *f++ = f6; + *f++ = f7; + *f++ = f8; + *f++ = f9; + *f = f10; + } + + void PutFloats(float f1, float f2, float f3, float f4, float f5, float f6, + float f7, float f8, float f9, float f10, float f11, + float f12) { + assert(!finalized_); + int s = static_cast(fvals_.size()); + fvals_.resize(fvals_.size() + 12); + float* f = &(fvals_[s]); + *f++ = f1; + *f++ = f2; + *f++ = f3; + *f++ = f4; + *f++ = f5; + *f++ = f6; + *f++ = f7; + *f++ = f8; + *f++ = f9; + *f++ = f10; + *f++ = f11; + *f = f12; + } + + void PutFloats(float f1, float f2, float f3, float f4, float f5, float f6, + float f7, float f8, float f9, float f10, float f11, float f12, + float f13, float f14, float f15) { + assert(!finalized_); + int s = static_cast(fvals_.size()); + fvals_.resize(fvals_.size() + 15); + float* f = &(fvals_[s]); + *f++ = f1; + *f++ = f2; + *f++ = f3; + *f++ = f4; + *f++ = f5; + *f++ = f6; + *f++ = f7; + *f++ = f8; + *f++ = f9; + *f++ = f10; + *f++ = f11; + *f++ = f12; + *f++ = f13; + *f++ = f14; + *f = f15; + } + + void PutFloatArray16(const float* f_in) { + assert(!finalized_); + int s = static_cast(fvals_.size()); + fvals_.resize(fvals_.size() + 16); + float* f = &(fvals_[s]); + memcpy(f, f_in, sizeof(float) * 16); + } + + void PutMatrices(const std::vector& mats) { + assert(!finalized_); + static_assert(sizeof(Matrix44f[2]) == sizeof(float[16][2])); + ivals_.push_back(static_cast(mats.size())); + int s = static_cast(fvals_.size()); + int f_count = static_cast(16 * mats.size()); + fvals_.resize(fvals_.size() + f_count); + float* f = &(fvals_[s]); + const float* f_in = mats[0].m; + memcpy(f, f_in, sizeof(float) * f_count); + } + + void PutInt(int val) { + assert(!finalized_); + ivals_.push_back(val); + } + + void PutModel(ModelData* model) { + assert(frame_def_); + assert(!finalized_); + frame_def_->AddComponent(Object::Ref(model)); + models_.push_back(model); + } + + void PutTexture(TextureData* texture) { + assert(frame_def_); + assert(!finalized_); + frame_def_->AddComponent(Object::Ref(texture)); + textures_.push_back(texture); + } + + void PutTexture(const Object::Ref& texture) { + assert(texture.exists()); + PutTexture(texture.get()); + } + + void PutCubeMapTexture(TextureData* texture) { + assert(frame_def_); + assert(!finalized_); + frame_def_->AddComponent(Object::Ref(texture)); + textures_.push_back(texture); + } + + void PutMeshData(MeshData* mesh_data) { + assert(!finalized_); + mesh_datas_.push_back(mesh_data); + } + + // Return next item. + auto GetCommand() -> Command { + assert(finalized_); + assert(commands_index_ <= commands_.size()); + return (commands_index_ == commands_.size()) ? Command::kEnd + : commands_[commands_index_++]; + } + + auto GetInt() -> int { + assert(finalized_); + assert(ivals_index_ < ivals_.size()); + return ivals_[ivals_index_++]; + } + + auto GetFloat() -> float { + assert(finalized_); + assert(fvals_index_ < fvals_.size()); + return fvals_[fvals_index_++]; + } + + void GetFloats(float* f1, float* f2) { + assert(finalized_); + assert(fvals_index_ + 2 <= fvals_.size()); + *f1 = fvals_[fvals_index_++]; + *f2 = fvals_[fvals_index_++]; + } + + void GetFloats(float* f1, float* f2, float* f3) { + assert(finalized_); + assert(fvals_index_ + 3 <= fvals_.size()); + *f1 = fvals_[fvals_index_++]; + *f2 = fvals_[fvals_index_++]; + *f3 = fvals_[fvals_index_++]; + } + + void GetFloats(float* f1, float* f2, float* f3, float* f4) { + assert(finalized_); + assert(fvals_index_ + 4 <= fvals_.size()); + *f1 = fvals_[fvals_index_++]; + *f2 = fvals_[fvals_index_++]; + *f3 = fvals_[fvals_index_++]; + *f4 = fvals_[fvals_index_++]; + } + + void GetFloats(float* f1, float* f2, float* f3, float* f4, float* f5) { + assert(finalized_); + assert(fvals_index_ + 5 <= fvals_.size()); + *f1 = fvals_[fvals_index_++]; + *f2 = fvals_[fvals_index_++]; + *f3 = fvals_[fvals_index_++]; + *f4 = fvals_[fvals_index_++]; + *f5 = fvals_[fvals_index_++]; + } + + void GetFloats(float* f1, float* f2, float* f3, float* f4, float* f5, + float* f6) { + assert(finalized_); + assert(fvals_index_ + 6 <= fvals_.size()); + *f1 = fvals_[fvals_index_++]; + *f2 = fvals_[fvals_index_++]; + *f3 = fvals_[fvals_index_++]; + *f4 = fvals_[fvals_index_++]; + *f5 = fvals_[fvals_index_++]; + *f6 = fvals_[fvals_index_++]; + } + + void GetFloats(float* f1, float* f2, float* f3, float* f4, float* f5, + float* f6, float* f7) { + assert(finalized_); + assert(fvals_index_ + 7 <= fvals_.size()); + *f1 = fvals_[fvals_index_++]; + *f2 = fvals_[fvals_index_++]; + *f3 = fvals_[fvals_index_++]; + *f4 = fvals_[fvals_index_++]; + *f5 = fvals_[fvals_index_++]; + *f6 = fvals_[fvals_index_++]; + *f7 = fvals_[fvals_index_++]; + } + + void GetFloats(float* f1, float* f2, float* f3, float* f4, float* f5, + float* f6, float* f7, float* f8) { + assert(finalized_); + assert(fvals_index_ + 8 <= fvals_.size()); + *f1 = fvals_[fvals_index_++]; + *f2 = fvals_[fvals_index_++]; + *f3 = fvals_[fvals_index_++]; + *f4 = fvals_[fvals_index_++]; + *f5 = fvals_[fvals_index_++]; + *f6 = fvals_[fvals_index_++]; + *f7 = fvals_[fvals_index_++]; + *f8 = fvals_[fvals_index_++]; + } + + void GetFloats(float* f1, float* f2, float* f3, float* f4, float* f5, + float* f6, float* f7, float* f8, float* f9) { + assert(finalized_); + assert(fvals_index_ + 9 <= fvals_.size()); + *f1 = fvals_[fvals_index_++]; + *f2 = fvals_[fvals_index_++]; + *f3 = fvals_[fvals_index_++]; + *f4 = fvals_[fvals_index_++]; + *f5 = fvals_[fvals_index_++]; + *f6 = fvals_[fvals_index_++]; + *f7 = fvals_[fvals_index_++]; + *f8 = fvals_[fvals_index_++]; + *f9 = fvals_[fvals_index_++]; + } + + void GetFloats(float* f1, float* f2, float* f3, float* f4, float* f5, + float* f6, float* f7, float* f8, float* f9, float* f10) { + assert(finalized_); + assert(fvals_index_ + 10 <= fvals_.size()); + *f1 = fvals_[fvals_index_++]; + *f2 = fvals_[fvals_index_++]; + *f3 = fvals_[fvals_index_++]; + *f4 = fvals_[fvals_index_++]; + *f5 = fvals_[fvals_index_++]; + *f6 = fvals_[fvals_index_++]; + *f7 = fvals_[fvals_index_++]; + *f8 = fvals_[fvals_index_++]; + *f9 = fvals_[fvals_index_++]; + *f10 = fvals_[fvals_index_++]; + } + + void GetFloats(float* f1, float* f2, float* f3, float* f4, float* f5, + float* f6, float* f7, float* f8, float* f9, float* f10, + float* f11, float* f12) { + assert(finalized_); + assert(fvals_index_ + 12 <= fvals_.size()); + *f1 = fvals_[fvals_index_++]; + *f2 = fvals_[fvals_index_++]; + *f3 = fvals_[fvals_index_++]; + *f4 = fvals_[fvals_index_++]; + *f5 = fvals_[fvals_index_++]; + *f6 = fvals_[fvals_index_++]; + *f7 = fvals_[fvals_index_++]; + *f8 = fvals_[fvals_index_++]; + *f9 = fvals_[fvals_index_++]; + *f10 = fvals_[fvals_index_++]; + *f11 = fvals_[fvals_index_++]; + *f12 = fvals_[fvals_index_++]; + } + + void GetFloats(float* f1, float* f2, float* f3, float* f4, float* f5, + float* f6, float* f7, float* f8, float* f9, float* f10, + float* f11, float* f12, float* f13, float* f14, float* f15) { + assert(finalized_); + assert(fvals_index_ + 15 <= fvals_.size()); + *f1 = fvals_[fvals_index_++]; + *f2 = fvals_[fvals_index_++]; + *f3 = fvals_[fvals_index_++]; + *f4 = fvals_[fvals_index_++]; + *f5 = fvals_[fvals_index_++]; + *f6 = fvals_[fvals_index_++]; + *f7 = fvals_[fvals_index_++]; + *f8 = fvals_[fvals_index_++]; + *f9 = fvals_[fvals_index_++]; + *f10 = fvals_[fvals_index_++]; + *f11 = fvals_[fvals_index_++]; + *f12 = fvals_[fvals_index_++]; + *f13 = fvals_[fvals_index_++]; + *f14 = fvals_[fvals_index_++]; + *f15 = fvals_[fvals_index_++]; + } + + auto GetMatrix() -> Matrix44f* { + assert(finalized_); + assert(fvals_index_ + 16 <= fvals_.size()); + static_assert(sizeof(Matrix44f) == sizeof(float[16])); + auto* m = reinterpret_cast(&fvals_[fvals_index_]); + fvals_index_ += 16; + return m; + } + + auto GetMatrices(int* count) -> Matrix44f* { + assert(finalized_); + assert(ivals_index_ < ivals_.size()); + *count = ivals_[ivals_index_++]; + int f_count = 16 * (*count); + assert(fvals_index_ + f_count <= fvals_.size()); + static_assert(sizeof(Matrix44f[2]) == sizeof(float[16][2])); + auto* m = reinterpret_cast(&fvals_[fvals_index_]); + fvals_index_ += f_count; + return m; + } + + auto GetModel() -> const ModelData* { + assert(finalized_); + assert(models_index_ < models_.size()); + return models_[models_index_++]; + } + + template + auto GetMeshRendererData() -> T* { + assert(finalized_); + assert(mesh_datas_index_ < mesh_datas_.size()); + T* m; + m = static_cast(mesh_datas_[mesh_datas_index_]->renderer_data()); + assert(m); + assert( + m == dynamic_cast(mesh_datas_[mesh_datas_index_]->renderer_data())); + mesh_datas_index_++; + return m; + } + + auto GetTexture() -> const TextureData* { + assert(finalized_); + assert(textures_index_ < textures_.size()); + return textures_[textures_index_++]; + } + + void Reset() { + commands_.resize(0); + fvals_.resize(0); + ivals_.resize(0); + models_.resize(0); + textures_.resize(0); + mesh_datas_.resize(0); + finalized_ = false; + } + + // Call once done writing to buffer. + void Finalize() { + assert(!finalized_); + finalized_ = true; + } + + // Set up iterators to read back data. + void ReadBegin() { + assert(finalized_); + commands_index_ = 0; + fvals_index_ = 0; + ivals_index_ = 0; + models_index_ = 0; + textures_index_ = 0; + mesh_datas_index_ = 0; + } + auto has_draw_commands() const -> bool { + for (auto& command : commands_) { + switch (command) { + case Command::kDrawModel: + case Command::kDrawModelInstanced: + case Command::kDrawMesh: + case Command::kDrawScreenQuad: + return true; + default: + break; + } + } + return false; + } + + // Sanity check: Makes sure all buffer iterators are at their end. + auto IsEmpty() -> bool { + return ( + (commands_index_ == commands_.size()) && (fvals_index_ == fvals_.size()) + && (ivals_index_ == ivals_.size()) && (models_index_ == models_.size()) + && (textures_index_ == textures_.size()) + && (mesh_datas_index_ == mesh_datas_.size())); + } + + auto frame_def() const -> FrameDef* { + assert(frame_def_); + return frame_def_; + } + + void set_frame_def(FrameDef* f) { frame_def_ = f; } + + private: + std::vector commands_; + std::vector fvals_; + std::vector ivals_; + std::vector models_{}; + std::vector textures_{}; + std::vector mesh_datas_{}; + unsigned int commands_index_{}; + unsigned int fvals_index_{}; + unsigned int ivals_index_{}; + unsigned int models_index_{}; + unsigned int textures_index_{}; + unsigned int mesh_datas_index_{}; + bool finalized_{}; + FrameDef* frame_def_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_RENDER_COMMAND_BUFFER_H_ diff --git a/src/ballistica/graphics/render_pass.cc b/src/ballistica/graphics/render_pass.cc new file mode 100644 index 00000000..b5ace645 --- /dev/null +++ b/src/ballistica/graphics/render_pass.cc @@ -0,0 +1,507 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/render_pass.h" + +#include +#include + +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/graphics/renderer.h" + +// Turn this off to not draw any transparent stuff. +#define DRAW_TRANSPARENT 1 + +namespace ballistica { + +const float kCamNearClip = 4.0f; +const float kCamFarClip = 1000.0f; + +RenderPass::RenderPass(RenderPass::Type type_in, FrameDef* frame_def_in) + : type_(type_in), frame_def_(frame_def_in) { + // Create/init our command buffers. + if (UsesWorldLists()) { + for (auto& command : commands_) { + command = std::make_unique(); + + // FIXME: Could just pass in constructor? + command->set_frame_def(frame_def_); + } + } else { + commands_flat_transparent_ = std::make_unique(); + commands_flat_transparent_->set_frame_def(frame_def_); + commands_flat_ = std::make_unique(); + + // FIXME: Could just pass in constructor? + commands_flat_->set_frame_def(frame_def_); + } +} + +RenderPass::~RenderPass() = default; + +void RenderPass::Render(RenderTarget* render_target, bool transparent) { + assert(InGraphicsThread()); + + if (explicit_bool(!DRAW_TRANSPARENT) && transparent) { + return; + } +#undef DRAW_TRANSPRENT + + Renderer* renderer = g_graphics_server->renderer(); + + // Set up camera & depth. + switch (type()) { + case Type::kBeautyPass: { + g_graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); + // If this changes, make sure to change + // it before _drawCameraBuffer() too. + + // FIXME: + // If we're drawing our cam into its own buffer we could technically + // use its full depth range ...otherwise we need to share with the + // other onscreen elements (but maybe its good to use the limited + // range regardless to make sure we can) + renderer->SetDepthRange(kBackingDepth3, kBackingDepth4); + SetFrustum(cam_near_clip_, cam_far_clip_); + + tex_project_matrix_ = g_graphics_server->GetModelViewProjectionMatrix(); + model_view_matrix_ = g_graphics_server->model_view_matrix(); + model_view_projection_matrix_ = + g_graphics_server->GetModelViewProjectionMatrix(); + + // Store our matrix to get things in screen space. + tex_project_matrix_ *= Matrix44fScale(0.5f); + tex_project_matrix_ *= Matrix44fTranslate(0.5f, 0.5f, 0); + break; + } + case Type::kOverlay3DPass: { + g_graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); + + // If we drew the world directly to the screen we need to use a depth + // range that lies fully in front of that range so we don't get obscured + // by any of the world. + + // However if we drew the world to an offscreen buffer this isn't a + // problem; nothing exists in that range. In that case lets draw to the + // same range so we can do easy depth comparisons to the offscreen world's + // depth (for overlay fog, blurs, etc) + + // Use same region as world. + if (renderer->has_camera_render_target()) { + // Use beauty-pass depth region + renderer->SetDepthRange(kBackingDepth3, kBackingDepth4); + } else { + // Use region in front of world + renderer->SetDepthRange(kBackingDepth2, kBackingDepth3); + } + SetFrustum(cam_near_clip_, cam_far_clip_); + break; + } + case Type::kVRCoverPass: { + g_graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); + + // We use the front depth range where the overlays would + // live in the non-vr path. + renderer->SetDepthRange(kBackingDepth1, kBackingDepth2); + SetFrustum(cam_near_clip_, cam_far_clip_); + break; + } + case Type::kBlitPass: { + g_graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); + + // We render into a little sliver of the depth buffer in the + // back just in front of the backing blit. + assert(renderer->has_camera_render_target()); + renderer->SetDepthRange(kBackingDepth4, kBackingDepth5); + SetFrustum(cam_near_clip_, cam_far_clip_); + break; + } + case Type::kBeautyPassBG: { + g_graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); + renderer->SetDepthRange(kBackingDepth3, kBackingDepth4); + SetFrustum(cam_near_clip_, cam_far_clip_); + break; + } + case Type::kOverlayPass: + case Type::kOverlayFrontPass: + case Type::kOverlayFixedPass: + case Type::kOverlayFlatPass: { + // In vr mode we draw the flat-overlay into its own buffer so can use + // the full depth range (shouldn't matter but why not?...) shouldn't. + if (IsVRMode()) { + // In vr mode, our overlay-flat pass is ortho-projected + // while our regular overlay is just rendered in world space using + // the vr-overlay matrix. + if (type() == Type::kOverlayFlatPass) { + g_graphics_server->ModelViewReset(); + renderer->SetDepthRange(0, 1); // we can use full depth range!! + float amt = 0.5f * kVRBorder; + float w = virtual_width(); + float h = virtual_height(); + g_graphics_server->SetOrthoProjection( + -amt * w, (1.0f + amt) * w, -amt * h, (1.0f + amt) * h, -1, 1); + } else { + g_graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); + // We set the same depth ranges as the overlay-3d pass since + // we're essentially doing the same thing. See explanation in the + // overlay-3d case above the one difference is that we split the + // range between our fixed overlay and our regular overlay passes + // (we want fixed-overlay stuff on bottom). + if (renderer->has_camera_render_target()) { + if (type() == Type::kOverlayFrontPass) { + renderer->SetDepthRange(kBackingDepth3, kBackingDepth3B); + } else if (type() == Type::kOverlayPass) { + renderer->SetDepthRange(kBackingDepth3B, kBackingDepth3C); + } else { + renderer->SetDepthRange(kBackingDepth3C, kBackingDepth4); + } + } else { + if (type() == Type::kOverlayFrontPass) { + renderer->SetDepthRange(kBackingDepth2, kBackingDepth2B); + } else if (type() == Type::kOverlayPass) { + renderer->SetDepthRange(kBackingDepth2B, kBackingDepth2C); + } else { + renderer->SetDepthRange(kBackingDepth2C, kBackingDepth3); + } + } + SetFrustum(cam_near_clip_, cam_far_clip_); + + // Now move to wherever our 2d plane in space is to start with. + if (type() == Type::kOverlayPass + || type() == Type::kOverlayFrontPass) { + g_graphics_server->MultMatrix( + frame_def()->vr_overlay_screen_matrix()); + } else { + assert(type() == Type::kOverlayFixedPass); + g_graphics_server->MultMatrix( + frame_def()->vr_overlay_screen_matrix_fixed()); + } + } + } else { + // Nn non-vr mode both our overlays are just ortho projected. + g_graphics_server->ModelViewReset(); + if (type() == Type::kOverlayFrontPass) { + renderer->SetDepthRange(kBackingDepth1, kBackingDepth1B); + } else { + renderer->SetDepthRange(kBackingDepth1B, kBackingDepth2); + } + if (g_graphics_server->tv_border()) { + float amt = 0.5f * kTVBorder; + float w = virtual_width(); + float h = virtual_height(); + g_graphics_server->SetOrthoProjection( + -amt * w, (1.0f + amt) * w, -amt * h, (1.0f + amt) * h, -1, 1); + } else { + g_graphics_server->SetOrthoProjection(0, virtual_width(), 0, + virtual_height(), -1, 1); + } + } + break; + } + case Type::kLightPass: + case Type::kLightShadowPass: { + // Ortho shadows. + if (renderer->shadow_ortho()) { + g_graphics_server->ModelViewReset(); + g_graphics_server->SetOrthoProjection(-12, 12, -12, 12, 10, 100); + g_graphics_server->Translate(Vector3f(0, 0, renderer->light_tz())); + g_graphics_server->Rotate(80, Vector3f(1.0f, 0, 0)); + const Vector3f& soffs = renderer->shadow_offset(); + g_graphics_server->Translate(Vector3f(-soffs.x, -soffs.y, -soffs.z)); + g_graphics_server->scale(Vector3f(1.0f / renderer->shadow_scale_x(), + 1.0f, + 1.0f / renderer->shadow_scale_z())); + } else { + float fovy = 45.0f * kPi / 180.0f; + float fovx = fovy; + float near_val = 10; + float far_val = 100; + float x = near_val * tanf(fovx); + float y = near_val * tanf(fovy); + + g_graphics_server->SetProjectionMatrix( + Matrix44fFrustum(-x, x, -y, y, near_val, far_val)); + g_graphics_server->ModelViewReset(); + g_graphics_server->Translate( + Vector3f(0.0f, 0.0f, renderer->light_tz())); + g_graphics_server->Rotate(renderer->light_pitch(), + Vector3f(1.0f, 0.0f, 0.0f)); + g_graphics_server->Rotate(renderer->light_heading(), + Vector3f(0.0f, 1.0f, 0.0f)); + const Vector3f& soffs = renderer->shadow_offset(); + + // Well, this is slightly terrifying; '-soffs' is causing crashes + // here but multing by -1.000001f works. + // (generally just on android 4.3 on atom processors) + g_graphics_server->Translate(Vector3f( + -1.000001f * soffs.x, -1.000001f * soffs.y, -1.000001f * soffs.z)); + } + + // ...now store the matrix we'll use to project this as a texture + // FIXME: most of these calculations could be cached instead of + // redoing them every pass + tex_project_matrix_ = g_graphics_server->GetModelViewProjectionMatrix(); + model_view_matrix_ = g_graphics_server->model_view_matrix(); + model_view_projection_matrix_ = + g_graphics_server->GetModelViewProjectionMatrix(); + tex_project_matrix_ *= Matrix44fScale(0.5f); + tex_project_matrix_ *= Matrix44fTranslate(0.5f, 0.5f, 0); + g_graphics_server->SetLightShadowProjectionMatrix(tex_project_matrix_); + + break; + } + default: + throw Exception(); + } + + // Some passes draw stuff into the world bucketed by type. + if (UsesWorldLists()) { + // For opaque stuff, render non-reflected(above-ground), + // then reflected(below-ground) stuff (less overdraw that way) + // for transparent stuff we do the opposite so we get better layering. + ReflectionSubPass reflection_sub_passes[2]; + if (transparent) { + reflection_sub_passes[0] = ReflectionSubPass::kMirrored; + reflection_sub_passes[1] = ReflectionSubPass::kRegular; + } else { + reflection_sub_passes[0] = ReflectionSubPass::kRegular; + reflection_sub_passes[1] = ReflectionSubPass::kMirrored; + } + + for (auto reflection_sub_pass : reflection_sub_passes) { + bool doing_reflection = false; + if (reflection_sub_pass == ReflectionSubPass::kMirrored) { + // Only actually draw reflection pass if quality >= high + // and floor-reflections are on. + if (floor_reflection() + && frame_def()->quality() >= GraphicsQuality::kHigher) { + doing_reflection = true; + renderer->set_drawing_reflection(true); + g_graphics_server->PushTransform(); + Matrix44f m = Matrix44fScale(Vector3f(1, -1, 1)); + g_graphics_server->MultMatrix(m); + renderer->FlipCullFace(); // Flip into reflection drawing. + } else { + continue; + } + } else { + renderer->set_drawing_reflection(false); + } + + // Render everything with the same material together to + // minimize gl state changes. + + // Organize shaders that are likely to be occluding other stuff first. + ShadingType component_types_opaque[] = { + ShadingType::kSimpleColor, + ShadingType::kSimpleTexture, + ShadingType::kSimpleTextureModulated, + ShadingType::kSimpleTextureModulatedColorized, + ShadingType::kSimpleTextureModulatedColorized2, + ShadingType::kSimpleTextureModulatedColorized2Masked, + ShadingType::kObjectReflectLightShadow, + ShadingType::kObjectLightShadow, + ShadingType::kObjectReflect, + ShadingType::kObject, + ShadingType::kObjectReflectLightShadowDoubleSided, + ShadingType::kObjectReflectLightShadowColorized, + ShadingType::kObjectReflectLightShadowColorized2, + ShadingType::kObjectReflectLightShadowAdd, + ShadingType::kObjectReflectLightShadowAddColorized, + ShadingType::kObjectReflectLightShadowAddColorized2}; + + ShadingType component_types_transparent[] = { + ShadingType::kSimpleColorTransparent, + ShadingType::kSimpleColorTransparentDoubleSided, + ShadingType::kObjectTransparent, + ShadingType::kObjectLightShadowTransparent, + ShadingType::kObjectReflectTransparent, + ShadingType::kObjectReflectAddTransparent, + ShadingType::kSimpleTextureModulatedTransparent, + ShadingType::kSimpleTextureModulatedTransFlatness, + ShadingType::kSimpleTextureModulatedTransparentDoubleSided, + ShadingType::kSimpleTextureModulatedTransparentColorized, + ShadingType::kSimpleTextureModulatedTransparentColorized2, + ShadingType::kSimpleTextureModulatedTransparentColorized2Masked, + ShadingType::kSimpleTextureModulatedTransparentShadow, + ShadingType::kSimpleTexModulatedTransShadowFlatness, + ShadingType::kSimpleTextureModulatedTransparentGlow, + ShadingType::kSimpleTextureModulatedTransparentGlowMaskUV2, + ShadingType::kSmoke, + ShadingType::kSprite}; + + ShadingType* component_types; + int component_type_count; + if (transparent) { + component_types = component_types_transparent; + component_type_count = + (sizeof(component_types_transparent) / sizeof(ShadingType)); + } else { + component_types = component_types_opaque; + component_type_count = + (sizeof(component_types_opaque) / sizeof(ShadingType)); + } + + for (int c = 0; c < component_type_count; c++) { + renderer->ProcessRenderCommandBuffer( + commands_[static_cast(component_types[c])].get(), *this, + render_target); + } + + if (doing_reflection) { + renderer->FlipCullFace(); // Flip out of reflection drawing. + g_graphics_server->PopTransform(); + } + } + renderer->set_drawing_reflection(false); + } else { + // ..and some passes draw flat lists in order added. + if (transparent) { + renderer->ProcessRenderCommandBuffer(commands_flat_transparent_.get(), + *this, render_target); + } else { + renderer->ProcessRenderCommandBuffer(commands_flat_.get(), *this, + render_target); + } + } +} + +void RenderPass::SetCamera( + const Vector3f& pos, const Vector3f& target, const Vector3f& up, + float near_clip_in, float far_clip_in, float fov_x_in, float fov_y_in, + bool use_fov_tangents, float fov_tan_l, float fov_tan_r, float fov_tan_b, + float fov_tan_t, const std::vector& area_of_interest_points) { + cam_pos_ = pos; + cam_target_ = target; + cam_up_ = up; + cam_near_clip_ = near_clip_in; + cam_far_clip_ = far_clip_in; + cam_use_fov_tangents_ = use_fov_tangents; + cam_fov_x_ = fov_x_in; + cam_fov_y_ = fov_y_in; + cam_fov_l_tan_ = fov_tan_l; + cam_fov_r_tan_ = fov_tan_r; + cam_fov_b_tan_ = fov_tan_b; + cam_fov_t_tan_ = fov_tan_t; + cam_area_of_interest_points_ = area_of_interest_points; +} + +void RenderPass::Reset() { + virtual_width_ = 0; + virtual_height_ = 0; + physical_width_ = 0; + physical_height_ = 0; + floor_reflection_ = false; + cam_pos_ = {0.0f, 0.0f, 0.0f}; + cam_target_ = {0.0f, 0.0f, 1.0f}; + cam_up_ = {0.0f, 1.0f, 0.0f}; + cam_near_clip_ = kCamNearClip; + cam_far_clip_ = kCamFarClip; + cam_fov_x_ = -1.0f; + cam_fov_y_ = 40.0f; + tex_project_matrix_ = kMatrix44fIdentity; + + Renderer* renderer = g_graphics_server->renderer(); + + // Figure our our width/height for drawing commands to reference + // (we cant wait until the drawing is actually occurring because + // that happens in another thread later) + switch (type()) { + case Type::kBeautyPass: + case Type::kBeautyPassBG: + case Type::kOverlay3DPass: + case Type::kOverlayPass: + case Type::kOverlayFrontPass: + case Type::kOverlayFlatPass: + case Type::kVRCoverPass: + case Type::kOverlayFixedPass: + case Type::kBlitPass: + physical_width_ = g_graphics->screen_pixel_width(); + physical_height_ = g_graphics->screen_pixel_height(); + break; + case Type::kLightPass: + physical_width_ = physical_height_ = + static_cast(renderer->shadow_res()) / kLightResDiv; + break; + case Type::kLightShadowPass: + physical_width_ = physical_height_ = + static_cast(renderer->shadow_res()); + break; + default: + throw Exception(); + } + + // By default, logical width matches physical width, but for overlay passes + // it can be independent. + switch (type()) { + case Type::kOverlayPass: + case Type::kOverlayFrontPass: + case Type::kOverlayFixedPass: + case Type::kOverlayFlatPass: + virtual_width_ = g_graphics->screen_virtual_width(); + virtual_height_ = g_graphics->screen_virtual_height(); + break; + default: + virtual_width_ = physical_width_; + virtual_height_ = physical_height_; + break; + } + + // Clear the command buffers this pass cares about. + if (UsesWorldLists()) { + for (auto& command : commands_) { + command->Reset(); + } + } else { + commands_flat_->Reset(); + commands_flat_transparent_->Reset(); + } +} + +void RenderPass::SetFrustum(float near_val, float far_val) { + assert(InGraphicsThread()); + // If we're using fov-tangents: + if (cam_use_fov_tangents_) { + float l = near_val * cam_fov_l_tan_; + float r = near_val * cam_fov_r_tan_; + float t = near_val * cam_fov_t_tan_; + float b = near_val * cam_fov_b_tan_; + projection_matrix_ = Matrix44fFrustum(-l, r, -b, t, near_val, far_val); + } else { + // Old angle-based stuff: + float x; + float angleY = (cam_fov_y_ / 2.0f) * kPi / 180.0f; + float y = near_val * tanf(angleY); + + // Fov-x < 0 implies to use aspect ratio. + if (cam_fov_x_ > 0.0f) { + float angleX = (cam_fov_x_ / 2.0f) * kPi / 180.0f; + x = near_val * tanf(angleX); + } else { + x = y * GetPhysicalAspectRatio(); + } + projection_matrix_ = Matrix44fFrustum(-x, x, -y, y, near_val, far_val); + } + g_graphics_server->SetProjectionMatrix(projection_matrix_); +} + +void RenderPass::Finalize() { + if (UsesWorldLists()) { + for (auto& command : commands_) { + command->Finalize(); + } + } else { + commands_flat_->Finalize(); + commands_flat_transparent_->Finalize(); + } +} + +auto RenderPass::HasDrawCommands() const -> bool { + if (UsesWorldLists()) { + throw Exception(); + } else { + return (commands_flat_transparent_->has_draw_commands() + || commands_flat_->has_draw_commands()); + } +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/render_pass.h b/src/ballistica/graphics/render_pass.h new file mode 100644 index 00000000..6c794519 --- /dev/null +++ b/src/ballistica/graphics/render_pass.h @@ -0,0 +1,167 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_RENDER_PASS_H_ +#define BALLISTICA_GRAPHICS_RENDER_PASS_H_ + +#include +#include + +#include "ballistica/math/matrix44f.h" + +namespace ballistica { + +// A drawing context for one pass. This can be a render to the screen, a shadow +// pass, a window, etc. +class RenderPass { + public: + enum class ReflectionSubPass { kRegular, kMirrored }; + enum class Type { + kLightShadowPass, + kLightPass, + kBeautyPass, + kBeautyPassBG, + kBlitPass, + // Standard 2d overlay stuff. May be drawn in 2d or on a plane in 3d + // space (in vr). In VR, each of these elements are drawn individually + // and can thus have their own depth. also in VR this overlay repositions + // itself per level; use kOverlayFixedPass for items that shouldn't. + // this overlay may be obscured by UI. Use OVERLAY_FRONT_PASS if you need + // things to show up in front of UI. + kOverlayPass, + // Just like kOverlayPass but guaranteed to draw in front of UI. + kOverlayFrontPass, + // Actually drawn in regular 3d space - for life bars, names, etc that + // need to overlay regular 3d stuff but exist in the world. + kOverlay3DPass, + // Only used in VR - overlay stuff drawn into a flat 2d texture so that + // scissoring/etc works (the UI uses this). + kOverlayFlatPass, + /// Only used in VR - stuff that needs to cover absolutely everything + /// else (like the 3d wipe fade). + kVRCoverPass, + // Only used in VR - overlay elements that should always be fixed in space. + kOverlayFixedPass + }; + + RenderPass(Type type_in, FrameDef* frame_def); + virtual ~RenderPass(); + + auto type() const -> Type { return type_; } + + // The physical size of the drawing surface. + auto physical_width() const -> float { return physical_width_; } + auto physical_height() const -> float { return physical_height_; } + + // The virtual size of the drawing surface. + // This may or may not have anything to do with the physical size + // (for instance the overlay pass in VR has its own bounds which + // is completely independent of the physical surface it gets drawn into). + auto virtual_width() const -> float { return virtual_width_; } + auto virtual_height() const -> float { return virtual_height_; } + + // Should objects be rendered 'underground' in this pass? + auto floor_reflection() const -> bool { return floor_reflection_; } + void set_floor_reflection(bool val) { floor_reflection_ = val; } + auto GetPhysicalAspectRatio() const -> float { + return physical_width() / physical_height(); + } + void SetCamera(const Vector3f& pos, const Vector3f& target, + const Vector3f& up, float near_clip, float far_clip, + float fov_x, // Set to -1 for auto. + float fov_y, bool use_fov_tangents, float fov_tan_l, + float fov_tan_r, float fov_tan_b, float fov_tan_t, + const std::vector& area_of_interest_points); + auto frame_def() const -> FrameDef* { return frame_def_; } + void Render(RenderTarget* t, bool transparent); + auto tex_project_matrix() const -> const Matrix44f& { + return tex_project_matrix_; + } + auto projection_matrix() const -> const Matrix44f& { + return projection_matrix_; + } + auto model_view_matrix() const -> const Matrix44f& { + return model_view_matrix_; + } + auto model_view_projection_matrix() const -> const Matrix44f& { + return model_view_projection_matrix_; + } + auto HasDrawCommands() const -> bool; + void Finalize(); + void Reset(); + + // Whether this pass draws stuff from the per-shader command lists + auto UsesWorldLists() const -> bool { + switch (type()) { + case Type::kBeautyPass: + case Type::kBeautyPassBG: + return true; + case Type::kOverlayPass: + case Type::kOverlayFrontPass: + case Type::kOverlay3DPass: + case Type::kVRCoverPass: + case Type::kOverlayFlatPass: + case Type::kOverlayFixedPass: + case Type::kBlitPass: + case Type::kLightPass: + case Type::kLightShadowPass: + return false; + default: + throw Exception(); + } + } + auto commands_flat() const -> RenderCommandBuffer* { + return commands_flat_.get(); + } + auto commands_flat_transparent() const -> RenderCommandBuffer* { + return commands_flat_transparent_.get(); + } + auto GetCommands(ShadingType type) const -> RenderCommandBuffer* { + return commands_[static_cast(type)].get(); + } + + auto cam_area_of_interest_points() const -> const std::vector& { + return cam_area_of_interest_points_; + } + + private: + void SetFrustum(float near_val, float far_val); + + // Our pass holds sets of draw-commands bucketed by section and + // component-type. + std::unique_ptr + commands_[static_cast(ShadingType::kCount)]; + std::unique_ptr commands_flat_; + std::unique_ptr commands_flat_transparent_; + Vector3f cam_pos_{0.0f, 0.0f, 0.0f}; + Vector3f cam_target_{0.0f, 0.0f, 0.0f}; + Vector3f cam_up_{0.0f, 0.0f, 0.0f}; + float cam_near_clip_{}; + float cam_far_clip_{}; + float cam_fov_x_{}; + float cam_fov_y_{}; + + // We can now alternately supply left, right, top, bottom frustum tangents. + bool cam_use_fov_tangents_{}; + float cam_fov_l_tan_{1.0f}; + float cam_fov_r_tan_{1.0f}; + float cam_fov_t_tan_{1.0f}; + float cam_fov_b_tan_{1.0f}; + std::vector cam_area_of_interest_points_; + Type type_{}; + + // For lights/shadows. + Matrix44f tex_project_matrix_{kMatrix44fIdentity}; + Matrix44f projection_matrix_{kMatrix44fIdentity}; + Matrix44f model_view_matrix_{kMatrix44fIdentity}; + Matrix44f model_view_projection_matrix_{kMatrix44fIdentity}; + bool floor_reflection_{}; + FrameDef* frame_def_{}; + float physical_width_{}; + float physical_height_{}; + float virtual_width_{}; + float virtual_height_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_RENDER_PASS_H_ diff --git a/src/ballistica/graphics/render_target.cc b/src/ballistica/graphics/render_target.cc new file mode 100644 index 00000000..cdbb828c --- /dev/null +++ b/src/ballistica/graphics/render_target.cc @@ -0,0 +1,84 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/render_target.h" + +#include "ballistica/graphics/graphics_server.h" + +namespace ballistica { + +RenderTarget::RenderTarget(Type type) : type_(type) { + assert(InGraphicsThread()); +} + +RenderTarget::~RenderTarget() = default; + +void RenderTarget::ScreenSizeChanged() { + assert(type_ == Type::kScreen); + physical_width_ = g_graphics_server->screen_pixel_width(); + physical_height_ = g_graphics_server->screen_pixel_height(); +} + +auto RenderTarget::GetScissorX(float x) const -> float { + if (IsVRMode()) { + // map -0.05f to 1.1f in logical coordinates to 0 to 1 physical ones + float res_x_virtual = g_graphics_server->screen_virtual_width(); + return physical_width_ + * (((x / res_x_virtual) + (kVRBorder * 0.5f)) / (1.0f + kVRBorder)); + } else { + if (g_graphics_server->tv_border()) { + // map -0.05f to 1.1f in logical coordinates to 0 to 1 physical ones + float res_x_virtual = g_graphics_server->screen_virtual_width(); + return physical_width_ + * (((x / res_x_virtual) + (kTVBorder * 0.5f)) + / (1.0f + kTVBorder)); + } else { + return (physical_width_ / g_graphics_server->screen_virtual_width()) * x; + } + } +} +auto RenderTarget::GetScissorY(float y) const -> float { + if (IsVRMode()) { + // map -0.05f to 1.1f in logical coordinates to 0 to 1 physical ones + float res_y_virtual = g_graphics_server->screen_virtual_height(); + return physical_height_ + * (((y / res_y_virtual) + (kVRBorder * 0.5f)) / (1.0f + kVRBorder)); + } else { + if (g_graphics_server->tv_border()) { + // map -0.05f to 1.1f in logical coordinates to 0 to 1 physical ones + float res_y_virtual = g_graphics_server->screen_virtual_height(); + return physical_height_ + * (((y / res_y_virtual) + (kTVBorder * 0.5f)) + / (1.0f + kTVBorder)); + } else { + return (physical_height_ / g_graphics_server->screen_virtual_height()) + * y; + } + } +} +auto RenderTarget::GetScissorScaleX() const -> float { + if (IsVRMode()) { + float f = physical_width_ / g_graphics_server->screen_virtual_width(); + return f / (1.0f + kVRBorder); + } else { + float f = physical_width_ / g_graphics_server->screen_virtual_width(); + if (g_graphics_server->tv_border()) { + return f / (1.0f + kTVBorder); + } + return f; + } +} + +auto RenderTarget::GetScissorScaleY() const -> float { + if (IsVRMode()) { + float f = physical_height_ / g_graphics_server->screen_virtual_height(); + return f / (1.0f + kVRBorder); + } else { + float f = physical_height_ / g_graphics_server->screen_virtual_height(); + if (g_graphics_server->tv_border()) { + return f / (1.0f + kTVBorder); + } + return f; + } +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/render_target.h b/src/ballistica/graphics/render_target.h new file mode 100644 index 00000000..ef88585e --- /dev/null +++ b/src/ballistica/graphics/render_target.h @@ -0,0 +1,47 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_RENDER_TARGET_H_ +#define BALLISTICA_GRAPHICS_RENDER_TARGET_H_ + +#include "ballistica/core/object.h" +#include "ballistica/math/vector4f.h" + +namespace ballistica { + +// Encapsulates framebuffers, main windows, etc. +class RenderTarget : public Object { + public: + auto GetDefaultOwnerThread() const -> ThreadIdentifier override { + return ThreadIdentifier::kMain; + } + enum class Type { kScreen, kFramebuffer }; + explicit RenderTarget(Type type); + ~RenderTarget() override; + + // Clear depth, color, etc and get set to draw. + virtual void DrawBegin(bool clear, float clear_r, float clear_g, + float clear_b, float clear_a) = 0; + void DrawBegin(bool clear, + const Vector4f& clear_color = {0.0f, 0.0f, 0.0f, 1.0f}) { + DrawBegin(clear, clear_color.x, clear_color.y, clear_color.z, + clear_color.w); + } + + void ScreenSizeChanged(); + auto physical_width() const -> float { return physical_width_; } + auto physical_height() const -> float { return physical_height_; } + auto GetScissorScaleX() const -> float; + auto GetScissorScaleY() const -> float; + auto GetScissorX(float x) const -> float; + auto GetScissorY(float y) const -> float; + + protected: + float physical_width_{}; + float physical_height_{}; + bool depth_{}; + Type type_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_RENDER_TARGET_H_ diff --git a/src/ballistica/graphics/renderer.cc b/src/ballistica/graphics/renderer.cc new file mode 100644 index 00000000..ac5c7e14 --- /dev/null +++ b/src/ballistica/graphics/renderer.cc @@ -0,0 +1,850 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/renderer.h" + +#include +#include + +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/graphics/text/text_packer.h" +#include "ballistica/graphics/vr_graphics.h" + +// FIXME: Clear out conditional stuff. +#if BA_OSTYPE_MACOS && BA_SDL_BUILD && !BA_SDL2_BUILD +#include "ballistica/platform/min_sdl.h" +#endif + +#if BA_VR_BUILD +#include "ballistica/app/app_globals.h" +#endif + +namespace ballistica { + +#if BA_VR_BUILD +const float kBaseVRWorldScale = 1.38f; +const float kInvVRHeadScale = 1.0f / (kBaseVRWorldScale * kDefaultVRHeadScale); +#endif + +// There can be only one!.. at a time. +static bool have_renderer = false; + +Renderer::Renderer() { + assert(!have_renderer); + have_renderer = true; +} + +Renderer::~Renderer() { + assert(have_renderer); + have_renderer = false; +} + +void Renderer::PreprocessFrameDef(FrameDef* frame_def) { + assert(InGraphicsThread()); + + // If this frame_def was made in a different quality mode than we're + // currently in, don't try to render it. + if (frame_def->quality() != g_graphics_server->quality()) { + frame_def->set_rendering(false); + return; + } + frame_def->set_rendering(true); + + // Some VR environments muck with render states before/after + // they call us; resync as needed.... +#if BA_VR_BUILD + if (IsVRMode()) { + VRSyncRenderStates(); + } +#endif // BA_VR_BUILD + + // Setup various high level stuff to match the frame_def + // (tint colors, resolutions, etc). + UpdateSizesQualitiesAndColors(frame_def); + + // Handle a weird gamma reset issue on our legacy mac build (SDL 1.2). +#if BA_OSTYPE_MACOS && BA_SDL_BUILD && !BA_SDL2_BUILD + HandleFunkyMacGammaIssue(frame_def); +#endif + + // In some cases we draw to a lower-res backing buffer instead of native + // screen res. + UpdatePixelScaleAndBackingBuffer(frame_def); + + // Update the buffers for world drawing, blurred versions of that, etc. + UpdateCameraRenderTargets(frame_def); + + // (re)create our light/shadow buffers if need be + UpdateLightAndShadowBuffers(frame_def); + + // Update various VR values such as clip planes and head positions. +#if BA_VR_BUILD + VRPreprocess(frame_def); +#endif // BA_VR_BUILD + + // Pull latest mesh data in from this frame_def. + UpdateMeshes(frame_def->meshes(), frame_def->mesh_index_sizes(), + frame_def->mesh_buffers()); + + // Ensure all media used by this frame_def is loaded. + LoadMedia(frame_def); + + // Draw our light/shadow textures. + RenderLightAndShadowPasses(frame_def); + + // In vr mode we draw our UI into a buffer. +#if BA_VR_BUILD + VRDrawOverlayFlatPass(frame_def); +#endif // BA_VR_BUILD +} + +// actually render one of these frame_def suckers... +// (called within the graphics thread) +void Renderer::RenderFrameDef(FrameDef* frame_def) { + assert(InGraphicsThread()); + + // If preprocess decided not to render this. + if (!frame_def->rendering()) return; + + // Set camera/hand/etc positioning with latest VR data if applicable. + // (we do this here at render time as opposed to frame construction time + // so we have the most up-to-date data possible). +#if BA_VR_BUILD + VRUpdateForEyeRender(frame_def); +#endif // BA_VR_BUILD + + // In higher-quality modes we draw the world into the camera buffer + // which we'll later render into the backing buffer with depth-of-field + // and other stuff added. + if (camera_render_target_.exists()) { + DrawWorldToCameraBuffer(frame_def); + } + + // ..now draw everything into our backing target; either our camera + // buffer (high qual modes) or the world (med/low qual). + PushGroupMarker("Backing Opaque Pass"); + SetDepthWriting(true); + SetDepthTesting(true); + RenderTarget* backing; + if (backing_render_target_.exists()) { + backing = backing_render_target(); + } else { + backing = screen_render_target(); + } + + bool backing_needs_clear = frame_def->needs_clear(); +#if BA_CARDBOARD_BUILD + // On cardboard, our two eyes are drawn into the same FBO, + // so we can't invalidate the buffer when drawing our second eye + // (since that could wipe out the first eye which has already been drawn) + // ..so for the second eye we force a clear, which nicely stays within the + // already-set-up scissor-rect + if (vr_eye_ == 1) { + backing_needs_clear = true; + } +#endif + backing->DrawBegin(backing_needs_clear); + + bool overlays_in_3d = IsVRMode(); + bool overlays_in_2d = !overlays_in_3d; + + // Draw opaque stuff front-to-back. + if (overlays_in_2d) { + frame_def->overlay_front_pass()->Render(backing, false); + frame_def->overlay_pass()->Render(backing, false); + } + + // In vr mode, the front section of the depth buffer that would have been + // used for our 2d ortho overlays is instead used for our vr-fade pass, + // which is nothing but our little bomb shaped transition wipe thing + // (it needs its own depth section otherwise it intersects with stuff out in + // the world). + if (overlays_in_3d) { + frame_def->vr_cover_pass()->Render(backing, false); + frame_def->overlay_front_pass()->Render(backing, false); + frame_def->overlay_pass()->Render(backing, false); + frame_def->overlay_fixed_pass()->Render(backing, false); + } + if (camera_render_target_.exists()) { + UpdateDOFParams(frame_def); + // We've already drawn the world. + // Now just draw our blit shapes (opaque shapes which blit portions of the + // camera render to the screen) ..these is so we can do things like + // distortion on large areas without blitting any part of the bg more than + // once. (unlike if we did that in the overlay-3d pass or whatnot). + frame_def->blit_pass()->Render(backing, false); + } else { + // Otherwise just draw the world straight to the backing + // (lower quality modes). + frame_def->beauty_pass()->Render(backing, false); + frame_def->beauty_pass_bg()->Render(backing, false); + } + PopGroupMarker(); + PushGroupMarker("Backing Transparent Pass"); + SetDepthWriting(false); + + // We may run out of precision in our depth buffer for deeply nested UI stuff + // and whatnot. This ensures overlay stuff never gets occluded by stuff + // 'behind' it because of this lack of precision. + SetDrawAtEqualDepth(true); + + // Now draw transparent stuff back to front. + if (camera_render_target_.exists()) { + // When copying camera buffer to the backing there's nothing transparent + // to draw. + } else { + frame_def->beauty_pass_bg()->Render(backing, true); + frame_def->beauty_pass()->Render(backing, true); + } + frame_def->overlay_3d_pass()->Render(backing, true); + if (overlays_in_3d) { + frame_def->overlay_fixed_pass()->Render(backing, true); + frame_def->overlay_pass()->Render(backing, true); + frame_def->overlay_front_pass()->Render(backing, true); + } + if (overlays_in_2d) { + frame_def->overlay_pass()->Render(backing, true); + frame_def->overlay_front_pass()->Render(backing, true); + } + + // In vr mode, the front section of the depth buffer that would have been + // used for our 2d ortho overlays is instead used for our vr-fade pass, + // which is nothing but our little bomb shaped transition wipe thing + // (it needs its own depth section otherwise it intersects with stuff out + // in the world). + if (overlays_in_3d) { + frame_def->vr_cover_pass()->Render(backing, true); + } + + // For debugging our DOF passes, etc. + DrawDebug(); + PopGroupMarker(); + + // If we've been drawing to a backing buffer, blit it to the screen. + if (backing_render_target_.exists()) { + // FIXME - should we just be discarding both depth and color + // after the blit?.. (of course, this code path shouldn't be used on + // mobile/slow-stuff so maybe it doesn't matter) + + // We're now done with the depth buffer on our backing; just need to copy + // color to the screen buffer. + InvalidateFramebuffer(false, true, false); + + // Note: We're forcing a shader-based blit for the moment; hardware blit + // seems to be flaky on qualcomm hardware as of jan 14 (adreno 330, adreno + // 320). + BlitBuffer(backing, screen_render_target(), false, true, true, true); + } + + // Lastly, we no longer need depth on our screen target. + InvalidateFramebuffer(false, true, false); + + RenderFrameDefEnd(); +} + +void Renderer::FinishFrameDef(FrameDef* frame_def) { + frames_rendered_count_++; + + // Give the renderer a chance to check for/report errors. + CheckForErrors(); +} + +#if BA_VR_BUILD + +void Renderer::VRPreprocess(FrameDef* frame_def) { + if (!IsVRMode()) { + return; + } + + // if we're in VR mode, make sure we've got our VR overlay target + if (!vr_overlay_flat_render_target_.exists()) { + // find this res to be ideal on current gen equipment + // (2017-ish, 1st gen rift/gear-vr/etc) + // ..can revisit once higher-res stuff is commonplace + int base_res = 1024; + vr_overlay_flat_render_target_ = NewFramebufferRenderTarget( + base_res, + base_res + * (static_cast(kBaseVirtualResY) + / static_cast(kBaseVirtualResX)), + true, // linear_interp + true, // depth + true, // tex + false, // depthTex + true, // high-quality + false, // msaa + true // alpha + ); // NOLINT(whitespace/parens) + } + auto* vrgraphics = VRGraphics::get(); + + // Also store our custom near clip plane dist. + frame_def->set_vr_near_clip(vrgraphics->vr_near_clip()); + + Vector3f cam_pt(frame_def->cam_original().x, frame_def->cam_original().y, + frame_def->cam_original().z); + + float world_scale = + kBaseVRWorldScale * VRGraphics::get()->vr_test_head_scale(); + + float extra_yaw = + (frame_def->camera_mode() == CameraMode::kOrbit) ? -0.3f : 0.0f; + vr_base_transform_ = Matrix44fRotate(Vector3f(0, 1, 0), extra_yaw * kDegPi) + * Matrix44fScale(world_scale) + * Matrix44fTranslate(cam_pt.x, cam_pt.y, cam_pt.z); + + // given our raw VR head/hand transforms, calc our in-game transforms + vr_transform_right_hand_ = + Matrix44fRotate(Vector3f(0, 0, 1), -vr_raw_hands_state_.r.roll * kDegPi) + * Matrix44fRotate(Vector3f(1, 0, 0), + -vr_raw_hands_state_.r.pitch * kDegPi) + * Matrix44fRotate(Vector3f(0, 1, 0), + 180.0f + vr_raw_hands_state_.r.yaw * kDegPi) + * Matrix44fScale(kInvVRHeadScale) + * Matrix44fTranslate(vr_raw_hands_state_.r.tx, vr_raw_hands_state_.r.ty, + vr_raw_hands_state_.r.tz) + * vr_base_transform_; + vr_transform_left_hand_ = + Matrix44fRotate(Vector3f(0, 0, 1), -vr_raw_hands_state_.l.roll * kDegPi) + * Matrix44fRotate(Vector3f(1, 0, 0), + -vr_raw_hands_state_.l.pitch * kDegPi) + * Matrix44fRotate(Vector3f(0, 1, 0), + 180.0f + vr_raw_hands_state_.l.yaw * kDegPi) + * Matrix44fScale(kInvVRHeadScale) + * Matrix44fTranslate(vr_raw_hands_state_.l.tx, vr_raw_hands_state_.l.ty, + vr_raw_hands_state_.l.tz) + * vr_base_transform_; + vr_transform_head_ = + Matrix44fRotate(Vector3f(0, 0, 1), -vr_raw_head_roll_ * kDegPi) + * Matrix44fRotate(Vector3f(1, 0, 0), -vr_raw_head_pitch_ * kDegPi) + * Matrix44fRotate(Vector3f(0, 1, 0), 180.0f + vr_raw_head_yaw_ * kDegPi) + * Matrix44fScale(kInvVRHeadScale) + * Matrix44fTranslate(vr_raw_head_tx_, vr_raw_head_ty_, vr_raw_head_tz_) + * vr_base_transform_; + + if (g_app_globals->reset_vr_orientation) { + g_app_globals->reset_vr_orientation = false; + } + + Vector3f translate = vr_transform_head_.GetTranslate(); + Vector3f forward = vr_transform_head_.LocalZAxis(); + Vector3f up = vr_transform_head_.LocalYAxis(); + + // stuff this into our graphics state for rendered stuff to use + vrgraphics->set_vr_head_forward(forward); + vrgraphics->set_vr_head_up(up); + vrgraphics->set_vr_head_translate(translate); +} + +void Renderer::VRUpdateForEyeRender(FrameDef* frame_def) { + if (!IsVRMode()) { + return; + } + VREyeRenderBegin(); + float world_scale = + kBaseVRWorldScale * VRGraphics::get()->vr_test_head_scale(); + Matrix44f eye_transform = + Matrix44fRotate(Vector3f(0, 0, 1), -vr_eye_roll_ * kDegPi) + * Matrix44fRotate(Vector3f(1, 0, 0), -vr_eye_pitch_ * kDegPi) + * Matrix44fRotate(Vector3f(0, 1, 0), 180.0f + (vr_eye_yaw_)*kDegPi) + * Matrix44fScale(kInvVRHeadScale) + * Matrix44fTranslate(vr_eye_x_, vr_eye_y_, vr_eye_z_) + * vr_base_transform_; + + // lastly, plug our eye_transform into our render pass cameras + // NOTE - because VR has different clipping requirements, + // we may be setting a different near plane than our usual drawing + // which currently throws off some of our hard-coded shaders such as DOF.. + // need to look into refactoring those to behave with varied clip ranges. + // For now we work around it by minimizing DOF effects in VR mode. + Vector3f offs = eye_transform * Vector3f(0, 0, 0); + // shaking in VR is odd; turn it off for now. + float shake_amt = 0.00f; + float shake_pos_x = frame_def->shake_original().x * shake_amt; + float shake_pos_y = frame_def->shake_original().y * shake_amt; + float shake_pos_z = frame_def->shake_original().z * shake_amt; + Vector3f target_offs = + eye_transform + * Vector3f(0 + shake_pos_x, 0 + shake_pos_y, 1 + shake_pos_z); + Vector3f up = (eye_transform * Vector3f(0, 1, 0)) - offs; + float near_clip = frame_def->vr_near_clip(); + // if we're doing VR cameras, overwrite the default camera with + // the eye cam here.. + RenderPass* passes[] = {frame_def->beauty_pass(), + frame_def->beauty_pass_bg(), + frame_def->overlay_3d_pass(), + frame_def->blit_pass(), + frame_def->overlay_pass(), + frame_def->overlay_front_pass(), + frame_def->vr_cover_pass(), + frame_def->GetOverlayFixedPass(), + nullptr}; + for (RenderPass** p = passes; *p != nullptr; p++) { + (**p).SetCamera(offs, target_offs, up, near_clip, 1000.0f, + vr_fov_degrees_x_, vr_fov_degrees_y_, vr_use_fov_tangents_, + vr_fov_l_tan_, vr_fov_r_tan_, vr_fov_b_tan_, vr_fov_t_tan_, + passes[0]->cam_area_of_interest_points()); + } +} + +void Renderer::VRDrawOverlayFlatPass(FrameDef* frame_def) { + if (IsVRMode()) { + // The overlay-flat pass should generally only have commands in it + // when UI is visible; skip rendering it if not. + if (frame_def->overlay_flat_pass()->HasDrawCommands()) { + PushGroupMarker("VR Overlay Flat Pass"); + SetDepthWriting(true); + SetDepthTesting(true); + RenderTarget* r_target = vr_overlay_flat_render_target(); + r_target->DrawBegin(true, 0, 0, 0, 0); + frame_def->overlay_flat_pass()->Render(r_target, false); // opaque stuff + SetDepthWriting(false); + + // So our transparent stuff matching opaque stuff in depth gets drawn. + SetDrawAtEqualDepth(true); + + // Transparent stuff. + frame_def->overlay_flat_pass()->Render(r_target, true); + PopGroupMarker(); + SetDepthWriting(false); + SetDepthTesting(false); + SetDrawAtEqualDepth(false); + } + } +} + +void Renderer::VRTransformToRightHand() { + g_graphics_server->MultMatrix(vr_transform_right_hand_); +} + +void Renderer::VRTransformToLeftHand() { + g_graphics_server->MultMatrix(vr_transform_left_hand_); +} +void Renderer::VRTransformToHead() { + g_graphics_server->MultMatrix(vr_transform_head_); +} + +#endif // BA_VR_BUILD + +void Renderer::UpdateSizesQualitiesAndColors(FrameDef* frame_def) { + // If screen-size has changed, handle that. + if (screen_size_dirty_) { + msaa_enabled_dirty_ = true; + screen_render_target()->ScreenSizeChanged(); + + // These render targets are dependent on screen size so they need to be + // remade. + camera_render_target_.Clear(); + camera_msaa_render_target_.Clear(); + backing_render_target_.Clear(); + screen_size_dirty_ = false; + } + + // Update quality settings to match this frame_def. + if (last_render_quality_ != frame_def->quality()) { + light_render_target_.Clear(); + light_shadow_render_target_.Clear(); + if (IsVRMode()) { + vr_overlay_flat_render_target_.Clear(); + } + } + last_render_quality_ = frame_def->quality(); + set_shadow_offset(Vector3f(frame_def->shadow_offset().x, + frame_def->shadow_offset().y, + frame_def->shadow_offset().z)); + set_shadow_scale(frame_def->shadow_scale().x, frame_def->shadow_scale().y); + set_shadow_ortho(frame_def->shadow_ortho()); + set_tint(1.5f * frame_def->tint()); // FIXME; why the 1.5? + set_ambient_color(frame_def->ambient_color()); + set_vignette_inner(frame_def->vignette_inner()); + if (IsVRMode()) { + // In VR mode we dont want vignetting; + // just use the inner color for both in and out. + set_vignette_outer(frame_def->vignette_inner()); + } else { + set_vignette_outer(frame_def->vignette_outer()); + } + UpdateVignetteTex(false); +} + +void Renderer::UpdateLightAndShadowBuffers(FrameDef* frame_def) { + if (!light_render_target_.exists() || !light_shadow_render_target_.exists()) { + assert(screen_render_target_.exists()); + + // Base shadow res on quality. + if (frame_def->quality() >= GraphicsQuality::kHigher) { + shadow_res_ = 1024; + // NOLINTNEXTLINE(bugprone-branch-clone) + } else if (frame_def->quality() >= GraphicsQuality::kHigh) { + shadow_res_ = 512; + } else if (frame_def->quality() >= GraphicsQuality::kMedium) { + shadow_res_ = 512; + } else { + shadow_res_ = 256; + } + + // 16 bit dithering is a bit noticeable here.. + bool high_qual = true; + light_render_target_ = Object::MakeRefCounted(NewFramebufferRenderTarget( + shadow_res_ / kLightResDiv, shadow_res_ / kLightResDiv, + true, // linear_interp + false, // depth + true, // tex + false, // depthTex + high_qual, // high-quality + false, // msaa + false // alpha + )); // NOLINT(whitespace/parens) + light_shadow_render_target_ = Object::MakeRefCounted( + NewFramebufferRenderTarget(shadow_res_, shadow_res_, + true, // linear_interp + false, // depth + true, // tex + false, // depthTex + high_qual, // high-quality + false, // msaa + false // alpha + )); // NOLINT(whitespace/parens) + } +} + +void Renderer::RenderLightAndShadowPasses(FrameDef* frame_def) { + float light_pitch = 90; + float light_heading = 0; + float light_tz = -22; + SetLight(light_pitch, light_heading, light_tz); + + // Draw our light/shadow buffers. + SetDepthWriting(false); + SetDepthTesting(false); + SetDrawAtEqualDepth(false); + PushGroupMarker("Light Pass"); + RenderTarget* r_target = light_render_target(); + r_target->DrawBegin(true, kShadowNeutral, kShadowNeutral, kShadowNeutral, + 1.0f); + frame_def->light_pass()->Render(r_target, true); + PopGroupMarker(); + PushGroupMarker("LightShadow Pass"); + r_target = light_shadow_render_target(); + r_target->DrawBegin(true, kShadowNeutral, kShadowNeutral, kShadowNeutral, + 1.0f); + frame_def->light_shadow_pass()->Render(r_target, true); + PopGroupMarker(); +} + +void Renderer::UpdateCameraRenderTargets(FrameDef* frame_def) { + // Create or destroy our camera render-target as necessary. + // In higher-quality modes we render the world into a buffer + // so we can do depth-of-field filtering and whatnot. + if (frame_def->quality() >= GraphicsQuality::kHigh) { + if (!camera_render_target_.exists()) { + float pixel_scale_fin = std::min(1.0f, std::max(0.1f, pixel_scale_)); + int w = static_cast(screen_render_target_->physical_width() + * pixel_scale_fin); + int h = static_cast(screen_render_target_->physical_height() + * pixel_scale_fin); + + // Calc and store the number of blur levels we'll want + // based on this resolution. + int max_res = std::max(w, h); + blur_res_count_ = 0; + int blur_res = max_res; + while (blur_res > 250) { + blur_res_count_++; + blur_res /= 2; + } + + // Enforce a minimum. + if (blur_res_count_ < 4) { + blur_res_count_ = 4; + } + + // We limit to a single blur pass in high-quality. + if (frame_def->quality() == GraphicsQuality::kHigh + && blur_res_count_ > 1) { + blur_res_count_ = 1; + } + + // Now tweak our cam render target res so that its evenly divisible by + // 2 for that many levels. + int foo = 1; + for (int i = 0; i < blur_res_count_; i++) { + foo *= 2; + } + w = ((w % foo == 0) ? w : (w + (foo - (w % foo)))); + h = ((h % foo == 0) ? h : (h + (foo - (h % foo)))); + camera_render_target_ = Object::MakeRefCounted(NewFramebufferRenderTarget( + w, h, + true, // linear-interp + true, // depth + true, // tex + true, // depth-tex + false, // high-qual + false, // msaa + false // alpha + )); // NOLINT(whitespace/parens) + + // If screen size just changed or whatnot, + // update whether we should do msaa. + if (msaa_enabled_dirty_) { + UpdateMSAAEnabled(); + msaa_enabled_dirty_ = false; + } + + // If we're doing msaa, also create a multi-sample version of the same. + // We'll draw into this and then blit it to our normal texture-backed + // camera-target. + if (IsMSAAEnabled()) { + camera_msaa_render_target_ = + NewFramebufferRenderTarget(w, h, + false, // linear-interp + true, // depth + false, // tex + false, // depth-tex + false, // high-qual + true, // msaa + false // alpha + ); // NOLINT(whitespace/parens) + } + } + } else { + camera_render_target_.Clear(); + camera_msaa_render_target_.Clear(); + blur_res_count_ = 0; + } +} + +void Renderer::UpdatePixelScaleAndBackingBuffer(FrameDef* frame_def) { + // If our pixel-scale is changing its essentially the same as a resolution + // change, so we wanna rebuild our light/shadow buffers and all that. + if (pixel_scale_requested_ != pixel_scale_) { + ScreenSizeChanged(); + } + + // Create or destroy our backing render-target as necessary. + // We need our backing buffer for non-1.0 pixel-scales. + if (pixel_scale_requested_ != 1.0f) { + if (pixel_scale_requested_ != pixel_scale_ + || !backing_render_target_.exists()) { + float pixel_scale_fin = + std::min(1.0f, std::max(0.1f, pixel_scale_requested_)); + int w = static_cast(screen_render_target_->physical_width() + * pixel_scale_fin); + int h = static_cast(screen_render_target_->physical_height() + * pixel_scale_fin); + backing_render_target_ = + NewFramebufferRenderTarget(w, h, + true, // linear interp + true, // depth + true, // tex + false, // depth tex + false, // highquality + false, // msaa, + false // alpha + ); // NOLINT(whitespace/parens) + } + } else { + // Otherwise we don't need backing buffer. Kill it if it exists. + if (backing_render_target_.exists()) { + backing_render_target_.Clear(); + } + } + pixel_scale_ = pixel_scale_requested_; +} + +void Renderer::LoadMedia(FrameDef* frame_def) { + millisecs_t t = GetRealTime(); + for (auto&& i : frame_def->media_components()) { + MediaComponentData* mc = i.get(); + assert(mc); + mc->Load(); + + // Also mark them as used so they get kept around for a bit. + mc->set_last_used_time(t); + } +} + +#if BA_OSTYPE_MACOS && BA_SDL_BUILD && !BA_SDL2_BUILD +void Renderer::HandleFunkyMacGammaIssue(FrameDef* frame_def) { + // FIXME - for some reason, on mac, gamma is getting switched back to + // default about 1 second after a res change, etc... + // so if we're using a non-1.0 gamma, lets keep setting it periodically + // to force the issue + millisecs_t t = GetRealTime(); + if (screen_gamma_requested_ != screen_gamma_ + || (t - last_screen_gamma_update_time_ > 300 && screen_gamma_ != 1.0f)) { + screen_gamma_ = screen_gamma_requested_; + SDL_SetGamma(screen_gamma_, screen_gamma_, screen_gamma_); + last_screen_gamma_update_time_ = t; + } +} +#endif + +void Renderer::DrawWorldToCameraBuffer(FrameDef* frame_def) { +#if BA_CARDBOARD_BUILD + // On cardboard theres a scissor setup enabled when we come in; + // we need to turn that off while drawing to our other framebuffer since it + // screws things up there. + CardboardDisableScissor(); +#endif + + PushGroupMarker("Camera Opaque Pass"); + SetDepthWriting(true); + SetDepthTesting(true); + RenderTarget* cam_target = has_camera_msaa_render_target() + ? camera_msaa_render_target() + : camera_render_target(); + cam_target->DrawBegin(frame_def->needs_clear()); + + // Draw opaque stuff front-to-back. + frame_def->beauty_pass()->Render(cam_target, false); + frame_def->beauty_pass_bg()->Render(cam_target, false); + PopGroupMarker(); + PushGroupMarker("Camera Transparent Pass"); + + // Draw transparent stuff back-to-front. + SetDepthWriting(false); + frame_def->beauty_pass_bg()->Render(cam_target, true); + frame_def->beauty_pass()->Render(cam_target, true); + + // If we drew into the MSAA version, blit it over to the texture version. + if (has_camera_msaa_render_target()) { + BlitBuffer(camera_msaa_render_target(), camera_render_target(), + true, // Depth. + false, // linear_interpolation + false, // force_shader_blit + true // invalidate_source + ); // NOLINT(whitespace/parens) + } + GenerateCameraBufferBlurPasses(); + PopGroupMarker(); + +#if BA_CARDBOARD_BUILD + CardboardEnableScissor(); +#endif +} + +void Renderer::UpdateDOFParams(FrameDef* frame_def) { + RenderPass* beauty_pass = frame_def->beauty_pass(); + assert(beauty_pass); + const std::vector& areas_of_interest( + beauty_pass->cam_area_of_interest_points()); + float min_z, max_z; + if (!areas_of_interest.empty()) { + // find min/max z for our areas of interest + min_z = 9999.0f; + max_z = -9999.0f; + for (auto i : areas_of_interest) { + float z = (beauty_pass->model_view_projection_matrix() * i).z; + if (z > max_z) { + max_z = z; + } + if (z < min_z) { + min_z = z; + } + } + } else { + min_z = max_z = 0; + } + + if ((frame_def->real_time() - dof_update_time_ > 100)) { + dof_update_time_ = frame_def->real_time() - 100; + } + float smoothing = 0.995f; + while (dof_update_time_ < frame_def->real_time()) { + dof_update_time_++; + dof_near_smoothed_ = + smoothing * dof_near_smoothed_ + (1.0f - smoothing) * min_z; + dof_far_smoothed_ = + smoothing * dof_far_smoothed_ + (1.0f - smoothing) * max_z; + } +} + +void Renderer::ScreenSizeChanged() { + assert(InGraphicsThread()); + + // We can actually get these events at times when we don't have a valid + // gl context, so instead of doing any GL work here let's just make a note to + // do so next time we render. + screen_size_dirty_ = true; +} + +void Renderer::CheckCapabilities() {} + +void Renderer::Unload() { + light_render_target_.Clear(); + light_shadow_render_target_.Clear(); + vr_overlay_flat_render_target_.Clear(); + screen_render_target_.Clear(); + backing_render_target_.Clear(); +} + +void Renderer::Load() { + screen_render_target_ = Object::MakeRefCounted(NewScreenRenderTarget()); + + // Restore current gamma value. + if (screen_gamma_ != 1.0f) { +#if BA_SDL2_BUILD + // Not supporting gamma in SDL2 currently. +#elif BA_SDL_BUILD + SDL_SetGamma(screen_gamma_, screen_gamma_, screen_gamma_); +#endif + } +} + +void Renderer::PostLoad() { + // This is called after all loading is done; + // the renderer may choose to do any final setting up here. +} + +void Renderer::SetLight(float pitch, float heading, float tz) { + light_pitch_ = pitch; + light_heading_ = heading; + light_tz_ = tz; +} + +#if BA_VR_BUILD +void Renderer::VRSetHead(float tx, float ty, float tz, float yaw, float pitch, + float roll) { + vr_raw_head_tx_ = tx; + vr_raw_head_ty_ = ty; + vr_raw_head_tz_ = tz; + vr_raw_head_yaw_ = yaw; + vr_raw_head_pitch_ = pitch; + vr_raw_head_roll_ = roll; +} +void Renderer::VRSetEye(int eye, float yaw, float pitch, float roll, + float tan_l, float tan_r, float tan_b, float tan_t, + float eye_x, float eye_y, float eye_z, int viewport_x, + int viewport_y) { + // these are flipped for whatever reason... grumble grumble math grumble + vr_fov_l_tan_ = tan_r; + vr_fov_r_tan_ = tan_l; + vr_fov_b_tan_ = tan_b; + vr_fov_t_tan_ = tan_t; + vr_eye_x_ = eye_x; + vr_eye_y_ = eye_y; + vr_eye_z_ = eye_z; + vr_use_fov_tangents_ = true; + vr_fov_degrees_x_ = vr_fov_degrees_y_ = 30.0f; + vr_eye_ = eye; + vr_eye_yaw_ = yaw; + vr_eye_pitch_ = pitch; + vr_eye_roll_ = roll; + vr_viewport_x_ = viewport_x; + vr_viewport_y_ = viewport_y; +} +#endif // BA_VR_BUILD + +auto Renderer::GetZBufferValue(RenderPass* pass, float dist) -> float { + float z = std::min(1.0f, std::max(-1.0f, dist)); + // Remap from -1,1 to our depth-buffer-range. + z = 0.5f * (z + 1.0f); + z = kBackingDepth3 + z * (kBackingDepth4 - kBackingDepth3); + return z; +} + +auto Renderer::GetAutoAndroidRes() -> std::string { + throw Exception("This should be overridden."); +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/renderer.h b/src/ballistica/graphics/renderer.h new file mode 100644 index 00000000..b0ff2946 --- /dev/null +++ b/src/ballistica/graphics/renderer.h @@ -0,0 +1,298 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_RENDERER_H_ +#define BALLISTICA_GRAPHICS_RENDERER_H_ + +#include +#include +#include + +#include "ballistica/core/object.h" +#include "ballistica/graphics/frame_def.h" +#include "ballistica/graphics/framebuffer.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/mesh/image_mesh.h" +#include "ballistica/graphics/mesh/mesh.h" +#include "ballistica/graphics/mesh/mesh_buffer.h" +#include "ballistica/graphics/mesh/mesh_buffer_base.h" +#include "ballistica/graphics/mesh/mesh_buffer_vertex_simple_full.h" +#include "ballistica/graphics/mesh/mesh_buffer_vertex_smoke_full.h" +#include "ballistica/graphics/mesh/mesh_buffer_vertex_sprite.h" +#include "ballistica/graphics/mesh/mesh_data.h" +#include "ballistica/graphics/mesh/mesh_data_client_handle.h" +#include "ballistica/graphics/mesh/mesh_index_buffer_16.h" +#include "ballistica/graphics/mesh/mesh_index_buffer_32.h" +#include "ballistica/graphics/mesh/mesh_indexed.h" +#include "ballistica/graphics/mesh/mesh_indexed_dual_texture_full.h" +#include "ballistica/graphics/mesh/mesh_indexed_object_split.h" +#include "ballistica/graphics/mesh/mesh_indexed_simple_full.h" +#include "ballistica/graphics/mesh/mesh_indexed_simple_split.h" +#include "ballistica/graphics/mesh/mesh_indexed_smoke_full.h" +#include "ballistica/graphics/mesh/mesh_indexed_static_dynamic.h" +#include "ballistica/graphics/mesh/mesh_non_indexed.h" +#include "ballistica/graphics/mesh/sprite_mesh.h" +#include "ballistica/graphics/mesh/text_mesh.h" +#include "ballistica/graphics/render_command_buffer.h" +#include "ballistica/graphics/render_pass.h" +#include "ballistica/graphics/render_target.h" +#include "ballistica/graphics/text/text_group.h" +#include "ballistica/math/matrix44f.h" +#include "ballistica/math/vector3f.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/media/data/model_data.h" +#include "ballistica/media/media.h" + +namespace ballistica { + +// The renderer is responsible for converting a frame_def to onscreen pixels +class Renderer { + public: + Renderer(); + virtual ~Renderer(); + + // Given a z-distance in world-space, returns a beauty-pass z-buffer + // value from 0 to 1. + auto GetZBufferValue(RenderPass* pass, float dist) -> float; + + // All 3 of these must be called during a render. + void PreprocessFrameDef(FrameDef* frame_def); + void RenderFrameDef(FrameDef* frame_def); + void FinishFrameDef(FrameDef* frame_def); + + // This needs to be generalized. + void SetLight(float pitch, float heading, float tz); + void set_shadow_offset(const Vector3f& offset) { shadow_offset_ = offset; } + void set_shadow_scale(float x, float z) { + shadow_scale_x_ = x; + shadow_scale_z_ = z; + } + void set_shadow_ortho(bool ortho) { shadow_ortho_ = ortho; } + void set_tint(const Vector3f& val) { tint_ = val; } + void set_ambient_color(const Vector3f& val) { ambient_color_ = val; } + void set_vignette_outer(const Vector3f& val) { vignette_outer_ = val; } + void set_vignette_inner(const Vector3f& val) { vignette_inner_ = val; } + auto tint() const -> const Vector3f& { return tint_; } + auto ambient_color() const -> const Vector3f& { return ambient_color_; } + auto vignette_outer() const -> const Vector3f& { return vignette_outer_; } + auto vignette_inner() const -> const Vector3f& { return vignette_inner_; } + auto shadow_ortho() const -> bool { return shadow_ortho_; } + auto shadow_offset() const -> const Vector3f& { return shadow_offset_; } + auto shadow_scale_x() const -> float { return shadow_scale_x_; } + auto shadow_scale_z() const -> float { return shadow_scale_z_; } + auto light_tz() const -> float { return light_tz_; } + auto light_pitch() const -> float { return light_pitch_; } + auto light_heading() const -> float { return light_heading_; } + void set_pixel_scale(float s) { pixel_scale_requested_ = s; } + void set_screen_gamma(float val) { screen_gamma_requested_ = val; } + void set_debug_draw_mode(bool debugModeIn) { debug_draw_mode_ = debugModeIn; } + auto debug_draw_mode() -> bool { return debug_draw_mode_; } + + // Used when recreating contexts. + virtual void Unload(); + virtual void Load(); + virtual void PostLoad(); + virtual void CheckCapabilities(); + virtual auto GetAutoGraphicsQuality() -> GraphicsQuality = 0; + virtual auto GetAutoTextureQuality() -> TextureQuality = 0; + + virtual auto GetAutoAndroidRes() -> std::string; + + void ScreenSizeChanged(); + auto has_camera_render_target() const -> bool { + return camera_render_target_.exists(); + } + auto has_camera_msaa_render_target() const -> bool { + return camera_msaa_render_target_.exists(); + } + auto camera_render_target() -> RenderTarget* { + assert(camera_render_target_.exists()); + return camera_render_target_.get(); + } + auto camera_msaa_render_target() -> RenderTarget* { + assert(camera_msaa_render_target_.exists()); + return camera_msaa_render_target_.get(); + } + auto backing_render_target() -> RenderTarget* { + assert(backing_render_target_.exists()); + return backing_render_target_.get(); + } + auto screen_render_target() -> RenderTarget* { + assert(screen_render_target_.exists()); + return screen_render_target_.get(); + } + auto light_render_target() -> RenderTarget* { + assert(light_render_target_.exists()); + return light_render_target_.get(); + } + auto light_shadow_render_target() -> RenderTarget* { + assert(light_shadow_render_target_.exists()); + return light_shadow_render_target_.get(); + } + auto vr_overlay_flat_render_target() -> RenderTarget* { + assert(vr_overlay_flat_render_target_.exists()); + return vr_overlay_flat_render_target_.get(); + } + auto shadow_res() const -> int { return shadow_res_; } + auto blur_res_count() const -> int { return blur_res_count_; } + auto drawing_reflection() const -> bool { return drawing_reflection_; } + void set_drawing_reflection(bool val) { drawing_reflection_ = val; } + auto dof_near_smoothed() const -> float { return dof_near_smoothed_; } + auto dof_far_smoothed() const -> float { return dof_far_smoothed_; } + auto total_frames_rendered() -> int { return frames_rendered_count_; } + +#if BA_VR_BUILD + void VRSetHead(float tx, float ty, float tz, float yaw, float pitch, + float roll); + void VRSetHands(const VRHandsState& state) { vr_raw_hands_state_ = state; } + void VRSetEye(int eye, float yaw, float pitch, float roll, float tanL, + float tanR, float tanB, float tanT, float eyeX, float eyeY, + float eyeZ, int viewport_x, int viewport_y); + int VRGetViewportX() const { return vr_viewport_x_; } + int VRGetViewportY() const { return vr_viewport_y_; } +#endif // BA_VR_BUILD + + virtual auto NewModelData(const ModelData& model) -> ModelRendererData* = 0; + virtual auto NewTextureData(const TextureData& texture) + -> TextureRendererData* = 0; + virtual auto NewMeshData(MeshDataType t, MeshDrawType drawType) + -> MeshRendererData* = 0; + virtual void DeleteMeshData(MeshRendererData* data, MeshDataType t) = 0; + virtual void ProcessRenderCommandBuffer(RenderCommandBuffer* buffer, + const RenderPass& pass, + RenderTarget* render_target) = 0; + virtual void SetDepthRange(float min, float max) = 0; + virtual void FlipCullFace() = 0; + + protected: + virtual void DrawDebug() = 0; + virtual void CheckForErrors() = 0; + virtual void UpdateVignetteTex(bool force) = 0; + virtual void GenerateCameraBufferBlurPasses() = 0; + virtual void UpdateMeshes( + const std::vector>& meshes, + const std::vector& index_sizes, + const std::vector>& buffers) = 0; + virtual void SetDepthWriting(bool enable) = 0; + virtual void SetDepthTesting(bool enable) = 0; + virtual void SetDrawAtEqualDepth(bool enable) = 0; + virtual void InvalidateFramebuffer(bool color, bool depth, + bool target_read_framebuffer) = 0; + virtual auto NewScreenRenderTarget() -> RenderTarget* = 0; + virtual auto NewFramebufferRenderTarget(int width, int height, + bool linear_interp, bool depth, + bool texture, bool depth_texture, + bool high_quality, bool msaa, + bool alpha) -> RenderTarget* = 0; + virtual void PushGroupMarker(const char* label) = 0; + virtual void PopGroupMarker() = 0; + virtual void BlitBuffer(RenderTarget* src, RenderTarget* dst, bool depth, + bool linear_interpolation, bool force_shader_blit, + bool invalidate_source) = 0; + virtual auto IsMSAAEnabled() const -> bool = 0; + virtual void UpdateMSAAEnabled() = 0; + virtual void VREyeRenderBegin() = 0; + virtual void RenderFrameDefEnd() = 0; + virtual void CardboardDisableScissor() = 0; + virtual void CardboardEnableScissor() = 0; +#if BA_VR_BUILD + void VRTransformToRightHand(); + void VRTransformToLeftHand(); + void VRTransformToHead(); + virtual void VRSyncRenderStates() = 0; +#endif + + private: + void UpdateLightAndShadowBuffers(FrameDef* frame_def); + void RenderLightAndShadowPasses(FrameDef* frame_def); + void UpdateSizesQualitiesAndColors(FrameDef* frame_def); + void DrawWorldToCameraBuffer(FrameDef* frame_def); + void UpdatePixelScaleAndBackingBuffer(FrameDef* frame_def); + void UpdateCameraRenderTargets(FrameDef* frame_def); +#if BA_OSTYPE_MACOS && BA_SDL_BUILD && !BA_SDL2_BUILD + void HandleFunkyMacGammaIssue(FrameDef* frame_def); +#endif + void LoadMedia(FrameDef* frame_def); + void UpdateDOFParams(FrameDef* frame_def); +#if BA_VR_BUILD + void VRPreprocess(FrameDef* frame_def); + void VRUpdateForEyeRender(FrameDef* frame_def); + void VRDrawOverlayFlatPass(FrameDef* frame_def); + // raw values from vr system + VRHandsState vr_raw_hands_state_; + float vr_raw_head_tx_ = 0.0f; + float vr_raw_head_ty_ = 0.0f; + float vr_raw_head_tz_ = 0.0f; + float vr_raw_head_yaw_ = 0.0f; + float vr_raw_head_pitch_ = 0.0f; + float vr_raw_head_roll_ = 0.0f; + // final game-space transforms + Matrix44f vr_base_transform_ = kMatrix44fIdentity; + Matrix44f vr_transform_right_hand_ = kMatrix44fIdentity; + Matrix44f vr_transform_left_hand_ = kMatrix44fIdentity; + Matrix44f vr_transform_head_ = kMatrix44fIdentity; + // values for current eye render + bool vr_use_fov_tangents_ = false; + float vr_fov_l_tan_ = 1.0f; + float vr_fov_r_tan_ = 1.0f; + float vr_fov_b_tan_ = 1.0f; + float vr_fov_t_tan_ = 1.0f; + float vr_fov_degrees_x_ = 30.0f; + float vr_fov_degrees_y_ = 30.0f; + float vr_eye_x_ = 0.0f; + float vr_eye_y_ = 0.0f; + float vr_eye_z_ = 0.0f; + int vr_eye_ = 0; + float vr_eye_yaw_ = 0.0f; + float vr_eye_pitch_ = 0.0f; + float vr_eye_roll_ = 0.0f; + int vr_viewport_x_ = 0; + int vr_viewport_y_ = 0; +#endif // BA_VR_BUILD + bool screen_size_dirty_{}; + bool msaa_enabled_dirty_{}; + millisecs_t dof_update_time_{}; + bool dof_delay_{true}; + float dof_near_smoothed_{}; + float dof_far_smoothed_{}; + bool drawing_reflection_{}; + int blur_res_count_{}; + float light_pitch_{}; + float light_heading_{}; + float light_tz_{-22.0f}; + Vector3f shadow_offset_{0.0f, 0.0f, 0.0f}; + float shadow_scale_x_{1.0f}; + float shadow_scale_z_{1.0f}; + bool shadow_ortho_{}; + Vector3f tint_{1.0f, 1.0f, 1.0f}; + Vector3f ambient_color_{1.0f, 1.0f, 1.0f}; + Vector3f vignette_outer_{0.0f, 0.0f, 0.0f}; + Vector3f vignette_inner_{1.0f, 1.0f, 1.0f}; + int shadow_res_{-1}; + float screen_gamma_requested_{1.0f}; + float screen_gamma_{1.0f}; + float pixel_scale_requested_{1.0f}; + float pixel_scale_{1.0f}; + Object::Ref screen_render_target_; + Object::Ref backing_render_target_; + Object::Ref camera_render_target_; + Object::Ref camera_msaa_render_target_; + Object::Ref light_render_target_; + Object::Ref light_shadow_render_target_; + Object::Ref vr_overlay_flat_render_target_; + millisecs_t last_screen_gamma_update_time_{}; + int last_commands_buffer_size_{}; + int last_f_vals_buffer_size_{}; + int last_i_vals_buffer_size_{}; + int last_models_buffer_size_{}; + int last_textures_buffer_size_{}; + bool debug_draw_mode_{}; + int frames_rendered_count_{}; + + // The *actual* current quality (set based on the + // currently-rendering frame_def) + GraphicsQuality last_render_quality_{GraphicsQuality::kLow}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_RENDERER_H_ diff --git a/src/ballistica/graphics/text/font_page_map_data.h b/src/ballistica/graphics/text/font_page_map_data.h new file mode 100644 index 00000000..d1a20d0a --- /dev/null +++ b/src/ballistica/graphics/text/font_page_map_data.h @@ -0,0 +1,89 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_TEXT_FONT_PAGE_MAP_DATA_H_ +#define BALLISTICA_GRAPHICS_TEXT_FONT_PAGE_MAP_DATA_H_ + +// this file was generated automatically from the font construction tool on +// 2015-06-27 +// NOTE: IT HAS BEEN MODIFIED BY HAND SINCE!!! IF WE RECREATE IT VIA TOOL +// AT SOME POINT WE NEED TO UPDATE THE TOOL!!!!!!!! + +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/text/text_graphics.h" + +namespace ballistica { +// the total number of glyph pages we have +#define BA_GLYPH_PAGE_COUNT 8 + +// the total number of glyphs we have +const int kGlyphCount = 1280; + +// the starting glyph index for each page +uint32_t g_glyph_page_start_index_map[8] = {0, 258, 416, 546, + 698, 981, 1138, 1276}; + +// the number of glyphs on each page +uint32_t g_glyph_page_glyph_counts[8] = {258, 158, 130, 152, 283, 157, 138, 4}; + +// our dynamically-loaded glyph structs for each page +TextGraphics::Glyph* g_glyph_pages[8] = {nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr}; + +// the page index for each glyph +uint16_t g_glyph_map[kGlyphCount] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 7, 7, 7, 7}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_TEXT_FONT_PAGE_MAP_DATA_H_ diff --git a/src/ballistica/graphics/text/text_graphics.cc b/src/ballistica/graphics/text/text_graphics.cc new file mode 100644 index 00000000..aed7d7fa --- /dev/null +++ b/src/ballistica/graphics/text/text_graphics.cc @@ -0,0 +1,1176 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/text/text_graphics.h" + +#include +#include + +#include "ballistica/ballistica.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/text/font_page_map_data.h" +#include "ballistica/platform/platform.h" + +namespace ballistica { + +class TextGraphics::TextSpanBoundsCacheEntry : public Object { + public: + std::string string; + Rect r; + float width{}; + std::map>::iterator + map_iterator_; + std::list>::iterator list_iterator_; +}; + +void TextGraphics::Init() { + assert(InGameThread()); + assert(g_text_graphics == nullptr); + g_text_graphics = new TextGraphics(); +} + +TextGraphics::TextGraphics() { + // Init glyph values for our custom font pages + // (just a 5x5 array currently). + for (int page = 0; page < 4; page++) { + for (int x = 0; x < 5; x++) { + for (int y = 0; y < 5; y++) { + int index = 25 * page + y * 5 + x; + Glyph& g(glyphs_extras_[index]); + + float extra_advance = 0.0f; + + g.pen_offset_x = 0.1f; + g.pen_offset_y = -0.2f; + + g.x_size = 1.0f; + g.y_size = 1.0f; + + // Euro symbol should be a bit smaller. + if (index == 0) { + g.x_size = 0.8f; + g.y_size = 0.8f; + } + + // Move all arrows down a bit. + if (index > 0 && index < 5) { + g.pen_offset_y -= 0.1f; + } + + // Shrink account logos and move them up a bit. + if (index == 29 || index == 32 || index == 33 || index == 38 + || index == 40 || index == 48 || index == 49) { + g.pen_offset_y += 0.4f; + extra_advance += 0.08f; + g.x_size *= 0.55f; + g.y_size *= 0.55f; + } + + // Same with the logo and all the icons on sheets 3 and 4. + if (index == 30 || (index >= 50 && index < 100)) { + // A few are *extra* big + if (index == 67 || index == 65 || index == 70 || index == 72 + || index == 73 || index == 75 || index == 76 || index == 78 + || index == 79) { + g.pen_offset_y += 0.31f; + if (index == 70) g.pen_offset_y -= 0.02f; + extra_advance += 0.04f; + g.x_size *= 0.75f; + g.y_size *= 0.75f; + } else { + g.pen_offset_y += 0.4f; + extra_advance += 0.08f; + g.x_size *= 0.55f; + g.y_size *= 0.55f; + } + } + g.advance = g.x_size - 0.09f + extra_advance; + + // Ticket overlay should be big and shouldn't advance us at all. + if (index == 41) { + g.x_size *= 1.1f; + g.y_size *= 1.1f; + g.pen_offset_x -= 0.3f; + g.pen_offset_y -= 0.1f; + g.advance = 0; + } + + // Trophies should be big. + if (index >= 42 && index <= 47) { + float s = 1.5f; + g.x_size *= s; + g.y_size *= s; + g.pen_offset_x -= 0.07f; + g.pen_offset_y -= 0.2f; + g.advance *= s; + } + + // Up/down arrows are a bit thinner. + if (index == 3 || index == 4) { + g.advance -= 0.3f; + g.pen_offset_x -= 0.15f; + } + + g.tex_min_x = 0.2f * static_cast(x); + g.tex_min_y = 0.2f * static_cast(y + 1); + g.tex_max_x = 0.2f * static_cast(x + 1); + g.tex_max_y = 0.2f * static_cast(y); + } + } + } + + // init glyph values for our big font page + // (a 8x8 array) + { + float x_offs = 0.009f; + float y_offs = 0.0059f; + float scale_extra = -0.012f; + for (int x = 0; x < 8; x++) { + for (int y = 0; y < 8; y++) { + int c = y * 8 + x; + Glyph& g(glyphs_big_[c]); + g.pen_offset_x = 0.05f; + g.pen_offset_y = -0.215f; + float w = 0.41f; + float bot_offset = 0.0f; + float left_offset = 0.0f; + float right_offset = 0.0f; + float top_offset = 0.0f; + switch (c) { + case 0: // NOLINT(bugprone-branch-clone) + w = 0.415f; + break; // A + case 1: + w = 0.415f; + break; // B + case 2: + w = 0.40f; + break; // C + case 4: + w = 0.315f; + break; // E + case 5: + w = 0.31f; + break; // F + case 7: + w = 0.42f; + break; // H + case 8: + w = 0.215f; + break; // I + case 9: + w = 0.38f; + break; // J + case 10: + w = 0.42f; + break; // K + case 11: + w = 0.345f; + break; // L + case 12: + w = 0.56f; + break; // M + case 13: + w = 0.42f; + break; // N + case 15: + w = 0.38f; + break; // P + case 16: + bot_offset = 0.07f; + break; // Q + case 18: // NOLINT(bugprone-branch-clone) + w = 0.375f; + break; // S + case 19: + w = 0.375f; + break; // T + case 20: + w = 0.43f; + break; // U + case 21: + w = 0.42f; + break; // V + case 22: + w = 0.625f; + break; // W + case 23: + w = 0.36f; + break; // X + case 24: + w = 0.4f; + break; // Y + case 25: + w = 0.34f; + break; // Z + case 26: + w = 0.37f; + break; // 0 + case 27: + w = 0.28f; + break; // 1 + case 28: // NOLINT(bugprone-branch-clone) + w = 0.37f; + break; // 2 + case 29: + w = 0.37f; + break; // 3 + case 30: + w = 0.37f; + break; // 4 + case 31: + w = 0.37f; + break; // 5 + case 32: // NOLINT(bugprone-branch-clone) + w = 0.36f; + break; // 6 + case 33: + w = 0.36f; + break; // 7 + case 34: // NOLINT(bugprone-branch-clone) + w = 0.37f; + break; // 8 + case 35: + w = 0.37f; + break; // 9 + case 36: + w = 0.18f; + break; // ! + case 37: + w = 0.35f; + break; // ? + case 38: + w = 0.21f; + top_offset = -0.72f; + break; // . + case 39: + w = 0.30f; + top_offset = -0.44f; + bot_offset = -0.3f; + break; // - + case 40: + w = 0.20f; + top_offset = -0.3f; + bot_offset = 0.0f; + break; // : + case 41: + w = 0.6f; + top_offset = -0.19f; + bot_offset = -0.1f; + break; // % + case 42: + w = 0.54f; + top_offset = -0.16f; + bot_offset = -0.1f; + break; // # + case 43: // NOLINT(bugprone-branch-clone) + w = 0.18f; + break; // upside-down ! + case 44: + w = 0.18f; + break; // space + default: + break; + } + bot_offset += 0.04f; + right_offset += 0.04f; + top_offset += 0.03f; + left_offset += 0.03f; + + g.advance = w * 1.15f; + g.x_size = 1.03f; + g.y_size = 1.03f; + g.tex_min_x = (1.0f / 8.0f) * static_cast(x) + x_offs; + g.tex_min_y = + (1.0f / 8.0f) * static_cast(y + 1) + y_offs + scale_extra; + g.tex_max_x = + (1.0f / 8.0f) * static_cast(x + 1) + x_offs + scale_extra; + g.tex_max_y = (1.0f / 8.0f) * static_cast(y) + y_offs; + + // just scooted letters over.. account for that + float foo_x = 0.0183f; + float foo_y = 0.000f; + g.tex_min_x += foo_x; + g.tex_max_x += foo_x; + g.tex_min_y += foo_y; + g.tex_max_y += foo_y; + + // clamp based on char width + float scale = w * 1.32f; + g.x_size *= scale; + g.tex_max_x = g.tex_min_x + (g.tex_max_x - g.tex_min_x) * scale; + + // add bot offset + if (bot_offset != 0.0f) { + g.tex_min_y = g.tex_max_y + + (g.tex_min_y - g.tex_max_y) + * ((g.y_size + bot_offset) / g.y_size); + g.pen_offset_y -= bot_offset; + g.y_size += bot_offset; + } + // add left offset + if (left_offset != 0.0f) { + g.tex_min_x = g.tex_max_x + + (g.tex_min_x - g.tex_max_x) + * ((g.x_size + left_offset) / g.x_size); + g.pen_offset_x -= left_offset; + g.x_size += left_offset; + } + // add right offset + if (right_offset != 0.0f) { + g.tex_max_x = g.tex_min_x + + (g.tex_max_x - g.tex_min_x) + * ((g.x_size + right_offset) / g.x_size); + g.x_size += right_offset; + } + // add top offset + if (top_offset != 0.0f) { + g.tex_max_y = g.tex_min_y + + (g.tex_max_y - g.tex_min_y) + * ((g.y_size + top_offset) / g.y_size); + g.y_size += top_offset; + } + + if (g.tex_max_x > 1.0f || g.tex_max_x < 0.0f || g.tex_min_x > 1.0 + || g.tex_min_x < 0.0f || g.tex_max_y > 1.0f || g.tex_max_y < 0.0 + || g.tex_min_y > 1.0f || g.tex_min_y < 0.0f) { + BA_LOG_ONCE("Warning: glyph bounds error"); + } + } + } + } +} + +static auto GetBigGlyphIndex(uint32_t char_val) -> int { + int index; + switch (char_val) { + case 'A': + case 'a': + case 0x00C0: + case 0x00E0: + case 0x00C1: + case 0x00E1: + case 0x00C2: + case 0x00E2: + case 0x00C3: + case 0x00E3: + case 0x00C4: + case 0x00E4: + case 0x00C5: + case 0x00E5: + case 0x0100: + case 0x0101: + case 0x0102: + case 0x0103: + case 0x0104: + case 0x0105: + index = 0; + break; + case 'B': + case 'b': + index = 1; + break; + case 'C': + case 'c': + case 0x0106: + case 0x0107: + case 0x0108: + case 0x0109: + case 0x010A: + case 0x010B: + case 0x010C: + case 0x010D: + index = 2; + break; + case 'D': + case 'd': + case 0x00D0: + case 0x010E: + case 0x010F: + case 0x0110: + case 0x0111: + index = 3; + break; + case 'E': + case 'e': + case 0x00C8: + case 0x00E8: + case 0x00C9: + case 0x00E9: + case 0x00CA: + case 0x00EA: + case 0x00CB: + case 0x00EB: + case 0x0112: + case 0x0113: + case 0x0114: + case 0x0115: + case 0x0116: + case 0x0117: + case 0x0118: + case 0x0119: + case 0x011A: + case 0x011B: + index = 4; + break; + case 'F': + case 'f': + index = 5; + break; + case 'G': + case 'g': + case 0x011C: + case 0x011D: + case 0x011E: + case 0x011F: + case 0x0120: + case 0x0121: + case 0x0122: + case 0x0123: + index = 6; + break; + case 'H': + case 'h': + case 0x0124: + case 0x0125: + case 0x0126: + case 0x0127: + index = 7; + break; + case 'I': + case 'i': + case 0x00CD: + case 0x00ED: + case 0x00CE: + case 0x00EE: + case 0x00CF: + case 0x00EF: + case 0x0128: + case 0x0129: + case 0x012A: + case 0x012B: + case 0x012C: + case 0x012D: + case 0x012E: + case 0x012F: + case 0x0130: + index = 8; + break; + case 'J': + case 'j': + case 0x0134: + case 0x0135: + index = 9; + break; + case 'K': + case 'k': + case 0x0136: + case 0x0137: + case 0x0138: + index = 10; + break; + case 'L': + case 'l': + case 0x0139: + case 0x013A: + case 0x013B: + case 0x013C: + case 0x013D: + case 0x013E: + case 0x013F: + case 0x0140: + case 0x0141: + case 0x0142: + index = 11; + break; + case 'M': + case 'm': + index = 12; + break; + case 'N': + case 'n': + case 0x00D1: + case 0x00F1: + case 0x0143: + case 0x0144: + case 0x0145: + case 0x0146: + case 0x0147: + case 0x0148: + case 0x0149: + case 0x014A: + case 0x014B: + index = 13; + break; + case 'O': + case 'o': + case 0x00D2: + case 0x00F2: + case 0x00D3: + case 0x00F3: + case 0x00D4: + case 0x00F4: + case 0x00D5: + case 0x00F5: + case 0x00D6: + case 0x00F6: + case 0x014C: + case 0x014D: + case 0x014E: + case 0x014F: + case 0x0150: + case 0x0151: + index = 14; + break; + case 'P': + case 'p': + index = 15; + break; + case 'Q': + case 'q': + index = 16; + break; + case 'R': + case 'r': + case 0x0154: + case 0x0155: + case 0x0156: + case 0x0157: + case 0x0158: + case 0x0159: + index = 17; + break; + case 'S': + case 's': + case 0x015A: + case 0x015B: + case 0x015C: + case 0x015D: + case 0x015E: + case 0x015F: + case 0x0160: + case 0x0161: + index = 18; + break; + case 'T': + case 't': + case 0x0162: + case 0x0163: + case 0x0164: + case 0x0165: + case 0x0166: + case 0x0167: + index = 19; + break; + case 'U': + case 'u': + case 0x00D9: + case 0x00F9: + case 0x00DA: + case 0x00FA: + case 0x00DB: + case 0x00FB: + case 0x00DC: + case 0x00FC: + case 0x0168: + case 0x0169: + case 0x016A: + case 0x016B: + case 0x016C: + case 0x016D: + case 0x016E: + case 0x016F: + case 0x0170: + case 0x0171: + case 0x0172: + case 0x0173: + index = 20; + break; + case 'V': + case 'v': + index = 21; + break; + case 'W': + case 'w': + case 0x0174: + case 0x0175: + index = 22; + break; + case 'X': + case 'x': + index = 23; + break; + case 'Y': + case 'y': + case 0x00DD: + case 0x00FD: + case 0x00FF: + case 0x0176: + case 0x0177: + case 0x0178: + index = 24; + break; + case 'Z': + case 'z': + case 0x0179: + case 0x017A: + case 0x017B: + case 0x017C: + case 0x017D: + case 0x017E: + index = 25; + break; + case '0': + index = 26; + break; + case '1': + index = 27; + break; + case '2': + index = 28; + break; + case '3': + index = 29; + break; + case '4': + index = 30; + break; + case '5': + index = 31; + break; + case '6': + index = 32; + break; + case '7': + index = 33; + break; + case '8': + index = 34; + break; + case '9': + index = 35; + break; + case '!': + index = 36; + break; + case '?': + index = 37; + break; + case '.': + index = 38; + break; + case '-': + index = 39; + break; + case ':': + index = 40; + break; + case '%': + index = 41; + break; + case '#': + index = 42; + break; + case 161: + index = 43; + break; // upside-down ! + case ' ': + index = 44; + break; + default: + index = -1; + break; + } + return index; +} + +auto TextGraphics::GetBigCharIndex(int c) -> int { + int index; + if (c >= 'a' && c <= 'z') { + index = c - 'a'; + } else if (c >= 'A' && c <= 'Z') { + index = c - 'A'; + } else if (c >= '0' && c <= '9') { + index = c - '0' + 26; + } else { + switch (c) { + case '!': + index = 36; + break; + case '?': + index = 37; + break; + case '.': + index = 38; + break; + case '-': + index = 39; + break; + case ':': + index = 40; + break; + case '%': + index = 41; + break; + case '#': + index = 42; + break; + + case 192: + case 193: + case 194: + case 195: + case 196: + case 197: + case 198: + index = 'a' - 'a'; + break; + case 199: + index = 'c' - 'a'; + break; + case 200: + case 201: + case 202: + case 203: + index = 'e' - 'a'; + break; + case 204: + case 205: + case 206: + case 207: + index = 'i' - 'a'; + break; + case 208: + index = 'd' - 'a'; + break; + case 209: + index = 'n' - 'a'; + break; + case 210: + case 211: + case 212: + case 213: + case 216: + index = 'o' - 'a'; + break; + case 217: + case 218: + case 219: + case 220: + index = 'u' - 'a'; + break; + case 221: + index = 'y' - 'a'; + break; + case 224: + case 225: + case 226: + case 227: + case 228: + case 229: + case 230: + index = 'a' - 'a'; + break; + case 231: + index = 'c' - 'a'; + break; + case 232: + case 233: + case 234: + case 235: + index = 'e' - 'a'; + break; + case 236: + case 237: + case 238: + case 239: + index = 'i' - 'a'; + break; + case 240: + index = 'o' - 'a'; + break; + case 241: + index = 'n' - 'a'; + break; + case 242: + case 243: + case 244: + case 245: + case 246: + case 248: + index = 'o' - 'a'; + break; + case 249: + case 250: + case 251: + case 252: + index = 'u' - 'a'; + break; + case 253: + index = 'y' - 'a'; + break; + case 254: + index = 'p' - 'a'; + break; + case 255: + index = 'y' - 'a'; + break; + default: + index = -1; + } + } + return index; +} + +void TextGraphics::LoadGlyphPage(uint32_t index) { + std::lock_guard lock(glyph_load_mutex_); + + // Its possible someone else coulda loaded it since we last checked. + if (g_glyph_pages[index] == nullptr) { + char buffer[256]; + snprintf(buffer, sizeof(buffer), "ba_data/fonts/fontSmall%d.fdata", index); + FILE* f = g_platform->FOpen(buffer, "rb"); + BA_PRECONDITION(f); + BA_PRECONDITION(sizeof(TextGraphics::Glyph[2]) == sizeof(float[18])); + uint32_t total_size = sizeof(Glyph) * g_glyph_page_glyph_counts[index]; + g_glyph_pages[index] = static_cast(malloc(total_size)); + BA_PRECONDITION(g_glyph_pages[index]); + BA_PRECONDITION(fread(g_glyph_pages[index], total_size, 1, f) == 1); + fclose(f); + } +} + +void TextGraphics::GetFontPageCharRange(int page, uint32_t* first_char, + uint32_t* last_char) { + // Our special pages: + switch (page) { + case static_cast(FontPage::kOSRendered): { + // we allow the OS to render anything not in one of our glyph textures + // (technically this overlaps the private-use range which we use our own + // textures for, but that's handled as a special-case by + // TextGroup::setText + (*first_char) = kGlyphCount; + (*last_char) = kTextMaxUnicodeVal; // hmm what's the max unicode value we + // should ever see?.. + break; + } + case static_cast(FontPage::kExtras1): { + (*first_char) = 0xE000; + (*last_char) = (*first_char) + 24; + break; + } + case static_cast(FontPage::kExtras2): { + (*first_char) = 0xE000 + 25; + (*last_char) = (*first_char) + 24; + break; + } + case static_cast(FontPage::kExtras3): { + (*first_char) = 0xE000 + 50; + (*last_char) = (*first_char) + 24; + break; + } + case static_cast(FontPage::kExtras4): { + (*first_char) = 0xE000 + 75; + (*last_char) = (*first_char) + 24; + break; + } + default: { + assert(page < BA_GLYPH_PAGE_COUNT); + (*first_char) = g_glyph_page_start_index_map[page]; + (*last_char) = (*first_char) + g_glyph_page_glyph_counts[page] - 1; + break; + } + } +} + +void TextGraphics::GetFontPagesForText(const std::string& text, + std::set* font_pages) { + int last_page = -1; + std::vector unicode = Utils::UnicodeFromUTF8(text, "c03853"); + for (uint32_t val : unicode) { + int page; + + // Hack: allow showing euro even if we don't support unicode font rendering. + if (g_buildconfig.enable_os_font_rendering()) { + if (val == 8364) { + val = 0xE000; + } + } + + // For values in the custom-char range (U+E000–U+F8FF) we point at our own + // custom page(s) + if (val >= 0xE000 && val <= 0xF8FF) { + // The 25 chars after this are in our fontExtras sheet. + if (val < 0xE000 + 25) { + // Special value denoting our custom font page. + page = static_cast(FontPage::kExtras1); + } else if (val < 0xE000 + 50) { + // Special value denoting our custom font page. + page = static_cast(FontPage::kExtras2); + } else if (val < 0xE000 + 75) { + // Special value denoting our custom font page. + page = static_cast(FontPage::kExtras3); + } else if (val < 0xE000 + 100) { + // Special value denoting our custom font page. + page = static_cast(FontPage::kExtras4); + } else { + // We dont cover this.. just go with '?' + val = '?'; + page = g_glyph_map[val]; + } + } else if (val >= kGlyphCount) { + // Otherwise if its outside of our texture-coverage area. + if (g_buildconfig.enable_os_font_rendering()) { + page = static_cast(FontPage::kOSRendered); + } else { + val = '?'; + page = g_glyph_map[val]; + } + } else { + // yay we cover it! + page = g_glyph_map[val]; + } + // compare to lastPage to avoid doing a set insert for *everything* since + // most will be the same + if (page != last_page) { + (*font_pages).insert(page); + last_page = page; + } + } +} + +auto TextGraphics::HaveBigChars(const std::string& text) -> bool { + std::vector unicode = Utils::UnicodeFromUTF8(text, "fnc93rh"); + for (unsigned int val : unicode) { + if (GetBigGlyphIndex(val) == -1) { + // Don't count misses for newlines, spaces, etc. + if ((val != '\n') && (val != '\r')) { + return false; + } + } + } + return true; // Success! +} + +inline auto IsSpecialChar(uint32_t val) -> bool { + return (val >= 0xE000 && val < (0xE000 + 100)); +} + +auto TextGraphics::HaveChars(const std::string& text) -> bool { + if (g_buildconfig.enable_os_font_rendering()) { + return true; + } else { + std::vector unicode = Utils::UnicodeFromUTF8(text, "c957fj"); + for (auto&& val : unicode) { + // There's a few special chars we have. + if (val >= kGlyphCount && !IsSpecialChar(val)) { + return false; + } + } + return true; // Success! + } +} + +auto TextGraphics::GetGlyph(uint32_t val, bool big) -> TextGraphics::Glyph* { + if (big) { + int index = GetBigGlyphIndex(val); + if (index == -1) index = 37; // default to '?' + return &glyphs_big_[index]; + } else { + // Special case; if its in our custom range, handle it special. + if (IsSpecialChar(val)) { + return &glyphs_extras_[val - 0xE000]; + } else if (val >= kGlyphCount) { + return nullptr; + } + uint32_t page = g_glyph_map[val]; + uint32_t start_index = g_glyph_page_start_index_map[page]; + uint32_t local_index = val - start_index; + if (g_glyph_pages[page] == nullptr) { + LoadGlyphPage(page); + } + return &g_glyph_pages[page][local_index]; + } +} + +void TextGraphics::GetOSTextSpanBoundsAndWidth(const std::string& s, Rect* r, + float* width) { + assert(InGameThread()); + + // Asking the OS to calculate text bounds sounds expensive, + // so let's use a cache of recent results. + auto i = text_span_bounds_cache_map_.find(s); + if (i != text_span_bounds_cache_map_.end()) { + Object::Ref entry = i->second; + *r = entry->r; + *width = entry->width; + + // Send this entry to the back of the list since we used it. + text_span_bounds_cache_.erase(entry->list_iterator_); + + // I guess inspection doesn't realize entry lives on after this?... +#pragma clang diagnostic push +#pragma ide diagnostic ignored "UnusedValue" + entry->list_iterator_ = + text_span_bounds_cache_.insert(text_span_bounds_cache_.end(), entry); +#pragma clang diagnostic pop + return; + } + auto entry(Object::New()); + entry->string = s; + if (g_buildconfig.enable_os_font_rendering()) { + g_platform->GetTextBoundsAndWidth(s, &entry->r, &entry->width); + } else { + BA_LOG_ONCE( + "FIXME: GetOSTextSpanBoundsAndWidth unimplemented on this platform"); + r->l = 0.0f; + r->r = 1.0f; + r->t = 1.0f; + r->b = 0.0f; + *width = 1.0f; + } + entry->list_iterator_ = + text_span_bounds_cache_.insert(text_span_bounds_cache_.end(), entry); + entry->map_iterator_ = + text_span_bounds_cache_map_.insert(std::make_pair(s, entry)).first; + *r = entry->r; + *width = entry->width; + + // Keep cache from growing too large. + while (text_span_bounds_cache_.size() > 300) { + text_span_bounds_cache_map_.erase( + text_span_bounds_cache_.front()->map_iterator_); + text_span_bounds_cache_.pop_front(); + } +} + +auto TextGraphics::GetStringWidth(const char* text, bool big) -> float { + assert(Utils::IsValidUTF8(text)); + + // even if they ask for the big font, their string might not support it... + big = (big && TextGraphics::HaveBigChars(text)); + + float char_width = 32.0f; + const char* t = text; + float line_length = 0; + float max_line_length = 0; + + // We have the OS render some chars, broken into single-line spans. + std::vector os_span; + + while (*t != 0) { + if (*t == '\n') { + // Add/reset os-span. + if (!os_span.empty()) { + std::string s = Utils::UTF8FromUnicode(os_span); + line_length += GetOSTextSpanWidth(s); + os_span.clear(); + } + if (line_length > max_line_length) max_line_length = line_length; + line_length = 0; + t++; + } else { + uint32_t val = Utils::GetUTF8Value(t); + Utils::AdvanceUTF8(&t); + // Special case: if we're already doing an OS-span, tack certain + // chars onto it instead of switching back to glyph mode. + // (to reduce the number of times we switch back and forth) + if (TextGraphics::IsOSDrawableAscii(val) && !os_span.empty()) { + os_span.push_back(val); + } else if (Glyph* g = GetGlyph(val, big)) { + // If we *had* been building a span, add its length. + if (!os_span.empty()) { + std::string s = Utils::UTF8FromUnicode(os_span); + line_length += GetOSTextSpanWidth(s); + os_span.clear(); + } + line_length += char_width * g->advance; + } else { + // Add to os-span. + if (g_buildconfig.enable_os_font_rendering()) { + os_span.push_back(val); + } + } + } + } + // Tally final span if there is one. + if (!os_span.empty()) { + std::string s = Utils::UTF8FromUnicode(os_span); + line_length += GetOSTextSpanWidth(s); + os_span.clear(); + } + // Check last line. + if (line_length > max_line_length) { + max_line_length = line_length; + } + return max_line_length; +} + +auto TextGraphics::GetStringHeight(const char* text) -> float { + size_t str_size = strlen(text); + int char_val; + float y_offset = 0; + for (size_t i = 0; i < str_size; i++) { + char_val = ((unsigned char*)text)[i]; + if (char_val == '\n') y_offset += kTextRowHeight; + } + return y_offset + kTextRowHeight; +} + +void TextGraphics::BreakUpString(const char* text, float width, + std::vector* v) { + assert(Utils::IsValidUTF8(text)); + v->clear(); + std::vector buffer_(strlen(text) + 1); + char* buffer(&(buffer_[0])); + strcpy(buffer, text); // NOLINT + float char_width = 32.0f; + float line_length = 0; + const char* s_begin = buffer; + const char* t = buffer; + while (true) { + // If we hit a newline or string end, dump a string. + if (*t == '\n' || *t == 0) { + bool is_end = (*t == 0); + // So we can just use s_begin as a string. + *(char*)t = 0; // NOLINT hmmm this code is ugly + v->push_back(Utils::GetValidUTF8(s_begin, "gbus")); + line_length = 0.0f; + if (is_end) { + break; // done! + } else { + t++; + s_begin = t; + } + } else { + if (*t == 0) throw Exception(); + uint32_t val = Utils::GetUTF8Value(t); + Utils::AdvanceUTF8(&t); + + // Special case: if we're already doing an OS-span, tack certain + // chars onto it instead of switching back to glyph mode. + // (to reduce the number of times we switch back and forth). + // NOLINTNEXTLINE(bugprone-branch-clone) + if (TextGraphics::IsOSDrawableAscii(val) && explicit_bool(false)) { + // I think I disabled this for consistency?... + // FIXME FIXME FIXME - handle this along with stuff below.. + } else if (Glyph* g = GetGlyph(val, false)) { + line_length += char_width * g->advance; + } else { + // FIXME FIXME FIXME - need to clump non-glyph characters into + // spans and use OS text stuff to get their lengths. + } + + // If this char puts us over the width, clip a line. + if (line_length > width) { + line_length = 0.0f; + char tmp = *t; + *(char*)t = 0; // NOLINT temp for string copy + v->push_back(Utils::GetValidUTF8(s_begin, "gbus2")); + *(char*)t = tmp; // NOLINT + s_begin = t; + } + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/text/text_graphics.h b/src/ballistica/graphics/text/text_graphics.h new file mode 100644 index 00000000..aca2e9f2 --- /dev/null +++ b/src/ballistica/graphics/text/text_graphics.h @@ -0,0 +1,111 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_TEXT_TEXT_GRAPHICS_H_ +#define BALLISTICA_GRAPHICS_TEXT_TEXT_GRAPHICS_H_ + +#include +#include +#include +#include +#include +#include + +#include "ballistica/core/object.h" +#include "ballistica/math/rect.h" + +namespace ballistica { + +// Largest unicode value we ask the OS to draw for us. +const int kTextMaxUnicodeVal = 999999; +const float kTextRowHeight = 32.0f; + +// Encapsulates text-display functionality used by the game thread. +class TextGraphics { + public: + static void Init(); + + TextGraphics(); + + enum class FontPage { + kOSRendered = 9989, + kExtras1 = 9990, + kExtras2 = 9991, + kExtras3 = 9992, + kExtras4 = 9993 + }; + + struct Glyph { + float pen_offset_x; + float pen_offset_y; + float advance; + float x_size; + float y_size; + float tex_min_x; + float tex_min_y; + float tex_max_x; + float tex_max_y; + }; + + static auto GetBigCharIndex(int c) -> int; + + // Returns a glyph or nullptr if it is unavailable. + auto GetGlyph(uint32_t value, bool big) -> Glyph*; + static auto HaveBigChars(const std::string& string) -> bool; + static auto HaveChars(const std::string& string) -> bool; + void GetFontPagesForText(const std::string& text, std::set* font_pages); + void GetFontPageCharRange(int page, uint32_t* first_char, + uint32_t* last_char); + auto GetOSTextSpanWidth(const std::string& s) -> float { + Rect r; + float width; + GetOSTextSpanBoundsAndWidth(s, &r, &width); + return width; + } + void GetOSTextSpanBoundsAndWidth(const std::string& s, Rect* r, float* width); + + // Returns the width of a string + auto GetStringWidth(const char* s, bool big = false) -> float; + auto GetStringWidth(const std::string& s, bool big = false) -> float { + return GetStringWidth(s.c_str(), big); + } + + // Returns the height of a string + auto GetStringHeight(const char* s) -> float; + auto GetStringHeight(const std::string& s) -> float { + return GetStringHeight(s.c_str()); + } + + // Given a target width, breaks the string up into multiple strings so they + // fit within it + void BreakUpString(const char* text, float width, + std::vector* v); + + // Some chars we allow the OS to draw in some cases but draw ourselves in + // others (to minimize the amount of switching back and forth). + static auto IsOSDrawableAscii(int val) -> bool { + // ( exclude a few that usually come in pairs so we + // avoid one side looking different than the other ) + return (((val >= ' ' && val <= '/') || (val >= ':' && val <= '@') + || (val >= '[' && val <= '`') || (val >= '{' && val <= '~')) + && (val != '\'') && (val != '"') && (val != '[') && (val != ']') + && (val != '{') && (val != '}') && (val != '(') && (val != ')')); + } + + private: + class TextSpanBoundsCacheEntry; + void LoadGlyphPage(uint32_t index); + + // Map of entries for fast lookup. + std::map > + text_span_bounds_cache_map_; + + // List of entries for sorting by last-use-time + std::list > text_span_bounds_cache_; + std::mutex glyph_load_mutex_; + Glyph glyphs_extras_[100]{}; + Glyph glyphs_big_[64]{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_TEXT_TEXT_GRAPHICS_H_ diff --git a/src/ballistica/graphics/text/text_group.cc b/src/ballistica/graphics/text/text_group.cc new file mode 100644 index 00000000..0b5d7947 --- /dev/null +++ b/src/ballistica/graphics/text/text_group.cc @@ -0,0 +1,324 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/text/text_group.h" + +#include +#include +#include + +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/text/text_graphics.h" +#include "ballistica/graphics/text/text_packer.h" + +namespace ballistica { + +void TextGroup::SetText(const std::string& text, TextMesh::HAlign alignment_h, + TextMesh::VAlign alignment_v, bool big, + float resolution_scale) { + text_ = text; + + // In order to *actually* draw big, all our letters + // must be available in the big font. + big_ = (big && TextGraphics::HaveBigChars(text)); + + // If we had an OS texture for custom drawing, release it. + // (it should stick around for a while; we'll be able to re-grab + // the same one if we havn't changed) + os_texture_.Clear(); + + // If we're drawing big we always just need 1 font page (the big one). + if (big_) { + // Now create entries for each page we use. + entries_.clear(); + std::unique_ptr entry(new TextMeshEntry()); + entry->u_scale = entry->v_scale = 1.5f; + entry->can_color = true; + entry->max_flatness = 1.0f; + entry->mesh.SetText(text, alignment_h, alignment_v, true, 0, 65535, + TextMeshEntryType::kRegular, nullptr); + entry->tex = g_media->GetTexture(SystemTextureID::kFontBig); + entries_.push_back(std::move(entry)); + + } else { + // Drawing non-big; we might use any number of font pages. + + // First, calc which font pages we'll need to draw this text. + std::set font_pages; + g_text_graphics->GetFontPagesForText(text, &font_pages); + + // Now create entries for each page we use. + // (we iterate this in reverse so that our custom pages draw first; + // we want that stuff to show up underneath normal text since we + // sometimes use it as backing elements,etc) + entries_.clear(); + for (auto i = font_pages.rbegin(); i != font_pages.rend(); i++) { + uint32_t min, max; + g_text_graphics->GetFontPageCharRange(*i, &min, &max); + std::unique_ptr entry(new TextMeshEntry()); + + // Our custom font page IDs start at value 9990 (kExtras1); + // make sure for all private-use unicode chars (U+E000–U+F8FF) + // that we only use these font pages and not OS rendering or other + // pages (even if those technically support that range) + if (*i >= static_cast(TextGraphics::FontPage::kExtras1)) { + entry->type = TextMeshEntryType::kExtras; + entry->u_scale = entry->v_scale = 3.0f; + entry->max_flatness = 1.0f; + } else if (*i == static_cast(TextGraphics::FontPage::kOSRendered)) { + entry->type = TextMeshEntryType::kOSRendered; + + // Let's allow partial flattening of OS text (keeps emojis somewhat + // recognizable but allows us to set it apart from our icons and + // whatnot which are always full color) + // entry->max_flatness = 0.65f; + + // UPDATE: scratch that; washed out emojis just look crappy. + entry->max_flatness = 0.0f; + + // We'll set uv_scale for this guy below; we don't know what it is + // until we've generated our text-packer. + } else { + entry->type = TextMeshEntryType::kRegular; + entry->u_scale = entry->v_scale = 1.0f; + entry->max_flatness = 1.0f; + } + + // Currently we can color or flatten everything except the second, third, + // and fourth extras pages (those are all pre-colored characters; + // flattening or coloring would mess them up) + entry->can_color = + ((*i != static_cast(TextGraphics::FontPage::kExtras2)) + && (*i != static_cast(TextGraphics::FontPage::kExtras3)) + && (*i != static_cast(TextGraphics::FontPage::kExtras4))); + + // For the few we can't color, we don't want to be able to + // flatten them either. + if (!entry->can_color) entry->max_flatness = 0.0f; + + // For OS-rendered text we fill out a text-packer will all the spans + // we'll need. we then hand that over to the OS to draw and create + // our texture from that. + Object::Ref packer; + if (entry->type == TextMeshEntryType::kOSRendered) { + packer = Object::New(resolution_scale); + } + + entry->mesh.SetText(text, alignment_h, alignment_v, false, min, max, + entry->type, packer.get()); + + if (packer.exists()) { + // If we made a text-packer, we need to fetch/generate a texture + // that matches it. + // There should only ever be one of these. + assert(!os_texture_.exists()); + { + Media::MediaListsLock lock; + os_texture_ = g_media->GetTextureData(packer.get()); + } + + // We also need to know what uv-scales to use for shadows/etc. + // This should be proportional to the font-scale over the texture + // dimension so that its always visually similar. + float t_scale = packer->text_scale() * 500.0f; + entry->u_scale = t_scale / static_cast(packer->texture_width()); + entry->v_scale = t_scale / static_cast(packer->texture_height()); + } + switch (*i) { + case 0: + entry->tex = g_media->GetTexture(SystemTextureID::kFontSmall0); + break; + case 1: + entry->tex = g_media->GetTexture(SystemTextureID::kFontSmall1); + break; + case 2: + entry->tex = g_media->GetTexture(SystemTextureID::kFontSmall2); + break; + case 3: + entry->tex = g_media->GetTexture(SystemTextureID::kFontSmall3); + break; + case 4: + entry->tex = g_media->GetTexture(SystemTextureID::kFontSmall4); + break; + case 5: + entry->tex = g_media->GetTexture(SystemTextureID::kFontSmall5); + break; + case 6: + entry->tex = g_media->GetTexture(SystemTextureID::kFontSmall6); + break; + case 7: + entry->tex = g_media->GetTexture(SystemTextureID::kFontSmall7); + break; + case static_cast(TextGraphics::FontPage::kOSRendered): + entry->tex = os_texture_; + break; + case static_cast(TextGraphics::FontPage::kExtras1): + entry->tex = g_media->GetTexture(SystemTextureID::kFontExtras); + break; + case static_cast(TextGraphics::FontPage::kExtras2): + entry->tex = g_media->GetTexture(SystemTextureID::kFontExtras2); + break; + case static_cast(TextGraphics::FontPage::kExtras3): + entry->tex = g_media->GetTexture(SystemTextureID::kFontExtras3); + break; + case static_cast(TextGraphics::FontPage::kExtras4): + entry->tex = g_media->GetTexture(SystemTextureID::kFontExtras4); + break; + default: + throw Exception(); + } + entries_.push_back(std::move(entry)); + } + } +} + +void TextGroup::GetCaratPts(const std::string& text_in, + TextMesh::HAlign alignment_h, + TextMesh::VAlign alignment_v, int carat_position, + float* carat_x, float* carat_y) { + assert(carat_x && carat_y); + assert(Utils::IsValidUTF8(text_in)); + const char* txt = text_in.c_str(); + float x = 0; + float x_offset; + float y_offset; + x_offset = x; + float char_width{32.0}; + uint32_t char_val; + float row_height = kTextRowHeight; + float line_length; + float l{0.0f}; + float r{0.0f}; + float b{0.0f}; + float t{0.0f}; + float text_height; + float char_offset_h{-3.0f}; + float char_offset_v{-3.0f}; + + // Calc the height of the text where needed. + switch (alignment_v) { + case TextMesh::VAlign::kNone: + case TextMesh::VAlign::kTop: + text_height = 0; // Not used here. + break; + case TextMesh::VAlign::kCenter: + case TextMesh::VAlign::kBottom: { + int rows = 1; + for (const char* c = txt; *c != 0; c++) { + if (*c == '\n') rows++; + } + text_height = static_cast(rows) * row_height; + break; + } + default: + throw Exception(); + } + switch (alignment_v) { + case TextMesh::VAlign::kNone: + y_offset = b + char_offset_v; + break; + case TextMesh::VAlign::kTop: + y_offset = b + char_offset_v + (t - b) - row_height; + break; + case TextMesh::VAlign::kCenter: + y_offset = + b + char_offset_v + ((t - b) / 2) + (text_height / 2) - row_height; + break; + case TextMesh::VAlign::kBottom: + y_offset = b + char_offset_v + text_height - row_height; + break; + default: + throw Exception(); + } + const char* tc = txt; + bool first_char = true; + std::vector line; + int char_num = 0; + while (*tc != 0) { + const char* tv_prev = tc; + char_val = Utils::GetUTF8Value(tc); + Utils::AdvanceUTF8(&tc); + + // Reset alignment on new lines. + if (first_char || char_val == '\n') { + switch (alignment_h) { + case TextMesh::HAlign::kLeft: + x_offset = l + char_offset_h; + line.clear(); + break; + case TextMesh::HAlign::kCenter: + case TextMesh::HAlign::kRight: { + // Find the length of this line. + line_length = 0; + const char* c; + + // If this was the first char, include it in this line tally + // if it was a newline, don't. + if (first_char) { + c = tv_prev; + } else { + c = tc; + } + while (true) { + // Note Sept 2019: this was set to uint8_t. Assuming that was an + // accident? + uint32_t val; + if (*c == 0) { // NOLINT(bugprone-branch-clone) + break; + } else if (*c == '\n') { + break; + } else { + val = Utils::GetUTF8Value(c); + Utils::AdvanceUTF8(&c); + + // Special case: if we're already doing an OS-span, tack certain + // chars onto it instead of switching back to glyph mode. + // (to reduce the number of times we switch back and forth) + if (TextGraphics::Glyph* g = + g_text_graphics->GetGlyph(val, big_)) { + line_length += char_width * g->advance; + } else { + // TODO(ericf): add non-glyph chars into spans and ask + // the OS for their length + } + } + } + if (alignment_h == TextMesh::HAlign::kCenter) { + x_offset = l + char_offset_h + ((r - l) / 2) - (line_length / 2); + line.clear(); + } else { + x_offset = l + char_offset_h + (r - l) - line_length; + line.clear(); + } + break; + } + default: + throw Exception(); + } + first_char = false; + } + switch (char_val) { + case '\n': + y_offset -= row_height; + break; + case '\r': + case ' ': + break; + default: { + } + } + if (carat_position == char_num) { + break; + } + if (char_val != '\n') { + line.push_back(char_val); + } + char_num++; + } + *carat_x = + x_offset + + g_text_graphics->GetStringWidth(Utils::UTF8FromUnicode(line).c_str()); + *carat_y = y_offset; +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/text/text_group.h b/src/ballistica/graphics/text/text_group.h new file mode 100644 index 00000000..a9756e25 --- /dev/null +++ b/src/ballistica/graphics/text/text_group.h @@ -0,0 +1,85 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_TEXT_TEXT_GROUP_H_ +#define BALLISTICA_GRAPHICS_TEXT_TEXT_GROUP_H_ + +#include +#include +#include + +#include "ballistica/core/object.h" +#include "ballistica/graphics/mesh/text_mesh.h" +#include "ballistica/media/data/texture_data.h" +#include "ballistica/media/media.h" + +namespace ballistica { + +// encapsulates the multiple meshes and textures necessary to +// draw arbitrary text. To actually draw the text, iterate over the meshes +// and textures this class provides to you, drawing each in the same manner +class TextGroup : public Object { + public: + // the number of meshes needing to be drawn for this text + auto GetElementCount() -> int { return static_cast(entries_.size()); } + auto GetElementMesh(int index) const -> TextMesh* { + assert(index < static_cast(entries_.size())); + return &(entries_[index]->mesh); + } + auto GetElementTexture(int index) const -> TextureData* { + assert(index < static_cast(entries_.size())); + return entries_[index]->tex.get(); + } + // if you are doing any shader effects in UV-space (such as drop-shadows), + // scale them by this ..this will account for different character sheets + // with different sized characters + auto GetElementUScale(int index) -> float { + assert(index < static_cast(entries_.size())); + return entries_[index]->u_scale; + } + auto GetElementVScale(int index) -> float { + assert(index < static_cast(entries_.size())); + return entries_[index]->v_scale; + } + auto GetElementMaxFlatness(int index) const -> float { + assert(index < static_cast(entries_.size())); + return entries_[index]->max_flatness; + } + auto GetElementCanColor(int index) const -> bool { + assert(index < static_cast(entries_.size())); + return entries_[index]->can_color; + } + auto GetElementMaskUV2Texture(int index) const -> TextureData* { + assert(index < static_cast(entries_.size())); + return g_media->GetTexture(entries_[index]->type + == TextMeshEntryType::kOSRendered + ? SystemTextureID::kSoftRect2 + : SystemTextureID::kSoftRect); + } + void SetText(const std::string& text, + TextMesh::HAlign alignment_h = TextMesh::HAlign::kLeft, + TextMesh::VAlign alignment_v = TextMesh::VAlign::kNone, + bool big = false, float resolution_scale = 1.0f); + auto getText() const -> const std::string& { return text_; } + void GetCaratPts(const std::string& text_in, TextMesh::HAlign alignment_h, + TextMesh::VAlign alignment_v, int carat_pos, float* carat_x, + float* carat_y); + + private: + struct TextMeshEntry { + TextMeshEntryType type; + Object::Ref tex; + TextMesh mesh; + float u_scale; + float v_scale; + bool can_color; + float max_flatness; + }; + Object::Ref os_texture_; + std::vector> entries_; + std::string text_; + bool big_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_TEXT_TEXT_GROUP_H_ diff --git a/src/ballistica/graphics/text/text_packer.cc b/src/ballistica/graphics/text/text_packer.cc new file mode 100644 index 00000000..384d1f46 --- /dev/null +++ b/src/ballistica/graphics/text/text_packer.cc @@ -0,0 +1,205 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/text/text_packer.h" + +#include + +namespace ballistica { + +TextPacker::TextPacker(float resolution_scale) + : resolution_scale_{resolution_scale} {} + +TextPacker::~TextPacker() = default; + +void TextPacker::AddSpan(const std::string& text, float x, float y, + const Rect& bounds) { + spans_.emplace_back(); + Span& s(spans_.back()); + s.string = text; + s.x = x; + s.y = y; + s.u_min = 0.0f; + s.u_max = 1.0f; + s.v_min = 0.0f; + s.v_max = 1.0f; + s.bounds = bounds; +} + +// FIXME - we currently run into minor problems because we measure our text +// bounds at one size and then scale that linearly when trying to fit things +// into the texture. However, fonts don't always scale linearly (and even when +// that's an option it can be expensive). + +void TextPacker::compile() { + assert(!compiled_); + if (spans_.empty()) { + compiled_ = true; + return; + } + float max_width = 2048.0; + float max_height = 2048.0; + float width = 32.0; + float height = 32.0; + float scale = resolution_scale_ * 2.0f; + float span_buffer = 3.0f; // Note: buffer scales along with text. + float widest_unscaled_span_width = 0.0f; + + // Find our widest span width; we'll use this to determine the width + // of the texture (and whether we need to scale our text down to fit). + for (auto& span : spans_) { + float w = span.bounds.width() + 2.0f * span_buffer; + if (w > widest_unscaled_span_width) widest_unscaled_span_width = w; + } + + // Ok, lets crank our width up until its a bit wider than the widest span + // width (should hopefully allow for at least a few spans per line in + // general). + while (width < (widest_unscaled_span_width * scale * 1.2f) + && width < max_width) { + width *= 2; + } + + // Alternately, if we're too big, crank our scale down so that our widest span + // fits. + if (widest_unscaled_span_width * scale > width * 0.9f) { + scale *= ((width * 0.9f) / (widest_unscaled_span_width * scale)); + } + float start_height = height; + int mini_shrink_tries = 0; + + // Ok; we've now locked in a width and scale. + // Now we go through and position our spans. + // We may need to do this more than once if our height comes out too big. + // (hopefully this will never be a problem in practice) + while (true) { + height = start_height; + + // We currently just lay out left-to-right, top-to-bottom. + // This could be somewhat wasteful in particular configurations. + // (leaving half-filled lines, etc) so it might be worth improving later. + float widest_fill_right = 0.0f; + float fill_right = 0.0f; + float fill_bottom = 0.0f; + float line_height = 0.0f; + for (auto&& i : spans_) { + float span_width = (i.bounds.width() + 2.0f * span_buffer) * scale; + float span_height = + (std::abs(i.bounds.height()) + 2.0f * span_buffer) * scale; + + // Start a new line if this would put us past the end. + if (fill_right + span_width > width) { + if (fill_right > widest_fill_right) { + widest_fill_right = fill_right; // Keep track of how far over we go. + } + fill_right = 0.0f; + fill_bottom += line_height; + line_height = 0.0f; + } + + // Position x such that x + left bound - buffer lines up with our current + // right point. + float to_left = (i.bounds.l - span_buffer) * scale; + i.tex_x = fill_right - to_left; + fill_right += span_width; + + // Position y such that y - top bound - buffer lines up with our current + // bottom point. + float to_top = (-i.bounds.t - span_buffer) * scale; + i.tex_y = fill_bottom - to_top; + + // If our total height is greater than the current line height, expand the + // line's. + if (span_height > line_height) { + line_height = span_height; + } + + // Increase height if need be. + while ((fill_bottom + line_height) > height) { + height *= 2; + } + } + if (fill_right > widest_fill_right) widest_fill_right = fill_right; + + float mini_shrink_threshold_h = 0.55f; + float mini_shrink_threshold_v = 0.55f; + + if (height > max_height) { + // If it doesn't fit, repeat again with a smaller scale until it does. + + // Dropping our scale has a disproportional effect on the final height + // (since it opens up more relative horizontal space). + // I'm not sure how to figure out how much to drop by other than + // incrementally dropping values until we fit. + scale *= 0.75f; + + } else if (((widest_fill_right < (width * mini_shrink_threshold_h) + && width > 16) + || fill_bottom + line_height + < (height * mini_shrink_threshold_v)) + && mini_shrink_tries < 3) { + // If we're here it means we *barely* use more than half of the texture in + // one direction or the other; let's shrink just a tiny bit and we should + // be able to chop our texture size in half + if (widest_fill_right < width * mini_shrink_threshold_h && width > 16) { + float scale_val = 0.99f * (((width * 0.5f) / widest_fill_right)); + if (scale_val < 1.0f) { + // FIXME - should think about a fixed multiplier here; + // under the hood the system might be caching glyphs based on scale + // and this would leave us with fewer different scales in the end and + // thus better caching performance + scale *= scale_val; + } + width /= 2; + } else { + float scale_val = 0.99f * (height * 0.5f) / (fill_bottom + line_height); + if (scale_val < 1.0f) { + // FIXME - should think about a fixed multiplier here; + // under the hood the system might be caching glyphs based on scale + // and this would leave us with fewer different scales in the end and + // thus better caching performance + scale *= scale_val; + } + } + mini_shrink_tries += 1; + } else { + // we fit; hooray! + break; + } + } + + // Lastly, now that our texture width and height are completely finalized, we + // can calculate UVs. + for (auto&& i : spans_) { + // Now store uv coords for this span; they should include the buffer. + i.u_min = (i.tex_x + (i.bounds.l - span_buffer) * scale) / width; + i.u_max = (i.tex_x + (i.bounds.r + span_buffer) * scale) / width; + i.v_max = (i.tex_y + (-i.bounds.b + span_buffer) * scale) / height; + i.v_min = (i.tex_y + (-i.bounds.t - span_buffer) * scale) / height; + + // Also calculate draw-bounds which accounts for our buffer. + i.draw_bounds.l = (i.bounds.l - span_buffer); + i.draw_bounds.r = (i.bounds.r + span_buffer); + i.draw_bounds.t = (i.bounds.t + span_buffer); + i.draw_bounds.b = (i.bounds.b - span_buffer); + } + + // TODO(ericf): now we calculate a hash that's unique to this text + // configuration; we'll use that as a key for the texture we'll generate/use. + // ..this way multiple meshes can share the same generated texture. + // *technically* we could calculate this hash and check for an existing + // texture before we bother laying out our spans, but that might not save us + // much time and would complicate things. + hash_ = std::to_string(resolution_scale_); + for (auto&& i : spans_) { + char buffer[64]; + snprintf(buffer, sizeof(buffer), "!SP!%f|%f|", i.x, i.y); + hash_ += buffer; + hash_ += i.string; + } + texture_width_ = static_cast(width); + texture_height_ = static_cast(height); + text_scale_ = scale; + compiled_ = true; +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/text/text_packer.h b/src/ballistica/graphics/text/text_packer.h new file mode 100644 index 00000000..0a8d0509 --- /dev/null +++ b/src/ballistica/graphics/text/text_packer.h @@ -0,0 +1,85 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_TEXT_TEXT_PACKER_H_ +#define BALLISTICA_GRAPHICS_TEXT_TEXT_PACKER_H_ + +#include +#include +#include + +#include "ballistica/core/object.h" +#include "ballistica/math/rect.h" + +namespace ballistica { + +class TextPacker : public Object { + public: + explicit TextPacker(float resolution_scale); + ~TextPacker() override; + + // Adds a span. We could calculate bounds ourselves, but it's often needed + // outside of here anyway so might as well recycle. + void AddSpan(const std::string& text, float x, float y, const Rect& bounds); + + auto hash() const -> const std::string& { + assert(compiled_); + return hash_; + } + + struct Span { + std::vector unichars; + std::string string; + + // Position to draw this span at. + float x; + float y; + + // Bounds to draw this span with. + Rect draw_bounds; + + // Texture position to draw this span's text at. + float tex_x; + float tex_y; + + // Text-space bounds. + Rect bounds; + float u_min; + float u_max; + float v_min; + float v_max; + }; + + // Once done adding spans, call this to calculate final span UV values, + // texture configuration, and hash. + void compile(); + + auto spans() const -> const std::list& { return spans_; } + + auto texture_width() const -> int { + assert(compiled_); + return texture_width_; + } + + auto texture_height() const -> int { + assert(compiled_); + return texture_height_; + } + + auto text_scale() const -> float { + assert(compiled_); + return text_scale_; + } + + private: + float resolution_scale_; + int texture_width_{}; + int texture_height_{}; + float text_scale_{}; + std::string hash_; + bool compiled_{false}; + std::list spans_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_TEXT_TEXT_PACKER_H_ diff --git a/src/ballistica/graphics/texture/dds.cc b/src/ballistica/graphics/texture/dds.cc new file mode 100644 index 00000000..b3748ba2 --- /dev/null +++ b/src/ballistica/graphics/texture/dds.cc @@ -0,0 +1,148 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/texture/dds.h" + +#include + +#include "ballistica/ballistica.h" +#include "ballistica/platform/platform.h" + +#if BA_ENABLE_OPENGL + +/* DDS loader written by Jon Watte 2002 */ +/* Permission granted to use freely, as long as Jon Watte */ +/* is held harmless for all possible damages resulting from */ +/* your use or failure to use this code. */ +/* No warranty is expressed or implied. Use at your own risk, */ +/* or not at all. */ + +namespace ballistica { + +// Should tidy this up to use unsigned vals but don't want to touch for now. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" + +struct DdsLoadInfo { + bool compressed; + bool swap; + bool palette; + unsigned int divSize; + unsigned int blockBytes; + TextureFormat internal_format; + int externalFormat; + int type; +}; + +DdsLoadInfo loadInfoDXT1 = {true, false, false, 4, 8, TextureFormat::kDXT1}; +DdsLoadInfo loadInfoDXT5 = {true, false, false, 4, 16, TextureFormat::kDXT5}; +DdsLoadInfo loadInfoETC1 = {true, false, false, 4, 8, TextureFormat::kETC1}; + +void LoadDDS(const std::string& file_name, unsigned char** buffers, int* widths, + int* heights, TextureFormat* formats, size_t* sizes, + TextureQuality texture_quality, int min_quality, int* base_level) { + (*base_level) = 0; + + FILE* f = g_platform->FOpen(file_name.c_str(), "rb"); + if (!f) throw Exception("can't open file: \"" + file_name + "\""); + + DDS_header hdr{}; + + // DDS is so simple to read, too + BA_PRECONDITION(fread(&hdr, sizeof(hdr), 1, f) == 1); + BA_PRECONDITION(hdr.dwMagic == DDS_MAGIC); + BA_PRECONDITION(hdr.dwSize == 124); + + if (hdr.dwMagic != DDS_MAGIC || hdr.dwSize != 124 + || !(hdr.dwFlags & DDSD_PIXELFORMAT) || !(hdr.dwFlags & DDSD_CAPS)) { + throw Exception("invalid DDS file: \"" + file_name + "\""); + } + + int x_size = static_cast_check_fit(hdr.dwWidth); + int y_size = static_cast_check_fit(hdr.dwHeight); + + BA_PRECONDITION(!(x_size & (x_size - 1))); + BA_PRECONDITION(!(y_size & (y_size - 1))); + + DdsLoadInfo* li; + + if (PF_IS_DXT1(hdr.sPixelFormat)) { + li = &loadInfoDXT1; + } else if (PF_IS_DXT5(hdr.sPixelFormat)) { + li = &loadInfoDXT5; + } else if (PF_IS_EXTENDED(hdr.sPixelFormat)) { + DDS_header_DX10 hExt; + + BA_PRECONDITION(fread(&hExt, sizeof(hExt), 1, f) == 1); + + // Format should be unknown. + // Hmmm we have no way of determining that this is etc1 data so we just + // assume.. ew. + BA_PRECONDITION(hExt.dxgiFormat == 0); + + // Dimension should be tex2d(3). + BA_PRECONDITION(hExt.resourceDimension == 3); + BA_PRECONDITION(hExt.arraySize == 1); + + li = &loadInfoETC1; + + } else { + throw Exception("Unsupported data type in DDS file \"" + file_name + "\""); + } + + auto x = static_cast(x_size); + auto y = static_cast(y_size); + + int mip_map_count = (hdr.dwFlags & DDSD_MIPMAPCOUNT) + ? static_cast(hdr.dwMipMapCount) + : 1; + + // try dropping a level for med/low quality.. + if ((texture_quality == TextureQuality::kLow + || texture_quality == TextureQuality::kMedium) + && (min_quality < 2) && mip_map_count >= (*base_level) + 1) + (*base_level)++; + + // and one more for low in some cases.... + if (texture_quality == TextureQuality::kLow && (min_quality < 1) + && (x_size > 128) && (y_size > 128) && mip_map_count >= (*base_level) + 1) + (*base_level)++; + + if (li->compressed) { + size_t size = std::max(li->divSize, x) / li->divSize + * std::max(li->divSize, y) / li->divSize * li->blockBytes; + + for (int ix = 0; ix < mip_map_count; ++ix) { + // Load or skip levels depending on our quality. + if ((*base_level) <= ix) { + sizes[ix] = static_cast(size); + buffers[ix] = (unsigned char*)malloc(size); + BA_PRECONDITION(buffers[ix]); + widths[ix] = static_cast(x); + heights[ix] = static_cast(y); + formats[ix] = li->internal_format; + BA_PRECONDITION(fread(buffers[ix], size, 1, f) == 1); + } else { + buffers[ix] = nullptr; + BA_PRECONDITION(fseek(f, static_cast_check_fit(size), // NOLINT + SEEK_CUR) + == 0); + } + + x = (x + 1u) >> 1u; + y = (y + 1u) >> 1u; + size = std::max(li->divSize, x) / li->divSize * std::max(li->divSize, y) + / li->divSize * li->blockBytes; + } + } else if (li->palette) { + throw Exception("palette support disabled"); + } else { + throw Exception("regular tex dds support disabled"); + } + fclose(f); +} + +#pragma clang diagnostic pop + +} // namespace ballistica + +#endif // BA_ENABLE_OPENGL diff --git a/src/ballistica/graphics/texture/dds.h b/src/ballistica/graphics/texture/dds.h new file mode 100644 index 00000000..4d7a050d --- /dev/null +++ b/src/ballistica/graphics/texture/dds.h @@ -0,0 +1,157 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_TEXTURE_DDS_H_ +#define BALLISTICA_GRAPHICS_TEXTURE_DDS_H_ + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "OCUnusedMacroInspection" + +/* DDS loader written by Jon Watte 2002 */ +/* Permission granted to use freely, as long as Jon Watte */ +/* is held harmless for all possible damages resulting from */ +/* your use or failure to use this code. */ +/* No warranty is expressed or implied. Use at your own risk, */ +/* or not at all. */ + +#include + +#include "ballistica/ballistica.h" + +#if BA_ENABLE_OPENGL + +// little-endian, of course +#define DDS_MAGIC 0x20534444 + +// DDS_header.dwFlags +#define DDSD_CAPS 0x00000001 +#define DDSD_HEIGHT 0x00000002 +#define DDSD_WIDTH 0x00000004 +#define DDSD_PITCH 0x00000008 +#define DDSD_PIXELFORMAT 0x00001000 +#define DDSD_MIPMAPCOUNT 0x00020000 +#define DDSD_LINEARSIZE 0x00080000 +#define DDSD_DEPTH 0x00800000 + +// DDS_header.sPixelFormat.dwFlags +#define DDPF_ALPHAPIXELS 0x00000001 +#define DDPF_FOURCC 0x00000004 +#define DDPF_INDEXED 0x00000020 +#define DDPF_RGB 0x00000040 + +// DDS_header.sCaps.dwCaps1 +#define DDSCAPS_COMPLEX 0x00000008 +#define DDSCAPS_TEXTURE 0x00001000 +#define DDSCAPS_MIPMAP 0x00400000 + +// DDS_header.sCaps.dwCaps2 +#define DDSCAPS2_CUBEMAP 0x00000200 +#define DDSCAPS2_CUBEMAP_POSITIVEX 0x00000400 +#define DDSCAPS2_CUBEMAP_NEGATIVEX 0x00000800 +#define DDSCAPS2_CUBEMAP_POSITIVEY 0x00001000 +#define DDSCAPS2_CUBEMAP_NEGATIVEY 0x00002000 +#define DDSCAPS2_CUBEMAP_POSITIVEZ 0x00004000 +#define DDSCAPS2_CUBEMAP_NEGATIVEZ 0x00008000 +#define DDSCAPS2_VOLUME 0x00200000 + +#define D3DFMT_DXT1 0x31545844 // '1TXD' - DXT1 compression texture format +#define D3DFMT_DXT2 0x32545844 // '2TXD' - DXT2 compression texture format +#define D3DFMT_DXT3 0x33545844 // '3TXD' - DXT3 compression texture format +#define D3DFMT_DXT4 0x34545844 // '4TXD' - DXT4 compression texture format +#define D3DFMT_DXT5 0x35545844 // '5TXD' - DXT5 compression texture format + +#define D3DFMT_EXTENDED 0x30315844 // '01XD' - newer dds format + +#define PF_IS_EXTENDED(pf) \ + (((pf).dwFlags & DDPF_FOURCC) && ((pf).dwFourCC == D3DFMT_EXTENDED)) + +#define PF_IS_DXT1(pf) \ + (((pf).dwFlags & DDPF_FOURCC) && ((pf).dwFourCC == D3DFMT_DXT1)) + +#define PF_IS_DXT3(pf) \ + (((pf).dwFlags & DDPF_FOURCC) && ((pf).dwFourCC == D3DFMT_DXT3)) + +#define PF_IS_DXT5(pf) \ + (((pf).dwFlags & DDPF_FOURCC) && ((pf).dwFourCC == D3DFMT_DXT5)) + +#define PF_IS_BGRA8(pf) \ + (((pf).dwFlags & DDPF_RGB) && ((pf).dwFlags & DDPF_ALPHAPIXELS) \ + && ((pf).dwRGBBitCount == 32) && ((pf).dwRBitMask == 0xff0000) \ + && ((pf).dwGBitMask == 0xff00) && ((pf).dwBBitMask == 0xff) \ + && ((pf).dwAlphaBitMask == 0xff000000U)) + +#define PF_IS_BGR8(pf) \ + (((pf).dwFlags & DDPF_ALPHAPIXELS) && !((pf).dwFlags & DDPF_ALPHAPIXELS) \ + && ((pf).dwRGBBitCount == 24) && ((pf).dwRBitMask == 0xff0000) \ + && ((pf).dwGBitMask == 0xff00) && ((pf).dwBBitMask == 0xff)) + +#define PF_IS_BGR5A1(pf) \ + (((pf).dwFlags & DDPF_RGB) && ((pf).dwFlags & DDPF_ALPHAPIXELS) \ + && ((pf).dwRGBBitCount == 16) && ((pf).dwRBitMask == 0x00007c00) \ + && ((pf).dwGBitMask == 0x000003e0) && ((pf).dwBBitMask == 0x0000001f) \ + && ((pf).dwAlphaBitMask == 0x00008000)) + +#define PF_IS_BGR565(pf) \ + (((pf).dwFlags & DDPF_RGB) && !((pf).dwFlags & DDPF_ALPHAPIXELS) \ + && ((pf).dwRGBBitCount == 16) && ((pf).dwRBitMask == 0x0000f800) \ + && ((pf).dwGBitMask == 0x000007e0) && ((pf).dwBBitMask == 0x0000001f)) + +#define PF_IS_INDEX8(pf) \ + (((pf).dwFlags & DDPF_INDEXED) && ((pf).dwRGBBitCount == 8)) + +#pragma clang diagnostic pop + +namespace ballistica { + +union DDS_header { + struct { + unsigned int dwMagic; + unsigned int dwSize; + unsigned int dwFlags; + unsigned int dwHeight; + unsigned int dwWidth; + unsigned int dwPitchOrLinearSize; + unsigned int dwDepth; + unsigned int dwMipMapCount; + unsigned int dwReserved1[11]; + + // DDPIXELFORMAT + struct { + unsigned int dwSize; + unsigned int dwFlags; + unsigned int dwFourCC; + unsigned int dwRGBBitCount; + unsigned int dwRBitMask; + unsigned int dwGBitMask; + unsigned int dwBBitMask; + unsigned int dwAlphaBitMask; + } sPixelFormat; + + // DDCAPS2 + struct { + unsigned int dwCaps1; + unsigned int dwCaps2; + unsigned int dwDDSX; + unsigned int dwReserved; + } sCaps; + unsigned int dwReserved2; + }; + char data[128]; +}; + +typedef struct { + unsigned int dxgiFormat; + unsigned int resourceDimension; + unsigned int miscFlag; + unsigned int arraySize; + unsigned int reserved; +} DDS_header_DX10; + +void LoadDDS(const std::string& file_name, unsigned char** buffers, int* widths, + int* heights, TextureFormat* formats, size_t* sizes, + TextureQuality texture_quality, int min_quality, int* base_level); + +} // namespace ballistica + +#endif // BA_ENABLE_OPENGL + +#endif // BALLISTICA_GRAPHICS_TEXTURE_DDS_H_ diff --git a/src/ballistica/graphics/texture/ktx.cc b/src/ballistica/graphics/texture/ktx.cc new file mode 100644 index 00000000..ec3b3f12 --- /dev/null +++ b/src/ballistica/graphics/texture/ktx.cc @@ -0,0 +1,2291 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/texture/ktx.h" + +#include "ballistica/platform/platform.h" + +#if !BA_HEADLESS_BUILD + +namespace ballistica { + +// Inspection is not terribly happy about this file but it works so not +// gonna touch it for now. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#pragma ide diagnostic ignored "bugprone-narrowing-conversions" +#pragma ide diagnostic ignored "bugprone-macro-parentheses" +#pragma ide diagnostic ignored "UnusedValue" + +#pragma ide diagnostic ignored "cppcoreguidelines-narrowing-conversions" +#pragma ide diagnostic ignored "OCUnusedMacroInspection" +#pragma ide diagnostic ignored "clang-analyzer-deadcode.DeadStores" +#pragma clang diagnostic ignored "-Wunused-variable" + +// We don't want this to be dependent on GL so +// lets just define the few bits we need here +#ifndef GL_COMPRESSED_RGB8_ETC2 +#define GL_COMPRESSED_RGB8_ETC2 0x9274 +#endif +#ifndef GL_COMPRESSED_RGBA8_ETC2_EAC +#define GL_COMPRESSED_RGBA8_ETC2_EAC 0x9278 +#endif +#ifndef GL_ETC1_RGB8_OES +#define GL_ETC1_RGB8_OES 0x8D64 +#endif +#ifndef GL_RGB +#define GL_RGB 0x1907 +#endif +#ifndef GL_RGBA +#define GL_RGBA 0x1908 +#endif +#ifndef GL_UNSIGNED_BYTE +#define GL_UNSIGNED_BYTE 0x1401 +#endif +typedef uint8_t GLubyte; +typedef unsigned int GLenum; +typedef int GLint; + +#define KTX_IDENTIFIER_REF \ + { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A } +#define KTX_ENDIAN_REF (0x04030201) +#define KTX_ENDIAN_REF_REV (0x01020304) +#define KTX_HEADER_SIZE (64) + +typedef struct KTX_header_t { + uint8_t identifier[12]; + uint32_t endianness; + uint32_t glType; + uint32_t glTypeSize; + uint32_t glFormat; + uint32_t glInternalFormat; + uint32_t glBaseInternalFormat; + uint32_t pixelWidth; + uint32_t pixelHeight; + uint32_t pixelDepth; + uint32_t numberOfArrayElements; + uint32_t numberOfFaces; + uint32_t numberOfMipmapLevels; + uint32_t bytesOfKeyValueData; +} KTX_header; + +void LoadKTX(const std::string& file_name, unsigned char** buffers, int* widths, + int* heights, TextureFormat* formats, size_t* sizes, + TextureQuality texture_quality, int min_quality, int* base_level) { + FILE* f = g_platform->FOpen(file_name.c_str(), "rb"); + if (!f) throw Exception("can't open file: \"" + file_name + "\""); + + KTX_header_t header{}; + static_assert(sizeof(header) == KTX_HEADER_SIZE); + BA_PRECONDITION(fread(&header, sizeof(header), 1, f) == 1); + + // Make some assumptions; we don't support arrays, more than 1 face, or kv + // data of any form. + BA_PRECONDITION(header.endianness == KTX_ENDIAN_REF); + BA_PRECONDITION(header.numberOfArrayElements == 0); + BA_PRECONDITION(header.numberOfFaces == 1); + BA_PRECONDITION(header.bytesOfKeyValueData == 0); + BA_PRECONDITION(header.pixelWidth > 0 && header.pixelHeight > 0 + && header.pixelDepth == 0); + + TextureFormat internal_format; + + if (header.glInternalFormat == GL_COMPRESSED_RGB8_ETC2) { + internal_format = TextureFormat::kETC2_RGB; + // NOLINTNEXTLINE(bugprone-branch-clone) + } else if (header.glInternalFormat == GL_COMPRESSED_RGBA8_ETC2_EAC) { + internal_format = TextureFormat::kETC2_RGBA; + } else if (header.glInternalFormat == GL_COMPRESSED_RGBA8_ETC2_EAC) { + internal_format = TextureFormat::kETC2_RGBA; + } else if (header.glInternalFormat == GL_ETC1_RGB8_OES) { + internal_format = TextureFormat::kETC1; + } else { + throw Exception(); + } + + uint32_t size = 0; + uint32_t sizeRounded = 0; + + int x = header.pixelWidth; + int y = header.pixelHeight; + + (*base_level) = 0; + + // Try dropping a level for med/low quality. + if ((texture_quality == TextureQuality::kLow + || texture_quality == TextureQuality::kMedium) + && (min_quality < 2) + && static_cast_check_fit(header.numberOfMipmapLevels) + >= (*base_level) + 1) { + (*base_level)++; + } + + // And one more for low in some cases. + if (texture_quality == TextureQuality::kLow && (min_quality < 1) + && (header.pixelWidth > 128) && (header.pixelHeight > 128) + && static_cast_check_fit(header.numberOfMipmapLevels) + >= (*base_level) + 1) { + (*base_level)++; + } + + for (uint32_t level = 0; level < header.numberOfMipmapLevels; ++level) { + if (fread(&size, sizeof(size), 1, f) != 1) + throw Exception("Error reading texture: '" + file_name + "'"); + sizeRounded = (size + 3) & ~(uint32_t)3; + BA_PRECONDITION( + size == sizeRounded); // Not currently handling this. Is it necessary? + + if ((*base_level) <= static_cast_check_fit(level)) { + sizes[level] = size; + buffers[level] = (unsigned char*)malloc(size); + BA_PRECONDITION(buffers[level]); + widths[level] = static_cast(x); + heights[level] = static_cast(y); + formats[level] = internal_format; + BA_PRECONDITION(fread(buffers[level], size, 1, f) == 1); + } else { + buffers[level] = nullptr; + BA_PRECONDITION(fseek(f, static_cast_check_fit(size), SEEK_CUR) + == 0); + } + x = (x + 1) >> 1; + y = (y + 1) >> 1; + } + fclose(f); +} + +/** + + @~English + @page licensing Licensing + + @section etcdec etcdec.cxx License + + etcdec.cxx is made available under the terms and conditions of the following + License Agreement. + + Software License Agreement + + PLEASE REVIEW THE FOLLOWING TERMS AND CONDITIONS PRIOR TO USING THE + ERICSSON TEXTURE COMPRESSION CODEC SOFTWARE (THE "SOFTWARE"). THE USE + OF THE SOFTWARE IS SUBJECT TO THE TERMS AND CONDITIONS OF THE + FOLLOWING SOFTWARE LICENSE AGREEMENT (THE "SLA"). IF YOU DO NOT ACCEPT + SUCH TERMS AND CONDITIONS YOU MAY NOT USE THE SOFTWARE. + + Subject to the terms and conditions of the SLA, the licensee of the + Software (the "Licensee") hereby, receives a non-exclusive, + non-transferable, limited, free-of-charge, perpetual and worldwide + license, to copy, use, distribute and modify the Software, but only + for the purpose of developing, manufacturing, selling, using and + distributing products including the Software in binary form, which + products are used for compression and/or decompression according to + the Khronos standard specifications OpenGL, OpenGL ES and + WebGL. Notwithstanding anything of the above, Licensee may distribute + [etcdec.cxx] in source code form provided (i) it is in unmodified + form; and (ii) it is included in software owned by Licensee. + + If Licensee institutes, or threatens to institute, patent litigation + against Ericsson or Ericsson's affiliates for using the Software for + developing, having developed, manufacturing, having manufactured, + selling, offer for sale, importing, using, leasing, operating, + repairing and/or distributing products (i) within the scope of the + Khronos framework; or (ii) using software or other intellectual + property rights owned by Ericsson or its affiliates and provided under + the Khronos framework, Ericsson shall have the right to terminate this + SLA with immediate effect. Moreover, if Licensee institutes, or + threatens to institute, patent litigation against any other licensee + of the Software for using the Software in products within the scope of + the Khronos framework, Ericsson shall have the right to terminate this + SLA with immediate effect. However, should Licensee institute, or + threaten to institute, patent litigation against any other licensee of + the Software based on such other licensee's use of any other software + together with the Software, then Ericsson shall have no right to + terminate this SLA. + + This SLA does not transfer to Licensee any ownership to any Ericsson + or third party intellectual property rights. All rights not expressly + granted by Ericsson under this SLA are hereby expressly + reserved. Furthermore, nothing in this SLA shall be construed as a + right to use or sell products in a manner which conveys or purports to + convey whether explicitly, by principles of implied license, or + otherwise, any rights to any third party, under any patent of Ericsson + or of Ericsson's affiliates covering or relating to any combination of + the Software with any other software or product (not licensed + hereunder) where the right applies specifically to the combination and + not to the software or product itself. + + THE SOFTWARE IS PROVIDED "AS IS". ERICSSON MAKES NO REPRESENTATIONS OF + ANY KIND, EXTENDS NO WARRANTIES OR CONDITIONS OF ANY KIND, EITHER + EXPRESS, IMPLIED OR STATUTORY; INCLUDING, BUT NOT LIMITED TO, EXPRESS, + IMPLIED OR STATUTORY WARRANTIES OR CONDITIONS OF TITLE, + MERCHANTABILITY, SATISFACTORY QUALITY, SUITABILITY, AND FITNESS FOR A + PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE + OF THE SOFTWARE IS WITH THE LICENSEE. SHOULD THE SOFTWARE PROVE + DEFECTIVE, THE LICENSEE ASSUMES THE COST OF ALL NECESSARY SERVICING, + REPAIR OR CORRECTION. ERICSSON MAKES NO WARRANTY THAT THE MANUFACTURE, + SALE, OFFERING FOR SALE, DISTRIBUTION, LEASE, USE OR IMPORTATION UNDER + THE SLA WILL BE FREE FROM INFRINGEMENT OF PATENTS, COPYRIGHTS OR OTHER + INTELLECTUAL PROPERTY RIGHTS OF OTHERS, AND THE VALIDITY OF THE + LICENSE AND THE SLA ARE SUBJECT TO LICENSEE'S SOLE RESPONSIBILITY TO + MAKE SUCH DETERMINATION AND ACQUIRE SUCH LICENSES AS MAY BE NECESSARY + WITH RESPECT TO PATENTS, COPYRIGHT AND OTHER INTELLECTUAL PROPERTY OF + THIRD PARTIES. + + THE LICENSEE ACKNOWLEDGES AND ACCEPTS THAT THE SOFTWARE (I) IS NOT + LICENSED FOR; (II) IS NOT DESIGNED FOR OR INTENDED FOR; AND (III) MAY + NOT BE USED FOR; ANY MISSION CRITICAL APPLICATIONS SUCH AS, BUT NOT + LIMITED TO OPERATION OF NUCLEAR OR HEALTHCARE COMPUTER SYSTEMS AND/OR + NETWORKS, AIRCRAFT OR TRAIN CONTROL AND/OR COMMUNICATION SYSTEMS OR + ANY OTHER COMPUTER SYSTEMS AND/OR NETWORKS OR CONTROL AND/OR + COMMUNICATION SYSTEMS ALL IN WHICH CASE THE FAILURE OF THE SOFTWARE + COULD LEAD TO DEATH, PERSONAL INJURY, OR SEVERE PHYSICAL, MATERIAL OR + ENVIRONMENTAL DAMAGE. LICENSEE'S RIGHTS UNDER THIS LICENSE WILL + TERMINATE AUTOMATICALLY AND IMMEDIATELY WITHOUT NOTICE IF LICENSEE + FAILS TO COMPLY WITH THIS PARAGRAPH. + + IN NO EVENT SHALL ERICSSON BE LIABLE FOR ANY DAMAGES WHATSOEVER, + INCLUDING BUT NOT LIMITED TO PERSONAL INJURY, ANY GENERAL, SPECIAL, + INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF OR IN + CONNECTION WITH THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING + BUT NOT LIMITED TO LOSS OF PROFITS, BUSINESS INTERUPTIONS, OR ANY + OTHER COMMERCIAL DAMAGES OR LOSSES, LOSS OF DATA OR DATA BEING + RENDERED INACCURATE OR LOSSES SUSTAINED BY THE LICENSEE OR THIRD + PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER + SOFTWARE) REGARDLESS OF THE THEORY OF LIABILITY (CONTRACT, TORT, OR + OTHERWISE), EVEN IF THE LICENSEE OR ANY OTHER PARTY HAS BEEN ADVISED + OF THE POSSIBILITY OF SUCH DAMAGES. + + Licensee acknowledges that "ERICSSON ///" is the corporate trademark + of Telefonaktiebolaget LM Ericsson and that both "Ericsson" and the + figure "///" are important features of the trade names of + Telefonaktiebolaget LM Ericsson. Nothing contained in these terms and + conditions shall be deemed to grant Licensee any right, title or + interest in the word "Ericsson" or the figure "///". No delay or + omission by Ericsson to exercise any right or power shall impair any + such right or power to be construed to be a waiver thereof. Consent by + Ericsson to, or waiver of, a breach by the Licensee shall not + constitute consent to, waiver of, or excuse for any other different or + subsequent breach. + + This SLA shall be governed by the substantive law of Sweden. Any + dispute, controversy or claim arising out of or in connection with + this SLA, or the breach, termination or invalidity thereof, shall be + submitted to the exclusive jurisdiction of the Swedish Courts. + +*/ + +//// etcpack v2.74 +//// +//// NO WARRANTY +//// +//// BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE THE PROGRAM IS PROVIDED +//// "AS IS". ERICSSON MAKES NO REPRESENTATIONS OF ANY KIND, EXTENDS NO +//// WARRANTIES OR CONDITIONS OF ANY KIND; EITHER EXPRESS, IMPLIED OR +//// STATUTORY; INCLUDING, BUT NOT LIMITED TO, EXPRESS, IMPLIED OR +//// STATUTORY WARRANTIES OR CONDITIONS OF TITLE, MERCHANTABILITY, +//// SATISFACTORY QUALITY, SUITABILITY AND FITNESS FOR A PARTICULAR +//// PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +//// PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME +//// THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. ERICSSON +//// MAKES NO WARRANTY THAT THE MANUFACTURE, SALE, OFFERING FOR SALE, +//// DISTRIBUTION, LEASE, USE OR IMPORTATION UNDER THE LICENSE WILL BE FREE +//// FROM INFRINGEMENT OF PATENTS, COPYRIGHTS OR OTHER INTELLECTUAL +//// PROPERTY RIGHTS OF OTHERS, AND THE VALIDITY OF THE LICENSE IS SUBJECT +//// TO YOUR SOLE RESPONSIBILITY TO MAKE SUCH DETERMINATION AND ACQUIRE +//// SUCH LICENSES AS MAY BE NECESSARY WITH RESPECT TO PATENTS, COPYRIGHT +//// AND OTHER INTELLECTUAL PROPERTY OF THIRD PARTIES. +//// +//// FOR THE AVOIDANCE OF DOUBT THE PROGRAM (I) IS NOT LICENSED FOR; (II) +//// IS NOT DESIGNED FOR OR INTENDED FOR; AND (III) MAY NOT BE USED FOR; +//// ANY MISSION CRITICAL APPLICATIONS SUCH AS, BUT NOT LIMITED TO +//// OPERATION OF NUCLEAR OR HEALTHCARE COMPUTER SYSTEMS AND/OR NETWORKS, +//// AIRCRAFT OR TRAIN CONTROL AND/OR COMMUNICATION SYSTEMS OR ANY OTHER +//// COMPUTER SYSTEMS AND/OR NETWORKS OR CONTROL AND/OR COMMUNICATION +//// SYSTEMS ALL IN WHICH CASE THE FAILURE OF THE PROGRAM COULD LEAD TO +//// DEATH, PERSONAL INJURY, OR SEVERE PHYSICAL, MATERIAL OR ENVIRONMENTAL +//// DAMAGE. YOUR RIGHTS UNDER THIS LICENSE WILL TERMINATE AUTOMATICALLY +//// AND IMMEDIATELY WITHOUT NOTICE IF YOU FAIL TO COMPLY WITH THIS +//// PARAGRAPH. +//// +//// IN NO EVENT WILL ERICSSON, BE LIABLE FOR ANY DAMAGES WHATSOEVER, +//// INCLUDING BUT NOT LIMITED TO PERSONAL INJURY, ANY GENERAL, SPECIAL, +//// INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR IN +//// CONNECTION WITH THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT +//// NOT LIMITED TO LOSS OF PROFITS, BUSINESS INTERUPTIONS, OR ANY OTHER +//// COMMERCIAL DAMAGES OR LOSSES, LOSS OF DATA OR DATA BEING RENDERED +//// INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF +//// THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS) REGARDLESS OF THE +//// THEORY OF LIABILITY (CONTRACT, TORT OR OTHERWISE), EVEN IF SUCH HOLDER +//// OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +//// +//// (C) Ericsson AB 2013. All Rights Reserved. +//// + +#include +#include + +// Typedefs +typedef unsigned char uint8; +typedef unsigned short uint16; +typedef short int16; + +// Macros to help with bit extraction/insertion +#define SHIFT(size, startpos) ((startpos) - (size) + 1) +#define MASK(size, startpos) (((2 << (size - 1)) - 1) << SHIFT(size, startpos)) +#define PUTBITS(dest, data, size, startpos) \ + dest = ((dest & ~MASK(size, startpos)) \ + | ((data << SHIFT(size, startpos)) & MASK(size, startpos))) +#define SHIFTHIGH(size, startpos) (((startpos)-32) - (size) + 1) +#define MASKHIGH(size, startpos) \ + (((1 << (size)) - 1) << SHIFTHIGH(size, startpos)) +#define PUTBITSHIGH(dest, data, size, startpos) \ + dest = ((dest & ~MASKHIGH(size, startpos)) \ + | ((data << SHIFTHIGH(size, startpos)) & MASKHIGH(size, startpos))) +#define GETBITS(source, size, startpos) \ + (((source) >> ((startpos) - (size) + 1)) & ((1 << (size)) - 1)) +#define GETBITSHIGH(source, size, startpos) \ + (((source) >> (((startpos)-32) - (size) + 1)) & ((1 << (size)) - 1)) +#ifndef PGMOUT +#define PGMOUT 0 +#endif +// Thumb macros and definitions +#define R_BITS59T 4 +#define G_BITS59T 4 +#define B_BITS59T 4 +#define R_BITS58H 4 +#define G_BITS58H 4 +#define B_BITS58H 4 +#define MAXIMUM_ERROR (255 * 255 * 16 * 1000) +#define R 0 +#define G 1 +#define B 2 +#define BLOCKHEIGHT 4 +#define BLOCKWIDTH 4 +#define BINPOW(power) (1 << (power)) +#define TABLE_BITS_59T 3 +#define TABLE_BITS_58H 3 + +// Helper Macros +#define CLAMP(ll, x, ul) (((x) < (ll)) ? (ll) : (((x) > (ul)) ? (ul) : (x))) +#define JAS_ROUND(x) (((x) < 0.0) ? ((int)((x)-0.5)) : ((int)((x) + 0.5))) + +#define RED_CHANNEL(img, width, x, y, channels) \ + img[channels * (y * width + x) + 0] +#define GREEN_CHANNEL(img, width, x, y, channels) \ + img[channels * (y * width + x) + 1] +#define BLUE_CHANNEL(img, width, x, y, channels) \ + img[channels * (y * width + x) + 2] +#define ALPHA_CHANNEL(img, width, x, y, channels) \ + img[channels * (y * width + x) + 3] + +// Global tables +static uint8 table59T[8] = {3, 6, 11, 16, 23, + 32, 41, 64}; // 3-bit table for the 59 bit T-mode +static uint8 table58H[8] = {3, 6, 11, 16, 23, + 32, 41, 64}; // 3-bit table for the 58 bit H-mode +static int compressParams[16][4] = { + {-8, -2, 2, 8}, {-8, -2, 2, 8}, {-17, -5, 5, 17}, + {-17, -5, 5, 17}, {-29, -9, 9, 29}, {-29, -9, 9, 29}, + {-42, -13, 13, 42}, {-42, -13, 13, 42}, {-60, -18, 18, 60}, + {-60, -18, 18, 60}, {-80, -24, 24, 80}, {-80, -24, 24, 80}, + {-106, -33, 33, 106}, {-106, -33, 33, 106}, {-183, -47, 47, 183}, + {-183, -47, 47, 183}}; +static int unscramble[4] = {2, 3, 1, 0}; +int alphaTableInitialized = 0; +int alphaTable[256][8]; +int alphaBase[16][4] = { + {-15, -9, -6, -3}, {-13, -10, -7, -3}, {-13, -8, -5, -2}, {-13, -6, -4, -2}, + {-12, -8, -6, -3}, {-11, -9, -7, -3}, {-11, -8, -7, -4}, {-11, -8, -5, -3}, + {-10, -8, -6, -2}, {-10, -8, -5, -2}, {-10, -8, -4, -2}, {-10, -7, -5, -2}, + {-10, -7, -4, -3}, {-10, -3, -2, -1}, {-9, -8, -6, -4}, {-9, -7, -5, -3}}; + +// Global variables +int formatSigned = 0; + +// Enums +enum { PATTERN_H = 0, PATTERN_T = 1 }; + +// Code used to create the valtab +// NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights +// Reserved. +void setupAlphaTable() { + if (alphaTableInitialized) return; + alphaTableInitialized = 1; + + // read table used for alpha compression + int buf; + for (int i = 16; i < 32; i++) { + for (int j = 0; j < 8; j++) { + buf = alphaBase[i - 16][3 - j % 4]; + if (j < 4) + alphaTable[i][j] = buf; + else + alphaTable[i][j] = (-buf - 1); + } + } + + // beyond the first 16 values, the rest of the table is implicit.. so + // calculate that! + for (int i = 0; i < 256; i++) { + // fill remaining slots in table with multiples of the first ones. + int mul = i / 16; + int old = 16 + i % 16; + for (int j = 0; j < 8; j++) { + alphaTable[i][j] = alphaTable[old][j] * mul; + // note: we don't do clamping here, though we could, because we'll be + // clamped afterwards anyway. + } + } +} + +// Read a word in big endian style +// NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights +// Reserved. +// void read_big_endian_2byte_word(unsigned short* blockadr, FILE* f) { +// uint8 bytes[2]; +// unsigned short block; + +// fread(&bytes[0], 1, 1, f); +// fread(&bytes[1], 1, 1, f); + +// block = 0; +// block |= bytes[0]; +// block = block << 8; +// block |= bytes[1]; + +// blockadr[0] = block; +// } + +// Read a word in big endian style +// NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights +// Reserved. +// void read_big_endian_4byte_word(unsigned int* blockadr, FILE* f) { +// uint8 bytes[4]; +// unsigned int block; + +// fread(&bytes[0], 1, 1, f); +// fread(&bytes[1], 1, 1, f); +// fread(&bytes[2], 1, 1, f); +// fread(&bytes[3], 1, 1, f); + +// block = 0; +// block |= bytes[0]; +// block = block << 8; +// block |= bytes[1]; +// block = block << 8; +// block |= bytes[2]; +// block = block << 8; +// block |= bytes[3]; + +// blockadr[0] = block; +// } + +// The format stores the bits for the three extra modes in a roundabout way to +// be able to fit them without increasing the bit rate. This function converts +// them into something that is easier to work with. NO WARRANTY --- SEE +// STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights Reserved. +void unstuff57bits(unsigned int planar_word1, unsigned int planar_word2, + unsigned int& planar57_word1, unsigned int& planar57_word2) { + // Get bits from twotimer configuration for 57 bits + // + // Go to this bit layout: + // + // 63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 + // 40 39 38 37 36 35 34 33 32 + // ----------------------------------------------------------------------------------------------- + // |R0 |G01G02 |B01B02 ;B03 |RH1 |RH2|GH | + // ----------------------------------------------------------------------------------------------- + // + // 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 + // 8 7 6 5 4 3 2 1 0 + // ----------------------------------------------------------------------------------------------- + // |BH |RV |GV |BV | not used + // | + // ----------------------------------------------------------------------------------------------- + // + // From this: + // + // 63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 + // 40 39 38 37 36 35 34 33 32 + // ------------------------------------------------------------------------------------------------ + // |//|R0 |G01|/|G02 |B01|/ // //|B02 |//|B03 + // |RH1 |df|RH2| + // ------------------------------------------------------------------------------------------------ + // + // 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 + // 8 7 6 5 4 3 2 1 0 + // ----------------------------------------------------------------------------------------------- + // |GH |BH |RV |GV |BV | + // ----------------------------------------------------------------------------------------------- + // + // 63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 + // 40 39 38 37 36 35 34 33 32 + // --------------------------------------------------------------------------------------------------- + // | base col1 | dcol 2 | base col1 | dcol 2 | base col 1 | dcol 2 + // | table | table |diff|flip| | R1' (5 bits) | dR2 | G1' (5 bits) | + // dG2 | B1' (5 bits) | dB2 | cw 1 | cw 2 |bit |bit | + // --------------------------------------------------------------------------------------------------- + + uint8 RO, GO1, GO2, BO1, BO2, BO3, RH1, RH2, GH, BH, RV, GV, BV; + + RO = static_cast(GETBITSHIGH(planar_word1, 6, 62)); + GO1 = static_cast(GETBITSHIGH(planar_word1, 1, 56)); + GO2 = static_cast(GETBITSHIGH(planar_word1, 6, 54)); + BO1 = static_cast(GETBITSHIGH(planar_word1, 1, 48)); + BO2 = static_cast(GETBITSHIGH(planar_word1, 2, 44)); + BO3 = static_cast(GETBITSHIGH(planar_word1, 3, 41)); + RH1 = static_cast(GETBITSHIGH(planar_word1, 5, 38)); + RH2 = static_cast(GETBITSHIGH(planar_word1, 1, 32)); + GH = static_cast(GETBITS(planar_word2, 7, 31)); + BH = static_cast(GETBITS(planar_word2, 6, 24)); + RV = static_cast(GETBITS(planar_word2, 6, 18)); + GV = static_cast(GETBITS(planar_word2, 7, 12)); + BV = static_cast(GETBITS(planar_word2, 6, 5)); + + planar57_word1 = 0; + planar57_word2 = 0; + PUTBITSHIGH(planar57_word1, RO, 6, 63); + PUTBITSHIGH(planar57_word1, GO1, 1, 57); + PUTBITSHIGH(planar57_word1, GO2, 6, 56); + PUTBITSHIGH(planar57_word1, BO1, 1, 50); + PUTBITSHIGH(planar57_word1, BO2, 2, 49); + PUTBITSHIGH(planar57_word1, BO3, 3, 47); + PUTBITSHIGH(planar57_word1, RH1, 5, 44); + PUTBITSHIGH(planar57_word1, RH2, 1, 39); + PUTBITSHIGH(planar57_word1, GH, 7, 38); + PUTBITS(planar57_word2, BH, 6, 31); + PUTBITS(planar57_word2, RV, 6, 25); + PUTBITS(planar57_word2, GV, 7, 19); + PUTBITS(planar57_word2, BV, 6, 12); +} + +// The format stores the bits for the three extra modes in a roundabout way to +// be able to fit them without increasing the bit rate. This function converts +// them into something that is easier to work with. NO WARRANTY --- SEE +// STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights Reserved. +void unstuff58bits(unsigned int thumbH_word1, unsigned int thumbH_word2, + unsigned int& thumbH58_word1, unsigned int& thumbH58_word2) { + // Go to this layout: + // + // |63 62 61 60 59 58|57 56 55 54 53 52 51|50 49|48 47 46 45 44 43 42 41 + // 40 39 38 37 36 35 34 33|32 | + // |-------empty-----|part0---------------|part1|part2------------------------------------------|part3| + // + // from this: + // + // 63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 + // 40 39 38 37 36 35 34 33 32 + // --------------------------------------------------------------------------------------------------| + // |//|part0 |// // //|part1|//|part2 |df|part3| + // --------------------------------------------------------------------------------------------------| + + unsigned int part0, part1, part2, part3; + + // move parts + part0 = GETBITSHIGH(thumbH_word1, 7, 62); + part1 = GETBITSHIGH(thumbH_word1, 2, 52); + part2 = GETBITSHIGH(thumbH_word1, 16, 49); + part3 = GETBITSHIGH(thumbH_word1, 1, 32); + thumbH58_word1 = 0; + PUTBITSHIGH(thumbH58_word1, part0, 7, 57); + PUTBITSHIGH(thumbH58_word1, part1, 2, 50); + PUTBITSHIGH(thumbH58_word1, part2, 16, 48); + PUTBITSHIGH(thumbH58_word1, part3, 1, 32); + + thumbH58_word2 = thumbH_word2; +} + +// The format stores the bits for the three extra modes in a roundabout way to +// be able to fit them without increasing the bit rate. This function converts +// them into something that is easier to work with. NO WARRANTY --- SEE +// STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights Reserved. +void unstuff59bits(unsigned int thumbT_word1, unsigned int thumbT_word2, + unsigned int& thumbT59_word1, unsigned int& thumbT59_word2) { + // Get bits from twotimer configuration 59 bits. + // + // Go to this bit layout: + // + // |63 62 61 60 59|58 57 56 55|54 53 52 51|50 49 48 47|46 45 44 43|42 41 + // 40 39|38 37 36 35|34 33 32| + // |----empty-----|---red 0---|--green 0--|--blue 0---|---red 1---|--green + // 1--|--blue 1---|--dist--| + // + // |31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 + // 08 07 06 05 04 03 02 01 00| + // |----------------------------------------index + // bits---------------------------------------------| + // + // + // From this: + // + // 63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 + // 40 39 38 37 36 35 34 33 32 + // ----------------------------------------------------------------------------------------------- + // |// // //|R0a |//|R0b |G0 |B0 |R1 |G1 |B1 |da + // |df|db| + // ----------------------------------------------------------------------------------------------- + // + // |31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 + // 08 07 06 05 04 03 02 01 00| + // |----------------------------------------index + // bits---------------------------------------------| + // + // 63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 + // 40 39 38 37 36 35 34 33 32 + // ----------------------------------------------------------------------------------------------- + // | base col1 | dcol 2 | base col1 | dcol 2 | base col 1 | dcol 2 + // | table | table |df|fp| | R1' (5 bits) | dR2 | G1' (5 bits) | dG2 + // | B1' (5 bits) | dB2 | cw 1 | cw 2 |bt|bt| + // ------------------------------------------------------------------------------------------------ + + uint8 R0a; + + // Fix middle part + thumbT59_word1 = thumbT_word1 >> 1; + // Fix db (lowest bit of d) + PUTBITSHIGH(thumbT59_word1, thumbT_word1, 1, 32); + // Fix R0a (top two bits of R0) + R0a = static_cast(GETBITSHIGH(thumbT_word1, 2, 60)); + PUTBITSHIGH(thumbT59_word1, R0a, 2, 58); + + // Zero top part (not needed) + PUTBITSHIGH(thumbT59_word1, 0, 5, 63); + + thumbT59_word2 = thumbT_word2; +} + +// The color bits are expanded to the full color +// NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights +// Reserved. +void decompressColor(int R_B, int G_B, int B_B, uint8(colors_RGB444)[2][3], + uint8(colors)[2][3]) { + // The color should be retrieved as: + // + // c = round(255/(r_bits^2-1))*comp_color + // + // This is similar to bit replication + // + // Note -- this code only work for bit replication from 4 bits and up --- 3 + // bits needs two copy operations. + + colors[0][R] = (colors_RGB444[0][R] << (8 - R_B)) + | (colors_RGB444[0][R] >> (R_B - (8 - R_B))); + colors[0][G] = (colors_RGB444[0][G] << (8 - G_B)) + | (colors_RGB444[0][G] >> (G_B - (8 - G_B))); + colors[0][B] = (colors_RGB444[0][B] << (8 - B_B)) + | (colors_RGB444[0][B] >> (B_B - (8 - B_B))); + colors[1][R] = (colors_RGB444[1][R] << (8 - R_B)) + | (colors_RGB444[1][R] >> (R_B - (8 - R_B))); + colors[1][G] = (colors_RGB444[1][G] << (8 - G_B)) + | (colors_RGB444[1][G] >> (G_B - (8 - G_B))); + colors[1][B] = (colors_RGB444[1][B] << (8 - B_B)) + | (colors_RGB444[1][B] >> (B_B - (8 - B_B))); +} + +void calculatePaintColors59T(uint8 d, uint8 p, uint8(colors)[2][3], + uint8(possible_colors)[4][3]) { + ////////////////////////////////////////////// + // + // C3 C1 C4----C1---C2 + // | | | + // | | | + // |-------| | + // | | | + // | | | + // C4 C2 C3 + // + ////////////////////////////////////////////// + + // C4 + possible_colors[3][R] = + static_cast(CLAMP(0, colors[1][R] - table59T[d], 255)); + possible_colors[3][G] = + static_cast(CLAMP(0, colors[1][G] - table59T[d], 255)); + possible_colors[3][B] = + static_cast(CLAMP(0, colors[1][B] - table59T[d], 255)); + + if (p == PATTERN_T) { + // C3 + possible_colors[0][R] = colors[0][R]; + possible_colors[0][G] = colors[0][G]; + possible_colors[0][B] = colors[0][B]; + // C2 + possible_colors[1][R] = + static_cast(CLAMP(0, colors[1][R] + table59T[d], 255)); + possible_colors[1][G] = + static_cast(CLAMP(0, colors[1][G] + table59T[d], 255)); + possible_colors[1][B] = + static_cast(CLAMP(0, colors[1][B] + table59T[d], 255)); + // C1 + possible_colors[2][R] = colors[1][R]; + possible_colors[2][G] = colors[1][G]; + possible_colors[2][B] = colors[1][B]; + + } else { + printf("Invalid pattern. Terminating"); + exit(1); + } +} +// Decompress a T-mode block (simple packing) +// Simple 59T packing: +//|63 62 61 60 59|58 57 56 55|54 53 52 51|50 49 48 47|46 45 44 43|42 41 40 39|38 +// 37 36 35|34 33 32| +//|----empty-----|---red 0---|--green 0--|--blue 0---|---red 1---|--green +// 1--|--blue 1---|--dist--| +// +//|31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 +// 05 04 03 02 01 00| +//|----------------------------------------index +// bits---------------------------------------------| +// NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights +// Reserved. +void decompressBlockTHUMB59Tc(unsigned int block_part1, + unsigned int block_part2, uint8* img, int width, + int height, int startx, int starty, + int channels) { + uint8 colorsRGB444[2][3]; + uint8 colors[2][3]; + uint8 paint_colors[4][3]; + uint8 distance; + uint8 block_mask[4][4]; + + // First decode left part of block. + colorsRGB444[0][R] = static_cast(GETBITSHIGH(block_part1, 4, 58)); + colorsRGB444[0][G] = static_cast(GETBITSHIGH(block_part1, 4, 54)); + colorsRGB444[0][B] = static_cast(GETBITSHIGH(block_part1, 4, 50)); + + colorsRGB444[1][R] = static_cast(GETBITSHIGH(block_part1, 4, 46)); + colorsRGB444[1][G] = static_cast(GETBITSHIGH(block_part1, 4, 42)); + colorsRGB444[1][B] = static_cast(GETBITSHIGH(block_part1, 4, 38)); + + distance = static_cast(GETBITSHIGH(block_part1, TABLE_BITS_59T, 34)); + + // Extend the two colors to RGB888 + decompressColor(R_BITS59T, G_BITS59T, B_BITS59T, colorsRGB444, colors); + calculatePaintColors59T(distance, PATTERN_T, colors, paint_colors); + + // Choose one of the four paint colors for each texel + for (uint8 x = 0; x < BLOCKWIDTH; ++x) { + for (uint8 y = 0; y < BLOCKHEIGHT; ++y) { + // block_mask[x][y] = GETBITS(block_part2,2,31-(y*4+x)*2); + block_mask[x][y] = + static_cast(GETBITS(block_part2, 1, (y + x * 4) + 16) << 1); + block_mask[x][y] |= GETBITS(block_part2, 1, (y + x * 4)); + img[channels * ((starty + y) * width + startx + x) + R] = + static_cast( + CLAMP(0, paint_colors[block_mask[x][y]][R], 255)); // RED + img[channels * ((starty + y) * width + startx + x) + G] = + static_cast( + CLAMP(0, paint_colors[block_mask[x][y]][G], 255)); // GREEN + img[channels * ((starty + y) * width + startx + x) + B] = + static_cast( + CLAMP(0, paint_colors[block_mask[x][y]][B], 255)); // BLUE + } + } +} + +void decompressBlockTHUMB59T(unsigned int block_part1, unsigned int block_part2, + uint8* img, int width, int height, int startx, + int starty) { + decompressBlockTHUMB59Tc(block_part1, block_part2, img, width, height, startx, + starty, 3); +} + +// Calculate the paint colors from the block colors +// using a distance d and one of the H- or T-patterns. +// NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights +// Reserved. +void calculatePaintColors58H(uint8 d, uint8 p, uint8(colors)[2][3], + uint8(possible_colors)[4][3]) { + ////////////////////////////////////////////// + // + // C3 C1 C4----C1---C2 + // | | | + // | | | + // |-------| | + // | | | + // | | | + // C4 C2 C3 + // + ////////////////////////////////////////////// + + // C4 + possible_colors[3][R] = + static_cast(CLAMP(0, colors[1][R] - table58H[d], 255)); + possible_colors[3][G] = + static_cast(CLAMP(0, colors[1][G] - table58H[d], 255)); + possible_colors[3][B] = + static_cast(CLAMP(0, colors[1][B] - table58H[d], 255)); + + if (p == PATTERN_H) { + // C1 + possible_colors[0][R] = + static_cast(CLAMP(0, colors[0][R] + table58H[d], 255)); + possible_colors[0][G] = + static_cast(CLAMP(0, colors[0][G] + table58H[d], 255)); + possible_colors[0][B] = + static_cast(CLAMP(0, colors[0][B] + table58H[d], 255)); + // C2 + possible_colors[1][R] = + static_cast(CLAMP(0, colors[0][R] - table58H[d], 255)); + possible_colors[1][G] = + static_cast(CLAMP(0, colors[0][G] - table58H[d], 255)); + possible_colors[1][B] = + static_cast(CLAMP(0, colors[0][B] - table58H[d], 255)); + // C3 + possible_colors[2][R] = + static_cast(CLAMP(0, colors[1][R] + table58H[d], 255)); + possible_colors[2][G] = + static_cast(CLAMP(0, colors[1][G] + table58H[d], 255)); + possible_colors[2][B] = + static_cast(CLAMP(0, colors[1][B] + table58H[d], 255)); + } else { + printf("Invalid pattern. Terminating"); + exit(1); + } +} + +// Decompress an H-mode block +// NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights +// Reserved. +void decompressBlockTHUMB58Hc(unsigned int block_part1, + unsigned int block_part2, uint8* img, int width, + int height, int startx, int starty, + int channels) { + unsigned int col0, col1; + uint8 colors[2][3]; + uint8 colorsRGB444[2][3]; + uint8 paint_colors[4][3]; + uint8 distance; + uint8 block_mask[4][4]; + + // First decode left part of block. + colorsRGB444[0][R] = static_cast(GETBITSHIGH(block_part1, 4, 57)); + colorsRGB444[0][G] = static_cast(GETBITSHIGH(block_part1, 4, 53)); + colorsRGB444[0][B] = static_cast(GETBITSHIGH(block_part1, 4, 49)); + + colorsRGB444[1][R] = static_cast(GETBITSHIGH(block_part1, 4, 45)); + colorsRGB444[1][G] = static_cast(GETBITSHIGH(block_part1, 4, 41)); + colorsRGB444[1][B] = static_cast(GETBITSHIGH(block_part1, 4, 37)); + + distance = 0; + distance = static_cast((GETBITSHIGH(block_part1, 2, 33)) << 1); + + col0 = GETBITSHIGH(block_part1, 12, 57); + col1 = GETBITSHIGH(block_part1, 12, 45); + + if (col0 >= col1) { + distance |= 1; + } + + // Extend the two colors to RGB888 + decompressColor(R_BITS58H, G_BITS58H, B_BITS58H, colorsRGB444, colors); + + calculatePaintColors58H(distance, PATTERN_H, colors, paint_colors); + + // Choose one of the four paint colors for each texel + for (uint8 x = 0; x < BLOCKWIDTH; ++x) { + for (uint8 y = 0; y < BLOCKHEIGHT; ++y) { + // block_mask[x][y] = GETBITS(block_part2,2,31-(y*4+x)*2); + block_mask[x][y] = + static_cast(GETBITS(block_part2, 1, (y + x * 4) + 16) << 1); + block_mask[x][y] |= GETBITS(block_part2, 1, (y + x * 4)); + img[channels * ((starty + y) * width + startx + x) + R] = + static_cast( + CLAMP(0, paint_colors[block_mask[x][y]][R], 255)); // RED + img[channels * ((starty + y) * width + startx + x) + G] = + static_cast( + CLAMP(0, paint_colors[block_mask[x][y]][G], 255)); // GREEN + img[channels * ((starty + y) * width + startx + x) + B] = + static_cast( + CLAMP(0, paint_colors[block_mask[x][y]][B], 255)); // BLUE + } + } +} +void decompressBlockTHUMB58H(unsigned int block_part1, unsigned int block_part2, + uint8* img, int width, int height, int startx, + int starty) { + decompressBlockTHUMB58Hc(block_part1, block_part2, img, width, height, startx, + starty, 3); +} + +// Decompress the planar mode. +// NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights +// Reserved. +void decompressBlockPlanar57c(unsigned int compressed57_1, + unsigned int compressed57_2, uint8* img, + int width, int height, int startx, int starty, + int channels) { + uint8 colorO[3], colorH[3], colorV[3]; + + colorO[0] = static_cast(GETBITSHIGH(compressed57_1, 6, 63)); + colorO[1] = static_cast(GETBITSHIGH(compressed57_1, 7, 57)); + colorO[2] = static_cast(GETBITSHIGH(compressed57_1, 6, 50)); + colorH[0] = static_cast(GETBITSHIGH(compressed57_1, 6, 44)); + colorH[1] = static_cast(GETBITSHIGH(compressed57_1, 7, 38)); + colorH[2] = static_cast(GETBITS(compressed57_2, 6, 31)); + colorV[0] = static_cast(GETBITS(compressed57_2, 6, 25)); + colorV[1] = static_cast(GETBITS(compressed57_2, 7, 19)); + colorV[2] = static_cast(GETBITS(compressed57_2, 6, 12)); + + colorO[0] = (colorO[0] << 2) | (colorO[0] >> 4); + colorO[1] = (colorO[1] << 1) | (colorO[1] >> 6); + colorO[2] = (colorO[2] << 2) | (colorO[2] >> 4); + + colorH[0] = (colorH[0] << 2) | (colorH[0] >> 4); + colorH[1] = (colorH[1] << 1) | (colorH[1] >> 6); + colorH[2] = (colorH[2] << 2) | (colorH[2] >> 4); + + colorV[0] = (colorV[0] << 2) | (colorV[0] >> 4); + colorV[1] = (colorV[1] << 1) | (colorV[1] >> 6); + colorV[2] = (colorV[2] << 2) | (colorV[2] >> 4); + + int xx, yy; + + for (xx = 0; xx < 4; xx++) { + for (yy = 0; yy < 4; yy++) { + img[channels * width * (starty + yy) + channels * (startx + xx) + 0] = + static_cast( + CLAMP(0, + ((xx * (colorH[0] - colorO[0]) + + yy * (colorV[0] - colorO[0]) + 4 * colorO[0] + 2) + >> 2), + 255)); + img[channels * width * (starty + yy) + channels * (startx + xx) + 1] = + static_cast( + CLAMP(0, + ((xx * (colorH[1] - colorO[1]) + + yy * (colorV[1] - colorO[1]) + 4 * colorO[1] + 2) + >> 2), + 255)); + img[channels * width * (starty + yy) + channels * (startx + xx) + 2] = + static_cast( + CLAMP(0, + ((xx * (colorH[2] - colorO[2]) + + yy * (colorV[2] - colorO[2]) + 4 * colorO[2] + 2) + >> 2), + 255)); + + // Equivalent method + /*img[channels*width*(starty+yy) + channels*(startx+xx) + 0] = +(int)CLAMP(0, JAS_ROUND((xx*(colorH[0]-colorO[0])/4.0f + +yy*(colorV[0]-colorO[0])/4.0f + colorO[0])), 255); +img[channels*width*(starty+yy) ++ channels*(startx+xx) + 1] = (int)CLAMP(0, +JAS_ROUND((xx*(colorH[1]-colorO[1])/4.0f + yy*(colorV[1]-colorO[1])/4.0f + +colorO[1])), 255); +img[channels*width*(starty+yy) + channels*(startx+xx) + 2] = (int)CLAMP(0, +JAS_ROUND((xx*(colorH[2]-colorO[2])/4.0f + yy*(colorV[2]-colorO[2])/4.0f + +colorO[2])), 255);*/ + } + } +} +void decompressBlockPlanar57(unsigned int compressed57_1, + unsigned int compressed57_2, uint8* img, int width, + int height, int startx, int starty) { + decompressBlockPlanar57c(compressed57_1, compressed57_2, img, width, height, + startx, starty, 3); +} +// Decompress an ETC1 block (or ETC2 using individual or differential mode). +// NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights +// Reserved. +void decompressBlockDiffFlipC(unsigned int block_part1, + unsigned int block_part2, uint8* img, int width, + int height, int startx, int starty, + int channels) { + uint8 avg_color[3], enc_color1[3], enc_color2[3]; + signed char diff[3]; + int table; + int index, shift; + int r, g, b; + int diffbit; + int flipbit; + + diffbit = (GETBITSHIGH(block_part1, 1, 33)); + flipbit = (GETBITSHIGH(block_part1, 1, 32)); + + if (!diffbit) { + // We have diffbit = 0. + + // First decode left part of block. + avg_color[0] = static_cast(GETBITSHIGH(block_part1, 4, 63)); + avg_color[1] = static_cast(GETBITSHIGH(block_part1, 4, 55)); + avg_color[2] = static_cast(GETBITSHIGH(block_part1, 4, 47)); + + // Here, we should really multiply by 17 instead of 16. This can + // be done by just copying the four lower bits to the upper ones + // while keeping the lower bits. + avg_color[0] |= (avg_color[0] << 4); + avg_color[1] |= (avg_color[1] << 4); + avg_color[2] |= (avg_color[2] << 4); + + table = GETBITSHIGH(block_part1, 3, 39) << 1; + + unsigned int pixel_indices_MSB, pixel_indices_LSB; + + pixel_indices_MSB = GETBITS(block_part2, 16, 31); + pixel_indices_LSB = GETBITS(block_part2, 16, 15); + + if ((flipbit) == 0) { + // We should not flip + shift = 0; + for (int x = startx; x < startx + 2; x++) { + for (int y = starty; y < starty + 4; y++) { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index = unscramble[index]; + + r = RED_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[0] + compressParams[table][index], 255)); + g = GREEN_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[1] + compressParams[table][index], 255)); + b = BLUE_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[2] + compressParams[table][index], 255)); + } + } + } else { + // We should flip + shift = 0; + for (int x = startx; x < startx + 4; x++) { + for (int y = starty; y < starty + 2; y++) { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index = unscramble[index]; + + r = RED_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[0] + compressParams[table][index], 255)); + g = GREEN_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[1] + compressParams[table][index], 255)); + b = BLUE_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[2] + compressParams[table][index], 255)); + } + shift += 2; + } + } + + // Now decode other part of block. + avg_color[0] = static_cast(GETBITSHIGH(block_part1, 4, 59)); + avg_color[1] = static_cast(GETBITSHIGH(block_part1, 4, 51)); + avg_color[2] = static_cast(GETBITSHIGH(block_part1, 4, 43)); + + // Here, we should really multiply by 17 instead of 16. This can + // be done by just copying the four lower bits to the upper ones + // while keeping the lower bits. + avg_color[0] |= (avg_color[0] << 4); + avg_color[1] |= (avg_color[1] << 4); + avg_color[2] |= (avg_color[2] << 4); + + table = GETBITSHIGH(block_part1, 3, 36) << 1; + pixel_indices_MSB = GETBITS(block_part2, 16, 31); + pixel_indices_LSB = GETBITS(block_part2, 16, 15); + + if ((flipbit) == 0) { + // We should not flip + shift = 8; + for (int x = startx + 2; x < startx + 4; x++) { + for (int y = starty; y < starty + 4; y++) { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index = unscramble[index]; + + r = RED_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[0] + compressParams[table][index], 255)); + g = GREEN_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[1] + compressParams[table][index], 255)); + b = BLUE_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[2] + compressParams[table][index], 255)); + } + } + } else { + // We should flip + shift = 2; + for (int x = startx; x < startx + 4; x++) { + for (int y = starty + 2; y < starty + 4; y++) { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index = unscramble[index]; + + r = RED_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[0] + compressParams[table][index], 255)); + g = GREEN_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[1] + compressParams[table][index], 255)); + b = BLUE_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[2] + compressParams[table][index], 255)); + } + shift += 2; + } + } + } else { + // We have diffbit = 1. + + // 63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 + // 40 39 38 37 36 35 34 33 32 + // --------------------------------------------------------------------------------------------------- + // | base col1 | dcol 2 | base col1 | dcol 2 | base col 1 | dcol + // 2 | table | table |diff|flip| | R1' (5 bits) | dR2 | G1' (5 + // bits) | dG2 | B1' (5 bits) | dB2 | cw 1 | cw 2 |bit |bit | + // --------------------------------------------------------------------------------------------------- + // + // + // c) bit layout in bits 31 through 0 (in both cases) + // + // 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 + // 8 7 6 5 4 3 2 1 0 + // -------------------------------------------------------------------------------------------------- + // | most significant pixel index bits | least + // significant pixel index bits | | p| o| n| m| l| k| j| i| h| g| + // f| e| d| c| b| a| p| o| n| m| l| k| j| i| h| g| f| e| d| c | b | a | + // -------------------------------------------------------------------------------------------------- + + // First decode left part of block. + enc_color1[0] = static_cast(GETBITSHIGH(block_part1, 5, 63)); + enc_color1[1] = static_cast(GETBITSHIGH(block_part1, 5, 55)); + enc_color1[2] = static_cast(GETBITSHIGH(block_part1, 5, 47)); + + // Expand from 5 to 8 bits + avg_color[0] = (enc_color1[0] << 3) | (enc_color1[0] >> 2); + avg_color[1] = (enc_color1[1] << 3) | (enc_color1[1] >> 2); + avg_color[2] = (enc_color1[2] << 3) | (enc_color1[2] >> 2); + + table = GETBITSHIGH(block_part1, 3, 39) << 1; + + unsigned int pixel_indices_MSB, pixel_indices_LSB; + + pixel_indices_MSB = GETBITS(block_part2, 16, 31); + pixel_indices_LSB = GETBITS(block_part2, 16, 15); + + if ((flipbit) == 0) { + // We should not flip + shift = 0; + for (int x = startx; x < startx + 2; x++) { + for (int y = starty; y < starty + 4; y++) { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index = unscramble[index]; + + r = RED_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[0] + compressParams[table][index], 255)); + g = GREEN_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[1] + compressParams[table][index], 255)); + b = BLUE_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[2] + compressParams[table][index], 255)); + } + } + } else { + // We should flip + shift = 0; + for (int x = startx; x < startx + 4; x++) { + for (int y = starty; y < starty + 2; y++) { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index = unscramble[index]; + + r = RED_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[0] + compressParams[table][index], 255)); + g = GREEN_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[1] + compressParams[table][index], 255)); + b = BLUE_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[2] + compressParams[table][index], 255)); + } + shift += 2; + } + } + + // Now decode right part of block. + diff[0] = static_cast(GETBITSHIGH(block_part1, 3, 58)); + diff[1] = static_cast(GETBITSHIGH(block_part1, 3, 50)); + diff[2] = static_cast(GETBITSHIGH(block_part1, 3, 42)); + + // Extend sign bit to entire byte. + diff[0] = (diff[0] << 5); + diff[1] = (diff[1] << 5); + diff[2] = (diff[2] << 5); + diff[0] = diff[0] >> 5; + diff[1] = diff[1] >> 5; + diff[2] = diff[2] >> 5; + + // Calculate second color + enc_color2[0] = enc_color1[0] + diff[0]; + enc_color2[1] = enc_color1[1] + diff[1]; + enc_color2[2] = enc_color1[2] + diff[2]; + + // Expand from 5 to 8 bits + avg_color[0] = (enc_color2[0] << 3) | (enc_color2[0] >> 2); + avg_color[1] = (enc_color2[1] << 3) | (enc_color2[1] >> 2); + avg_color[2] = (enc_color2[2] << 3) | (enc_color2[2] >> 2); + + table = GETBITSHIGH(block_part1, 3, 36) << 1; + pixel_indices_MSB = GETBITS(block_part2, 16, 31); + pixel_indices_LSB = GETBITS(block_part2, 16, 15); + + if ((flipbit) == 0) { + // We should not flip + shift = 8; + for (int x = startx + 2; x < startx + 4; x++) { + for (int y = starty; y < starty + 4; y++) { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index = unscramble[index]; + + r = RED_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[0] + compressParams[table][index], 255)); + g = GREEN_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[1] + compressParams[table][index], 255)); + b = BLUE_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[2] + compressParams[table][index], 255)); + } + } + } else { + // We should flip + shift = 2; + for (int x = startx; x < startx + 4; x++) { + for (int y = starty + 2; y < starty + 4; y++) { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index = unscramble[index]; + + r = RED_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[0] + compressParams[table][index], 255)); + g = GREEN_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[1] + compressParams[table][index], 255)); + b = BLUE_CHANNEL(img, width, x, y, channels) = static_cast( + CLAMP(0, avg_color[2] + compressParams[table][index], 255)); + } + shift += 2; + } + } + } +} +void decompressBlockDiffFlip(unsigned int block_part1, unsigned int block_part2, + uint8* img, int width, int height, int startx, + int starty) { + decompressBlockDiffFlipC(block_part1, block_part2, img, width, height, startx, + starty, 3); +} + +// Decompress an ETC2 RGB block +// NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights +// Reserved. +void decompressBlockETC2c(unsigned int block_part1, unsigned int block_part2, + uint8* img, int width, int height, int startx, + int starty, int channels) { + int diffbit; + signed char color1[3]; + signed char diff[3]; + signed char red, green, blue; + + diffbit = (GETBITSHIGH(block_part1, 1, 33)); + + if (diffbit) { + // We have diffbit = 1; + + // Base color + color1[0] = static_cast(GETBITSHIGH(block_part1, 5, 63)); + color1[1] = static_cast(GETBITSHIGH(block_part1, 5, 55)); + color1[2] = static_cast(GETBITSHIGH(block_part1, 5, 47)); + + // Diff color + diff[0] = static_cast(GETBITSHIGH(block_part1, 3, 58)); + diff[1] = static_cast(GETBITSHIGH(block_part1, 3, 50)); + diff[2] = static_cast(GETBITSHIGH(block_part1, 3, 42)); + + // Extend sign bit to entire byte. + diff[0] = (diff[0] << 5); + diff[1] = (diff[1] << 5); + diff[2] = (diff[2] << 5); + diff[0] = diff[0] >> 5; + diff[1] = diff[1] >> 5; + diff[2] = diff[2] >> 5; + + red = color1[0] + diff[0]; + green = color1[1] + diff[1]; + blue = color1[2] + diff[2]; + + if (red < 0 || red > 31) { + unsigned int block59_part1, block59_part2; + unstuff59bits(block_part1, block_part2, block59_part1, block59_part2); + decompressBlockTHUMB59Tc(block59_part1, block59_part2, img, width, height, + startx, starty, channels); + } else if (green < 0 || green > 31) { + unsigned int block58_part1, block58_part2; + unstuff58bits(block_part1, block_part2, block58_part1, block58_part2); + decompressBlockTHUMB58Hc(block58_part1, block58_part2, img, width, height, + startx, starty, channels); + } else if (blue < 0 || blue > 31) { + unsigned int block57_part1, block57_part2; + + unstuff57bits(block_part1, block_part2, block57_part1, block57_part2); + decompressBlockPlanar57c(block57_part1, block57_part2, img, width, height, + startx, starty, channels); + } else { + decompressBlockDiffFlipC(block_part1, block_part2, img, width, height, + startx, starty, channels); + } + } else { + // We have diffbit = 0; + decompressBlockDiffFlipC(block_part1, block_part2, img, width, height, + startx, starty, channels); + } +} +void decompressBlockETC2(unsigned int block_part1, unsigned int block_part2, + uint8* img, int width, int height, int startx, + int starty) { + decompressBlockETC2c(block_part1, block_part2, img, width, height, startx, + starty, 3); +} +// Decompress an ETC2 block with punchthrough alpha +// NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights +// Reserved. +void decompressBlockDifferentialWithAlphaC(unsigned int block_part1, + unsigned int block_part2, uint8* img, + uint8* alpha, int width, int height, + int startx, int starty, + int channelsRGB) { + uint8 avg_color[3], enc_color1[3], enc_color2[3]; + signed char diff[3]; + int table; + int index, shift; + int r, g, b; + int diffbit; + int flipbit; + int channelsA; + + if (channelsRGB == 3) { + // We will decode the alpha data to a separate memory area. + channelsA = 1; + } else { + // We will decode the RGB data and the alpha data to the same memory area, + // interleaved as RGBA. + channelsA = 4; + alpha = &img[0 + 3]; + } + + // the diffbit now encodes whether or not the entire alpha channel is 255. + diffbit = (GETBITSHIGH(block_part1, 1, 33)); + flipbit = (GETBITSHIGH(block_part1, 1, 32)); + + // First decode left part of block. + enc_color1[0] = static_cast(GETBITSHIGH(block_part1, 5, 63)); + enc_color1[1] = static_cast(GETBITSHIGH(block_part1, 5, 55)); + enc_color1[2] = static_cast(GETBITSHIGH(block_part1, 5, 47)); + + // Expand from 5 to 8 bits + avg_color[0] = (enc_color1[0] << 3) | (enc_color1[0] >> 2); + avg_color[1] = (enc_color1[1] << 3) | (enc_color1[1] >> 2); + avg_color[2] = (enc_color1[2] << 3) | (enc_color1[2] >> 2); + + table = GETBITSHIGH(block_part1, 3, 39) << 1; + + unsigned int pixel_indices_MSB, pixel_indices_LSB; + + pixel_indices_MSB = GETBITS(block_part2, 16, 31); + pixel_indices_LSB = GETBITS(block_part2, 16, 15); + + if ((flipbit) == 0) { + // We should not flip + shift = 0; + for (int x = startx; x < startx + 2; x++) { + for (int y = starty; y < starty + 4; y++) { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index = unscramble[index]; + + int mod = compressParams[table][index]; + if (diffbit == 0 && (index == 1 || index == 2)) { + mod = 0; + } + + r = RED_CHANNEL(img, width, x, y, channelsRGB) = + static_cast(CLAMP(0, avg_color[0] + mod, 255)); + g = GREEN_CHANNEL(img, width, x, y, channelsRGB) = + static_cast(CLAMP(0, avg_color[1] + mod, 255)); + b = BLUE_CHANNEL(img, width, x, y, channelsRGB) = + static_cast(CLAMP(0, avg_color[2] + mod, 255)); + if (diffbit == 0 && index == 1) { + alpha[(y * width + x) * channelsA] = 0; + r = RED_CHANNEL(img, width, x, y, channelsRGB) = 0; + g = GREEN_CHANNEL(img, width, x, y, channelsRGB) = 0; + b = BLUE_CHANNEL(img, width, x, y, channelsRGB) = 0; + } else { + alpha[(y * width + x) * channelsA] = 255; + } + } + } + } else { + // We should flip + shift = 0; + for (int x = startx; x < startx + 4; x++) { + for (int y = starty; y < starty + 2; y++) { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index = unscramble[index]; + int mod = compressParams[table][index]; + if (diffbit == 0 && (index == 1 || index == 2)) { + mod = 0; + } + r = RED_CHANNEL(img, width, x, y, channelsRGB) = + static_cast(CLAMP(0, avg_color[0] + mod, 255)); + g = GREEN_CHANNEL(img, width, x, y, channelsRGB) = + static_cast(CLAMP(0, avg_color[1] + mod, 255)); + b = BLUE_CHANNEL(img, width, x, y, channelsRGB) = + static_cast(CLAMP(0, avg_color[2] + mod, 255)); + if (diffbit == 0 && index == 1) { + alpha[(y * width + x) * channelsA] = 0; + r = RED_CHANNEL(img, width, x, y, channelsRGB) = 0; + g = GREEN_CHANNEL(img, width, x, y, channelsRGB) = 0; + b = BLUE_CHANNEL(img, width, x, y, channelsRGB) = 0; + } else { + alpha[(y * width + x) * channelsA] = 255; + } + } + shift += 2; + } + } + // Now decode right part of block. + diff[0] = static_cast(GETBITSHIGH(block_part1, 3, 58)); + diff[1] = static_cast(GETBITSHIGH(block_part1, 3, 50)); + diff[2] = static_cast(GETBITSHIGH(block_part1, 3, 42)); + + // Extend sign bit to entire byte. + diff[0] = (diff[0] << 5); + diff[1] = (diff[1] << 5); + diff[2] = (diff[2] << 5); + diff[0] = diff[0] >> 5; + diff[1] = diff[1] >> 5; + diff[2] = diff[2] >> 5; + + // Calculate second color + enc_color2[0] = enc_color1[0] + diff[0]; + enc_color2[1] = enc_color1[1] + diff[1]; + enc_color2[2] = enc_color1[2] + diff[2]; + + // Expand from 5 to 8 bits + avg_color[0] = (enc_color2[0] << 3) | (enc_color2[0] >> 2); + avg_color[1] = (enc_color2[1] << 3) | (enc_color2[1] >> 2); + avg_color[2] = (enc_color2[2] << 3) | (enc_color2[2] >> 2); + + table = GETBITSHIGH(block_part1, 3, 36) << 1; + pixel_indices_MSB = GETBITS(block_part2, 16, 31); + pixel_indices_LSB = GETBITS(block_part2, 16, 15); + + if ((flipbit) == 0) { + // We should not flip + shift = 8; + for (int x = startx + 2; x < startx + 4; x++) { + for (int y = starty; y < starty + 4; y++) { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index = unscramble[index]; + int mod = compressParams[table][index]; + if (diffbit == 0 && (index == 1 || index == 2)) { + mod = 0; + } + + r = RED_CHANNEL(img, width, x, y, channelsRGB) = + static_cast(CLAMP(0, avg_color[0] + mod, 255)); + g = GREEN_CHANNEL(img, width, x, y, channelsRGB) = + static_cast(CLAMP(0, avg_color[1] + mod, 255)); + b = BLUE_CHANNEL(img, width, x, y, channelsRGB) = + static_cast(CLAMP(0, avg_color[2] + mod, 255)); + if (diffbit == 0 && index == 1) { + alpha[(y * width + x) * channelsA] = 0; + r = RED_CHANNEL(img, width, x, y, channelsRGB) = 0; + g = GREEN_CHANNEL(img, width, x, y, channelsRGB) = 0; + b = BLUE_CHANNEL(img, width, x, y, channelsRGB) = 0; + } else { + alpha[(y * width + x) * channelsA] = 255; + } + } + } + } else { + // We should flip + shift = 2; + for (int x = startx; x < startx + 4; x++) { + for (int y = starty + 2; y < starty + 4; y++) { + index = ((pixel_indices_MSB >> shift) & 1) << 1; + index |= ((pixel_indices_LSB >> shift) & 1); + shift++; + index = unscramble[index]; + int mod = compressParams[table][index]; + if (diffbit == 0 && (index == 1 || index == 2)) { + mod = 0; + } + + r = RED_CHANNEL(img, width, x, y, channelsRGB) = + static_cast(CLAMP(0, avg_color[0] + mod, 255)); + g = GREEN_CHANNEL(img, width, x, y, channelsRGB) = + static_cast(CLAMP(0, avg_color[1] + mod, 255)); + b = BLUE_CHANNEL(img, width, x, y, channelsRGB) = + static_cast(CLAMP(0, avg_color[2] + mod, 255)); + if (diffbit == 0 && index == 1) { + alpha[(y * width + x) * channelsA] = 0; + r = RED_CHANNEL(img, width, x, y, channelsRGB) = 0; + g = GREEN_CHANNEL(img, width, x, y, channelsRGB) = 0; + b = BLUE_CHANNEL(img, width, x, y, channelsRGB) = 0; + } else { + alpha[(y * width + x) * channelsA] = 255; + } + } + shift += 2; + } + } +} +void decompressBlockDifferentialWithAlpha(unsigned int block_part1, + unsigned int block_part2, uint8* img, + uint8* alpha, int width, int height, + int startx, int starty) { + decompressBlockDifferentialWithAlphaC(block_part1, block_part2, img, alpha, + width, height, startx, starty, 3); +} + +// similar to regular decompression, but alpha channel is set to 0 if pixel +// index is 2, otherwise 255. NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) +// Ericsson AB 2013. All Rights Reserved. +void decompressBlockTHUMB59TAlphaC(unsigned int block_part1, + unsigned int block_part2, uint8* img, + uint8* alpha, int width, int height, + int startx, int starty, int channelsRGB) { + uint8 colorsRGB444[2][3]; + uint8 colors[2][3]; + uint8 paint_colors[4][3]; + uint8 distance; + uint8 block_mask[4][4]; + int channelsA; + + if (channelsRGB == 3) { + // We will decode the alpha data to a separate memory area. + channelsA = 1; + } else { + // We will decode the RGB data and the alpha data to the same memory area, + // interleaved as RGBA. + channelsA = 4; + alpha = &img[0 + 3]; + } + + // First decode left part of block. + colorsRGB444[0][R] = static_cast(GETBITSHIGH(block_part1, 4, 58)); + colorsRGB444[0][G] = static_cast(GETBITSHIGH(block_part1, 4, 54)); + colorsRGB444[0][B] = static_cast(GETBITSHIGH(block_part1, 4, 50)); + + colorsRGB444[1][R] = static_cast(GETBITSHIGH(block_part1, 4, 46)); + colorsRGB444[1][G] = static_cast(GETBITSHIGH(block_part1, 4, 42)); + colorsRGB444[1][B] = static_cast(GETBITSHIGH(block_part1, 4, 38)); + + distance = static_cast(GETBITSHIGH(block_part1, TABLE_BITS_59T, 34)); + + // Extend the two colors to RGB888 + decompressColor(R_BITS59T, G_BITS59T, B_BITS59T, colorsRGB444, colors); + calculatePaintColors59T(distance, PATTERN_T, colors, paint_colors); + + // Choose one of the four paint colors for each texel + for (uint8 x = 0; x < BLOCKWIDTH; ++x) { + for (uint8 y = 0; y < BLOCKHEIGHT; ++y) { + // block_mask[x][y] = GETBITS(block_part2,2,31-(y*4+x)*2); + block_mask[x][y] = + static_cast(GETBITS(block_part2, 1, (y + x * 4) + 16) << 1); + block_mask[x][y] |= GETBITS(block_part2, 1, (y + x * 4)); + img[channelsRGB * ((starty + y) * width + startx + x) + R] = + static_cast( + CLAMP(0, paint_colors[block_mask[x][y]][R], 255)); // RED + img[channelsRGB * ((starty + y) * width + startx + x) + G] = + static_cast( + CLAMP(0, paint_colors[block_mask[x][y]][G], 255)); // GREEN + img[channelsRGB * ((starty + y) * width + startx + x) + B] = + static_cast( + CLAMP(0, paint_colors[block_mask[x][y]][B], 255)); // BLUE + if (block_mask[x][y] == 2) { + alpha[channelsA * (x + startx + (y + starty) * width)] = 0; + img[channelsRGB * ((starty + y) * width + startx + x) + R] = 0; + img[channelsRGB * ((starty + y) * width + startx + x) + G] = 0; + img[channelsRGB * ((starty + y) * width + startx + x) + B] = 0; + } else + alpha[channelsA * (x + startx + (y + starty) * width)] = 255; + } + } +} +void decompressBlockTHUMB59TAlpha(unsigned int block_part1, + unsigned int block_part2, uint8* img, + uint8* alpha, int width, int height, + int startx, int starty) { + decompressBlockTHUMB59TAlphaC(block_part1, block_part2, img, alpha, width, + height, startx, starty, 3); +} + +// Decompress an H-mode block with alpha +// NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights +// Reserved. +void decompressBlockTHUMB58HAlphaC(unsigned int block_part1, + unsigned int block_part2, uint8* img, + uint8* alpha, int width, int height, + int startx, int starty, int channelsRGB) { + unsigned int col0, col1; + uint8 colors[2][3]; + uint8 colorsRGB444[2][3]; + uint8 paint_colors[4][3]; + uint8 distance; + uint8 block_mask[4][4]; + int channelsA; + + if (channelsRGB == 3) { + // We will decode the alpha data to a separate memory area. + channelsA = 1; + } else { + // We will decode the RGB data and the alpha data to the same memory area, + // interleaved as RGBA. + channelsA = 4; + alpha = &img[0 + 3]; + } + + // First decode left part of block. + colorsRGB444[0][R] = static_cast(GETBITSHIGH(block_part1, 4, 57)); + colorsRGB444[0][G] = static_cast(GETBITSHIGH(block_part1, 4, 53)); + colorsRGB444[0][B] = static_cast(GETBITSHIGH(block_part1, 4, 49)); + + colorsRGB444[1][R] = static_cast(GETBITSHIGH(block_part1, 4, 45)); + colorsRGB444[1][G] = static_cast(GETBITSHIGH(block_part1, 4, 41)); + colorsRGB444[1][B] = static_cast(GETBITSHIGH(block_part1, 4, 37)); + + distance = 0; + distance = static_cast((GETBITSHIGH(block_part1, 2, 33)) << 1); + + col0 = GETBITSHIGH(block_part1, 12, 57); + col1 = GETBITSHIGH(block_part1, 12, 45); + + if (col0 >= col1) { + distance |= 1; + } + + // Extend the two colors to RGB888 + decompressColor(R_BITS58H, G_BITS58H, B_BITS58H, colorsRGB444, colors); + + calculatePaintColors58H(distance, PATTERN_H, colors, paint_colors); + + // Choose one of the four paint colors for each texel + for (uint8 x = 0; x < BLOCKWIDTH; ++x) { + for (uint8 y = 0; y < BLOCKHEIGHT; ++y) { + // block_mask[x][y] = GETBITS(block_part2,2,31-(y*4+x)*2); + block_mask[x][y] = + static_cast(GETBITS(block_part2, 1, (y + x * 4) + 16) << 1); + block_mask[x][y] |= GETBITS(block_part2, 1, (y + x * 4)); + img[channelsRGB * ((starty + y) * width + startx + x) + R] = + static_cast( + CLAMP(0, paint_colors[block_mask[x][y]][R], 255)); // RED + img[channelsRGB * ((starty + y) * width + startx + x) + G] = + static_cast( + CLAMP(0, paint_colors[block_mask[x][y]][G], 255)); // GREEN + img[channelsRGB * ((starty + y) * width + startx + x) + B] = + static_cast( + CLAMP(0, paint_colors[block_mask[x][y]][B], 255)); // BLUE + + if (block_mask[x][y] == 2) { + alpha[channelsA * (x + startx + (y + starty) * width)] = 0; + img[channelsRGB * ((starty + y) * width + startx + x) + R] = 0; + img[channelsRGB * ((starty + y) * width + startx + x) + G] = 0; + img[channelsRGB * ((starty + y) * width + startx + x) + B] = 0; + } else + alpha[channelsA * (x + startx + (y + starty) * width)] = 255; + } + } +} +void decompressBlockTHUMB58HAlpha(unsigned int block_part1, + unsigned int block_part2, uint8* img, + uint8* alpha, int width, int height, + int startx, int starty) { + decompressBlockTHUMB58HAlphaC(block_part1, block_part2, img, alpha, width, + height, startx, starty, 3); +} +// Decompression function for ETC2_RGBA1 format. +// NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights +// Reserved. +void decompressBlockETC21BitAlphaC(unsigned int block_part1, + unsigned int block_part2, uint8* img, + uint8* alphaimg, int width, int height, + int startx, int starty, int channelsRGB) { + int diffbit; + signed char color1[3]; + signed char diff[3]; + signed char red, green, blue; + int channelsA; + + if (channelsRGB == 3) { + // We will decode the alpha data to a separate memory area. + channelsA = 1; + } else { + // We will decode the RGB data and the alpha data to the same memory area, + // interleaved as RGBA. + channelsA = 4; + alphaimg = &img[0 + 3]; + } + + diffbit = (GETBITSHIGH(block_part1, 1, 33)); + + if (diffbit) { + // We have diffbit = 1, meaning no transparent pixels. regular + // decompression. + + // Base color + color1[0] = static_cast(GETBITSHIGH(block_part1, 5, 63)); + color1[1] = static_cast(GETBITSHIGH(block_part1, 5, 55)); + color1[2] = static_cast(GETBITSHIGH(block_part1, 5, 47)); + + // Diff color + diff[0] = static_cast(GETBITSHIGH(block_part1, 3, 58)); + diff[1] = static_cast(GETBITSHIGH(block_part1, 3, 50)); + diff[2] = static_cast(GETBITSHIGH(block_part1, 3, 42)); + + // Extend sign bit to entire byte. + diff[0] = (diff[0] << 5); + diff[1] = (diff[1] << 5); + diff[2] = (diff[2] << 5); + diff[0] = diff[0] >> 5; + diff[1] = diff[1] >> 5; + diff[2] = diff[2] >> 5; + + red = color1[0] + diff[0]; + green = color1[1] + diff[1]; + blue = color1[2] + diff[2]; + + if (red < 0 || red > 31) { + unsigned int block59_part1, block59_part2; + unstuff59bits(block_part1, block_part2, block59_part1, block59_part2); + decompressBlockTHUMB59Tc(block59_part1, block59_part2, img, width, height, + startx, starty, channelsRGB); + } else if (green < 0 || green > 31) { + unsigned int block58_part1, block58_part2; + unstuff58bits(block_part1, block_part2, block58_part1, block58_part2); + decompressBlockTHUMB58Hc(block58_part1, block58_part2, img, width, height, + startx, starty, channelsRGB); + } else if (blue < 0 || blue > 31) { + unsigned int block57_part1, block57_part2; + + unstuff57bits(block_part1, block_part2, block57_part1, block57_part2); + decompressBlockPlanar57c(block57_part1, block57_part2, img, width, height, + startx, starty, channelsRGB); + } else { + decompressBlockDifferentialWithAlphaC(block_part1, block_part2, img, + alphaimg, width, height, startx, + starty, channelsRGB); + } + for (int x = startx; x < startx + 4; x++) { + for (int y = starty; y < starty + 4; y++) { + alphaimg[channelsA * (x + y * width)] = 255; + } + } + } else { + // We have diffbit = 0, transparent pixels. Only T-, H- or regular diff-mode + // possible. + + // Base color + color1[0] = static_cast(GETBITSHIGH(block_part1, 5, 63)); + color1[1] = static_cast(GETBITSHIGH(block_part1, 5, 55)); + color1[2] = static_cast(GETBITSHIGH(block_part1, 5, 47)); + + // Diff color + diff[0] = static_cast(GETBITSHIGH(block_part1, 3, 58)); + diff[1] = static_cast(GETBITSHIGH(block_part1, 3, 50)); + diff[2] = static_cast(GETBITSHIGH(block_part1, 3, 42)); + + // Extend sign bit to entire byte. + diff[0] = (diff[0] << 5); + diff[1] = (diff[1] << 5); + diff[2] = (diff[2] << 5); + diff[0] = diff[0] >> 5; + diff[1] = diff[1] >> 5; + diff[2] = diff[2] >> 5; + + red = color1[0] + diff[0]; + green = color1[1] + diff[1]; + blue = color1[2] + diff[2]; + if (red < 0 || red > 31) { + unsigned int block59_part1, block59_part2; + unstuff59bits(block_part1, block_part2, block59_part1, block59_part2); + decompressBlockTHUMB59TAlphaC(block59_part1, block59_part2, img, alphaimg, + width, height, startx, starty, channelsRGB); + } else if (green < 0 || green > 31) { + unsigned int block58_part1, block58_part2; + unstuff58bits(block_part1, block_part2, block58_part1, block58_part2); + decompressBlockTHUMB58HAlphaC(block58_part1, block58_part2, img, alphaimg, + width, height, startx, starty, channelsRGB); + } else if (blue < 0 || blue > 31) { + unsigned int block57_part1, block57_part2; + + unstuff57bits(block_part1, block_part2, block57_part1, block57_part2); + decompressBlockPlanar57c(block57_part1, block57_part2, img, width, height, + startx, starty, channelsRGB); + for (int x = startx; x < startx + 4; x++) { + for (int y = starty; y < starty + 4; y++) { + alphaimg[channelsA * (x + y * width)] = 255; + } + } + } else + decompressBlockDifferentialWithAlphaC(block_part1, block_part2, img, + alphaimg, width, height, startx, + starty, channelsRGB); + } +} +void decompressBlockETC21BitAlpha(unsigned int block_part1, + unsigned int block_part2, uint8* img, + uint8* alphaimg, int width, int height, + int startx, int starty) { + decompressBlockETC21BitAlphaC(block_part1, block_part2, img, alphaimg, width, + height, startx, starty, 3); +} +// +// Utility functions used for alpha compression +// + +// bit number frompos is extracted from input, and moved to bit number topos in +// the return value. NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson +// AB 2013. All Rights Reserved. +auto getbit(uint8 input, int frompos, int topos) -> uint8 { + // uint8 output=0; + if (frompos > topos) + return static_cast(((1 << frompos) & input) >> (frompos - topos)); + return static_cast(((1 << frompos) & input) << (topos - frompos)); +} + +// takes as input a value, returns the value clamped to the interval [0,255]. +// NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights +// Reserved. +auto clamp(int val) -> int { + if (val < 0) val = 0; + if (val > 255) val = 255; + return val; +} + +// Decodes tha alpha component in a block coded with +// GL_COMPRESSED_RGBA8_ETC2_EAC. Note that this decoding is slightly different +// from that of GL_COMPRESSED_R11_EAC. However, a hardware decoder can share +// gates between the two formats as explained in the specification under +// GL_COMPRESSED_R11_EAC. NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) +// Ericsson AB 2013. All Rights Reserved. +void decompressBlockAlphaC(uint8* data, uint8* img, int width, int height, + int ix, int iy, int channels) { + int alpha = data[0]; + int table = data[1]; + + int bit = 0; + int byte = 2; + // extract an alpha value for each pixel. + for (int x = 0; x < 4; x++) { + for (int y = 0; y < 4; y++) { + // Extract table index + int index = 0; + for (int bitpos = 0; bitpos < 3; bitpos++) { + index |= getbit(data[byte], 7 - bit, 2 - bitpos); + bit++; + if (bit > 7) { + bit = 0; + byte++; + } + } + img[(ix + x + (iy + y) * width) * channels] = + static_cast(clamp(alpha + alphaTable[table][index])); + } + } +} +void decompressBlockAlpha(uint8* data, uint8* img, int width, int height, + int ix, int iy) { + decompressBlockAlphaC(data, img, width, height, ix, iy, 1); +} + +// Does decompression and then immediately converts from 11 bit signed to a +// 16-bit format. +// +// NO WARRANTY --- SEE STATEMENT IN TOP OF FILE (C) Ericsson AB 2013. All Rights +// Reserved. +auto get16bits11signed(int base, int table, int mul, int index) -> int16 { + int elevenbase = base - 128; + if (elevenbase == -128) elevenbase = -127; + elevenbase *= 8; + // i want the positive value here + int tabVal = -alphaBase[table][3 - index % 4] - 1; + // and the sign, please + int sign = 1 - (index / 4); + + if (sign) tabVal = tabVal + 1; + int elevenTabVal = tabVal * 8; + + if (mul != 0) + elevenTabVal *= mul; + else + elevenTabVal /= 8; + + if (sign) elevenTabVal = -elevenTabVal; + + // calculate sum + int elevenbits = elevenbase + elevenTabVal; + + // clamp.. + if (elevenbits >= 1024) + elevenbits = 1023; + else if (elevenbits < -1023) + elevenbits = -1023; + // this is the value we would actually output.. + // but there aren't any good 11-bit file or uncompressed GL formats + // so we extend to 15 bits signed. + sign = elevenbits < 0; + elevenbits = abs(elevenbits); + auto fifteenbits = static_cast((elevenbits << 5) + (elevenbits >> 5)); + auto sixteenbits = static_cast(fifteenbits); + + if (sign) sixteenbits = -sixteenbits; + + return sixteenbits; +} + +// Does decompression and then immediately converts from 11 bit signed to a +// 16-bit format Calculates the 11 bit value represented by base, table, mul and +// index, and extends it to 16 bits. NO WARRANTY --- SEE STATEMENT IN TOP OF +// FILE (C) Ericsson AB 2013. All Rights Reserved. +auto get16bits11bits(int base, int table, int mul, int index) -> uint16 { + int elevenbase = base * 8 + 4; + + // i want the positive value here + int tabVal = -alphaBase[table][3 - index % 4] - 1; + // and the sign, please + int sign = 1 - (index / 4); + + if (sign) tabVal = tabVal + 1; + int elevenTabVal = tabVal * 8; + + if (mul != 0) + elevenTabVal *= mul; + else + elevenTabVal /= 8; + + if (sign) elevenTabVal = -elevenTabVal; + + // calculate sum + int elevenbits = elevenbase + elevenTabVal; + + // clamp.. + if (elevenbits >= 256 * 8) + elevenbits = 256 * 8 - 1; + else if (elevenbits < 0) + elevenbits = 0; + // elevenbits now contains the 11 bit alpha value as defined in the spec. + + // extend to 16 bits before returning, since we don't have any good 11-bit + // file formats. + auto sixteenbits = static_cast((elevenbits << 5) + (elevenbits >> 6)); + + return sixteenbits; +} + +// Decompresses a block using one of the GL_COMPRESSED_R11_EAC or +// GL_COMPRESSED_SIGNED_R11_EAC-formats NO WARRANTY --- SEE STATEMENT IN TOP OF +// FILE (C) Ericsson AB 2013. All Rights Reserved. +void decompressBlockAlpha16bitC(uint8* data, uint8* img, int width, int height, + int ix, int iy, int channels) { + int alpha = data[0]; + int table = data[1]; + + if (formatSigned) { + // if we have a signed format, the base value is given as a signed byte. We + // convert it to (0-255) here, so more code can be shared with the unsigned + // mode. + alpha = *((signed char*)(&data[0])); + alpha = alpha + 128; + } + + int bit = 0; + int byte = 2; + // extract an alpha value for each pixel. + for (int x = 0; x < 4; x++) { + for (int y = 0; y < 4; y++) { + // Extract table index + int index = 0; + for (int bitpos = 0; bitpos < 3; bitpos++) { + index |= getbit(data[byte], 7 - bit, 2 - bitpos); + bit++; + if (bit > 7) { + bit = 0; + byte++; + } + } + int windex = channels * (2 * (ix + x + (iy + y) * width)); +#if !PGMOUT + if (formatSigned) { + *(int16*)&img[windex] = + get16bits11signed(alpha, (table % 16), (table / 16), index); + } else { + *(uint16*)&img[windex] = + get16bits11bits(alpha, (table % 16), (table / 16), index); + } +#else + // make data compatible with the .pgm format. See the comment in + // compressBlockAlpha16() for details. + uint16 uSixteen; + if (formatSigned) { + // the pgm-format only allows unsigned images, + // so we add 2^15 to get a 16-bit value. + uSixteen = get16bits11signed(alpha, (table % 16), (table / 16), index) + + 256 * 128; + } else { + uSixteen = get16bits11bits(alpha, (table % 16), (table / 16), index); + } + // byte swap for pgm + img[windex] = uSixteen / 256; + img[windex + 1] = uSixteen % 256; +#endif + } + } +} + +void decompressBlockAlpha16bit(uint8* data, uint8* img, int width, int height, + int ix, int iy) { + decompressBlockAlpha16bitC(data, img, width, height, ix, iy, 1); +} + +static void readBigEndian4byteWord(uint32_t* pBlock, const GLubyte* s) { + *pBlock = (s[0] << 24) | (s[1] << 16) | (s[2] << 8) | s[3]; +} + +void KTXUnpackETC(const GLubyte* srcETC, const GLenum srcFormat, + uint32_t activeWidth, uint32_t activeHeight, + GLubyte** dstImage, GLenum* format, GLenum* internal_format, + GLenum* type, GLint R16Formats, bool supportsSRGB) { + unsigned int width, height; + unsigned int block_part1, block_part2; + unsigned int x, y; + /*const*/ auto* src = (GLubyte*)srcETC; + // AF_11BIT is used to compress R11 & RG11 though its not alpha data. + enum { AF_NONE, AF_1BIT, AF_8BIT, AF_11BIT } alphaFormat = AF_NONE; + int dstChannels, dstChannelBytes; + + switch (srcFormat) { + // case GL_COMPRESSED_SIGNED_R11_EAC: + // if (R16Formats & _KTX_R16_FORMATS_SNORM) { + // dstChannelBytes = sizeof(GLshort); + // dstChannels = 1; + // formatSigned = GL_TRUE; + // *internal_format = GL_R16_SNORM; + // *format = GL_RED; + // *type = GL_SHORT; + // alphaFormat = AF_11BIT; + // } else + // return KTX_UNSUPPORTED_TEXTURE_TYPE; + // break; + + // case GL_COMPRESSED_R11_EAC: + // if (R16Formats & _KTX_R16_FORMATS_NORM) { + // dstChannelBytes = sizeof(GLshort); + // dstChannels = 1; + // formatSigned = GL_FALSE; + // *internal_format = GL_R16; + // *format = GL_RED; + // *type = GL_UNSIGNED_SHORT; + // alphaFormat = AF_11BIT; + // } else + // return KTX_UNSUPPORTED_TEXTURE_TYPE; + // break; + + // case GL_COMPRESSED_SIGNED_RG11_EAC: + // if (R16Formats & _KTX_R16_FORMATS_SNORM) { + // dstChannelBytes = sizeof(GLshort); + // dstChannels = 2; + // formatSigned = GL_TRUE; + // *internal_format = GL_RG16_SNORM; + // *format = GL_RG; + // *type = GL_SHORT; + // alphaFormat = AF_11BIT; + // } else + // return KTX_UNSUPPORTED_TEXTURE_TYPE; + // break; + + // case GL_COMPRESSED_RG11_EAC: + // if (R16Formats & _KTX_R16_FORMATS_NORM) { + // dstChannelBytes = sizeof(GLshort); + // dstChannels = 2; + // formatSigned = GL_FALSE; + // *internal_format = GL_RG16; + // *format = GL_RG; + // *type = GL_UNSIGNED_SHORT; + // alphaFormat = AF_11BIT; + // } else + // return KTX_UNSUPPORTED_TEXTURE_TYPE; + // break; + + case GL_ETC1_RGB8_OES: + case GL_COMPRESSED_RGB8_ETC2: + dstChannelBytes = sizeof(GLubyte); + dstChannels = 3; + //*internal_format = GL_RGB8; + *format = GL_RGB; + *type = GL_UNSIGNED_BYTE; + break; + + case GL_COMPRESSED_RGBA8_ETC2_EAC: + dstChannelBytes = sizeof(GLubyte); + dstChannels = 4; + //*internal_format = GL_RGBA8; + *format = GL_RGBA; + *type = GL_UNSIGNED_BYTE; + alphaFormat = AF_8BIT; + break; + + // case GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2: + // dstChannelBytes = sizeof(GLubyte); + // dstChannels = 4; + // *internal_format = GL_RGBA8; + // *format = GL_RGBA; + // *type = GL_UNSIGNED_BYTE; + // alphaFormat = AF_1BIT; + // break; + + // case GL_COMPRESSED_SRGB8_ETC2: + // if (supportsSRGB) { + // dstChannelBytes = sizeof(GLubyte); + // dstChannels = 3; + // *internal_format = GL_SRGB8; + // *format = GL_RGB; + // *type = GL_UNSIGNED_BYTE; + // } else + // return KTX_UNSUPPORTED_TEXTURE_TYPE; + // break; + + // case GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC: + // if (supportsSRGB) { + // dstChannelBytes = sizeof(GLubyte); + // dstChannels = 4; + // *internal_format = GL_SRGB8_ALPHA8; + // *format = GL_RGBA; + // *type = GL_UNSIGNED_BYTE; + // alphaFormat = AF_8BIT; + // } else + // return KTX_UNSUPPORTED_TEXTURE_TYPE; + // break; + + // case GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2: + // if (supportsSRGB) { + // dstChannelBytes = sizeof(GLubyte); + // dstChannels = 4; + // *internal_format = GL_SRGB8_ALPHA8; + // *format = GL_RGBA; + // *type = GL_UNSIGNED_BYTE; + // alphaFormat = AF_1BIT; + // } else + // return KTX_UNSUPPORTED_TEXTURE_TYPE; + // break; + + default: + throw Exception(); + // assert(0); // Upper levels should be passing only one of the above + // srcFormats. + } + + /* active_{width,height} show how many pixels contain active data, + * (the rest are just for making sure we have a 2*a x 4*b size). + */ + + /* Compute the full width & height. */ + width = ((activeWidth + 3) / 4) * 4; + height = ((activeHeight + 3) / 4) * 4; + + /* printf("Width = %d, Height = %d\n", width, height); */ + /* printf("active pixel area: top left %d x %d area.\n", activeWidth, + * activeHeight); */ + + *dstImage = (GLubyte*)malloc(dstChannels * dstChannelBytes * width * height); + if (!*dstImage) { + throw Exception(); + // return KTX_OUT_OF_MEMORY; + } + + if (alphaFormat != AF_NONE) setupAlphaTable(); + + // NOTE: none of the decompress functions actually use the parameter + if (alphaFormat == AF_11BIT) { + throw Exception(); + // // One or two 11-bit alpha channels for R or RG. + // for (y = 0; y < height / 4; y++) { + // for (x = 0; x < width / 4; x++) { + // decompressBlockAlpha16bitC(src, *dstImage, width, height, 4 * x, 4 + // * y, + // dstChannels); + // src += 8; + // // if (srcFormat == GL_COMPRESSED_RG11_EAC || srcFormat == + // // GL_COMPRESSED_SIGNED_RG11_EAC) { + // decompressBlockAlpha16bitC(src, + // // *dstImage + dstChannelBytes, width, height, 4*x, 4*y, + // dstChannels); + // // src += 8; + // // } + // } + // } + } else { + for (y = 0; y < height / 4; y++) { + for (x = 0; x < width / 4; x++) { + // Decode alpha channel for RGBA + if (alphaFormat == AF_8BIT) { + decompressBlockAlphaC(src, *dstImage + 3, width, height, 4 * x, 4 * y, + dstChannels); + src += 8; + } + // Decode color dstChannels + readBigEndian4byteWord(&block_part1, src); + src += 4; + readBigEndian4byteWord(&block_part2, src); + src += 4; + if (alphaFormat == AF_1BIT) + decompressBlockETC21BitAlphaC(block_part1, block_part2, *dstImage, + nullptr, width, height, 4 * x, 4 * y, + dstChannels); + else + decompressBlockETC2c(block_part1, block_part2, *dstImage, width, + height, 4 * x, 4 * y, dstChannels); + } + } + } + + /* Ok, now write out the active pixels to the destination image. + * (But only if the active pixels differ from the total pixels) + */ + + if (!(height == activeHeight && width == activeWidth)) { + int dstPixelBytes = dstChannels * dstChannelBytes; + int dstRowBytes = dstPixelBytes * width; + int activeRowBytes = activeWidth * dstPixelBytes; + auto* newimg = (GLubyte*)malloc(dstPixelBytes * activeWidth * activeHeight); + unsigned int xx, yy; + int zz; + + if (!newimg) { + free(*dstImage); + // return KTX_OUT_OF_MEMORY; + throw Exception(); + } + + /* Convert from total area to active area: */ + + for (yy = 0; yy < activeHeight; yy++) { + for (xx = 0; xx < activeWidth; xx++) { + for (zz = 0; zz < dstPixelBytes; zz++) { + // NOLINTNEXTLINE + newimg[yy * activeRowBytes + xx * dstPixelBytes + zz] = + (*dstImage)[yy * dstRowBytes + xx * dstPixelBytes + zz]; + } + } + } + + free(*dstImage); + *dstImage = newimg; + } +} + +#pragma clang diagnostic pop + +} // namespace ballistica + +#endif // !BA_HEADLESS_BUILD diff --git a/src/ballistica/graphics/texture/ktx.h b/src/ballistica/graphics/texture/ktx.h new file mode 100644 index 00000000..0f9463b8 --- /dev/null +++ b/src/ballistica/graphics/texture/ktx.h @@ -0,0 +1,29 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_TEXTURE_KTX_H_ +#define BALLISTICA_GRAPHICS_TEXTURE_KTX_H_ + +#include + +#include "ballistica/ballistica.h" + +// currently need gl for this stuff.. probably not necessary. +#if BA_ENABLE_OPENGL + +namespace ballistica { + +void LoadKTX(const std::string& file_name, unsigned char** buffers, int* widths, + int* heights, TextureFormat* formats, size_t* sizes, + TextureQuality texture_quality, int min_quality, int* base_level); + +void KTXUnpackETC(const uint8_t* src_etc, unsigned int src_format, + uint32_t active_width, uint32_t active_height, + uint8_t** dst_image, unsigned int* format, + unsigned int* internal_format, unsigned int* type, + int r16_formats, bool supports_srgb); + +} // namespace ballistica + +#endif // BA_ENABLE_OPENGL + +#endif // BALLISTICA_GRAPHICS_TEXTURE_KTX_H_ diff --git a/src/ballistica/graphics/texture/pvr.cc b/src/ballistica/graphics/texture/pvr.cc new file mode 100644 index 00000000..94762aaf --- /dev/null +++ b/src/ballistica/graphics/texture/pvr.cc @@ -0,0 +1,248 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/graphics/texture/pvr.h" + +#include +#include +#include + +#include "ballistica/ballistica.h" +#include "ballistica/platform/platform.h" + +namespace ballistica { + +#define PVR_TEXTURE_FLAG_TYPE_MASK 0xffu + +enum { kPVRTextureFlagTypePVRTC_2 = 24, kPVRTextureFlagTypePVRTC_4 }; + +typedef struct _PVRTexHeader { + uint32_t headerLength; + uint32_t height; + uint32_t width; + uint32_t numMipmaps; + uint32_t flags; + uint32_t dataLength; + uint32_t bpp; + uint32_t bitmaskRed; + uint32_t bitmaskGreen; + uint32_t bitmaskBlue; + uint32_t bitmaskAlpha; + uint32_t pvrTag; + uint32_t num_surfs; +} PVRTexHeader; + +typedef struct _PVRTexHeader2 { + uint32_t version; + uint32_t flags; + uint64_t pixel_format; + uint32_t color_space; + uint32_t channel_type; + uint32_t height; + uint32_t width; + uint32_t depth; + uint32_t num_surfs; + uint32_t num_faces; + uint32_t numMipmaps; + uint32_t metaSize; +} PVRTexHeader2; + +void LoadPVR(const std::string& file_name, unsigned char** buffers, int* widths, + int* heights, TextureFormat* formats, size_t* sizes, + TextureQuality texture_quality, int min_quality, int* base_level) { + (*base_level) = 0; + + FILE* f = g_platform->FOpen(file_name.c_str(), "rb"); + if (!f) throw Exception("can't open file: \"" + file_name + "\""); + + TextureFormat internal_format; + + uint32_t block_size, width_blocks, height_blocks; + uint32_t width, height, bpp, format_flags; + + if (explicit_bool(true)) { + _PVRTexHeader2 hdr2{}; + + BA_PRECONDITION(fread(&hdr2, 52, 1, f) == 1); + BA_PRECONDITION(hdr2.version == 0x03525650); + BA_PRECONDITION(hdr2.flags == 0); + BA_PRECONDITION(hdr2.color_space == 0); // linear RGB + BA_PRECONDITION(hdr2.channel_type == 0); // unsigned byte normalized + BA_PRECONDITION(hdr2.pixel_format == 2 + || hdr2.pixel_format == 3); // PVRTC 4pp RGB/RGBA + BA_PRECONDITION(hdr2.num_surfs == 1); + BA_PRECONDITION(hdr2.num_faces == 1); + BA_PRECONDITION(hdr2.depth == 1); + + internal_format = TextureFormat::kPVR4; + + // Skip over metadata. + BA_PRECONDITION(fseek(f, + static_cast_check_fit(hdr2.metaSize), // NOLINT + SEEK_CUR) + == 0); + + width = hdr2.width; + height = hdr2.height; + + int mip_map_count = static_cast_check_fit(hdr2.numMipmaps); + + // Try dropping a level for med/low quality. + if ((texture_quality == TextureQuality::kLow + || texture_quality == TextureQuality::kMedium) + && (min_quality < 2) + && static_cast_check_fit(mip_map_count) >= (*base_level) + 1) + (*base_level)++; + + // And one more for low in some cases. + if (texture_quality == TextureQuality::kLow && (min_quality < 1) + && (width > 128) && (height > 128) + && mip_map_count >= (*base_level) + 1) + (*base_level)++; + + // Calculate the data size for each texture level and respect the minimum + // number of blocks + for (int ix = 0; ix < mip_map_count; ix++) { + { + block_size = 4 * 4; // Pixel by pixel block size for 4bpp + width_blocks = width / 4; + height_blocks = height / 4; + bpp = 4; + } + + // Clamp to minimum number of blocks + if (width_blocks < 2) { + width_blocks = 2; + } + if (height_blocks < 2) { + height_blocks = 2; + } + + uint32_t data_size{width_blocks * height_blocks + * ((block_size * bpp) / 8)}; + + // Load or skip levels depending on our quality. + if ((*base_level) <= ix) { + sizes[ix] = data_size; + buffers[ix] = (unsigned char*)malloc(data_size); + BA_PRECONDITION(buffers[ix]); + widths[ix] = width; + heights[ix] = height; + formats[ix] = internal_format; + BA_PRECONDITION(fread(buffers[ix], data_size, 1, f) == 1); + } else { + buffers[ix] = nullptr; + BA_PRECONDITION(fseek(f, + static_cast_check_fit(data_size), // NOLINT + SEEK_CUR) + == 0); + } + width = std::max(width >> 1u, 1u); + height = std::max(height >> 1u, 1u); + } + } else { + uint32_t pvrTag; + + uint32_t data_offset = 0; + + _PVRTexHeader hdr{}; + + BA_PRECONDITION(fread(&hdr, sizeof(hdr), 1, f) == 1); + BA_PRECONDITION(hdr.headerLength == sizeof(_PVRTexHeader)); + + pvrTag = hdr.pvrTag; + if (gPVRTexIdentifier[0] != ((pvrTag >> 0u) & 0xffu) + || gPVRTexIdentifier[1] != ((pvrTag >> 8u) & 0xffu) + || gPVRTexIdentifier[2] != ((pvrTag >> 16u) & 0xffu) + || gPVRTexIdentifier[3] != ((pvrTag >> 24u) & 0xffu)) { + throw Exception("Invalid PVR file: \"" + file_name + "\""); + } + + format_flags = hdr.flags & PVR_TEXTURE_FLAG_TYPE_MASK; + + if (format_flags != kPVRTextureFlagTypePVRTC_4 + && format_flags != kPVRTextureFlagTypePVRTC_2) + throw Exception("Invalid PVR format in file: \"" + file_name + "\""); + + if (format_flags == kPVRTextureFlagTypePVRTC_4) { + internal_format = TextureFormat::kPVR4; + } else if (explicit_bool(format_flags == kPVRTextureFlagTypePVRTC_2)) { + internal_format = TextureFormat::kPVR2; + } else { + throw Exception(); + } + + uint32_t data_length{hdr.dataLength}; + + width = hdr.width; + height = hdr.height; + + int mip_map_count = static_cast_check_fit(hdr.numMipmaps + 1); + + // Try dropping a level for med/low quality. + if ((texture_quality == TextureQuality::kLow + || texture_quality == TextureQuality::kMedium) + && (min_quality < 2) && mip_map_count >= (*base_level) + 1) { + (*base_level)++; + } + + // And one more for low in some cases. + if (texture_quality == TextureQuality::kLow && (min_quality < 1) + && (width > 128) && (height > 128) + && mip_map_count >= (*base_level) + 1) + (*base_level)++; + + // Calculate the data size for each texture level and respect the minimum + // number of blocks + int ix = 0; + while (data_offset < data_length) { + if (format_flags == kPVRTextureFlagTypePVRTC_4) { + block_size = 4 * 4; // Pixel by pixel block size for 4bpp + width_blocks = width / 4; + height_blocks = height / 4; + bpp = 4; + } else { + block_size = 8 * 4; // Pixel by pixel block size for 2bpp + width_blocks = width / 8; + height_blocks = height / 4; + bpp = 2; + } + + // Clamp to minimum number of blocks. + if (width_blocks < 2) { + width_blocks = 2; + } + if (height_blocks < 2) { + height_blocks = 2; + } + + uint32_t data_size{width_blocks * height_blocks + * ((block_size * bpp) / 8)}; + + // Load or skip levels depending on our quality. + if ((*base_level) <= ix) { + sizes[ix] = data_size; + buffers[ix] = (unsigned char*)malloc(data_size); + BA_PRECONDITION(buffers[ix]); + widths[ix] = width; + heights[ix] = height; + formats[ix] = internal_format; + BA_PRECONDITION(fread(buffers[ix], data_size, 1, f) == 1); + } else { + buffers[ix] = nullptr; + BA_PRECONDITION(fseek(f, + static_cast_check_fit(data_size), // NOLINT + SEEK_CUR) + == 0); + } + data_offset += data_size; + + width = std::max(width >> 1u, 1u); + height = std::max(height >> 1u, 1u); + ix++; + } + BA_PRECONDITION(ix == mip_map_count); + } + fclose(f); +} + +} // namespace ballistica diff --git a/src/ballistica/graphics/texture/pvr.h b/src/ballistica/graphics/texture/pvr.h new file mode 100644 index 00000000..27a50f08 --- /dev/null +++ b/src/ballistica/graphics/texture/pvr.h @@ -0,0 +1,20 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_TEXTURE_PVR_H_ +#define BALLISTICA_GRAPHICS_TEXTURE_PVR_H_ + +#include + +#include "ballistica/ballistica.h" + +namespace ballistica { + +static char gPVRTexIdentifier[5] = "PVR!"; + +void LoadPVR(const std::string& file_name, unsigned char** buffers, int* widths, + int* heights, TextureFormat* formats, size_t* sizes, + TextureQuality texture_quality, int min_quality, int* base_level); + +} // namespace ballistica + +#endif // BALLISTICA_GRAPHICS_TEXTURE_PVR_H_ diff --git a/src/ballistica/graphics/vr_graphics.cc b/src/ballistica/graphics/vr_graphics.cc new file mode 100644 index 00000000..0342d428 --- /dev/null +++ b/src/ballistica/graphics/vr_graphics.cc @@ -0,0 +1,336 @@ +// Copyright (c) 2011-2020 Eric Froemling +#if BA_VR_BUILD + +#include "ballistica/graphics/vr_graphics.h" + +#include "ballistica/app/app_globals.h" +#include "ballistica/game/game.h" +#include "ballistica/graphics/camera.h" +#include "ballistica/graphics/component/object_component.h" +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/graphics/component/special_component.h" +#include "ballistica/graphics/frame_def.h" +#include "ballistica/graphics/render_pass.h" + +namespace ballistica { + +static auto ValueTestFloat(float* storage, double* absval, double* deltaval) + -> double { + if (absval) { + *storage = static_cast(*absval); + } + if (deltaval) { + *storage += static_cast(*deltaval); + } + return *storage; +} + +static auto ValueTestBool(bool* storage, double* absval, double* deltaval) + -> double { + if (absval) { + *storage = static_cast(*absval); + } + if (deltaval) { + *storage = (*deltaval > 0.5); + } + return static_cast(*storage); +} + +auto VRGraphics::ValueTest(const std::string& arg, double* absval, + double* deltaval, double* outval) -> bool { + if (arg == "vrOverlayScale") { + *outval = ValueTestFloat(&vr_overlay_scale_, absval, deltaval); + } else if (arg == "lockVROverlay") { + *outval = ValueTestBool(&lock_vr_overlay_, absval, deltaval); + } else if (arg == "showOverlayBounds") { + *outval = ValueTestBool(&draw_overlay_bounds_, absval, deltaval); + } else if (arg == "headScale") { + *outval = ValueTestFloat(&vr_test_head_scale_, absval, deltaval); + } else if (arg == "vrCamOffsetY") { + Camera* camera = g_graphics->camera(); + if (camera) { + Vector3f val = camera->vr_extra_offset(); + if (deltaval) { + camera->set_vr_extra_offset(Vector3f(val.x, val.y + *deltaval, val.z)); + } + if (absval) { + camera->set_vr_extra_offset(Vector3f(val.x, *absval, val.z)); + } + *outval = camera->vr_extra_offset().y; + } + } else if (arg == "vrCamOffsetZ") { + Camera* camera = g_graphics->camera(); + if (camera) { + Vector3f val = camera->vr_extra_offset(); + if (deltaval) { + camera->set_vr_extra_offset(Vector3f(val.x, val.y, val.z + *deltaval)); + } + if (absval) { + camera->set_vr_extra_offset(Vector3f(val.x, val.y, *absval)); + } + *outval = camera->vr_extra_offset().z; + } + } else { + // Unhandled. + return false; + } + return true; +} + +void VRGraphics::ApplyCamera(FrameDef* frame_def) { + Graphics::ApplyCamera(frame_def); + + CalcVROverlayMatrices(frame_def); +} + +void VRGraphics::DrawWorld(Session* session, FrameDef* frame_def) { + // Draw the standard world. + Graphics::DrawWorld(session, frame_def); + + // Draw extra VR-Only bits. + DrawVRControllers(frame_def); +} + +void VRGraphics::DrawUI(FrameDef* frame_def) { + // Draw the UI normally, but then blit its texture into 3d space. + Graphics::DrawUI(frame_def); + + // In VR mode we have to draw our overlay-flat texture into space as + // part of the regular overlay pass. + DrawVROverlay(frame_def); + + // We may want to see the bounds of our overlay. + DrawOverlayBounds(frame_def->overlay_pass()); +} + +void VRGraphics::CalcVROverlayMatrices(FrameDef* frame_def) { + // For VR mode, calc our overlay matrix for use in positioning overlay + // elements. + if (IsVRMode()) { + Vector3f cam_target_pt(frame_def->cam_target_original()); + Matrix44f vr_overlay_matrix{kMatrix44fIdentity}; + Matrix44f vr_overlay_matrix_fixed{kMatrix44fIdentity}; + + // In orbit mode, we sit in the middle and face the camera. + if (frame_def->camera_mode() == CameraMode::kOrbit) { + Vector3f cam_pt(frame_def->cam_original()); + Vector3f cam_target_pt_2(0, 11, -3.3f); + vr_overlay_matrix_fixed = vr_overlay_matrix = + CalcVROverlayMatrix(cam_pt, cam_target_pt_2); + + } else { + // Follow mode. + + // In vr follow-mode the cam point gets tweaked. + // FIXME: Should probably just do this on the camera end. + Vector3f cam_pt = frame_def->cam_original(); + + // During gameplay lets just affix X to our camera (the camera tries to + // match the target's x anyway).. this results in less shuffling. + if (frame_def->camera_mode() == CameraMode::kFollow) { + cam_target_pt.x = cam_pt.x; + } + + // Calc y and z values that are completely fixed to the camera center. + float fixed_y = cam_pt.y + kVRFixedOverlayOffsetY; + float fixed_z = cam_pt.z + kVRFixedOverlayOffsetZ; + + // We smoothly blend our target point between the map-specific + // center-point and our fixed point (between levels we want our two + // overlays to line up since there may be elements coordinated across + // them). + + // FIXME: This shouldn't be based on frames. + { + float this_y, this_z; + if (vr_overlay_center_enabled_) { + this_y = vr_overlay_center_.y; + this_z = vr_overlay_center_.z; + } else { + this_y = fixed_y; + this_z = fixed_z; + } + float smoothing = 0.93f; + float smoothing_inv = 1.0f - smoothing; + + vr_cam_target_pt_smoothed_y_ = + smoothing * vr_cam_target_pt_smoothed_y_ + smoothing_inv * this_y; + vr_cam_target_pt_smoothed_z_ = + smoothing * vr_cam_target_pt_smoothed_z_ + smoothing_inv * this_z; + + cam_target_pt.y = vr_cam_target_pt_smoothed_y_; + cam_target_pt.z = vr_cam_target_pt_smoothed_z_; + } + + vr_overlay_matrix = CalcVROverlayMatrix(cam_pt, cam_target_pt); + + // We also always calc a completely fixed matrix for some elements that + // should *never* move such as score-screens. + cam_target_pt.y = fixed_y; + cam_target_pt.z = fixed_z; + vr_overlay_matrix_fixed = CalcVROverlayMatrix(cam_pt, cam_target_pt); + } + + // Calc a screen-matrix that gives us a drawing area of + // kBaseVirtualResX by kBaseVirtualResY. + frame_def->set_vr_overlay_screen_matrix( + Matrix44fTranslate(-0.5f * kBaseVirtualResX, -0.5f * kBaseVirtualResY, + 0.0f) + * Matrix44fScale( + Vector3f(1.0f / (kBaseVirtualResX * (1.0f + kVRBorder)), + 1.0f / (kBaseVirtualResY * (1.0f + kVRBorder)), + 1.0f / (kBaseVirtualResX * (1.0f + kVRBorder)))) + * vr_overlay_matrix); + + // If we have a fixed-version of the matrix, do the same calcs for it; + // otherwise just copy the non-fixed. + frame_def->set_vr_overlay_screen_matrix_fixed( + Matrix44fTranslate(-0.5f * kBaseVirtualResX, -0.5f * kBaseVirtualResY, + 0.0f) + * Matrix44fScale( + Vector3f(1.0f / (kBaseVirtualResX * (1.0f + kVRBorder)), + 1.0f / (kBaseVirtualResY * (1.0f + kVRBorder)), + 1.0f / (kBaseVirtualResX * (1.0f + kVRBorder)))) + * vr_overlay_matrix_fixed); + + if (lock_vr_overlay_) { + frame_def->set_vr_overlay_screen_matrix( + frame_def->vr_overlay_screen_matrix_fixed()); + } + } +} + +auto VRGraphics::CalcVROverlayMatrix(const Vector3f& cam_pt, + const Vector3f& cam_target_pt) const + -> Matrix44f { + Matrix44f m = Matrix44fTranslate(cam_target_pt); + Vector3f diff = cam_pt - cam_target_pt; + diff.Normalize(); + Vector3f side = Vector3f::Cross(diff, Vector3f(0.0f, -1.0f, 0.0f)); + Vector3f up = Vector3f::Cross(diff, side); + m = Matrix44fOrient(diff, up) * m; + + // Push up and out towards the eye a bit. + m = Matrix44fTranslate(0, 2, 1) * m; + + // Scale based on distance to the camera so we're always roughly the same size + // in view. + float dist = (cam_target_pt - cam_pt).Length(); + float base_scale = dist * 1.08f * 1.1f * vr_overlay_scale_; + return Matrix44fScale(Vector3f(base_scale, + base_scale + * (static_cast(kBaseVirtualResY) + / static_cast(kBaseVirtualResX)), + base_scale)) + * m; +} +void VRGraphics::DrawVROverlay(FrameDef* frame_def) { + // In vr mode we have draw our overlay-flat texture in to space + // as part of our regular overlay pass. + // NOTE: this assumes nothing after this point gets drawn into + // the overlay-flat pass (otherwise it may get skipped). + // This should be a safe assumption since this is pretty much just for + // widgets. + if (IsVRMode() && frame_def->overlay_flat_pass()->HasDrawCommands()) { + // Draw our overlay-flat stuff into our overlay pass. + SpecialComponent c(frame_def->overlay_pass(), + SpecialComponent::Source::kVROverlayBuffer); + c.PushTransform(); + c.Translate(0.5f * kBaseVirtualResX, 0.5f * kBaseVirtualResY, 0.0f); + c.Scale(kBaseVirtualResX * (1.0f + kVRBorder), + kBaseVirtualResY * (1.0f + kVRBorder), + kBaseVirtualResX * (1.0f + kVRBorder)); + c.DrawModel(g_media->GetModel(SystemModelID::kVROverlay)); + c.PopTransform(); + c.Submit(); + } +} +void VRGraphics::DrawOverlayBounds(RenderPass* pass) { + // We can optionally draw a guide to show the edges of the overlay pass + if (draw_overlay_bounds_) { + SimpleComponent c(pass); + c.SetColor(1, 0, 0); + c.PushTransform(); + float width = screen_virtual_width(); + float height = screen_virtual_height(); + + // Slight offset in z to reduce z fighting. + c.Translate(0.5f * width, 0.5f * height, 1.0f); + c.Scale(width, height, 100.0f); + c.DrawModel(g_media->GetModel(SystemModelID::kOverlayGuide)); + c.PopTransform(); + c.Submit(); + } +} + +void VRGraphics::DrawVRControllers(FrameDef* frame_def) { + if (!IsVRMode()) { + return; + } + + // Disabling this for now. + return; + + // DEBUG - draw boxing glove just in front of our head transform to verify + // it's in the right place + if (false) { + ObjectComponent c(frame_def->beauty_pass()); + c.SetColor(1, 0, 0); + c.SetTexture(g_media->GetTexture(SystemTextureID::kBoxingGlove)); + c.SetReflection(ReflectionType::kSoft); + c.SetReflectionScale(0.4f, 0.4f, 0.4f); + c.PushTransform(); + c.VRTransformToHead(); + c.Translate(0, 0, 5); + c.Scale(2, 2, 2); + c.DrawModel(g_media->GetModel(SystemModelID::kBoxingGlove)); + c.PopTransform(); + c.Submit(); + } + + // test right hand + const VRHandsState& s(g_game->vr_hands_state()); + + switch (s.r.type) { + case VRHandType::kOculusTouchR: + case VRHandType::kDaydreamRemote: { + ObjectComponent c(frame_def->beauty_pass()); + c.SetColor(0, 1, 0); + c.SetTexture(g_media->GetTexture(SystemTextureID::kBoxingGlove)); + c.SetReflection(ReflectionType::kSoft); + c.SetReflectionScale(0.4f, 0.4f, 0.4f); + c.PushTransform(); + c.VRTransformToRightHand(); + c.Scale(10, 10, 10); + c.DrawModel(g_media->GetModel(SystemModelID::kBoxingGlove)); + c.PopTransform(); + c.Submit(); + break; + } + default: + break; + } + + switch (s.l.type) { + case VRHandType::kOculusTouchL: { + ObjectComponent c(frame_def->beauty_pass()); + c.SetColor(0, 0, 1); + c.SetTexture(g_media->GetTexture(SystemTextureID::kBoxingGlove)); + c.SetReflection(ReflectionType::kSoft); + c.SetReflectionScale(0.4f, 0.4f, 0.4f); + c.PushTransform(); + c.VRTransformToLeftHand(); + c.Scale(10, 10, 10); + c.DrawModel(g_media->GetModel(SystemModelID::kBoxingGlove)); + c.PopTransform(); + c.Submit(); + break; + } + default: + break; + } +} + +} // namespace ballistica + +#endif // BA_VR_BUILD diff --git a/src/ballistica/graphics/vr_graphics.h b/src/ballistica/graphics/vr_graphics.h new file mode 100644 index 00000000..aad80eba --- /dev/null +++ b/src/ballistica/graphics/vr_graphics.h @@ -0,0 +1,86 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GRAPHICS_VR_GRAPHICS_H_ +#define BALLISTICA_GRAPHICS_VR_GRAPHICS_H_ + +#if BA_VR_BUILD + +#include + +#include "ballistica/graphics/graphics.h" + +namespace ballistica { + +const float kDefaultVRHeadScale = 18.0f; +const float kVRFixedOverlayOffsetY = -7.0f; +const float kVRFixedOverlayOffsetZ = -22.0f; + +class VRGraphics : public Graphics { + public: + /// Return g_graphics as a VRGraphics. (assumes it actually is one). + static VRGraphics* get() { + assert(g_graphics != nullptr); + assert(dynamic_cast(g_graphics) + == static_cast(g_graphics)); + return static_cast(g_graphics); + } + void ApplyCamera(FrameDef* frame_def) override; + void DrawWorld(Session* session, FrameDef* frame_def) override; + void DrawUI(FrameDef* frame_def) override; + + auto vr_head_forward() const -> const Vector3f& { return vr_head_forward_; } + auto vr_head_up() const -> const Vector3f& { return vr_head_up_; } + auto vr_head_translate() const -> const Vector3f& { + return vr_head_translate_; + } + void set_vr_head_forward(const Vector3f& v) { vr_head_forward_ = v; } + void set_vr_head_up(const Vector3f& v) { vr_head_up_ = v; } + void set_vr_head_translate(const Vector3f& v) { vr_head_translate_ = v; } + void set_vr_overlay_center(const Vector3f& val) { + assert(InGameThread()); + vr_overlay_center_ = val; + } + auto vr_overlay_center() const -> const Vector3f& { + return vr_overlay_center_; + } + void set_vr_overlay_center_enabled(bool val) { + assert(InGameThread()); + vr_overlay_center_enabled_ = val; + } + auto vr_overlay_center_enabled() const -> bool { + return vr_overlay_center_enabled_; + } + auto vr_near_clip() const -> float { return vr_near_clip_; } + void set_vr_near_clip(float val) { vr_near_clip_ = val; } + auto ValueTest(const std::string& arg, double* absval, double* deltaval, + double* outval) -> bool override; + + float vr_test_head_scale() const { return vr_test_head_scale_; } + + private: + void CalcVROverlayMatrices(FrameDef* frame_def); + auto CalcVROverlayMatrix(const Vector3f& cam_pt, + const Vector3f& cam_target_pt) const -> Matrix44f; + void DrawVROverlay(FrameDef* frame_def); + void DrawOverlayBounds(RenderPass* pass); + void DrawVRControllers(FrameDef* frame_def); + + float vr_overlay_scale_{1.0f}; + float vr_near_clip_{4.0f}; + float vr_cam_target_pt_smoothed_y_{}; + float vr_cam_target_pt_smoothed_z_{}; + Vector3f vr_head_forward_{0.0f, 0.0f, -1.0f}; + Vector3f vr_head_up_{0.0f, 1.0f, 0.0f}; + Vector3f vr_head_translate_{0.0f, 0.0f, 0.0f}; + Vector3f vr_overlay_center_{0.0f, 0.0f, 0.0f}; + bool vr_overlay_center_enabled_{}; + bool lock_vr_overlay_{}; + bool draw_overlay_bounds_{}; + float vr_test_head_scale_{kDefaultVRHeadScale}; +}; + +} // namespace ballistica + +#endif // BA_VR_BUILD + +#endif // BALLISTICA_GRAPHICS_VR_GRAPHICS_H_ diff --git a/src/ballistica/input/device/client_input_device.cc b/src/ballistica/input/device/client_input_device.cc new file mode 100644 index 00000000..8c7e32ee --- /dev/null +++ b/src/ballistica/input/device/client_input_device.cc @@ -0,0 +1,102 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/input/device/client_input_device.h" + +#include +#include + +#include "ballistica/game/connection/connection_to_client.h" +#include "ballistica/game/player.h" +#include "ballistica/networking/networking.h" + +namespace ballistica { + +ClientInputDevice::ClientInputDevice(int remote_device_id, + ConnectionToClient* connection_to_client) + : remote_device_id_(remote_device_id), + connection_to_client_(connection_to_client) {} + +// Hmm do we need to send a remote-detach in this case? +// I don't think so; if we're dying it means the connection is dying +// which means we probably couldn't communicate anyway and +// the other end will free the input-device up +ClientInputDevice::~ClientInputDevice() = default; + +auto ClientInputDevice::GetRawDeviceName() -> std::string { + return "Client Input Device"; +} + +auto ClientInputDevice::GetClientID() const -> int { + if (ConnectionToClient* c = connection_to_client_.get()) { + return c->id(); + } else { + Log("ClientInputDevice::get_client_id(): connection_to_client no longer " + "exists; returning -1.."); + return -1; + } +} + +auto ClientInputDevice::GetPlayerProfiles() const -> PyObject* { + if (connection_to_client_.exists()) { + return connection_to_client_->GetPlayerProfiles(); + } + return nullptr; +} + +auto ClientInputDevice::GetAccountName(bool full) const -> std::string { + assert(InGameThread()); + if (connection_to_client_.exists()) { + if (full) { + return connection_to_client_->peer_spec().GetDisplayString(); + } else { + return connection_to_client_->peer_spec().GetShortName(); + } + } + return "???"; +} + +auto ClientInputDevice::GetPublicAccountID() const -> std::string { + assert(InGameThread()); + if (connection_to_client_.exists()) { + return connection_to_client_->peer_public_account_id(); + } + return ""; +} + +void ClientInputDevice::AttachToLocalPlayer(Player* player) { + if (ConnectionToClient* c = connection_to_client_.get()) { + // Send a new-style message with a 32 bit player-id. + // (added during protocol 29; not always present) + { + std::vector data(6); + data[0] = BA_MESSAGE_ATTACH_REMOTE_PLAYER_2; + data[1] = static_cast_check_fit(remote_device_id_); + int val = player->id(); + memcpy(&(data[2]), &val, sizeof(val)); + c->SendReliableMessage(data); + } + + // We also need to send an old-style message as a fallback. + // FIXME: Can remove this once backwards-compat-protocol is > 29. + { + std::vector data(3); + data[0] = BA_MESSAGE_ATTACH_REMOTE_PLAYER; + data[1] = static_cast_check_fit(remote_device_id_); + data[2] = static_cast_check_fit(player->id()); + c->SendReliableMessage(data); + } + } + InputDevice::AttachToLocalPlayer(player); +} + +void ClientInputDevice::DetachFromPlayer() { + if (ConnectionToClient* c = connection_to_client_.get()) { + std::vector data(2); + data[0] = BA_MESSAGE_DETACH_REMOTE_PLAYER; + data[1] = static_cast_check_fit(remote_device_id_); + c->SendReliableMessage(data); + } + InputDevice::DetachFromPlayer(); +} + +} // namespace ballistica diff --git a/src/ballistica/input/device/client_input_device.h b/src/ballistica/input/device/client_input_device.h new file mode 100644 index 00000000..b24edbdd --- /dev/null +++ b/src/ballistica/input/device/client_input_device.h @@ -0,0 +1,44 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_INPUT_DEVICE_CLIENT_INPUT_DEVICE_H_ +#define BALLISTICA_INPUT_DEVICE_CLIENT_INPUT_DEVICE_H_ + +#include + +#include "ballistica/input/device/input_device.h" + +namespace ballistica { + +/// Represents a remote player on a client connected to us. +class ClientInputDevice : public InputDevice { + public: + ClientInputDevice(int remote_device_id, + ConnectionToClient* connection_to_client); + ~ClientInputDevice() override; + + auto GetRawDeviceName() -> std::string override; + auto IsRemoteClient() const -> bool override { return true; } + auto GetClientID() const -> int override; + auto IsLocal() -> bool override { return false; } + + // Return player-profiles dict if available; otherwise nullptr. + auto GetPlayerProfiles() const -> PyObject* override; + auto GetAccountName(bool full) const -> std::string override; + auto GetPublicAccountID() const -> std::string override; + void AttachToLocalPlayer(Player* player) override; + void DetachFromPlayer() override; + void PassInputCommand(InputType type, float value) { + InputCommand(type, value); + } + auto connection_to_client() const -> ConnectionToClient* { + return connection_to_client_.get(); + } + + private: + Object::WeakRef connection_to_client_; + int remote_device_id_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_INPUT_DEVICE_CLIENT_INPUT_DEVICE_H_ diff --git a/src/ballistica/input/device/input_device.cc b/src/ballistica/input/device/input_device.cc new file mode 100644 index 00000000..e0cb0215 --- /dev/null +++ b/src/ballistica/input/device/input_device.cc @@ -0,0 +1,320 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/input/device/input_device.h" + +#include +#include +#include + +#include "ballistica/app/app_globals.h" +#include "ballistica/game/connection/connection_to_host.h" +#include "ballistica/game/player.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/game/session/net_client_session.h" +#include "ballistica/game/session/replay_client_session.h" +#include "ballistica/networking/networking.h" +#include "ballistica/python/class/python_class_input_device.h" +#include "ballistica/python/python.h" + +namespace ballistica { + +static std::map* g_rand_name_registry = nullptr; +std::list g_default_names; + +InputDevice::InputDevice() = default; + +auto InputDevice::ShouldBeHiddenFromUser() -> bool { + // Ask the input system whether they want to ignore us.. + return g_input->ShouldCompletelyIgnoreInputDevice(this); +} + +auto InputDevice::GetDeviceName() -> std::string { + assert(InGameThread()); + return GetRawDeviceName(); +} + +void InputDevice::ResetRandomNames() { + assert(InGameThread()); + if (g_rand_name_registry == nullptr) return; + g_rand_name_registry->clear(); +} + +// Given a full name "SomeJoyStick #3" etc, reserves/returns a persistent random +// name for it. +static auto GetRandomName(const std::string& full_name) -> std::string { + assert(InGameThread()); + + // Hmm; statically allocating this is giving some crashes on shutdown :-( + if (g_rand_name_registry == nullptr) { + g_rand_name_registry = new std::map(); + } + + auto i = g_rand_name_registry->find(full_name); + if (i == g_rand_name_registry->end()) { + // Doesn't exist. Pull a random one and add it. + // Refill the global list if its empty. + if (g_default_names.empty()) { + const std::list& random_name_list = + Utils::GetRandomNameList(); + for (const auto& i2 : random_name_list) { + g_default_names.push_back(i2); + } + } + + // Ok now pull a random one off the list and assign it to us + int index = static_cast(rand() % g_default_names.size()); // NOLINT + auto i3 = g_default_names.begin(); + for (int j = 0; j < index; j++) { + i3++; + } + (*g_rand_name_registry)[full_name] = *i3; + g_default_names.erase(i3); + } + return (*g_rand_name_registry)[full_name]; +} + +auto InputDevice::GetPlayerProfiles() const -> PyObject* { return nullptr; } + +auto InputDevice::GetPublicAccountID() const -> std::string { + assert(InGameThread()); + + // this default implementation assumes the device is local + // so just returns the locally signed in account's public id.. + + // the master-server makes our public account-id available to us + // through a misc-read-val; look for that.. + std::string pub_id = + g_python->GetAccountMiscReadVal2String("resolvedAccountID"); + return pub_id; +} + +auto InputDevice::GetAccountName(bool full) const -> std::string { + assert(InGameThread()); + if (full) { + return PlayerSpec::GetAccountPlayerSpec().GetDisplayString(); + } else { + return PlayerSpec::GetAccountPlayerSpec().GetShortName(); + } +} + +auto InputDevice::IsRemoteClient() const -> bool { return false; } + +auto InputDevice::GetClientID() const -> int { return -1; } + +auto InputDevice::GetDefaultPlayerName() -> std::string { + assert(InGameThread()); + char buffer[256]; + snprintf(buffer, sizeof(buffer), "%s %s", GetDeviceName().c_str(), + GetPersistentIdentifier().c_str()); + std::string default_name = GetRandomName(buffer); + return default_name; +} + +auto InputDevice::GetButtonName(int id) -> std::string { + // By default just say 'button 1' or whatnot. + // FIXME: should return this in Lstr json form. + return g_game->GetResourceString("buttonText") + " " + std::to_string(id); +} + +auto InputDevice::GetAxisName(int id) -> std::string { + // By default just return 'axis 5' or whatnot. + // FIXME: should return this in Lstr json form. + return g_game->GetResourceString("axisText") + " " + std::to_string(id); +} + +auto InputDevice::HasMeaningfulButtonNames() -> bool { return false; } + +auto InputDevice::GetPersistentIdentifier() const -> std::string { + assert(InGameThread()); + char buffer[128]; + snprintf(buffer, sizeof(buffer), "#%d", number_); + return buffer; +} + +InputDevice::~InputDevice() { + assert(InGameThread()); + assert(!player_.exists()); + // release our python ref to ourself if we have one + if (py_ref_) { + Py_DECREF(py_ref_); + } +} + +// when the host-session tells us to attach to a player +void InputDevice::AttachToLocalPlayer(Player* player) { + if (player_.exists()) { + Log("Error: InputDevice::AttachToLocalPlayer() called with already " + "existing " + "player"); + return; + } + if (remote_player_.exists()) { + Log("Error: InputDevice::AttachToLocalPlayer() called with already " + "existing " + "remote-player"); + return; + } + player_ = player; + player_->SetInputDevice(this); +} + +void InputDevice::AttachToRemotePlayer(ConnectionToHost* connection_to_host, + int remote_player_id) { + assert(connection_to_host); + if (player_.exists()) { + Log("Error: InputDevice::AttachToRemotePlayer()" + " called with already existing " + "player"); + return; + } + if (remote_player_.exists()) { + Log("Error: InputDevice::AttachToRemotePlayer()" + " called with already existing " + "remote-player"); + return; + } + remote_player_ = connection_to_host; + remote_player_id_ = remote_player_id; +} + +void InputDevice::RemoveRemotePlayerFromGame() { + if (ConnectionToHost* connection_to_host = remote_player_.get()) { + std::vector data(2); + data[0] = BA_MESSAGE_REMOVE_REMOTE_PLAYER; + data[1] = static_cast_check_fit(index()); + connection_to_host->SendReliableMessage(data); + } else { + Log("Error: RemoveRemotePlayerFromGame called without remote player"); + } +} + +void InputDevice::DetachFromPlayer() { + if (player_.exists()) { + player_->SetInputDevice(nullptr); + player_.Clear(); + } + // Hmmm.. DetachFromPlayer() doesn't get called if the remote connection dies, + // but since its a weak-ref it should be all good since we don't do anything + // here except clear the weak-ref anyway... + if (remote_player_.exists()) { + remote_player_.Clear(); + } +} + +auto InputDevice::GetRemotePlayer() const -> ConnectionToHost* { + return remote_player_.get(); +} + +// Called to let the current host/client-session know that we'd like to control +// something please. +void InputDevice::RequestPlayer() { + assert(InGameThread()); + + // Make note that we're being used in some way. + last_input_time_ = g_game->master_time(); + + if (player_.exists()) { + Log("Error: InputDevice::RequestPlayer()" + " called with already-existing player"); + return; + } + if (remote_player_.exists()) { + Log("Error: InputDevice::RequestPlayer() called with already-existing " + "remote-player"); + return; + } + + // If we have a local host-session, ask it for a player.. otherwise if we have + // a client-session, ask it for a player. + assert(g_game); + if (auto* hs = dynamic_cast(g_game->GetForegroundSession())) { + { + Python::ScopedCallLabel label("requestPlayer"); + hs->RequestPlayer(this); + } + } else if (auto* client_session = dynamic_cast( + g_game->GetForegroundSession())) { + if (ConnectionToHost* connection_to_host = + client_session->connection_to_host()) { + std::vector data(2); + data[0] = BA_MESSAGE_REQUEST_REMOTE_PLAYER; + data[1] = static_cast_check_fit(index()); + connection_to_host->SendReliableMessage(data); + } + } + // If we're in a replay or the game is still bootstrapping, just ignore.. +} + +void InputDevice::ShipBufferIfFull() { + assert(remote_player_.exists()); + ConnectionToHost* hc = remote_player_.get(); + + // Ship the buffer once it gets big enough or once enough time has passed. + millisecs_t real_time = GetRealTime(); + size_t size = remote_input_commands_buffer_.size(); + if (size > 2 + && (static_cast(real_time - last_remote_input_commands_send_time_) + > g_app_globals->buffer_time + || size > 400)) { + last_remote_input_commands_send_time_ = real_time; + hc->SendReliableMessage(remote_input_commands_buffer_); + remote_input_commands_buffer_.clear(); + } +} + +// If we're attached to a remote player, ship completed packets every now and +// then. +void InputDevice::Update() { + if (remote_player_.exists()) { + ShipBufferIfFull(); + } +} + +void InputDevice::UpdateLastInputTime() { + last_input_time_ = g_game->master_time(); +} + +void InputDevice::InputCommand(InputType type, float value) { + assert(InGameThread()); + + // Make note that we're being used in some way. + UpdateLastInputTime(); + + if (Player* p = player_.get()) { + p->InputCommand(type, value); + } else if (remote_player_.exists()) { + // Add to existing buffer of input-commands. + { + size_t size = remote_input_commands_buffer_.size(); + // Init if empty; we'll fill in count(bytes 2+3) later. + if (size == 0) { + size = 2; + remote_input_commands_buffer_.resize(size); + remote_input_commands_buffer_[0] = + BA_MESSAGE_REMOTE_PLAYER_INPUT_COMMANDS; + remote_input_commands_buffer_[1] = + static_cast_check_fit(index()); + } + // Now add this command; add 1 byte for type, 4 for value. + remote_input_commands_buffer_.resize(remote_input_commands_buffer_.size() + + 5); + remote_input_commands_buffer_[size] = static_cast(type); + memcpy(&(remote_input_commands_buffer_[size + 1]), &value, 4); + } + } +} + +void InputDevice::ResetHeldStates() {} + +auto InputDevice::GetPyInputDevice(bool new_ref) -> PyObject* { + assert(InGameThread()); + if (py_ref_ == nullptr) { + py_ref_ = PythonClassInputDevice::Create(this); + } + if (new_ref) Py_INCREF(py_ref_); + return py_ref_; +} + +auto InputDevice::GetPartyButtonName() const -> std::string { return ""; } + +} // namespace ballistica diff --git a/src/ballistica/input/device/input_device.h b/src/ballistica/input/device/input_device.h new file mode 100644 index 00000000..6c852a51 --- /dev/null +++ b/src/ballistica/input/device/input_device.h @@ -0,0 +1,182 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_INPUT_DEVICE_INPUT_DEVICE_H_ +#define BALLISTICA_INPUT_DEVICE_INPUT_DEVICE_H_ + +#include +#include + +#include "ballistica/core/object.h" + +namespace ballistica { + +/// Base class for game input devices (keyboard, joystick, etc). +/// InputDevices can be allocated in any thread (generally on the main +/// thread in response to some system event). An AddInputDevice() call +/// should then be pushed to the game thread to inform it of the new device. +/// Deletion of the input-device is then handled by the game thread +/// and can be triggered by pushing a RemoveInputDevice() call to it. +class InputDevice : public Object { + public: + InputDevice(); + ~InputDevice() override; + + /// Called when the device is attached/detached to a local player. + virtual void AttachToLocalPlayer(Player* player); + virtual void AttachToRemotePlayer(ConnectionToHost* connection_to_host, + int remote_player_id); + virtual void DetachFromPlayer(); + + /// Issues a command to the remote game to remove the player we're attached + /// to. + void RemoveRemotePlayerFromGame(); + + /// Return the (not necessarily unique) name of the input device. + auto GetDeviceName() -> std::string; + virtual void ResetHeldStates(); + + /// Return the default base player name for players using this input device. + virtual auto GetDefaultPlayerName() -> std::string; + + /// Return the name of the signed-in account associated with this device + /// (for remote players, returns their account). + virtual auto GetAccountName(bool full) const -> std::string; + + /// Return the public Account ID of the signed-in account associated + /// with this device, or an empty string if not (yet) available. + /// Note that in some cases there may be a delay before this value + /// is available. (remote player account IDs are verified with the + /// master server before becoming available, etc) + virtual auto GetPublicAccountID() const -> std::string; + + /// Returns player-profiles dict if available; otherwise nullptr. + virtual auto GetPlayerProfiles() const -> PyObject*; + + /// Return the name of the button used to evoke the party menu. + virtual auto GetPartyButtonName() const -> std::string; + + /// Returns a number specific to this device type (saying this is the Nth + /// device of this type). + auto device_number() const -> int { return number_; } + auto GetPersistentIdentifier() const -> std::string; + auto attached_to_player() const -> bool { + return player_.exists() || remote_player_.exists(); + } + auto GetRemotePlayer() const -> ConnectionToHost*; + auto GetPlayer() const -> Player* { return player_.get(); } + + /// Return the overall device index; unique to all devices. + auto index() const -> int { return index_; } + + /// Read new control values from config. + virtual void UpdateMapping() {} + + /// Called during the game loop - for manual button repeats, etc. + virtual void Update(); + + /// Return client id or -1 if local. + virtual auto GetClientID() const -> int; + + // FIXME: redundant. + virtual auto IsRemoteClient() const -> bool; + +#if BA_SDL_BUILD || BA_MINSDL_BUILD + virtual void HandleSDLEvent(const SDL_Event* e) {} +#endif + virtual auto GetAllowsConfiguring() -> bool { return true; } + + virtual auto IsController() -> bool { return false; } + virtual auto IsSDLController() -> bool { return false; } + virtual auto IsTouchScreen() -> bool { return false; } + virtual auto IsRemoteControl() -> bool { return false; } + virtual auto IsTestInput() -> bool { return false; } + virtual auto IsKeyboard() -> bool { return false; } + virtual auto IsMFiController() -> bool { return false; } + virtual auto IsLocal() -> bool { return true; } + virtual auto IsUIOnly() -> bool { return false; } + virtual auto IsRemoteApp() -> bool { return false; } + + /// Override this to return true if you implement get_button_name(). + // virtual auto HasButtonNames() -> bool { return false; } + + /// Return a human-readable name for a button/key. + virtual auto GetButtonName(int index) -> std::string; + + /// Return a human-readable name for an axis. + virtual auto GetAxisName(int index) -> std::string; + + /// Return whether button-names returned by GetButtonName() for this + /// device are identifiable to the user on the input-device itself. + /// For example, if a gamepad returns 'A', 'B', 'X', 'Y', etc. as names, + /// this should return true, but if it returns 'button 123', 'button 124', + /// etc. then it should return false. + virtual auto HasMeaningfulButtonNames() -> bool; + + /// Should return true if the input device has a start button and + /// that button activates default widgets (will cause a start icon to show up + /// on them). + virtual auto start_button_activates_default_widget() -> bool { return false; } + auto NewPyRef() -> PyObject* { return GetPyInputDevice(true); } + auto BorrowPyRef() -> PyObject* { return GetPyInputDevice(false); } + auto has_py_ref() -> bool { return (py_ref_ != nullptr); } + auto last_input_time() const -> millisecs_t { return last_input_time_; } + virtual auto ShouldBeHiddenFromUser() -> bool; + static void ResetRandomNames(); + + protected: + void ShipBufferIfFull(); + + /// Pass some input command on to whatever we're connected to + /// (player or remote-player). + void InputCommand(InputType type, float value = 0.0f); + + /// Called for all devices when they've successfully been added + /// to the input-device list, have a valid ID, name, etc. + virtual void ConnectionComplete() {} + + /// Subclasses should call this to request a player in the local game. + void RequestPlayer(); + + /// Return a human-readable name for the device's type. + /// This is used for display and also for storing configs/etc. + virtual auto GetRawDeviceName() -> std::string { return "Input Device"; } + + /// Return any extra description for the device. + /// This portion is only used for display and not for storing configs. + /// An example is Mac PS3 controllers; they return "(bluetooth)" or "(usb)" + /// here depending on how they are connected. + virtual auto GetDeviceExtraDescription() -> std::string { return ""; } + + /// Devices that have a way of identifying uniquely against other devices of + /// the same type (a serial number, usb-port, etc) should return that here as + /// a string. + virtual auto GetDeviceIdentifier() -> std::string { return ""; } + + auto remote_player_id() const -> int { return remote_player_id_; } + void UpdateLastInputTime(); + + private: + millisecs_t last_remote_input_commands_send_time_ = 0; + std::vector remote_input_commands_buffer_; + + // note: this is in base-net-time + millisecs_t last_input_time_ = 0; + + // We're attached to *one* of these two. + Object::WeakRef player_; + Object::WeakRef remote_player_; + + int remote_player_id_ = -1; + PyObject* py_ref_ = nullptr; + auto GetPyInputDevice(bool new_ref) -> PyObject*; + void set_index(int index_in) { index_ = index_in; } + void set_numbered_identifier(int n) { number_ = n; } + int index_ = -1; // Our overall device index. + int number_ = -1; // Our type-specific number. + friend class Input; + BA_DISALLOW_CLASS_COPIES(InputDevice); +}; + +} // namespace ballistica + +#endif // BALLISTICA_INPUT_DEVICE_INPUT_DEVICE_H_ diff --git a/src/ballistica/input/device/joystick.cc b/src/ballistica/input/device/joystick.cc new file mode 100644 index 00000000..93662f6f --- /dev/null +++ b/src/ballistica/input/device/joystick.cc @@ -0,0 +1,1568 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/input/device/joystick.h" + +#include +#include + +#include "ballistica/app/app.h" +#include "ballistica/app/app_globals.h" +#include "ballistica/audio/audio.h" +#include "ballistica/game/player.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_command.h" +#include "ballistica/ui/root_ui.h" +#include "ballistica/ui/ui.h" +#include "ballistica/ui/widget/container_widget.h" + +namespace ballistica { + +const char* kMFiControllerName = "iOS/Mac Controller"; + +const int kJoystickRepeatDelay{500}; + +// Joy values below this are candidates for calibration. +const float kJoystickCalibrationThreshold{6000.0f}; + +// Joy events with at least this much movement break calibration. +const float kJoystickCalibrationBreakThreshold{300.0f}; + +// How long we gotta remain motionless for calibration to kick in. +const int kJoystickCalibrationTimeThreshold{1000}; + +// How fast calibration occurs. +const float kJoystickCalibrationSpeed = 0.7f; + +Joystick::Joystick(int sdl_joystick_id, const std::string& custom_device_name, + bool can_configure, bool calibrate) + : calibration_threshold_(kJoystickCalibrationThreshold), + calibration_break_threshold_(kJoystickCalibrationBreakThreshold), + custom_device_name_(custom_device_name), + can_configure_(can_configure), + creation_time_(GetRealTime()), + calibrate_(calibrate) { + // This is the default calibration for 'non-full' analog calibration. + for (float& analog_calibration_val : analog_calibration_vals_) { + analog_calibration_val = 0.6f; + } + + if (custom_device_name == "TestInput") { + is_test_input_ = true; + } + + sdl_joystick_id_ = sdl_joystick_id; + + // Non-negative values here mean its an SDL joystick. + if (sdl_joystick_id != -1) { +#if BA_ENABLE_SDL_JOYSTICKS + // Standard SDL joysticks should be getting created in the main thread. + // Custom joysticks can come from anywhere. + assert(InMainThread()); + + sdl_joystick_ = SDL_JoystickOpen(sdl_joystick_id); + assert(sdl_joystick_); + + // In SDL2 we're passed a device-id but that's only used to open the + // joystick; events and most everything else use an instance ID, so we store + // that instead. +#if BA_SDL2_BUILD + sdl_joystick_id_ = SDL_JoystickInstanceID(sdl_joystick_); + raw_sdl_joystick_name_ = SDL_JoystickName(sdl_joystick_); + + // Special case: on windows, xinput stuff comes in with unique names + // "XInput Controller #3", etc. Let's replace these with simply "XInput + // Controller" so configuring/etc is sane. + if (strstr(raw_sdl_joystick_name_.c_str(), "XInput Controller") + && raw_sdl_joystick_name_.size() >= 20 + && raw_sdl_joystick_name_.size() <= 22) { + raw_sdl_joystick_name_ = "XInput Controller"; + } +#else + raw_sdl_joystick_name_ = SDL_JoystickName(sdl_joystick_id_); +#endif // BA_SDL2_BUILD + + // If its an SDL joystick and we're using our custom sdl 1.2 build, ask it. +#if BA_XCODE_BUILD && BA_OSTYPE_MACOS && !BA_SDL2_BUILD + raw_sdl_joystick_identifier_ = SDL_JoystickIdentifier(sdl_joystick_id_); +#endif + + // Some special-cases on mac. + if (strstr(raw_sdl_joystick_name_.c_str(), "PLAYSTATION") != nullptr) { + is_mac_ps3_controller_ = true; + } + +#else // BA_ENABLE_SDL_JOYSTICKS + throw Exception(); // Shouldn't happen. +#endif // BA_ENABLE_SDL_JOYSTICKS + + } else { + // Its a manual joystick. + sdl_joystick_ = nullptr; + + is_mfi_controller_ = (custom_device_name_ == kMFiControllerName); + is_mac_wiimote_ = (custom_device_name_ == "Wiimote"); + + // Hard code a few remote controls. + // The newer way to do this is just set 'UI-Only' on the device config + is_remote_control_ = ((custom_device_name_ == "Amazon Remote") + || (custom_device_name_ == "Amazon Bluetooth Dev") + || (custom_device_name_ == "Amazon Fire TV Remote") + || (custom_device_name_ == "Nexus Remote")); + } +} + +auto Joystick::GetAxisName(int index) -> std::string { + // On android, lets return some popular axis names. + + if (g_buildconfig.ostype_android()) { + // Due to our stupid 1-based values we have to subtract 1 from our value to + // get the android motion-event constant. + // FIXME: should just make a call to android to get these values.. + switch (index) { + case 1: + return "Analog X"; + case 2: + return "Analog Y"; + case 12: + return "Analog Z"; + case 13: + return "Right Analog X"; + case 14: + return "Right Analog Y"; + case 15: + return "Right Analog Z"; + case 23: + return "Gas"; + case 24: + return "Brake"; + case 16: + return "Hat X"; + case 17: + return "Hat Y"; + case 18: + return "Left Trigger"; + case 19: + return "Right Trigger"; + default: + break; + } + } + + // Fall back to default implementation if we didn't cover it. + return InputDevice::GetAxisName(index); +} + +auto Joystick::HasMeaningfulButtonNames() -> bool { + // Only return true in cases where we know we have proper names + // for stuff. + if (is_mfi_controller_) { + return true; + } + return g_buildconfig.ostype_android(); +} + +auto Joystick::GetButtonName(int index) -> std::string { + // FIXME: Should get fancier here now that PS4 and XBone + // controllers are supported through this. + if (is_mfi_controller_) { + switch (index) { + case 1: + return "A"; + case 2: + return "X"; + case 3: + return "B"; + case 4: + return "Y"; + default: + break; + } + } + if (g_buildconfig.ostype_android()) { + // Special case: if this is a samsung controller, return the dice + // button icons. + if (strstr(GetDeviceName().c_str(), "Samsung Game Pad EI")) { + switch (index) { + case 101: + return g_game->CharStr(SpecialChar::kDiceButton4); // Y + case 100: + return g_game->CharStr(SpecialChar::kDiceButton3); // X + case 98: + return g_game->CharStr(SpecialChar::kDiceButton2); // B + case 97: + return g_game->CharStr(SpecialChar::kDiceButton1); // A + default: + break; + } + } + + // Some standard android button names: + switch (index) { + case 20: + return "Dpad Up"; + case 22: + return "Dpad Left"; + case 23: + return "Dpad Right"; + case 21: + return "Dpad Down"; + case 101: + return "Y"; + case 100: + return "X"; + case 98: + return "B"; + case 97: + return "A"; + case 83: + return "Menu"; + case 110: + return "Select"; + case 111: + return "Mode"; + case 109: + return "Start"; + case 107: + return "Thumb-L"; + case 108: + return "Thumb-R"; + case 103: + return "L1"; + case 104: + return "R1"; + case 105: + return "L2"; + case 106: + return "R2"; + case 126: + return "Forward"; + case 189: + return "B1"; + case 190: + return "B2"; + case 191: + return "B3"; + case 192: + return "B4"; + case 193: + return "B5"; + case 194: + return "B6"; + case 195: + return "B7"; + case 196: + return "B8"; + case 197: + return "B9"; + case 198: + return "B10"; + case 199: + return "B11"; + case 200: + return "B12"; + case 201: + return "B13"; + case 202: + return "B14"; + case 203: + return "B15"; + case 204: + return "B16"; + case 90: + return g_game->CharStr(SpecialChar::kRewindButton); + case 91: + return g_game->CharStr(SpecialChar::kFastForwardButton); + case 24: + return g_game->CharStr(SpecialChar::kDpadCenterButton); + case 86: + return g_game->CharStr(SpecialChar::kPlayPauseButton); + default: + break; + } + } + return InputDevice::GetButtonName(index); +} + +Joystick::~Joystick() { + if (!InGameThread()) { + Log("Error: Joystick dying in wrong thread."); + } + + // Kill our child if need be. + if (child_joy_stick_) { + g_input->RemoveInputDevice(child_joy_stick_, true); + child_joy_stick_ = nullptr; + } + + // If we're a wiimote, announce our departure. + if (g_buildconfig.ostype_macos() && is_mac_wiimote_) { + char msg[255]; + + int num = device_number(); + + // If we disconnected before any events came through, treat it as an error. + snprintf(msg, sizeof(msg), "Wii Remote #%d", num); + + // Ask the user to try again if the disconnect was immediate. + std::string s; + if (GetRealTime() - creation_time_ < 5000) { + s = g_game->GetResourceString("controllerDisconnectedTryAgainText"); + } else { + s = g_game->GetResourceString("controllerDisconnectedText"); + } + Utils::StringReplaceOne(&s, "${CONTROLLER}", msg); + ScreenMessage(s); + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kCorkPop)); + } + + // Have SDL actually close the joystick in the main thread. + // Send a message back to the main thread to close this SDL Joystick. + // HMMM - can we just have the main thread close the joystick immediately + // before informing us its dead?.. i don't think we actually use it at all + // here in the game thread.. + if (sdl_joystick_) { +#if BA_ENABLE_SDL_JOYSTICKS + assert(g_app); + auto joystick = sdl_joystick_; + g_app->PushCall([joystick] { SDL_JoystickClose(joystick); }); + sdl_joystick_ = nullptr; +#else + Log("sdl_joystick_ set in non-sdl-joystick build destructor."); +#endif // BA_ENABLE_SDL_JOYSTICKS + } +} + +auto Joystick::GetDefaultPlayerName() -> std::string { + if (!custom_default_player_name_.empty()) { + return custom_default_player_name_; + } + return InputDevice::GetDefaultPlayerName(); +} + +void Joystick::ConnectionComplete() { + assert(InGameThread()); + + // Special case for mac wiimotes. + if (g_buildconfig.ostype_macos() && is_mac_wiimote_) { + char msg[128]; + + int num = device_number(); + + snprintf(msg, sizeof(msg), "Wii Remote #%d", num); + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kGunCock)); + + // Replace ${CONTROLLER} with it in our message. + std::string s = g_game->GetResourceString("controllerConnectedText"); + Utils::StringReplaceOne(&s, "${CONTROLLER}", msg); + ScreenMessage(s); + return; + } +} + +auto Joystick::ShouldBeHiddenFromUser() -> bool { + std::string d_name = GetDeviceName(); + + // To lowercase. + int sz = static_cast(d_name.size()); + for (int i = 0; i < sz; i++) { + if (d_name[i] <= 'Z' && d_name[i] >= 'A') d_name[i] -= ('Z' - 'z'); + } + + const char* n = d_name.c_str(); + if (strstr(n, "mouse") || strstr(n, "keyboard") + || strstr(n, "athome_remote")) { + return true; + } else { + return InputDevice::ShouldBeHiddenFromUser(); + } +} + +auto Joystick::GetCalibratedValue(float raw, float neutral) const -> int32_t { + int32_t val; + float dead_zone = 0.5f; + float mag, target; + if (raw > neutral) { + mag = ((raw - neutral) / (calibration_threshold_ - neutral)); + target = calibration_threshold_; + } else { + mag = ((raw - neutral) / (-calibration_threshold_ - neutral)); + target = -calibration_threshold_; + } + if (mag < dead_zone) { + val = 0; + } else { + val = static_cast((1.0f - dead_zone) * mag * target); + } + return val; +} + +void Joystick::Update() { + InputDevice::Update(); + + assert(InGameThread()); + + // We seem to get a fair amount of bogus direction-pressed events from newly + // plugged in joysticks.. this leads to continuous scrolling in menus and such + // ...so lets reset our state once early after we're created. + if (!did_initial_reset_) { + ResetHeldStates(); + did_initial_reset_ = true; + } + + // Let's take this opportunity to update our calibration + // (should probably have a specific place to do that but this works) + if (calibrate_) { + millisecs_t time = GetRealTime(); + + // If we're doing 'aggressive' auto-recalibration we expand extents outward + // but suck them inward a tiny bit too to account for jitter or random fluke + // points. + if (auto_recalibrate_analog_stick_) { + int cell = static_cast( + (atan2(static_cast(jaxis_y_), static_cast(jaxis_x_)) + + kPi) + * ((kJoystickAnalogCalibrationDivisions) / (2.0f * kPi))); + cell = + std::min(kJoystickAnalogCalibrationDivisions - 1, std::max(0, cell)); + float x = jaxis_x_ / 32767.0f; + float y = jaxis_y_ / 32767.0f; + float mag = sqrtf(x * x + y * y); + if (mag > analog_calibration_vals_[cell]) { + analog_calibration_vals_[cell] = std::min(1.0f, mag); + + // Push the cell value up towards us a bit and also have it fall by a + // constant amount. + analog_calibration_vals_[cell] = std::min( + 1.0f, + std::max(0.25f, + 0.9f + * (analog_calibration_vals_[cell] + + (mag - analog_calibration_vals_[cell]) * 0.15f))); + } + } + + // Calibration: if we've been below our calibration thresholds for more than + // calibration-time, start averaging our current value into our calibrated + // neutral. + if (time - calibration_start_time_x_ > kJoystickCalibrationTimeThreshold + && (static_cast(std::abs(jaxis_raw_x_)) + < calibration_threshold_)) { + calibrated_neutral_x_ = + kJoystickCalibrationSpeed * jaxis_raw_x_ + + (1.0f - kJoystickCalibrationSpeed) * calibrated_neutral_x_; + + // Grab our new calibrated x value.. if it differs from the current, ship + // an event. + if (static_cast(std::abs(jaxis_raw_x_)) < calibration_threshold_) { + int32_t x = GetCalibratedValue(jaxis_raw_x_, calibrated_neutral_x_); + if (x != jaxis_x_) { + jaxis_x_ = x; + InputCommand(InputType::kLeftRight, + static_cast(jaxis_x_) / 32767.0f); + } + } + } + + if (time - calibration_start_time_y_ > kJoystickCalibrationTimeThreshold + && (static_cast(std::abs(jaxis_raw_y_)) + < calibration_threshold_)) { + calibrated_neutral_y_ = + kJoystickCalibrationSpeed * jaxis_raw_y_ + + (1.0f - kJoystickCalibrationSpeed) * calibrated_neutral_y_; + + // Grab our new calibrated x value.. if it differs from the current, ship + // an event. + if (fabs(static_cast(jaxis_raw_y_)) < calibration_threshold_) { + int32_t y = GetCalibratedValue(jaxis_raw_y_, calibrated_neutral_y_); + if (y != jaxis_y_) { + jaxis_y_ = y; + InputCommand(InputType::kUpDown, + static_cast(jaxis_y_) / 32767.0f); + } + } + } + } + + // If a button's being held, potentially pass repeats along. + if (up_held_ || down_held_ || left_held_ || right_held_) { + // Don't ask for the widget unless we have something held. + // (otherwise we prevent other inputs from getting at it) + if (g_ui->GetWidgetForInput(this)) { + millisecs_t repeat_delay = kJoystickRepeatDelay; + + millisecs_t t = GetRealTime(); + WidgetMessage::Type c = WidgetMessage::Type::kEmptyMessage; + if (t - last_hold_time_ < repeat_delay) { + return; + } + + if (t - last_hold_time_ >= repeat_delay) { + bool pass = false; + if (up_held_) { + pass = true; + c = WidgetMessage::Type::kMoveUp; + } else if (down_held_) { + pass = true; + c = WidgetMessage::Type::kMoveDown; + } else if (left_held_) { + pass = true; + c = WidgetMessage::Type::kMoveLeft; + } else if (right_held_) { + pass = true; + c = WidgetMessage::Type::kMoveRight; + } + if (pass) { + g_ui->SendWidgetMessage(WidgetMessage(c)); + } + + // Set another repeat to happen sooner. + last_hold_time_ = t - static_cast(repeat_delay * 0.8f); + } + } + } +} + +void Joystick::SetStandardExtendedButtons() { + // Assign some non-zero dpad values so we can drive them in custom joysticks. + up_button_ = 20; + down_button_ = 21; + left_button_ = 22; + right_button_ = 23; + run_trigger1_ = 10; + run_trigger2_ = 11; + back_button_ = 12; + remote_enter_button_ = 13; +} + +void Joystick::ResetHeldStates() { + // So we push events through even if there's a dialog in the way. + resetting_ = true; + + // Send ourself neutral joystick events. + SDL_Event e; + + dpad_right_held_ = dpad_left_held_ = dpad_up_held_ = dpad_down_held_ = false; + run_buttons_held_.clear(); + run_trigger1_value_ = run_trigger2_value_ = 0.0f; + UpdateRunningState(); + + if (hat_held_) { + e.type = SDL_JOYHATMOTION; + e.jhat.hat = static_cast_check_fit(hat_); + e.jhat.value = SDL_HAT_CENTERED; + HandleSDLEvent(&e); + } + + e.type = SDL_JOYAXISMOTION; + e.jaxis.axis = static_cast_check_fit(analog_lr_); + e.jaxis.value = static_cast(calibrated_neutral_x_); + HandleSDLEvent(&e); + + e.type = SDL_JOYAXISMOTION; + e.jaxis.axis = static_cast_check_fit(analog_ud_); + e.jaxis.value = static_cast(calibrated_neutral_y_); + HandleSDLEvent(&e); + + resetting_ = false; +} + +void Joystick::HandleSDLEvent(const SDL_Event* e) { + assert(InGameThread()); + + // If we've got a child joystick, send them any events they're set to handle. + if (child_joy_stick_) { + assert(g_game); + + bool send = false; + switch (e->type) { + case SDL_JOYAXISMOTION: { + // If its their analog stick or one of their run-triggers, send. + if (e->jaxis.axis == child_joy_stick_->analog_lr_ + || e->jaxis.axis == child_joy_stick_->analog_ud_ + || e->jaxis.axis == child_joy_stick_->run_trigger1_ + || e->jaxis.axis == child_joy_stick_->run_trigger2_) + send = true; + break; + } + case SDL_JOYHATMOTION: { + // If its their dpad hat, send. + if (e->jhat.hat == child_joy_stick_->hat_) send = true; + break; + } + case SDL_JOYBUTTONDOWN: + case SDL_JOYBUTTONUP: { + // If its one of their 4 action buttons, 2 run buttons, or start, send. + if (e->jbutton.button == child_joy_stick_->jump_button_ + || e->jbutton.button == child_joy_stick_->punch_button_ + || e->jbutton.button == child_joy_stick_->bomb_button_ + || e->jbutton.button == child_joy_stick_->pickup_button_ + || e->jbutton.button == child_joy_stick_->start_button_ + || e->jbutton.button == child_joy_stick_->start_button_2_ + || e->jbutton.button == child_joy_stick_->run_button1_ + || e->jbutton.button == child_joy_stick_->run_button2_) + send = true; + break; + } + default: + break; + } + if (send) { + g_input->PushJoystickEvent(*e, child_joy_stick_); + return; + } + } + + // If we're set to ignore events completely, do so. + if (ignore_completely_) { + return; + } + + millisecs_t time = GetRealTime(); + SDL_Event e2; + + // Ignore analog-stick input while we're holding a hat switch or d-pad + // buttons. + if ((e->type == SDL_JOYAXISMOTION + && (e->jaxis.axis == analog_lr_ || e->jaxis.axis == analog_ud_)) + && (hat_held_ || dpad_right_held_ || dpad_left_held_ || dpad_up_held_ + || dpad_down_held_)) + return; + + bool isHoldPositionEvent = false; + + // Keep track of whether hold-position is being held. If so, we don't send + // window events. (some joysticks always give us significant axis values but + // rely on hold position to keep from doing stuff usually). + if (e->type == SDL_JOYBUTTONDOWN + && e->jbutton.button == hold_position_button_) { + need_to_send_held_state_ = true; + hold_position_held_ = true; + isHoldPositionEvent = true; + } + if (e->type == SDL_JOYBUTTONUP + && e->jbutton.button == hold_position_button_) { + need_to_send_held_state_ = true; + hold_position_held_ = false; + isHoldPositionEvent = true; + } + + // Let's ignore events for just a moment after we're created. + // (some joysticks seem to spit out erroneous button-pressed events when + // first plugged in ). + if (time - creation_time_ < 250 && !isHoldPositionEvent) { + return; + } + + // If we're using dpad-buttons, let's convert those events into joystick + // events. + // FIXME: should we do the same for hat buttons just to keep things + // consistent? + if (up_button_ >= 0 || left_button_ >= 0 || right_button_ >= 0 + || down_button_ >= 0 || up_button2_ >= 0 || left_button2_ >= 0 + || right_button2_ >= 0 || down_button2_ >= 0) { + switch (e->type) { + case SDL_JOYBUTTONDOWN: + case SDL_JOYBUTTONUP: + if (e->jbutton.button == right_button_ + || e->jbutton.button == right_button2_) { // D-pad right. + e2.type = SDL_JOYAXISMOTION; + e2.jaxis.axis = static_cast_check_fit(analog_lr_); + dpad_right_held_ = (e->type == SDL_JOYBUTTONDOWN); + e2.jaxis.value = static_cast_check_fit( + dpad_right_held_ ? (dpad_left_held_ ? 0 : 32767) + : dpad_left_held_ ? -32767 : 0); + e = &e2; + } else if (e->jbutton.button == left_button_ + || e->jbutton.button == left_button2_) { + e2.type = SDL_JOYAXISMOTION; + e2.jaxis.axis = static_cast_check_fit(analog_lr_); + dpad_left_held_ = (e->type == SDL_JOYBUTTONDOWN); + e2.jaxis.value = static_cast_check_fit( + dpad_right_held_ ? (dpad_left_held_ ? 0 : 32767) + : dpad_left_held_ ? -32767 : 0); + e = &e2; + } else if (e->jbutton.button == up_button_ + || e->jbutton.button == up_button2_) { + e2.type = SDL_JOYAXISMOTION; + e2.jaxis.axis = static_cast_check_fit(analog_ud_); + dpad_up_held_ = (e->type == SDL_JOYBUTTONDOWN); + e2.jaxis.value = static_cast_check_fit( + dpad_up_held_ ? (dpad_down_held_ ? 0 : -32767) + : dpad_down_held_ ? 32767 : 0); + e = &e2; + } else if (e->jbutton.button == down_button_ + || e->jbutton.button == down_button2_) { + e2.type = SDL_JOYAXISMOTION; + e2.jaxis.axis = static_cast_check_fit(analog_ud_); + dpad_down_held_ = (e->type == SDL_JOYBUTTONDOWN); + e2.jaxis.value = static_cast_check_fit( + dpad_up_held_ ? (dpad_down_held_ ? 0 : -32767) + : dpad_down_held_ ? 32767 : 0); + e = &e2; + } + break; + default: + break; + } + } + + // Track our hat-held state independently. + if (e->type == SDL_JOYHATMOTION && e->jhat.hat == hat_) { + switch (e->jhat.value) { + case SDL_HAT_CENTERED: + hat_held_ = false; + break; + case SDL_HAT_UP: + case SDL_HAT_DOWN: + case SDL_HAT_LEFT: + case SDL_HAT_RIGHT: + case SDL_HAT_LEFTUP: // NOLINT (signed bitwise) + case SDL_HAT_RIGHTUP: // NOLINT (signed bitwise) + case SDL_HAT_RIGHTDOWN: // NOLINT (signed bitwise) + case SDL_HAT_LEFTDOWN: // NOLINT (signed bitwise) + hat_held_ = true; + break; + default: + BA_LOG_ONCE("Error: Invalid hat value: " + + std::to_string(static_cast(e->jhat.value))); + break; + } + } + + // If its the ignore button, ignore it. + if ((e->type == SDL_JOYBUTTONDOWN || e->type == SDL_JOYBUTTONUP) + && (e->jbutton.button == ignored_button_ + || e->jbutton.button == ignored_button2_ + || e->jbutton.button == ignored_button3_ + || e->jbutton.button == ignored_button4_)) { + return; + } + + // A little pre-filtering on mac PS3 gamepads. (try to filter out some noise + // we're seeing, etc). + if (g_buildconfig.ostype_macos() && is_mac_ps3_controller_) { + switch (e->type) { + case SDL_JOYAXISMOTION: { + // On my ps3 controller, I seem to be seeing occasional joy-axis-events + // coming in with values of -32768 when nothing is being touched. + // Filtering those out here.. Should look into this more and see if its + // SDL's fault or else forward a bug to apple. + if ((e->jaxis.axis == 0 || e->jaxis.axis == 1) + && e->jaxis.value == -32768 + && (time - ps3_last_joy_press_time_ > 2000) && !ps3_jaxis1_pressed_ + && !ps3_jaxis2_pressed_) { + printf( + "BAJoyStick notice: filtering out errand PS3 axis %d value of " + "%d\n", + static_cast(e->jaxis.axis), + static_cast(e->jaxis.value)); + fflush(stdout); + + // std::cout << "BSJoyStick notice: filtering out errant PS3 axis " << + // int(e->jaxis.axis) << " value of " << e->jaxis.value << std::endl; + return; + } + + if (abs(e->jaxis.value) >= kJoystickDiscreteThreshold) { + ps3_last_joy_press_time_ = time; + } + + // Keep track of whether its pressed for next time. + if (e->jaxis.axis == 0) { + ps3_jaxis1_pressed_ = (abs(e->jaxis.value) > 3000); + } else if (e->jaxis.axis == 1) { + ps3_jaxis2_pressed_ = (abs(e->jaxis.value) > 3000); + } + + break; + } + default: + break; + } + } + + // A few high level button press interceptions. + if (e->type == SDL_JOYBUTTONDOWN) { + if (e->jbutton.button == start_button_ + || e->jbutton.button == start_button_2_) { + // If there's some UI up already, we just pass this along to it. + // otherwise we request a main menu. + if (g_ui && g_ui->screen_root_widget() + && g_ui->screen_root_widget()->HasChildren()) { + // Do nothing in this case. + } else { + // If there's no menu up, + // tell the game to pop it up and snag menu ownership for ourself. + g_game->PushMainMenuPressCall(this); + return; + } + } + + // On our oculus build, select presses reset the orientation. + if (e->jbutton.button == vr_reorient_button_ && IsVRMode()) { + ScreenMessage(g_game->GetResourceString("vrOrientationResetText"), + {0, 1, 0}); + g_app_globals->reset_vr_orientation = true; + return; + } + } + + // Update some calibration parameters. + if (e->type == SDL_JOYAXISMOTION) { + if (e->jaxis.axis == analog_lr_) { + // If we've moved by more than a small amount, break calibration. + if (static_cast(abs(e->jaxis.value - jaxis_raw_x_)) + > calibration_break_threshold_) { + calibration_start_time_x_ = time; + } + jaxis_raw_x_ = e->jaxis.value; + + // Just take note if we're below our calibration threshold + // (actual calibration happens in update-repeats). + if (static_cast(abs(e->jaxis.value)) > calibration_threshold_) { + calibration_start_time_x_ = time; + } + } else if (e->jaxis.axis == analog_ud_) { + // If we've moved by more than a small amount, break calibration. + if (static_cast(abs(e->jaxis.value - jaxis_raw_y_)) + > calibration_break_threshold_) { + calibration_start_time_y_ = time; + } + jaxis_raw_y_ = e->jaxis.value; + + // Just take note if we're below our calibration threshold + // (actual calibration happens in update-repeats). + if (static_cast(abs(e->jaxis.value)) > calibration_threshold_) { + calibration_start_time_y_ = time; + } + } + } + + // If we're in a dialog, send dialog events. + // We keep track of special x/y values for dialog usage. + // These are formed as combinations of the actual joy value + // and the hold-position state. + // Think of hold-position as somewhat of a 'magnitude' to the joy event's + // direction. They're really one and the same event. (we just need to store + // their states ourselves since they don't both come through at once). + bool isAnalogStickJAxisEvent = false; + if (e->type == SDL_JOYAXISMOTION) { + if (e->jaxis.axis == analog_lr_) { + dialog_jaxis_x_ = e->jaxis.value; + isAnalogStickJAxisEvent = true; + } else if (e->jaxis.axis == analog_ud_) { + dialog_jaxis_y_ = e->jaxis.value; + isAnalogStickJAxisEvent = true; + } + } + int dialogJaxisX = dialog_jaxis_x_; + if (hold_position_held_) { + dialogJaxisX = 0; // Throttle is off. + } + int dialogJaxisY = dialog_jaxis_y_; + if (hold_position_held_) { + dialogJaxisY = 0; // Throttle is off. + } + + // We might not wanna grab at the UI if we're a axis-motion event + // below our 'pressed' threshold.. Otherwise fuzzy analog joystick + // readings would cause rampant UI stealing even if no events are being sent. + bool would_go_to_dialog = false; + WidgetMessage::Type wm = WidgetMessage::Type::kEmptyMessage; + + if (isAnalogStickJAxisEvent || isHoldPositionEvent) { + // Even when we're not sending, clear out some 'held' states. + if (left_held_ && dialogJaxisX >= -kJoystickDiscreteThreshold) { + left_held_ = false; + } + if (right_held_ && dialogJaxisX <= kJoystickDiscreteThreshold) { + right_held_ = false; + } + if (up_held_ && dialogJaxisY >= -kJoystickDiscreteThreshold) { + up_held_ = false; + } + if (down_held_ && dialogJaxisY <= kJoystickDiscreteThreshold) { + down_held_ = false; + } + if ((!right_held_) && dialogJaxisX > kJoystickDiscreteThreshold) + would_go_to_dialog = true; + if ((!left_held_) && dialogJaxisX < -kJoystickDiscreteThreshold) + would_go_to_dialog = true; + if ((!up_held_) && dialogJaxisY < -kJoystickDiscreteThreshold) + would_go_to_dialog = true; + if ((!down_held_) && dialogJaxisY > kJoystickDiscreteThreshold) + would_go_to_dialog = true; + } else if ((e->type == SDL_JOYHATMOTION && e->jhat.hat == hat_) + || (e->type == SDL_JOYBUTTONDOWN + && e->jbutton.button != hold_position_button_)) { + // Other button-downs and hat motions always go. + would_go_to_dialog = true; + } + + // Resets always circumvent dialogs. + if (resetting_) would_go_to_dialog = false; + + // Anything that would go to a dialog also counts to mark us as + // 'recently-used'. + if (would_go_to_dialog) { + UpdateLastInputTime(); + } + + if (would_go_to_dialog && g_ui->GetWidgetForInput(this)) { + bool pass = false; + + // Special case.. either joy-axis-motion or hold-position events trigger + // these. + if (isAnalogStickJAxisEvent || isHoldPositionEvent) { + if (dialogJaxisX > kJoystickDiscreteThreshold) { + // To the right. + if (!right_held_ && !up_held_ && !down_held_) { + last_hold_time_ = GetRealTime(); + right_held_ = true; + wm = WidgetMessage::Type::kMoveRight; + pass = true; + } + } else if (dialogJaxisX < -kJoystickDiscreteThreshold) { + if (!left_held_ && !up_held_ && !down_held_) { + last_hold_time_ = GetRealTime(); + wm = WidgetMessage::Type::kMoveLeft; + pass = true; + left_held_ = true; + } + } + if (dialogJaxisY > kJoystickDiscreteThreshold) { + if (!down_held_ && !left_held_ && !right_held_) { + last_hold_time_ = GetRealTime(); + wm = WidgetMessage::Type::kMoveDown; + pass = true; + down_held_ = true; + } + } else if (dialogJaxisY < -kJoystickDiscreteThreshold) { + if (!up_held_ && !left_held_ && !right_held_) { + last_hold_time_ = GetRealTime(); + wm = WidgetMessage::Type::kMoveUp; + pass = true; + up_held_ = true; + } + } + } + + switch (e->type) { + case SDL_JOYAXISMOTION: + break; + + case SDL_JOYHATMOTION: { + if (e->jhat.hat == hat_) { + switch (e->jhat.value) { + case SDL_HAT_LEFT: { + if (!left_held_) { + last_hold_time_ = GetRealTime(); + wm = WidgetMessage::Type::kMoveLeft; + pass = true; + left_held_ = true; + right_held_ = false; + } + break; + } + + case SDL_HAT_RIGHT: { + if (!right_held_) { + last_hold_time_ = GetRealTime(); + wm = WidgetMessage::Type::kMoveRight; + pass = true; + right_held_ = true; + left_held_ = false; + } + break; + } + case SDL_HAT_UP: { + if (!up_held_) { + last_hold_time_ = GetRealTime(); + wm = WidgetMessage::Type::kMoveUp; + pass = true; + up_held_ = true; + down_held_ = false; + } + break; + } + case SDL_HAT_DOWN: { + if (!down_held_) { + last_hold_time_ = GetRealTime(); + wm = WidgetMessage::Type::kMoveDown; + pass = true; + down_held_ = true; + up_held_ = false; + } + break; + } + case SDL_HAT_CENTERED: { + up_held_ = false; + down_held_ = false; + left_held_ = false; + right_held_ = false; + } + default: + break; + } + } + break; + } + case SDL_JOYBUTTONDOWN: { + if (e->jbutton.button != hold_position_button_) { + pass = true; + if (e->jbutton.button == start_button_ + || e->jbutton.button == start_button_2_) { + if (start_button_activates_default_widget_) + wm = WidgetMessage::Type::kStart; + else + pass = false; + } else if (e->jbutton.button == bomb_button_ + || e->jbutton.button == back_button_) { + wm = WidgetMessage::Type::kCancel; + } else { + // FIXME: Need a call we can make for this. + bool do_party_button = false; + int party_size = g_game->GetPartySize(); + if (party_size > 1 || g_game->connection_to_host() + || g_ui->root_ui()->always_draw_party_icon()) { + do_party_button = true; + } + + // Toggle the party UI if we're pressing the party button. + // (currently don't allow remote to do this.. need to make it + // customizable) + if (do_party_button && e->jbutton.button == pickup_button_ + && (!IsRemoteControl())) { + pass = false; + g_ui->root_ui()->ActivatePartyIcon(); + break; + } + wm = WidgetMessage::Type::kActivate; + } + } + } break; + default: + break; + } + if (pass) { + g_ui->SendWidgetMessage(WidgetMessage(wm)); + } + return; + } + + // If there's a UI up (even if we didn't get it) lets not pass events along. + // The only exception is if we're doing a reset. + Widget* root{}; + if (g_ui) { + root = g_ui->screen_root_widget(); + } + if (root && root->HasChildren() && !resetting_) { + return; + } + + if (!attached_to_player()) { + if (e->type == SDL_JOYBUTTONDOWN + && (e->jbutton.button != hold_position_button_) + && (e->jbutton.button != back_button_)) { + if (ui_only_ || e->jbutton.button == remote_enter_button_) { + millisecs_t current_time = GetRealTime(); + if (current_time - last_ui_only_print_time_ > 5000) { + g_python->obj(Python::ObjID::kUIRemotePressCall).Call(); + last_ui_only_print_time_ = current_time; + } + } else { + RequestPlayer(); + // we always want to inform new players of our hold-position-state.. + // make a note to do that. + need_to_send_held_state_ = true; + } + } + return; + } + + // Ok we've got a player; just send events along. + + // Held state is a special case; we wanna always send that along first thing + // if its changed. This is because some joysticks rely on it being on by + // default. + if (need_to_send_held_state_) { + if (hold_position_held_) { + InputCommand(InputType::kHoldPositionPress); + } else { + InputCommand(InputType::kHoldPositionRelease); + } + need_to_send_held_state_ = false; + } + + switch (e->type) { + case SDL_JOYAXISMOTION: { + // Handle run-trigger presses. + if (e->jaxis.axis == run_trigger1_ || e->jaxis.axis == run_trigger2_) { + if (e->jaxis.axis == run_trigger1_) { + float value = static_cast(e->jaxis.value) / 32767.0f; + + // If we're calibrating, update calibration bounds and calc a + // calibrated value. + if (calibrate_) { + if (value < run_trigger1_min_) { + run_trigger1_min_ = value; + } else if (value > run_trigger1_max_) { + run_trigger1_max_ = value; + } + run_trigger1_value_ = (value - run_trigger1_min_) + / (run_trigger1_max_ - run_trigger1_min_); + } else { + run_trigger1_value_ = value; + } + } else { + float value = static_cast(e->jaxis.value) / 32767.0f; + + // If we're calibrating, update calibration bounds and calc a + // calibrated value. + if (calibrate_) { + if (value < run_trigger2_min_) { + run_trigger2_min_ = value; + } else if (value > run_trigger2_max_) { + run_trigger2_max_ = value; + } + run_trigger2_value_ = (value - run_trigger2_min_) + / (run_trigger2_max_ - run_trigger2_min_); + } else { + run_trigger2_value_ = value; + } + } + UpdateRunningState(); + } + InputType input_type; + int32_t input_value; + if (e->jaxis.axis == analog_lr_) { + input_type = InputType::kLeftRight; + input_value = e->jaxis.value; + if (calibrate_) { + if (static_cast(abs(jaxis_raw_x_)) < calibration_threshold_ + && static_cast(abs(jaxis_raw_y_)) + < calibration_threshold_) { + input_value = + GetCalibratedValue(input_value, calibrated_neutral_x_); + } + } + if (input_value > 32767) { + input_value = 32767; + } else if (input_value < -32767) { + input_value = -32767; + } + jaxis_x_ = input_value; + } else if (e->jaxis.axis == analog_ud_) { + input_type = InputType::kUpDown; + input_value = e->jaxis.value; + if (calibrate_) { + if (static_cast(abs(jaxis_raw_x_)) < calibration_threshold_ + && static_cast(abs(jaxis_raw_y_)) + < calibration_threshold_) { + input_value = + GetCalibratedValue(input_value, calibrated_neutral_y_); + } + } + input_value = -input_value; + if (input_value > 32767) { + input_value = 32767; + } else if (input_value < -32767) { + input_value = -32767; + } + jaxis_y_ = input_value; + } else { + break; + } + + // Update extent calibration and scale based on that. + if (calibrate_) { + // Handle analog stick calibration.. 'full' auto-recalibration. + if (auto_recalibrate_analog_stick_) { + int cell = static_cast( + (atan2(static_cast(jaxis_y_), static_cast(jaxis_x_)) + + kPi) + * ((kJoystickAnalogCalibrationDivisions) / (2.0f * kPi))); + cell = std::min(kJoystickAnalogCalibrationDivisions - 1, + std::max(0, cell)); + input_value *= (1.0f / analog_calibration_vals_[cell]); + if (input_value > 32767) { + input_value = 32767; + } else if (input_value < -32767) { + input_value = -32767; + } + } + } + InputCommand(input_type, static_cast(input_value) / 32767.0f); + break; + } + case SDL_JOYBUTTONDOWN: { + if (unassigned_buttons_run_ || e->jbutton.button == punch_button_ + || e->jbutton.button == jump_button_ + || e->jbutton.button == bomb_button_ + || e->jbutton.button == pickup_button_ + || e->jbutton.button == run_button1_ + || e->jbutton.button == run_button2_) { + run_buttons_held_.insert(e->jbutton.button); + } + UpdateRunningState(); + if (e->jbutton.button == jump_button_) { + // FIXME: we should just do one or the other here depending on the game + // mode to reduce the number of events sent. + InputCommand(InputType::kJumpPress); + InputCommand(InputType::kFlyPress); + } else if (e->jbutton.button == punch_button_) { + InputCommand(InputType::kPunchPress); + } else if (e->jbutton.button == bomb_button_) { + InputCommand(InputType::kBombPress); + } else if (e->jbutton.button == pickup_button_) { + InputCommand(InputType::kPickUpPress); + } + break; + } + case SDL_JOYBUTTONUP: { + { + auto i = run_buttons_held_.find(e->jbutton.button); + if (i != run_buttons_held_.end()) { + run_buttons_held_.erase(i); + } + UpdateRunningState(); + } + if (e->jbutton.button == jump_button_) { + InputCommand(InputType::kJumpRelease); + InputCommand(InputType::kFlyRelease); + } else if (e->jbutton.button == punch_button_) { + InputCommand(InputType::kPunchRelease); + } else if (e->jbutton.button == bomb_button_) { + InputCommand(InputType::kBombRelease); + } else if (e->jbutton.button == pickup_button_) { + InputCommand(InputType::kPickUpRelease); + } + break; + } + case SDL_JOYBALLMOTION: { + break; + } + case SDL_JOYHATMOTION: { + if (e->jhat.hat == hat_) { + int16_t input_value_lr = 0; + int16_t input_value_ud = 0; + switch (e->jhat.value) { + case SDL_HAT_CENTERED: + input_value_lr = 0; + input_value_ud = 0; + break; + case SDL_HAT_UP: + input_value_lr = 0; + input_value_ud = 32767; + break; + case SDL_HAT_DOWN: + input_value_lr = 0; + input_value_ud = -32767; + break; + case SDL_HAT_LEFT: + input_value_lr = -32767; + input_value_ud = 0; + break; + case SDL_HAT_RIGHT: + input_value_lr = 32767; + input_value_ud = 0; + break; + case SDL_HAT_LEFTUP: // NOLINT (signed bitwise) + input_value_lr = -32767; + input_value_ud = 32767; + break; + case SDL_HAT_RIGHTUP: // NOLINT (signed bitwise) + input_value_lr = 32767; + input_value_ud = 32767; + break; + case SDL_HAT_RIGHTDOWN: // NOLINT (signed bitwise) + input_value_lr = 32767; + input_value_ud = -32767; + break; + case SDL_HAT_LEFTDOWN: // NOLINT (signed bitwise) + input_value_lr = -32767; + input_value_ud = -32767; + break; + default: + break; + } + InputCommand(InputType::kLeftRight, + static_cast(input_value_lr) / 32767.0f); + InputCommand(InputType::kUpDown, + static_cast(input_value_ud) / 32767.0f); + } + break; + } + default: + break; + } +} // NOLINT(readability/fn_size) Yes I know this is too long. + +void Joystick::UpdateRunningState() { + if (!attached_to_player()) { + return; + } + float value; + float prev_value = run_value_; + + // If there's a button held, our default value is 1.0. + if (!run_buttons_held_.empty()) { + value = 1.0f; + } else { + value = 0.0f; + } + + // Now check our analog run triggers. + value = std::max(value, run_trigger1_value_); + value = std::max(value, run_trigger2_value_); + + if (value != prev_value) { + run_value_ = value; + InputCommand(InputType::kRun, run_value_); + } +} + +void Joystick::UpdateMapping() { + assert(InGameThread()); + + // This doesn't apply to manual ones (except children which are). + if (!can_configure_ && !parent_joy_stick_) { + return; + } + + // If we're a child, use our parent's id to search for config values and just + // tack on a '2'. + Joystick* js = parent_joy_stick_ ? parent_joy_stick_ : this; + std::string ext = parent_joy_stick_ ? "_B" : ""; + + // Grab all button values from Python. Traditionally we stored these + // with the first index 1 so we need to subtract 1 to get the zero-indexed + // value. (grumble). + jump_button_ = g_python->GetControllerValue(js, "buttonJump" + ext) - 1; + punch_button_ = g_python->GetControllerValue(js, "buttonPunch" + ext) - 1; + bomb_button_ = g_python->GetControllerValue(js, "buttonBomb" + ext) - 1; + pickup_button_ = g_python->GetControllerValue(js, "buttonPickUp" + ext) - 1; + start_button_ = g_python->GetControllerValue(js, "buttonStart" + ext) - 1; + start_button_2_ = g_python->GetControllerValue(js, "buttonStart2" + ext) - 1; + hold_position_button_ = + g_python->GetControllerValue(js, "buttonHoldPosition" + ext) - 1; + run_button1_ = g_python->GetControllerValue(js, "buttonRun1" + ext) - 1; + run_button2_ = g_python->GetControllerValue(js, "buttonRun2" + ext) - 1; + vr_reorient_button_ = + g_python->GetControllerValue(js, "buttonVRReorient" + ext) - 1; + ignored_button_ = g_python->GetControllerValue(js, "buttonIgnored" + ext) - 1; + ignored_button2_ = + g_python->GetControllerValue(js, "buttonIgnored2" + ext) - 1; + ignored_button3_ = + g_python->GetControllerValue(js, "buttonIgnored3" + ext) - 1; + ignored_button4_ = + g_python->GetControllerValue(js, "buttonIgnored4" + ext) - 1; + int old_run_trigger_1 = run_trigger1_; + run_trigger1_ = g_python->GetControllerValue(js, "triggerRun1" + ext) - 1; + int old_run_trigger_2 = run_trigger2_; + run_trigger2_ = g_python->GetControllerValue(js, "triggerRun2" + ext) - 1; + up_button_ = g_python->GetControllerValue(js, "buttonUp" + ext) - 1; + left_button_ = g_python->GetControllerValue(js, "buttonLeft" + ext) - 1; + right_button_ = g_python->GetControllerValue(js, "buttonRight" + ext) - 1; + down_button_ = g_python->GetControllerValue(js, "buttonDown" + ext) - 1; + up_button2_ = g_python->GetControllerValue(js, "buttonUp2" + ext) - 1; + left_button2_ = g_python->GetControllerValue(js, "buttonLeft2" + ext) - 1; + right_button2_ = g_python->GetControllerValue(js, "buttonRight2" + ext) - 1; + down_button2_ = g_python->GetControllerValue(js, "buttonDown2" + ext) - 1; + unassigned_buttons_run_ = static_cast( + g_python->GetControllerValue(js, "unassignedButtonsRun" + ext)); + + // If our run trigger has changed, reset its calibration. + // NOTE: It looks like on Mac we're getting analog trigger values from -1 to 1 + // while on Android we're getting from 0 to 1.. adding this calibration stuff + // allows us to cover both cases though. + if (old_run_trigger_1 != run_trigger1_) { + run_trigger1_min_ = 0.2f; + run_trigger1_max_ = 0.8f; + } + if (old_run_trigger_2 != run_trigger2_) { + run_trigger2_min_ = 0.2f; + run_trigger2_max_ = 0.8f; + } + + int ival = g_python->GetControllerValue(js, "uiOnly" + ext); + if (ival == -1) { + ui_only_ = false; + } else { + ui_only_ = static_cast(ival); + } + + ival = g_python->GetControllerValue(js, "ignoreCompletely" + ext); + if (ival == -1) { + ignore_completely_ = false; + } else { + ignore_completely_ = static_cast(ival); + } + + ival = g_python->GetControllerValue(js, "autoRecalibrateAnalogSticks" + ext); + + { + bool was_on = auto_recalibrate_analog_stick_; + if (ival == -1) { + auto_recalibrate_analog_stick_ = false; + } else { + auto_recalibrate_analog_stick_ = static_cast(ival); + } + bool is_on = auto_recalibrate_analog_stick_; + + // If we're flipping on full auto-recalibration, start our extents small. + if (!was_on && is_on) { + for (float& analog_calibration_val : analog_calibration_vals_) { + analog_calibration_val = 0.25f; + } + } + + // If we're flipping it off, reset to default calibration values. + if (was_on && !is_on) { + for (float& analog_calibration_val : analog_calibration_vals_) { + analog_calibration_val = 0.6f; + } + } + } + + ival = g_python->GetControllerValue( + js, "startButtonActivatesDefaultWidget" + ext); + + if (ival == -1) { + start_button_activates_default_widget_ = true; + } else { + start_button_activates_default_widget_ = static_cast(ival); + } + + // Update calibration stuff. + float as = g_python->GetControllerFloatValue(js, "analogStickDeadZone" + ext); + + if (as < 0) { + as = 1.0f; + } + + // Avoid possibility of divide-by-zero errors. + if (as < 0.01f) { + as = 0.01f; + } + + calibration_threshold_ = kJoystickCalibrationThreshold * as; + calibration_break_threshold_ = kJoystickCalibrationBreakThreshold * as; + + hat_ = g_python->GetControllerValue(js, "dpad" + ext) - 1; + + // If unset, use our default. + if (hat_ == -2) { + if (parent_joy_stick_) { + hat_ = 1; + } else { + hat_ = 0; + } + } + + // Grab our analog stick. + analog_lr_ = g_python->GetControllerValue(js, "analogStickLR" + ext) - 1; + + // If we got unset, set to our default. + if (analog_lr_ == -2) { + if (parent_joy_stick_) { + analog_lr_ = 4; + } else { + analog_lr_ = 0; + } + } + + analog_ud_ = g_python->GetControllerValue(js, "analogStickUD" + ext) - 1; + + // If we got unset, set to our default. + if (analog_ud_ == -2) { + if (parent_joy_stick_) { + analog_ud_ = 5; + } else { + analog_ud_ = 1; + } + } + + // See whether we have a child-joystick and create it if need be. + if (!parent_joy_stick_) { + int enable = g_python->GetControllerValue(js, "enableSecondary"); + if (enable == -1) { + enable = 0; + } + + // Create if need be. + if (enable) { + char m[256]; + snprintf(m, sizeof(m), "%s B", GetDeviceName().c_str()); + if (!child_joy_stick_) { + child_joy_stick_ = + Object::NewDeferred(-1, // Not an sdl joystick. + m, // Device name. + false, // Allow configuring. + true); // Do calibrate. + child_joy_stick_->parent_joy_stick_ = this; + assert(g_input); + g_input->AddInputDevice(child_joy_stick_, true); + } + } else { + // Kill if need be. + if (child_joy_stick_) { + g_input->RemoveInputDevice(child_joy_stick_, true); + child_joy_stick_ = nullptr; + } + } + } +} + +auto Joystick::GetRawDeviceName() -> std::string { + if (!custom_device_name_.empty()) { + return custom_device_name_; + } + + // For sdl joysticks just return the sdl string. + if (sdl_joystick_) { + std::string s = raw_sdl_joystick_name_; + if (s.empty()) { + s = "untitled joystick"; + } + return s; + } else { + // The one case we can currently hit this is with android controllers - (if + // an empty name is passed for the controller type). + return "Unknown Input Device"; + } +} + +auto Joystick::GetDeviceExtraDescription() -> std::string { + std::string s; + + // On mac, PS3 controllers can connect via USB or bluetooth, + // and it can be confusing if one is doing both, + // so lets specify here. + if (GetDeviceName() == "PLAYSTATION(R)3 Controller") { + // For bluetooth we get a serial in the form "04-76-6e-d1-17-90" while + // on USB we get a simple int (the usb location id): "-9340234" + // so lets consider it wireless if its got a dash not at the beginning. + s = " (USB)"; + + auto dname = GetDeviceIdentifier(); + for (const char* tst = dname.c_str(); *tst; tst++) { + if (*tst == '-' && tst != dname) { + s = " (Bluetooth)"; + } + } + } + + return s; +} + +auto Joystick::GetDeviceIdentifier() -> std::string { + return raw_sdl_joystick_identifier_; +} + +auto Joystick::GetPartyButtonName() const -> std::string { + return g_game->CharStr(SpecialChar::kTopButton); +} + +} // namespace ballistica diff --git a/src/ballistica/input/device/joystick.h b/src/ballistica/input/device/joystick.h new file mode 100644 index 00000000..597ec4a4 --- /dev/null +++ b/src/ballistica/input/device/joystick.h @@ -0,0 +1,201 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_INPUT_DEVICE_JOYSTICK_H_ +#define BALLISTICA_INPUT_DEVICE_JOYSTICK_H_ + +#include +#include + +#include "ballistica/input/device/input_device.h" + +namespace ballistica { + +// iOS controllers feel more natural with a lower threshold here, +// but it throws off cheap controllers elsewhere. +// not sure what's the right answer.. (should revisit) +const int kJoystickDiscreteThreshold{15000}; +const float kJoystickDiscreteThresholdFloat{0.46f}; +const int kJoystickAnalogCalibrationDivisions{20}; +extern const char* kMFiControllerName; + +/// A physical game controller. +class Joystick : public InputDevice { + public: + // Create from an SDL joystick id. + // Pass -1 to create a manual joystick from a non-sdl-source. + // (in which case you are in charge of feeding it SDL events to make it go) + explicit Joystick(int index, const std::string& custom_device_name = "", + bool can_configure = true, bool calibrate = true); + + ~Joystick() override; + + void HandleSDLEvent(const SDL_Event* e) override; + + void UpdateMapping() override; + void Update() override; + void ResetHeldStates() override; + + auto sdl_joystick_id() const -> int { return sdl_joystick_id_; } + auto sdl_joystick() const -> SDL_Joystick* { return sdl_joystick_; } + + auto GetAllowsConfiguring() -> bool override { return can_configure_; } + + // We treat anything marked as 'ui-only' as a remote too. + // (perhaps should consolidate this with IsUIOnly?.. + // ...except there's some remotes we want to be able to join the game; hmmm) + auto IsRemoteControl() -> bool override { + return (is_remote_control_ || ui_only_); + } + + auto GetPartyButtonName() const -> std::string override; + auto GetDefaultPlayerName() -> std::string override; + + auto GetButtonName(int index) -> std::string override; + auto GetAxisName(int index) -> std::string override; + + auto IsController() -> bool override { return true; } + auto IsSDLController() -> bool override { return (sdl_joystick_ != nullptr); } + + auto ShouldBeHiddenFromUser() -> bool override; + + auto IsUIOnly() -> bool override { return ui_only_; } + + auto IsTestInput() -> bool override { return is_test_input_; } + auto IsRemoteApp() -> bool override { return is_remote_app_; } + auto IsMFiController() -> bool override { return is_mfi_controller_; } + + void set_is_remote_app(bool val) { is_remote_app_ = val; } + void set_is_mfi_controller(bool val) { is_mfi_controller_ = val; } + + void SetStandardExtendedButtons(); + void SetStartButtonActivatesDefaultWidget(bool value) { + start_button_activates_default_widget_ = value; + } + + void set_custom_default_player_name(const std::string& val) { + custom_default_player_name_ = val; + } + auto HasMeaningfulButtonNames() -> bool override; + + protected: + auto GetRawDeviceName() -> std::string override; + auto GetDeviceExtraDescription() -> std::string override; + auto GetDeviceIdentifier() -> std::string override; + void ConnectionComplete() override; + + auto start_button_activates_default_widget() -> bool override { + return start_button_activates_default_widget_; + } + + private: + void UpdateRunningState(); + auto GetCalibratedValue(float raw, float neutral) const -> int32_t; + + std::string custom_default_player_name_; + std::string raw_sdl_joystick_name_; + std::string raw_sdl_joystick_identifier_; + float run_value_{}; + Joystick* child_joy_stick_{}; + Joystick* parent_joy_stick_{}; + millisecs_t last_ui_only_print_time_{}; + bool ui_only_{}; + bool unassigned_buttons_run_{true}; + bool start_button_activates_default_widget_{true}; + bool auto_recalibrate_analog_stick_{}; + millisecs_t creation_time_{}; + bool did_initial_reset_{}; + + // FIXME - should take this out and replace it with a bool + // (we never actually access the sdl joystick directly outside of our + // constructor) + SDL_Joystick* sdl_joystick_{}; + + bool is_test_input_{}; + bool is_remote_control_{}; + bool is_remote_app_{}; + bool is_mfi_controller_{}; + bool is_mac_ps3_controller_{}; + bool is_mac_wiimote_{}; + + millisecs_t ps3_last_joy_press_time_{-10000}; + + // For dialogs. + bool left_held_{}; + bool right_held_{}; + bool up_held_{}; + bool down_held_{}; + bool hold_position_held_{}; + bool need_to_send_held_state_{}; + int hat_{}; + int analog_lr_{}; + int analog_ud_{1}; + millisecs_t last_hold_time_{}; + bool hat_held_{}; + bool dpad_right_held_{}; + bool dpad_left_held_{}; + bool dpad_up_held_{}; + bool dpad_down_held_{}; + + // Mappings of ba buttons to SDL buttons. + int jump_button_{}; + int punch_button_{1}; + int bomb_button_{2}; + int pickup_button_{3}; + int start_button_{5}; + int start_button_2_{-1}; + int hold_position_button_{25}; + int back_button_{-1}; + + // Used on rift build; we have one button which we disallow from joining but + // the rest we allow. (all devices are treated as one and the same there). + int remote_enter_button_{-1}; + bool ignore_completely_{}; + int ignored_button_{-1}; + int ignored_button2_{-1}; + int ignored_button3_{-1}; + int ignored_button4_{-1}; + int run_button1_{-1}; + int run_button2_{-1}; + int run_trigger1_{-1}; + int run_trigger2_{-1}; + int vr_reorient_button_{-1}; + float run_trigger1_min_{}; + float run_trigger1_max_{}; + float run_trigger2_min_{}; + float run_trigger2_max_{}; + float run_trigger1_value_{}; + float run_trigger2_value_{}; + int left_button_{-1}; + int right_button_{-1}; + int up_button_{-1}; + int down_button_{-1}; + int left_button2_{-1}; + int right_button2_{-1}; + int up_button2_{-1}; + int down_button2_{-1}; + std::set run_buttons_held_; + int sdl_joystick_id_{}; + bool ps3_jaxis1_pressed_{}; + bool ps3_jaxis2_pressed_{}; + float calibration_threshold_{}; + float calibration_break_threshold_{}; + float analog_calibration_vals_[kJoystickAnalogCalibrationDivisions]{}; + std::string custom_device_name_; + bool can_configure_{}; + int32_t dialog_jaxis_x_{}; + int32_t dialog_jaxis_y_{}; + int32_t jaxis_raw_x_{}; + int32_t jaxis_raw_y_{}; + int32_t jaxis_x_{}; + int32_t jaxis_y_{}; + millisecs_t calibration_start_time_x_{}; + float calibrated_neutral_x_{}; + millisecs_t calibration_start_time_y_{}; + float calibrated_neutral_y_{}; + bool resetting_{}; + bool calibrate_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_INPUT_DEVICE_JOYSTICK_H_ diff --git a/src/ballistica/input/device/keyboard_input.cc b/src/ballistica/input/device/keyboard_input.cc new file mode 100644 index 00000000..d3e739f5 --- /dev/null +++ b/src/ballistica/input/device/keyboard_input.cc @@ -0,0 +1,471 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/input/device/keyboard_input.h" + +#include "ballistica/game/player.h" +#include "ballistica/platform/platform.h" +#include "ballistica/python/python.h" +#include "ballistica/ui/ui.h" +#include "ballistica/ui/widget/container_widget.h" + +namespace ballistica { + +KeyboardInput::KeyboardInput(KeyboardInput* parentKeyboardInputIn) { + if (parentKeyboardInputIn) { + parent_keyboard_input_ = parentKeyboardInputIn; + assert(parent_keyboard_input_->child_keyboard_input_ == nullptr); + + // Currently we assume only 2 keyboard inputs. + assert(parent_keyboard_input_->parent_keyboard_input_ == nullptr); + parent_keyboard_input_->child_keyboard_input_ = this; + up_key_ = SDLK_w; + down_key_ = SDLK_s; + left_key_ = SDLK_a; + right_key_ = SDLK_d; + jump_key_ = SDLK_1; + punch_key_ = SDLK_2; + bomb_key_ = SDLK_3; + pick_up_key_ = SDLK_4; + hold_position_key_ = SDLK_6; + start_key_ = SDLK_KP_7; + } else { + up_key_ = SDLK_UP; + down_key_ = SDLK_DOWN; + left_key_ = SDLK_LEFT; + right_key_ = SDLK_RIGHT; + jump_key_ = SDLK_SPACE; + punch_key_ = SDLK_v; + bomb_key_ = SDLK_b; + pick_up_key_ = SDLK_c; + hold_position_key_ = SDLK_y; + start_key_ = SDLK_F5; + } +} + +KeyboardInput::~KeyboardInput() = default; + +auto KeyboardInput::HandleKey(const SDL_Keysym* keysym, bool repeat, bool down) + -> bool { + // Only allow the *main* keyboard to talk to the UI + if (parent_keyboard_input_ == nullptr) { + if (g_ui->GetWidgetForInput(this)) { + bool pass = false; + WidgetMessage::Type c = WidgetMessage::Type::kEmptyMessage; + if (down) { + switch (keysym->sym) { + case SDLK_TAB: + if (keysym->mod & KMOD_SHIFT) { // NOLINT (signed bitwise) + c = WidgetMessage::Type::kTabPrev; + } else { + c = WidgetMessage::Type::kTabNext; + } + pass = true; + break; + case SDLK_LEFT: + c = WidgetMessage::Type::kMoveLeft; + pass = true; + break; + case SDLK_RIGHT: + c = WidgetMessage::Type::kMoveRight; + pass = true; + break; + case SDLK_UP: + c = WidgetMessage::Type::kMoveUp; + pass = true; + break; + case SDLK_DOWN: + c = WidgetMessage::Type::kMoveDown; + pass = true; + break; + case SDLK_SPACE: + case SDLK_KP_ENTER: + case SDLK_RETURN: + if (!repeat) { + c = WidgetMessage::Type::kActivate; + pass = true; + } + break; + case SDLK_ESCAPE: + // (limit to kb1 so we don't get double-beeps on failure) + c = WidgetMessage::Type::kCancel; + pass = true; + break; + default: + + // for remaining keys, lets see if they map to our assigned + // movement/actions. If so, we handle them. + if (keysym->sym == start_key_ || keysym->sym == jump_key_ + || keysym->sym == punch_key_ || keysym->sym == pick_up_key_) { + c = WidgetMessage::Type::kActivate; + pass = true; + } else if (keysym->sym == bomb_key_) { + c = WidgetMessage::Type::kCancel; + pass = true; + } else if (keysym->sym == left_key_) { + c = WidgetMessage::Type::kMoveLeft; + pass = true; + } else if (keysym->sym == right_key_) { + c = WidgetMessage::Type::kMoveRight; + pass = true; + } else if (keysym->sym == up_key_) { + c = WidgetMessage::Type::kMoveUp; + pass = true; + } else if (keysym->sym == down_key_) { + c = WidgetMessage::Type::kMoveDown; + pass = true; + } + + // if we're keyboard 1 we always send at least a key press event + // along.. + if (!parent_keyboard_input_ && !pass) { + c = WidgetMessage::Type::kKey; + pass = true; + } + break; + } + } + if (pass) { + g_ui->SendWidgetMessage(WidgetMessage(c, keysym)); + } + return (pass); + } + } + + // Bring up menu if start is pressed. + if (keysym->sym == start_key_ && !repeat && g_ui && g_ui->screen_root_widget() + && g_ui->screen_root_widget()->GetChildCount() == 0) { + g_game->PushMainMenuPressCall(this); + return true; + } + + // At this point, if we have a child input, let it try to handle things. + if (child_keyboard_input_ && enable_child_) { + if (child_keyboard_input_->HandleKey(keysym, repeat, down)) { + return true; + } + } + + if (!attached_to_player()) { + if (down + && ((keysym->sym == jump_key_) || (keysym->sym == punch_key_) + || (keysym->sym == bomb_key_) + || (keysym->sym == pick_up_key_) + // Main keyboard accepts enter/return as join-request. + || (device_number() == 1 && (keysym->sym == SDLK_KP_ENTER)) + || (device_number() == 1 && (keysym->sym == SDLK_RETURN)))) { + RequestPlayer(); + return true; + } + return false; + } + InputType input_type{}; + bool have_input_2{}; + InputType input_type_2{}; + int16_t input_value{}; + int16_t input_value_2{}; + bool player_input{}; + + // Hack to prevent unused-value lint bug. + // (removing init values from input_type and input_type_2 gives a + // 'possibly uninited value used' warning but leaving them gives a + // 'values unused' warning. Grumble.) + explicit_bool(input_type + == (explicit_bool(false) ? input_type_2 : InputType::kLast)); + + if (!repeat) { + // Keyboard 1 supports assigned keys plus arrow keys if they're unused. + if (keysym->sym == left_key_ + || (device_number() == 1 && keysym->sym == SDLK_LEFT + && !left_key_assigned())) { + player_input = true; + input_type = InputType::kLeftRight; + left_held_ = down; + if (down) { + if (right_held_) { + input_value = 0; + } else { + input_value = -32767; + } + } else { + if (right_held_) { + input_value = 32767; + } + } + } else if (keysym->sym == right_key_ + || (device_number() == 1 && keysym->sym == SDLK_RIGHT + && !right_key_assigned())) { + // Keyboard 1 supports assigned keys plus arrow keys if they're unused. + player_input = true; + input_type = InputType::kLeftRight; + right_held_ = down; + if (down) { + if (left_held_) { + input_value = 0; + } else { + input_value = 32767; + } + } else { + if (left_held_) { + input_value = -32767; + } + } + } else if (keysym->sym == up_key_ + || (device_number() == 1 && keysym->sym == SDLK_UP + && !up_key_assigned())) { + player_input = true; + input_type = InputType::kUpDown; + up_held_ = down; + if (down) { + if (down_held_) { + input_value = 0; + } else { + input_value = 32767; + } + } else { + if (down_held_) input_value = -32767; + } + } else if (keysym->sym == down_key_ + || (device_number() == 1 && keysym->sym == SDLK_DOWN + && !down_key_assigned())) { + player_input = true; + input_type = InputType::kUpDown; + down_held_ = down; + if (down) { + if (up_held_) { + input_value = 0; + } else { + input_value = -32767; + } + } else { + if (up_held_) input_value = 32767; + } + } else if (keysym->sym == punch_key_) { + player_input = true; + UpdateRun(keysym->sym, down); + if (down) { + input_type = InputType::kPunchPress; + } else { + input_type = InputType::kPunchRelease; + } + } else if (keysym->sym == bomb_key_) { + player_input = true; + UpdateRun(keysym->sym, down); + if (down) + input_type = InputType::kBombPress; + else + input_type = InputType::kBombRelease; + } else if (keysym->sym == hold_position_key_) { + player_input = true; + if (down) { + input_type = InputType::kHoldPositionPress; + } else { + input_type = InputType::kHoldPositionRelease; + } + } else if (keysym->sym == pick_up_key_) { + player_input = true; + UpdateRun(keysym->sym, down); + if (down) { + input_type = InputType::kPickUpPress; + } else { + input_type = InputType::kPickUpRelease; + } + } else if ((device_number() == 1 && keysym->sym == SDLK_RETURN) + || (device_number() == 1 && keysym->sym == SDLK_KP_ENTER) + || keysym->sym == jump_key_) { + // Keyboard 1 claims certain keys if they are otherwise unclaimed + // (arrow keys, enter/return, etc). + player_input = true; + UpdateRun(keysym->sym, down); + if (down) { + input_type = InputType::kJumpPress; + have_input_2 = true; + input_type_2 = InputType::kFlyPress; + } else { + input_type = InputType::kJumpRelease; + have_input_2 = true; + input_type_2 = InputType::kFlyRelease; + } + } else { + // Any other keys get processed as run keys. + // keypad keys go to player 2 - anything else to player 1. + switch (keysym->sym) { + case SDLK_KP_0: + case SDLK_KP_1: + case SDLK_KP_2: + case SDLK_KP_3: + case SDLK_KP_4: + case SDLK_KP_5: + case SDLK_KP_6: + case SDLK_KP_7: + case SDLK_KP_8: + case SDLK_KP_9: + case SDLK_KP_PLUS: + case SDLK_KP_MINUS: + case SDLK_KP_ENTER: + if (device_number() == 2) { + UpdateRun(keysym->sym, down); + return true; + } + break; + default: + if (device_number() == 1) { + UpdateRun(keysym->sym, down); + return true; + } + break; + } + } + } + + if (player_input) { + InputCommand(input_type, static_cast(input_value) / 32767.0f); + if (have_input_2) { + InputCommand(input_type_2, static_cast(input_value_2) / 32767.0f); + } + return true; + } else { + return false; + } +} + +void KeyboardInput::ResetHeldStates() { + down_held_ = up_held_ = left_held_ = right_held_ = false; + bool was_held = false; + if (!keys_held_.empty()) { + was_held = true; + } + keys_held_.clear(); + if (was_held) { + InputCommand(InputType::kRun, 0.0f); + } +} + +void KeyboardInput::UpdateRun(SDL_Keycode key, bool down) { + bool was_held = (!keys_held_.empty()); + if (down) { + keys_held_.insert(key); + if (!was_held) { + InputCommand(InputType::kRun, 1.0f); + } + } else { + // Remove this key if we find it. + auto iter = keys_held_.find(key); + if (iter != keys_held_.end()) { + keys_held_.erase(iter); + } + bool is_held = (!keys_held_.empty()); + if (was_held && !is_held) { + InputCommand(InputType::kRun, 0.0f); + } + } +} + +void KeyboardInput::UpdateMapping() { + assert(InGameThread()); + + SDL_Keycode up_key_default, down_key_default, left_key_default, + right_key_default, jump_key_default, punch_key_default, bomb_key_default, + pick_up_key_default, hold_position_key_default, start_key_default; + + if (parent_keyboard_input_) { + up_key_default = SDLK_UP; + down_key_default = SDLK_DOWN; + left_key_default = SDLK_LEFT; + right_key_default = SDLK_RIGHT; + + jump_key_default = SDLK_KP_2; + punch_key_default = SDLK_KP_1; + bomb_key_default = SDLK_KP_6; + pick_up_key_default = SDLK_KP_5; + hold_position_key_default = (SDL_Keycode)-1; + start_key_default = SDLK_KP_7; + + } else { + up_key_default = SDLK_w; + down_key_default = SDLK_s; + left_key_default = SDLK_a; + right_key_default = SDLK_d; + jump_key_default = SDLK_k; + punch_key_default = SDLK_j; + bomb_key_default = SDLK_o; + pick_up_key_default = SDLK_i; + + hold_position_key_default = (SDL_Keycode)-1; + start_key_default = (SDL_Keycode)-1; + } + + // We keep track of whether anyone is using arrow keys + // If not, we allow them to function for movement. + left_key_assigned_ = right_key_assigned_ = up_key_assigned_ = + down_key_assigned_ = false; + + int val; + + val = g_python->GetControllerValue(this, "buttonJump"); + jump_key_ = (val == -1) ? jump_key_default : (SDL_Keycode)val; + UpdateArrowKeys(jump_key_); + + val = g_python->GetControllerValue(this, "buttonPunch"); + punch_key_ = (val == -1) ? punch_key_default : (SDL_Keycode)val; + UpdateArrowKeys(punch_key_); + + val = g_python->GetControllerValue(this, "buttonBomb"); + bomb_key_ = (val == -1) ? bomb_key_default : (SDL_Keycode)val; + UpdateArrowKeys(bomb_key_); + + val = g_python->GetControllerValue(this, "buttonPickUp"); + pick_up_key_ = (val == -1) ? pick_up_key_default : (SDL_Keycode)val; + UpdateArrowKeys(pick_up_key_); + + val = g_python->GetControllerValue(this, "buttonHoldPosition"); + hold_position_key_ = + (val == -1) ? hold_position_key_default : (SDL_Keycode)val; + UpdateArrowKeys(hold_position_key_); + + val = g_python->GetControllerValue(this, "buttonStart"); + start_key_ = (val == -1) ? start_key_default : (SDL_Keycode)val; + UpdateArrowKeys(start_key_); + + val = g_python->GetControllerValue(this, "buttonUp"); + up_key_ = (val == -1) ? up_key_default : (SDL_Keycode)val; + UpdateArrowKeys(up_key_); + + val = g_python->GetControllerValue(this, "buttonDown"); + down_key_ = (val == -1) ? down_key_default : (SDL_Keycode)val; + UpdateArrowKeys(down_key_); + + val = g_python->GetControllerValue(this, "buttonLeft"); + left_key_ = (val == -1) ? left_key_default : (SDL_Keycode)val; + UpdateArrowKeys(left_key_); + + val = g_python->GetControllerValue(this, "buttonRight"); + right_key_ = (val == -1) ? right_key_default : (SDL_Keycode)val; + UpdateArrowKeys(right_key_); + + enable_child_ = true; + + up_held_ = down_held_ = left_held_ = right_held_ = false; +} + +void KeyboardInput::UpdateArrowKeys(SDL_Keycode key) { + if (key == SDLK_UP) { + up_key_assigned_ = true; + } else if (key == SDLK_DOWN) { + down_key_assigned_ = true; + } else if (key == SDLK_LEFT) { + left_key_assigned_ = true; + } else if (key == SDLK_RIGHT) { + right_key_assigned_ = true; + } +} + +auto KeyboardInput::GetButtonName(int index) -> std::string { + return g_platform->GetKeyName(index); + // return InputDevice::GetButtonName(index); +} + +auto KeyboardInput::GetRawDeviceName() -> std::string { return "Keyboard"; } +auto KeyboardInput::GetPartyButtonName() const -> std::string { return "F5"; } +auto KeyboardInput::HasMeaningfulButtonNames() -> bool { return true; } + +} // namespace ballistica diff --git a/src/ballistica/input/device/keyboard_input.h b/src/ballistica/input/device/keyboard_input.h new file mode 100644 index 00000000..23e597c1 --- /dev/null +++ b/src/ballistica/input/device/keyboard_input.h @@ -0,0 +1,60 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_INPUT_DEVICE_KEYBOARD_INPUT_H_ +#define BALLISTICA_INPUT_DEVICE_KEYBOARD_INPUT_H_ + +#include +#include + +#include "ballistica/input/device/input_device.h" +#include "ballistica/platform/min_sdl.h" + +namespace ballistica { + +class KeyboardInput : public InputDevice { + public: + explicit KeyboardInput(KeyboardInput* parent); + ~KeyboardInput() override; + auto HandleKey(const SDL_Keysym* keysym, bool repeat, bool down) -> bool; + auto UpdateMapping() -> void override; + auto GetRawDeviceName() -> std::string override; + auto ResetHeldStates() -> void override; + auto left_key_assigned() const { return left_key_assigned_; } + auto right_key_assigned() const { return right_key_assigned_; } + auto up_key_assigned() const { return up_key_assigned_; } + auto down_key_assigned() const { return down_key_assigned_; } + auto GetPartyButtonName() const -> std::string override; + auto IsKeyboard() -> bool override { return true; } + auto HasMeaningfulButtonNames() -> bool override; + auto GetButtonName(int index) -> std::string override; + + private: + auto UpdateArrowKeys(SDL_Keycode key) -> void; + auto UpdateRun(SDL_Keycode key, bool down) -> void; + SDL_Keycode up_key_{}; + SDL_Keycode down_key_{}; + SDL_Keycode left_key_{}; + SDL_Keycode right_key_{}; + SDL_Keycode jump_key_{}; + SDL_Keycode punch_key_{}; + SDL_Keycode bomb_key_{}; + SDL_Keycode pick_up_key_{}; + SDL_Keycode hold_position_key_{}; + SDL_Keycode start_key_{}; + bool down_held_{}; + bool up_held_{}; + bool left_held_{}; + bool right_held_{}; + bool enable_child_{}; + bool left_key_assigned_{}; + bool right_key_assigned_{}; + bool up_key_assigned_{}; + bool down_key_assigned_{}; + KeyboardInput* parent_keyboard_input_{}; + KeyboardInput* child_keyboard_input_{}; + std::set keys_held_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_INPUT_DEVICE_KEYBOARD_INPUT_H_ diff --git a/src/ballistica/input/device/test_input.cc b/src/ballistica/input/device/test_input.cc new file mode 100644 index 00000000..be821d5c --- /dev/null +++ b/src/ballistica/input/device/test_input.cc @@ -0,0 +1,148 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/input/device/test_input.h" + +#include + +#include "ballistica/game/game.h" +#include "ballistica/input/device/joystick.h" +#include "ballistica/input/input.h" +#include "ballistica/platform/min_sdl.h" + +namespace ballistica { + +TestInput::TestInput() { + joystick_ = Object::NewDeferred(-1, // not an sdl joystick + "TestInput", // device name + false, // allow configuring? + false); // calibrate?; + g_input->PushAddInputDeviceCall(joystick_, true); +} + +TestInput::~TestInput() { g_input->PushRemoveInputDeviceCall(joystick_, true); } + +void TestInput::Reset() { + assert(InMainThread()); + reset_ = true; +} + +void TestInput::HandleAlreadyPressedTwice() { + if (print_already_did2_) { + print_already_did2_ = false; + } +} + +void TestInput::Process(millisecs_t time) { + assert(InMainThread()); + + if (reset_) { + reset_ = false; + join_end_time_ = time + 7000; // do joining for the next few seconds + join_start_time_ = time + 1000; + join_press_count_ = 0; + print_non_join_ = true; + print_already_did2_ = true; + } + + if (print_non_join_ && time >= join_end_time_) { + print_non_join_ = false; + } + + if (time > next_event_time_) { + next_event_time_ = time + static_cast(RandomFloat() * 300.0f); + + // Do absolutely nothing before join start time. + if (time < join_start_time_) { + return; + } + + float r = RandomFloat(); + + SDL_Event e; + if (r < 0.5f) { + // Movement change. + r = RandomFloat(); + if (r < 0.3f) { + lr_ = ud_ = 0; + } else { + lr_ = std::max( + -32767, + std::min(32767, + static_cast(-50000.0f + 100000.0f * RandomFloat()))); + ud_ = std::max( + -32767, + std::min(32767, + static_cast(-50000.0f + 100000.0f * RandomFloat()))); + } + e.type = SDL_JOYAXISMOTION; + e.jaxis.axis = 0; + e.jaxis.value = static_cast_check_fit(ud_); + g_input->PushJoystickEvent(e, joystick_); + e.jaxis.axis = 1; + e.jaxis.value = static_cast_check_fit(lr_); + g_input->PushJoystickEvent(e, joystick_); + } else { + // Button change. + r = RandomFloat(); + if (r > 0.75f) { + // Jump: + // Don't do more than 2 presses while joining. + if (!jump_pressed_ && time < join_end_time_ && join_press_count_ > 1) { + HandleAlreadyPressedTwice(); + } else { + jump_pressed_ = !jump_pressed_; + if (jump_pressed_) join_press_count_++; + e.type = jump_pressed_ ? SDL_JOYBUTTONDOWN : SDL_JOYBUTTONUP; + e.jbutton.button = 0; + g_input->PushJoystickEvent(e, joystick_); + } + } else if (r > 0.5f) { + // Bomb: + // Don't do more than 2 presses while joining. + if (!bomb_pressed_ && time < join_end_time_ && join_press_count_ > 1) { + HandleAlreadyPressedTwice(); + } else { + bomb_pressed_ = !bomb_pressed_; + + // This counts as a join press *only* if its the first. + // (presses after that simply change our character) + if (join_press_count_ == 0 && bomb_pressed_) { + // cout << "GOT BOMB AS FIRST PRESS " << this << endl; + join_press_count_++; + } + e.type = bomb_pressed_ ? SDL_JOYBUTTONDOWN : SDL_JOYBUTTONUP; + e.jbutton.button = 2; + g_input->PushJoystickEvent(e, joystick_); + } + + } else if (r > 0.25f) { + // Grab: + // Don't do more than 2 presses while joining. + if (!pickup_pressed_ && time < join_end_time_ + && join_press_count_ > 1) { + HandleAlreadyPressedTwice(); + } else { + pickup_pressed_ = !pickup_pressed_; + if (pickup_pressed_) join_press_count_++; + e.type = pickup_pressed_ ? SDL_JOYBUTTONDOWN : SDL_JOYBUTTONUP; + e.jbutton.button = 3; + g_input->PushJoystickEvent(e, joystick_); + } + } else { + // Punch: + // Don't do more than 2 presses while joining. + if (!punch_pressed_ && time < join_end_time_ && join_press_count_ > 1) { + HandleAlreadyPressedTwice(); + } else { + punch_pressed_ = !punch_pressed_; + if (punch_pressed_) join_press_count_++; + e.type = punch_pressed_ ? SDL_JOYBUTTONDOWN : SDL_JOYBUTTONUP; + e.jbutton.button = 1; + g_input->PushJoystickEvent(e, joystick_); + } + } + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/input/device/test_input.h b/src/ballistica/input/device/test_input.h new file mode 100644 index 00000000..5f349144 --- /dev/null +++ b/src/ballistica/input/device/test_input.h @@ -0,0 +1,37 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_INPUT_DEVICE_TEST_INPUT_H_ +#define BALLISTICA_INPUT_DEVICE_TEST_INPUT_H_ + +#include "ballistica/ballistica.h" + +namespace ballistica { + +class TestInput { + public: + TestInput(); + virtual ~TestInput(); + void Process(millisecs_t time); + void Reset(); + + private: + void HandleAlreadyPressedTwice(); + int lr_{}; + int ud_{}; + bool jump_pressed_{}; + bool bomb_pressed_{}; + bool pickup_pressed_{}; + bool punch_pressed_{}; + millisecs_t next_event_time_{}; + millisecs_t join_start_time_{}; + millisecs_t join_end_time_{9999}; + int join_press_count_{}; + bool reset_{true}; + Joystick* joystick_{}; + bool print_non_join_{}; + bool print_already_did2_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_INPUT_DEVICE_TEST_INPUT_H_ diff --git a/src/ballistica/input/device/touch_input.cc b/src/ballistica/input/device/touch_input.cc new file mode 100644 index 00000000..98cff073 --- /dev/null +++ b/src/ballistica/input/device/touch_input.cc @@ -0,0 +1,1077 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/input/device/touch_input.h" + +#include +#include + +#include "ballistica/app/app_config.h" +#include "ballistica/app/app_globals.h" +#include "ballistica/game/host_activity.h" +#include "ballistica/game/player.h" +#include "ballistica/graphics/camera.h" +#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_command.h" +#include "ballistica/scene/node/player_node.h" +#include "ballistica/ui/ui.h" + +namespace ballistica { + +const float kButtonSpread = 10.0f; +const float kDrawDepth = -0.07f; + +// Given coords within a (-1,-1) to (1,1) box, +// convert them such that their length is never greater than 1. +static void CircleToBoxCoords(float* lr, float* ud) { + if (std::abs((*lr)) < 0.0001f || std::abs((*ud)) < 0.0001f) { + return; // Not worth doing anything. + } + + // Project them out to hit the border. + float s; + if (std::abs((*lr)) > std::abs((*ud))) { + s = 1.0f / std::abs((*lr)); + } else { + s = 1.0f / std::abs((*ud)); + } + float proj_lr = (*lr) * s; + float proj_ud = (*ud) * s; + float proj_len = sqrtf(proj_lr * proj_lr + proj_ud * proj_ud); + (*lr) *= proj_len; + (*ud) *= proj_len; +} + +void TouchInput::HandleTouchEvent(TouchEvent::Type type, void* touch, float x, + float y) { + // Currently we completely ignore these when in editing mode; + // In that case we get fed in SDL mouse events + // (so we can properly mask interaction with widgets, etc). + if (editing()) { + return; + } + + switch (type) { + case TouchEvent::Type::kDown: { + HandleTouchDown(touch, x, y); + break; + } + case TouchEvent::Type::kCanceled: + case TouchEvent::Type::kUp: { + HandleTouchUp(touch, x, y); + break; + } + case TouchEvent::Type::kMoved: { + HandleTouchMoved(touch, x, y); + break; + } + default: + throw Exception(); + } +} + +TouchInput::TouchInput() { + switch (GetInterfaceType()) { + case UIScale::kSmall: + base_controls_scale_ = 2.0f; + world_draw_scale_ = 1.2f; + break; + case UIScale::kMedium: + base_controls_scale_ = 1.5f; + world_draw_scale_ = 1.1f; + break; + default: + base_controls_scale_ = 1.0f; + world_draw_scale_ = 1.0f; + break; + } + + assert(g_app_globals->touch_input == nullptr); + g_app_globals->touch_input = this; +} + +TouchInput::~TouchInput() = default; + +void TouchInput::UpdateButtons(bool new_touch) { + millisecs_t real_time = GetRealTime(); + float spread_scaled_actions = + kButtonSpread * base_controls_scale_ * controls_scale_actions_; + float width = g_graphics->screen_virtual_width(); + float height = g_graphics->screen_virtual_height(); + float edge_buffer = spread_scaled_actions; + + if (new_touch && action_control_type_ == ActionControlType::kSwipe) { + buttons_x_ = buttons_touch_x_; + buttons_y_ = buttons_touch_y_; + } + + // See which button we're closest to. + float bomb_mag = buttons_touch_x_ - buttons_x_; + float punch_mag = buttons_x_ - buttons_touch_x_; + float jump_mag = buttons_y_ - buttons_touch_y_; + float pickup_mag = buttons_touch_y_ - buttons_y_; + float max_mag = + std::max(std::max(std::max(bomb_mag, punch_mag), jump_mag), pickup_mag); + bool closest_to_bomb = false; + bool closest_to_punch = false; + bool closest_to_jump = false; + bool closest_to_pickup = false; + if (bomb_mag == max_mag) { + closest_to_bomb = true; + } else if (punch_mag == max_mag) { + closest_to_punch = true; + } else if (jump_mag == max_mag) { + closest_to_jump = true; + } else if (pickup_mag == max_mag) { + closest_to_pickup = true; + } else { + throw Exception(); + } + if (buttons_touch_) { + last_buttons_touch_time_ = GetRealTime(); + } + + // Handle swipe mode. + if (action_control_type_ == ActionControlType::kSwipe) { + // If we're dragging on one axis, center the other axis. + if (closest_to_bomb + // NOLINTNEXTLINE(bugprone-branch-clone) + && buttons_touch_x_ >= buttons_x_ + spread_scaled_actions) { + buttons_y_ = buttons_touch_y_; + } else if (closest_to_punch + && buttons_touch_x_ <= buttons_x_ - spread_scaled_actions) { + buttons_y_ = buttons_touch_y_; + } else if (closest_to_pickup + // NOLINTNEXTLINE(bugprone-branch-clone) + && buttons_touch_y_ >= buttons_y_ + spread_scaled_actions) { + buttons_x_ = buttons_touch_x_; + } else if (closest_to_jump + && buttons_touch_y_ <= buttons_y_ - spread_scaled_actions) { + buttons_x_ = buttons_touch_x_; + } + + // Drag along the axis we're dragging. + float spread_scaled_actions_extra = 1.01f * spread_scaled_actions; + if (closest_to_bomb + && buttons_touch_x_ >= buttons_x_ + spread_scaled_actions_extra) { + buttons_x_ = buttons_touch_x_ - spread_scaled_actions_extra; + } else if (closest_to_punch + && buttons_touch_x_ + <= buttons_x_ - spread_scaled_actions_extra) { + buttons_x_ = buttons_touch_x_ + spread_scaled_actions_extra; + } else if (closest_to_pickup + && buttons_touch_y_ + >= buttons_y_ + spread_scaled_actions_extra) { + buttons_y_ = buttons_touch_y_ - spread_scaled_actions_extra; + } else if (closest_to_jump + && buttons_touch_y_ + <= buttons_y_ - spread_scaled_actions_extra) { + buttons_y_ = buttons_touch_y_ + spread_scaled_actions_extra; + } + + // Keep them away from screen edges. + if (buttons_x_ > width - edge_buffer) { + buttons_x_ = width - edge_buffer; + } + if (buttons_y_ > height - edge_buffer) { + buttons_y_ = height - edge_buffer; + } else if (buttons_y_ < edge_buffer) { + buttons_y_ = edge_buffer; + } + + // Handle new presses. + if (buttons_touch_) { + if (!bomb_held_ + && buttons_touch_x_ >= buttons_x_ + spread_scaled_actions) { + bomb_held_ = true; + last_bomb_press_time_ = real_time; + InputCommand(InputType::kBombPress); + } + if (!punch_held_ + && buttons_touch_x_ <= buttons_x_ - spread_scaled_actions) { + punch_held_ = true; + last_punch_press_time_ = real_time; + InputCommand(InputType::kPunchPress); + } + if (!jump_held_ + && buttons_touch_y_ <= buttons_y_ - spread_scaled_actions) { + jump_held_ = true; + last_jump_press_time_ = real_time; + InputCommand(InputType::kJumpPress); + } + if (!pickup_held_ + && buttons_touch_y_ >= buttons_y_ + spread_scaled_actions) { + pickup_held_ = true; + last_pickup_press_time_ = real_time; + InputCommand(InputType::kPickUpPress); + } + } + + // Handle releases. + if (bomb_held_ + && (!buttons_touch_ + || buttons_touch_x_ < buttons_x_ + spread_scaled_actions)) { + bomb_held_ = false; + last_bomb_held_time_ = real_time; + InputCommand(InputType::kBombRelease); + } + if (punch_held_ + && (!buttons_touch_ + || buttons_touch_x_ > buttons_x_ - spread_scaled_actions)) { + punch_held_ = false; + last_punch_held_time_ = real_time; + InputCommand(InputType::kPunchRelease); + } + if (jump_held_ + && (!buttons_touch_ + || buttons_touch_y_ > buttons_y_ - spread_scaled_actions)) { + jump_held_ = false; + last_jump_held_time_ = real_time; + InputCommand(InputType::kJumpRelease); + } + if (pickup_held_ + && (!buttons_touch_ + || buttons_touch_y_ < buttons_y_ + spread_scaled_actions)) { + pickup_held_ = false; + last_pickup_held_time_ = real_time; + InputCommand(InputType::kPickUpRelease); + } + } else { + bool was_bomb_held = bomb_held_; + bool was_jump_held = jump_held_; + bool was_pickup_held = pickup_held_; + bool was_punch_held = punch_held_; + bomb_held_ = jump_held_ = pickup_held_ = punch_held_ = false; + if (buttons_touch_) { + if (closest_to_bomb) { + bomb_held_ = true; + if (!was_bomb_held) { + last_bomb_press_time_ = real_time; + InputCommand(InputType::kBombPress); + } + } else if (closest_to_punch) { + punch_held_ = true; + if (!was_punch_held) { + last_punch_press_time_ = real_time; + InputCommand(InputType::kPunchPress); + } + } else if (closest_to_jump) { + jump_held_ = true; + if (!was_jump_held) { + last_jump_press_time_ = real_time; + // fixme should just send one or the other.. + InputCommand(InputType::kJumpPress); + InputCommand(InputType::kFlyPress); + } + } else if (closest_to_pickup) { + pickup_held_ = true; + if (!was_pickup_held) { + last_pickup_press_time_ = real_time; + InputCommand(InputType::kPickUpPress); + } + } + } + + // Handle releases. + if (was_bomb_held && !bomb_held_) { + last_bomb_held_time_ = real_time; + InputCommand(InputType::kBombRelease); + } + if (was_punch_held && !punch_held_) { + punch_held_ = false; + last_punch_held_time_ = real_time; + InputCommand(InputType::kPunchRelease); + } + if (was_jump_held && !jump_held_) { + jump_held_ = false; + last_jump_held_time_ = real_time; + // fixme should just send one or the other.. + InputCommand(InputType::kJumpRelease); + InputCommand(InputType::kFlyRelease); + } + if (was_pickup_held && !pickup_held_) { + pickup_held_ = false; + last_pickup_held_time_ = real_time; + InputCommand(InputType::kPickUpRelease); + } + } +} + +void TouchInput::UpdateDPad() { + // Keep our base somewhat close to our drag point. + float max_dist = 30.0f * base_controls_scale_ * controls_scale_move_; + + // Keep it within a circle of max_dist radius. + float x = (d_pad_x_ - d_pad_base_x_) / max_dist; + float y = (d_pad_y_ - d_pad_base_y_) / max_dist; + float len = sqrtf(x * x + y * y); + + // In swipe mode we move our base around to follow the touch. + if (movement_control_type_ == MovementControlType::kSwipe) { + // If this is the first move event, scoot our base towards our current point + // by a small amount. This is meant to counter the fact that the first + // touch-moved event is always significantly far from the touch-down and + // allows us to start out moving slowly. + if (!did_first_move_ && (x != 0 || y != 0)) { + if (len != 0.0f) { + float offs = 0.8f * std::min(len, 0.8f); + d_pad_base_x_ += x * max_dist * (offs / len); + d_pad_base_y_ += y * max_dist * (offs / len); + x = (d_pad_x_ - d_pad_base_x_) / max_dist; + y = (d_pad_y_ - d_pad_base_y_) / max_dist; + len = sqrtf(x * x + y * y); + } + did_first_move_ = true; + } + + if (len > 1.0f) { + float inv_len = 1.0f / len; + x *= inv_len; + y *= inv_len; + d_pad_base_x_ = d_pad_x_ - x * max_dist; + d_pad_base_y_ = d_pad_y_ - y * max_dist; + } + } else { + // Likewise in joystick mode we keep our touch near the base. + if (len > 1.0f) { + float inv_len = 1.0f / len; + x *= inv_len; + y *= inv_len; + d_pad_x_ = d_pad_base_x_ + x * max_dist; + d_pad_y_ = d_pad_base_y_ + y * max_dist; + } + } + + d_pad_draw_x_ = x; + d_pad_draw_y_ = y; + + // Although its a circle we need to deliver box coords.. (ie: upper-left is + // -1,1). + CircleToBoxCoords(&x, &y); + + float remap = 1.0f; + InputCommand(InputType::kLeftRight, x * remap); + InputCommand(InputType::kUpDown, y * remap); +} + +void TouchInput::Draw(FrameDef* frame_def) { + assert(InGameThread()); + bool active = (!g_ui->IsWindowPresent()); + millisecs_t real_time = frame_def->real_time(); + + // Update our action center whenever possible in case screen is resized. + if (!buttons_touch_) { + float width = g_graphics->screen_virtual_width(); + float height = g_graphics->screen_virtual_height(); + buttons_x_ = width * buttons_default_frac_x_; + buttons_y_ = height * buttons_default_frac_y_; + } + // Same for dpad. + if (!d_pad_touch_) { + float width = g_graphics->screen_virtual_width(); + float height = g_graphics->screen_virtual_height(); + d_pad_x_ = d_pad_base_x_ = width * d_pad_default_frac_x_; + d_pad_y_ = d_pad_base_y_ = height * d_pad_default_frac_y_; + } + + // Update time-dependent stuff to this point. + if ((real_time - update_time_ > 500) && (real_time - update_time_ < 99999)) { + update_time_ = real_time - 500; + } + while (update_time_ < real_time) { + update_time_ += 10; + + // Update presence based on whether or not we're active. + if ((attached_to_player() && active) || editing_) { + presence_ = std::min(1.0f, presence_ + 0.06f); + } else { + presence_ = std::max(0.0f, presence_ - 0.06f); + } + + if (action_control_type_ == ActionControlType::kSwipe) { + // Overall backing opacity fades in and out based on whether we have a + // button touch. + if (buttons_touch_ || editing_) { + button_fade_ = std::min(1.0f, button_fade_ + 0.06f); + } else { + button_fade_ = std::max(0.0f, button_fade_ - 0.015f); + } + + // If there's a button touch but its not on a button, slowly move the + // center towards it (keeps us from slowly sliding onto a button press + // while trying to run and stuff). + if (buttons_touch_ && !bomb_held_ && !punch_held_ && !pickup_held_ + && !jump_held_) { + buttons_x_ += 0.015f * (buttons_touch_x_ - buttons_x_); + buttons_y_ += 0.015f * (buttons_touch_y_ - buttons_y_); + } + } else { + button_fade_ = 1.0f; + } + } + + if (presence_ > 0.0f) { + float width = g_graphics->screen_virtual_width(); + float height = g_graphics->screen_virtual_height(); + SimpleComponent c(frame_def->GetOverlayFlatPass()); + c.SetTransparent(true); + + float sc_move = base_controls_scale_ * controls_scale_move_ + * (200.0f - presence_ * 100.0f); + float sc_actions = base_controls_scale_ * controls_scale_actions_ + * (200.0f - presence_ * 100.0f); + + bool do_draw; + if (movement_control_type_ == MovementControlType::kSwipe) { + do_draw = (!d_pad_touch_ && !swipe_controls_hidden_); + } else { + do_draw = true; // always draw in joystick mode + } + + if (do_draw) { + float sc2 = sc_move; + if (movement_control_type_ == MovementControlType::kSwipe) sc2 *= 0.6f; + + if (movement_control_type_ == MovementControlType::kSwipe) { + c.SetTexture(g_media->GetTexture(SystemTextureID::kTouchArrows)); + if (editing_) { + float val = 1.5f + sinf(real_time * 0.02f); + c.SetColor(val, val, 1.0f, 1.0f); + } + } else { + float val; + if (editing_) { + val = 0.35f + 0.15f * sinf(real_time * 0.02f); + } else { + val = 0.35f; + } + c.SetColor(0.5f, 0.3f, 0.8f, val); + c.SetTexture(g_media->GetTexture(SystemTextureID::kCircle)); + } + + float x_offs = + width * (-0.1f - d_pad_default_frac_x_) * (1.0f - presence_); + float y_offs = + height * (-0.1f - d_pad_default_frac_y_) * (1.0f - presence_); + + c.PushTransform(); + c.Translate(d_pad_base_x_ + x_offs, d_pad_base_y_ + y_offs, kDrawDepth); + c.Scale(sc2, sc2); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + + if (movement_control_type_ == MovementControlType::kJoystick) { + float val; + if (editing_) { + val = 0.35f + 0.15f * sinf(real_time * 0.02f); + } else { + val = 0.35f; + } + c.SetColor(0.0f, 0.0f, 0.0f, val); + c.PushTransform(); + c.Translate(d_pad_x_ + x_offs, d_pad_y_ + y_offs, kDrawDepth); + c.Scale(sc_move * 0.5f, sc_move * 0.5f); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + } + } + + if (!buttons_touch_ && action_control_type_ == ActionControlType::kSwipe + && !swipe_controls_hidden_) { + float sc2 = sc_actions; + if (action_control_type_ == ActionControlType::kSwipe) sc2 *= 0.6f; + c.SetTexture(g_media->GetTexture(SystemTextureID::kTouchArrowsActions)); + if (editing_) { + float val = 1.5f + sinf(real_time * 0.02f); + c.SetColor(val, val, 1.0f, 1.0f); + } else { + c.SetColor(1.0f, 1.0f, 1.0f, 1.0f); + } + c.PushTransform(); + float x_offs = + width * (1.1f - buttons_default_frac_x_) * (1.0f - presence_); + float y_offs = + height * (-0.1f - buttons_default_frac_y_) * (1.0f - presence_); + c.Translate(buttons_x_ + x_offs, buttons_y_ + y_offs, kDrawDepth); + c.Scale(sc2, sc2); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + } + c.Submit(); + } + + bool have_player_position = false; + std::vector player_position(3); + if (attached_to_player()) { + PlayerNode* player_node = nullptr; + + // Try to come up with whichever scene is in the foreground, and try + // to pull a node for the player we're attached to. + + if (HostActivity* host_activity = + g_game->GetForegroundContext().GetHostActivity()) { + if (Player* player = GetPlayer()) { + player_node = host_activity->scene()->GetPlayerNode(player->id()); + } + } else { + if (Scene* scene = g_game->GetForegroundScene()) { + player_node = scene->GetPlayerNode(remote_player_id()); + } + } + if (player_node) { + have_player_position = true; + player_position = player_node->position(); + } + } + + SimpleComponent c(frame_def->GetOverlayFlatPass()); + c.SetTransparent(true); + + uint32_t residual_time = 130; + + // Draw buttons. + bool do_draw; + if (action_control_type_ == ActionControlType::kButtons) { + do_draw = (presence_ > 0.0f); + } else { + do_draw = (active); + } + + if (do_draw) { + float base_fade; + + if (action_control_type_ == ActionControlType::kSwipe) { + base_fade = 0.25f; + } else { + base_fade = 0.8f; + c.SetTexture(g_media->GetTexture(SystemTextureID::kActionButtons)); + } + + float x_offs; + float y_offs; + if (action_control_type_ == ActionControlType::kSwipe) { + x_offs = -buttons_x_; + y_offs = -buttons_y_ - 75; + } else { + x_offs = y_offs = 0.0f; + + // Do transition in button mode. + if (presence_ < 1.0f) { + float width = g_graphics->screen_virtual_width(); + float height = g_graphics->screen_virtual_height(); + x_offs = width * (1.1f - buttons_default_frac_x_) * (1.0f - presence_); + y_offs = + height * (-0.1f - buttons_default_frac_y_) * (1.0f - presence_); + } + } + + float s = 0.5f; + + // In buttons mode we draw based on our UI size. Otherwise we draw in the + // world at a constant scale. + if (action_control_type_ == ActionControlType::kButtons) { + s *= 3.0f * base_controls_scale_ * controls_scale_actions_; + } else { + // When not drawing under the character we obey ui size. + if (!have_player_position) { + s *= 0.5f * 1.5f * base_controls_scale_ * controls_scale_actions_; + } else { + s *= world_draw_scale_; + } + } + + float b_width = 50.0f * s; + float half_b_width = 0.0f; + + float button_spread_s = 0.0f * s; + + if (action_control_type_ == ActionControlType::kSwipe) { + button_spread_s *= 2.0f; + } + + bool was_held; + float pop; + float pop_time = 100.0f; + + c.PushTransform(); + + // In swipe mode we draw under our character when possible, and above the + // touch otherwise. + if (action_control_type_ == ActionControlType::kSwipe) { + if (have_player_position) { + c.TranslateToProjectedPoint(player_position[0], player_position[1], + player_position[2]); + } else { + float s2 = base_controls_scale_ * controls_scale_actions_; + c.Translate(buttons_touch_start_x_ - s2 * 50.0f, + buttons_touch_start_y_ + 75.0f + s2 * 50.0f, 0.0f); + } + } + + float squash = 1.3f; + float stretch = 1.3f; + + float s_extra = 1.0f; + if (editing_) s_extra = 0.7f + 0.3f * sinf(real_time * 0.02f); + + // Bomb. + was_held = + bomb_held_ || (real_time - last_bomb_press_time_ < residual_time); + if ((button_fade_ > 0.0f) || bomb_held_ || was_held) { + pop = std::max(0.0f, + 1.0f + - static_cast(real_time - last_bomb_press_time_) + / pop_time); + if (was_held) { + c.SetColor(1.5f, 2.0f * pop, 2.0f * pop, 1.0f); + } else { + c.SetColor(0.65f * s_extra, 0.0f, 0.0f, base_fade * button_fade_); + } + + c.PushTransform(); + c.Translate(buttons_x_ + button_spread_s + half_b_width + x_offs, + buttons_y_ + y_offs, kDrawDepth); + if (bomb_held_) { + c.Scale(stretch * b_width, squash * b_width); + } else { + c.Scale(b_width, b_width); + } + c.DrawModel(g_media->GetModel(SystemModelID::kActionButtonRight)); + c.PopTransform(); + } + + // Punch. + was_held = + punch_held_ || (real_time - last_punch_press_time_ < residual_time); + if ((button_fade_ > 0.0f) || punch_held_ || was_held) { + pop = std::max( + 0.0f, 1.0f + - static_cast(real_time - last_punch_press_time_) + / pop_time); + if (was_held) { + c.SetColor(1.3f + 2.0f * pop, 1.3f + 2.0f * pop, 0.0f + 2.0f * pop, + 1.0f); + } else { + c.SetColor(0.9f * s_extra, 0.9f * s_extra, 0.2f * s_extra, + base_fade * button_fade_); + } + c.PushTransform(); + c.Translate(buttons_x_ - button_spread_s - half_b_width + x_offs, + buttons_y_ + y_offs, kDrawDepth); + if (punch_held_) { + c.Scale(stretch * b_width, squash * b_width); + } else { + c.Scale(b_width, b_width); + } + c.DrawModel(g_media->GetModel(SystemModelID::kActionButtonLeft)); + c.PopTransform(); + } + + // Jump. + was_held = + jump_held_ || (real_time - last_jump_press_time_ < residual_time); + if ((button_fade_ > 0.0f) || jump_held_ || was_held) { + pop = std::max(0.0f, + 1.0f + - static_cast(real_time - last_jump_press_time_) + / pop_time); + if (was_held) { + c.SetColor(1.8f * pop, 1.2f + 0.9f * pop, 2.0f * pop, 1.0f); + } else { + c.SetColor(0.0f, 0.8f * s_extra, 0.0f, base_fade * button_fade_); + } + c.PushTransform(); + c.Translate(buttons_x_ + x_offs, + buttons_y_ - button_spread_s - half_b_width + y_offs, + kDrawDepth); + if (jump_held_) { + c.Scale(squash * b_width, stretch * b_width); + } else { + c.Scale(b_width, b_width); + } + c.DrawModel(g_media->GetModel(SystemModelID::kActionButtonBottom)); + c.PopTransform(); + } + + // Pickup. + was_held = + pickup_held_ || (real_time - last_pickup_press_time_ < residual_time); + if ((button_fade_ > 0.0f) || pickup_held_ || was_held) { + pop = std::max( + 0.0f, 1.0f + - static_cast(real_time - last_pickup_press_time_) + / pop_time); + if (was_held) { + c.SetColor(0.5f + 1.4f * pop, 0.8f + 2.4f * pop, 2.0f + 0.4f * pop, + 1.0f); + } else { + c.SetColor(0.3f * s_extra, 0.65f * s_extra, 1.0f * s_extra, + base_fade * button_fade_); + } + c.PushTransform(); + c.Translate(buttons_x_ + x_offs, + buttons_y_ + button_spread_s + half_b_width + y_offs, + kDrawDepth); + if (pickup_held_) { + c.Scale(squash * b_width, stretch * b_width); + } else { + c.Scale(b_width, b_width); + } + c.DrawModel(g_media->GetModel(SystemModelID::kActionButtonTop)); + c.PopTransform(); + } + + // Center point. + if (buttons_touch_ && action_control_type_ == ActionControlType::kSwipe) { + c.SetTexture(g_media->GetTexture(SystemTextureID::kCircle)); + c.SetColor(1.0f, 1.0f, 0.0f, 0.8f); + c.PushTransform(); + + // We need to scale this up/down relative to the scale we're drawing at + // since we're not drawing in screen space. + float diff_x = buttons_touch_x_ - buttons_x_; + float diff_y = buttons_touch_y_ - buttons_y_; + + if (have_player_position) { + c.Translate(buttons_x_ + + 2.3f * world_draw_scale_ * diff_x + / (base_controls_scale_ * controls_scale_actions_) + + x_offs, + buttons_y_ + + 2.3f * world_draw_scale_ * diff_y + / (base_controls_scale_ * controls_scale_actions_) + + y_offs, + kDrawDepth); + } else { + c.Translate(buttons_x_ + 0.5f * 1.55f * 2.3f * diff_x + x_offs, + buttons_y_ + 0.5f * 1.55f * 2.3f * diff_y + y_offs, + kDrawDepth); + } + c.Scale(b_width * 0.3f, b_width * 0.3f); + c.DrawModel(g_media->GetModel(SystemModelID::kImage1x1)); + c.PopTransform(); + } + c.PopTransform(); + } + c.Submit(); + + bool draw_in_world = have_player_position; + + // Always draw when we've got a world-pos. if not, only draw on screen in + // swipe mode. + if (d_pad_touch_ + && (draw_in_world + || movement_control_type_ == MovementControlType::kSwipe)) { + // Circle. + SimpleComponent c2(draw_in_world ? frame_def->overlay_3d_pass() + : frame_def->GetOverlayFlatPass()); + c2.SetTransparent(true); + if (buttons_touch_) { + c2.SetColor(1.0f, 0.3f, 0.2f, 0.45f); + } else { + c2.SetColor(1.0f, 1.0f, 0.0f, 0.45f); + } + + bool zero_len; + if (std::abs(d_pad_draw_x_) > 0.00001f + || std::abs(d_pad_draw_y_) > 0.00001f) { + d_pad_draw_dir_ = Vector3f(d_pad_draw_x_, 0.0f, -d_pad_draw_y_); + zero_len = false; + } else { + zero_len = true; + } + + // Line. + float dist = sqrtf(d_pad_draw_dir_.x * d_pad_draw_dir_.x + + d_pad_draw_dir_.z * d_pad_draw_dir_.z); + if (zero_len) { + dist = 0.05f; + } + + c2.SetTexture(g_media->GetTexture(SystemTextureID::kArrow)); + Matrix44f orient = + Matrix44fOrient(d_pad_draw_dir_, Vector3f(0.0f, 1.0f, 0.0f)); + c2.PushTransform(); + + // Drawing in the 3d world. + if (draw_in_world) { + c2.Translate(player_position[0], player_position[1] - 0.5f, + player_position[2]); + + // In happy thoughts mode show the arrow on the xy plane instead of xz. + if (g_graphics->camera()->happy_thoughts_mode()) { + c2.Translate(0.0f, 0.5f, 0.0f); + c2.Rotate(90.0f, 1.0f, 0.0f, 0.0f); + } + } else { + // Drawing on 2d overlay. + float s = base_controls_scale_ * controls_scale_move_; + c2.Translate(d_pad_start_x_ + s * 50.0f, d_pad_start_y_ + s * 50.0f, + 0.0f); + c2.ScaleUniform(s * 50.0f); + c2.Rotate(90.0f, 1.0f, 0.0f, 0.0f); + } + + c2.MultMatrix(orient.m); + c2.Rotate(-90.0f, 1.0f, 0.0f, 0.0f); + + c2.ScaleUniform(0.8f); + + c2.PushTransform(); + c2.Translate(0.0f, dist * -0.5f, 0.0f); + c2.Scale(0.15f, dist, 0.2f); + c2.DrawModel(g_media->GetModel(SystemModelID::kArrowBack)); + c2.PopTransform(); + + c2.PushTransform(); + c2.Translate(0.0f, dist * -1.0f - 0.15f, 0.0f); + c2.Scale(0.45f, 0.3f, 0.3f); + c2.DrawModel(g_media->GetModel(SystemModelID::kArrowFront)); + c2.PopTransform(); + + c2.PopTransform(); + c2.Submit(); + } +} + +void TouchInput::UpdateMapping() { + assert(InGameThread()); + + std::string touch_movement_type = + g_app_config->Resolve(AppConfig::StringID::kTouchMovementControlType); + if (touch_movement_type == "swipe") { + movement_control_type_ = TouchInput::MovementControlType::kSwipe; + } else if (touch_movement_type == "joystick") { + movement_control_type_ = TouchInput::MovementControlType::kJoystick; + } else { + Log("Error: Invalid touch-movement-type: " + touch_movement_type); + movement_control_type_ = TouchInput::MovementControlType::kSwipe; + } + std::string touch_action_type = + g_app_config->Resolve(AppConfig::StringID::kTouchActionControlType); + if (touch_action_type == "swipe") { + action_control_type_ = TouchInput::ActionControlType::kSwipe; + } else if (touch_action_type == "buttons") { + action_control_type_ = TouchInput::ActionControlType::kButtons; + } else { + Log("Error: Invalid touch-action-type: " + touch_action_type); + action_control_type_ = TouchInput::ActionControlType::kSwipe; + } + + controls_scale_move_ = + g_app_config->Resolve(AppConfig::FloatID::kTouchControlsScaleMovement); + controls_scale_actions_ = + g_app_config->Resolve(AppConfig::FloatID::kTouchControlsScaleActions); + swipe_controls_hidden_ = + g_app_config->Resolve(AppConfig::BoolID::kTouchControlsSwipeHidden); + + // Start with defaults. + switch (GetInterfaceType()) { + case UIScale::kSmall: + buttons_default_frac_x_ = 0.88f; + buttons_default_frac_y_ = 0.2f; + d_pad_default_frac_x_ = 0.12f; + d_pad_default_frac_y_ = 0.2f; + break; + case UIScale::kMedium: + buttons_default_frac_x_ = 0.89f; + buttons_default_frac_y_ = 0.2f; + d_pad_default_frac_x_ = 0.11f; + d_pad_default_frac_y_ = 0.2f; + break; + default: + buttons_default_frac_x_ = 0.9f; + buttons_default_frac_y_ = 0.3f; + d_pad_default_frac_x_ = 0.1f; + d_pad_default_frac_y_ = 0.3f; + break; + } + + // Now override with config. + d_pad_default_frac_x_ = + g_python->GetRawConfigValue("Touch DPad X", d_pad_default_frac_x_); + d_pad_default_frac_y_ = + g_python->GetRawConfigValue("Touch DPad Y", d_pad_default_frac_y_); + buttons_default_frac_x_ = + g_python->GetRawConfigValue("Touch Buttons X", buttons_default_frac_x_); + buttons_default_frac_y_ = + g_python->GetRawConfigValue("Touch Buttons Y", buttons_default_frac_y_); +} + +auto TouchInput::HandleTouchDown(void* touch, float x, float y) -> bool { + assert(InGameThread()); + + float width = g_graphics->screen_virtual_width(); + float height = g_graphics->screen_virtual_height(); + + // If we're in edit mode, see if the touch should become an edit-dpad touch or + // an edit-button touch. + if (editing_) { + float x_diff = x - d_pad_base_x_; + float y_diff = y - d_pad_base_y_; + float len = sqrtf(x_diff * x_diff + y_diff * y_diff) + / (base_controls_scale_ * controls_scale_move_); + if (len < 40.0f) { + d_pad_drag_touch_ = touch; + d_pad_drag_x_offs_ = x_diff; + d_pad_drag_y_offs_ = y_diff; + return true; + } + + x_diff = x - buttons_x_; + y_diff = y - buttons_y_; + len = sqrtf(x_diff * x_diff + y_diff * y_diff) + / (base_controls_scale_ * controls_scale_actions_); + if (len < 40.0f) { + buttons_drag_touch_ = touch; + buttons_drag_x_offs_ = x_diff; + buttons_drag_y_offs_ = y_diff; + return true; + } + return false; // We don't claim the event. + + } else { + // Normal in-game operation: + + // Normal operation is disabled while a UI is up. + if (g_ui->IsWindowPresent()) { + return false; + } + + if (!attached_to_player()) { + // Ignore touches at the very top (so we don't interfere with the menu). + if (y < height * 0.8f) { + RequestPlayer(); + + // Joining with the touchscreen can sometimes + // be accidental if there's a trackpad on the controller. + // ..so lets issue a warning to that effect if there's already + // controllers active.. (only if we got a player though). + if (attached_to_player() && g_input->HaveControllerWithPlayer()) { + ScreenMessage(g_game->GetResourceString("touchScreenJoinWarningText"), + {1.0f, 1.0f, 0.0f}); + } + } + } else { + // If its on the left side, this is our new dpad touch. + if (x < width * 0.5f) { + d_pad_touch_ = touch; + did_first_move_ = false; + if (movement_control_type_ == MovementControlType::kSwipe) { + d_pad_base_x_ = x; + d_pad_base_y_ = y; + } + d_pad_x_ = x; + d_pad_y_ = y; + d_pad_start_x_ = x; + d_pad_start_y_ = y; + + UpdateDPad(); + } else if (y < height * 0.8f) { + // Its on the right side (and below the menu), handle buttons. + // Start running if this is a new press. + if (buttons_touch_ == nullptr) { + InputCommand(InputType::kRun, 1.0f); + // in swipe mode we count this as a fly-press + if (action_control_type_ == ActionControlType::kSwipe) { + InputCommand(InputType::kFlyPress); + } + } + buttons_touch_ = touch; + buttons_touch_x_ = buttons_touch_start_x_ = x; + buttons_touch_y_ = buttons_touch_start_y_ = y; + + UpdateButtons(true); + } + } + } + return true; +} + +auto TouchInput::HandleTouchUp(void* touch, float x, float y) -> bool { + assert(InGameThread()); + + // Release dpad drag touch. + if (touch == d_pad_drag_touch_) { + d_pad_drag_touch_ = nullptr; + + // Write the current frac to our config. + g_python->SetRawConfigValue("Touch DPad X", d_pad_default_frac_x_); + g_python->SetRawConfigValue("Touch DPad Y", d_pad_default_frac_y_); + } + + if (touch == buttons_drag_touch_) { + buttons_drag_touch_ = nullptr; + + // Write the current frac to our config. + g_python->SetRawConfigValue("Touch Buttons X", buttons_default_frac_x_); + g_python->SetRawConfigValue("Touch Buttons Y", buttons_default_frac_y_); + } + + // Release on button touch. + if (touch == buttons_touch_) { + InputCommand(InputType::kRun, 0.0f); + if (action_control_type_ == ActionControlType::kSwipe) { + InputCommand(InputType::kFlyRelease); + } + buttons_touch_x_ = x; + buttons_touch_y_ = y; + buttons_touch_ = nullptr; + UpdateButtons(); + } + + // If it was our dpad touch, stop tracking. + if (touch == d_pad_touch_) { + d_pad_x_ = d_pad_base_x_; + d_pad_y_ = d_pad_base_y_; + d_pad_touch_ = nullptr; + UpdateDPad(); + } + return true; +} + +auto TouchInput::HandleTouchMoved(void* touch, float x, float y) -> bool { + assert(InGameThread()); + if (touch == d_pad_drag_touch_) { + float width = g_graphics->screen_virtual_width(); + float height = g_graphics->screen_virtual_height(); + float ratio_x = + std::min(0.45f, std::max(0.0f, (x - d_pad_drag_x_offs_) / width)); + float ratio_y = + std::min(0.9f, std::max(0.0f, (y - d_pad_drag_y_offs_) / height)); + d_pad_default_frac_x_ = ratio_x; + d_pad_default_frac_y_ = ratio_y; + } + if (touch == buttons_drag_touch_) { + float width = g_graphics->screen_virtual_width(); + float height = g_graphics->screen_virtual_height(); + float ratio_x = + std::min(1.0f, std::max(0.55f, (x - buttons_drag_x_offs_) / width)); + float ratio_y = + std::min(0.9f, std::max(0.0f, (y - buttons_drag_y_offs_) / height)); + buttons_default_frac_x_ = ratio_x; + buttons_default_frac_y_ = ratio_y; + } + + // Ignore button/pad touches while gui is up. + if (g_ui->IsWindowPresent()) { + return false; + } + if (touch == buttons_touch_) { + buttons_touch_x_ = x; + buttons_touch_y_ = y; + UpdateButtons(); + } + + // If it was our dpad touch, update tracking. + if (touch == d_pad_touch_) { + d_pad_x_ = x; + d_pad_y_ = y; + UpdateDPad(); + } + return true; +} + +auto TouchInput::GetRawDeviceName() -> std::string { return "TouchScreen"; } + +} // namespace ballistica diff --git a/src/ballistica/input/device/touch_input.h b/src/ballistica/input/device/touch_input.h new file mode 100644 index 00000000..a2d2f389 --- /dev/null +++ b/src/ballistica/input/device/touch_input.h @@ -0,0 +1,95 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_INPUT_DEVICE_TOUCH_INPUT_H_ +#define BALLISTICA_INPUT_DEVICE_TOUCH_INPUT_H_ + +#include + +#include "ballistica/input/device/input_device.h" +#include "ballistica/math/vector3f.h" + +namespace ballistica { + +/// A touchscreen based controller for mobile devices. +class TouchInput : public InputDevice { + public: + TouchInput(); + ~TouchInput() override; + auto GetAllowsConfiguring() -> bool override { return false; } + void HandleTouchEvent(TouchEvent::Type type, void* touch, float x, float y); + auto HandleTouchDown(void* touch, float x, float y) -> bool; + auto HandleTouchUp(void* touch, float x, float y) -> bool; + auto HandleTouchMoved(void* touch, float x, float y) -> bool; + void Draw(FrameDef* frame_def); + void set_editing(bool e) { editing_ = e; } + auto editing() const -> bool { return editing_; } + auto IsTouchScreen() -> bool override { return true; } + void UpdateMapping() override; + enum class MovementControlType { kJoystick, kSwipe }; + enum class ActionControlType { kButtons, kSwipe }; + + protected: + auto GetRawDeviceName() -> std::string override; + + private: + void UpdateDPad(); + void UpdateButtons(bool new_touch = false); + MovementControlType movement_control_type_{MovementControlType::kSwipe}; + ActionControlType action_control_type_{ActionControlType::kButtons}; + float controls_scale_move_{1.0f}; + float controls_scale_actions_{1.0f}; + bool swipe_controls_hidden_{}; + float presence_{}; + float button_fade_{}; + bool editing_{}; + void* d_pad_touch_{}; + void* d_pad_drag_touch_{}; + float d_pad_drag_x_offs_{}; + float d_pad_drag_y_offs_{}; + float d_pad_start_x_{}; + float d_pad_start_y_{}; + bool did_first_move_{}; + float d_pad_base_x_{}; + float d_pad_base_y_{}; + float d_pad_x_{}; + float d_pad_y_{}; + + // Button coordinates are provided in virtual screen space. + float buttons_default_frac_x_{}; + float buttons_default_frac_y_{}; + float d_pad_default_frac_x_{}; + float d_pad_default_frac_y_{}; + float buttons_x_{-100.0f}; + float buttons_y_{-100.0f}; + float buttons_touch_start_x_{}; + float buttons_touch_start_y_{}; + void* buttons_touch_{}; + float buttons_touch_x_{-100.0f}; + float buttons_touch_y_{-100.0f}; + void* buttons_drag_touch_{}; + float buttons_drag_x_offs_{}; + float buttons_drag_y_offs_{}; + float base_controls_scale_{1.0f}; + float world_draw_scale_{1.0f}; + bool bomb_held_{}; + bool punch_held_{}; + bool jump_held_{}; + bool pickup_held_{}; + float d_pad_draw_x_{}; + float d_pad_draw_y_{}; + Vector3f d_pad_draw_dir_{1.0f, 0.0f, 0.0f}; + millisecs_t last_buttons_touch_time_{}; + millisecs_t last_punch_held_time_{}; + millisecs_t last_pickup_held_time_{}; + millisecs_t last_bomb_held_time_{}; + millisecs_t last_jump_held_time_{}; + millisecs_t last_punch_press_time_{}; + millisecs_t last_pickup_press_time_{}; + millisecs_t last_bomb_press_time_{}; + millisecs_t last_jump_press_time_{}; + millisecs_t update_time_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_INPUT_DEVICE_TOUCH_INPUT_H_ diff --git a/src/ballistica/input/input.cc b/src/ballistica/input/input.cc new file mode 100644 index 00000000..f32b5026 --- /dev/null +++ b/src/ballistica/input/input.cc @@ -0,0 +1,1842 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/input/input.h" + +#include + +#include "ballistica/app/app_config.h" +#include "ballistica/app/app_globals.h" +#include "ballistica/audio/audio.h" +#include "ballistica/game/player.h" +#include "ballistica/graphics/camera.h" +#include "ballistica/input/device/joystick.h" +#include "ballistica/input/device/keyboard_input.h" +#include "ballistica/input/device/test_input.h" +#include "ballistica/input/device/touch_input.h" +#include "ballistica/python/python.h" +#include "ballistica/ui/console.h" +#include "ballistica/ui/root_ui.h" +#include "ballistica/ui/ui.h" +#include "ballistica/ui/widget/root_widget.h" + +namespace ballistica { + +// Though it seems strange, input is actually owned by the game thread, not the +// app thread. This keeps things simple for game logic interacting with input +// stuff (controller names, counts, etc) but means we need to be prudent about +// properly passing stuff between the game and app thread as needed. + +// The following was pulled from sdl2 +#if BA_SDL2_BUILD || BA_MINSDL_BUILD +static const char* const scancode_names[SDL_NUM_SCANCODES] = { + nullptr, + nullptr, + nullptr, + nullptr, + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + "Return", + "Escape", + "Backspace", + "Tab", + "Space", + "-", + "=", + "[", + "]", + "\\", + "#", + ";", + "'", + "`", + ",", + ".", + "/", + "CapsLock", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12", + "PrintScreen", + "ScrollLock", + "Pause", + "Insert", + "Home", + "PageUp", + "Delete", + "End", + "PageDown", + "Right", + "Left", + "Down", + "Up", + "Numlock", + "Keypad /", + "Keypad *", + "Keypad -", + "Keypad +", + "Keypad Enter", + "Keypad 1", + "Keypad 2", + "Keypad 3", + "Keypad 4", + "Keypad 5", + "Keypad 6", + "Keypad 7", + "Keypad 8", + "Keypad 9", + "Keypad 0", + "Keypad .", + nullptr, + "Application", + "Power", + "Keypad =", + "F13", + "F14", + "F15", + "F16", + "F17", + "F18", + "F19", + "F20", + "F21", + "F22", + "F23", + "F24", + "Execute", + "Help", + "Menu", + "Select", + "Stop", + "Again", + "Undo", + "Cut", + "Copy", + "Paste", + "Find", + "Mute", + "VolumeUp", + "VolumeDown", + nullptr, + nullptr, + nullptr, + "Keypad ,", + "Keypad = (AS400)", + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + "AltErase", + "SysReq", + "Cancel", + "Clear", + "Prior", + "Return", + "Separator", + "Out", + "Oper", + "Clear / Again", + "CrSel", + "ExSel", + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + "Keypad 00", + "Keypad 000", + "ThousandsSeparator", + "DecimalSeparator", + "CurrencyUnit", + "CurrencySubUnit", + "Keypad (", + "Keypad )", + "Keypad {", + "Keypad }", + "Keypad Tab", + "Keypad Backspace", + "Keypad A", + "Keypad B", + "Keypad C", + "Keypad D", + "Keypad E", + "Keypad F", + "Keypad XOR", + "Keypad ^", + "Keypad %", + "Keypad <", + "Keypad >", + "Keypad &", + "Keypad &&", + "Keypad |", + "Keypad ||", + "Keypad :", + "Keypad #", + "Keypad Space", + "Keypad @", + "Keypad !", + "Keypad MemStore", + "Keypad MemRecall", + "Keypad MemClear", + "Keypad MemAdd", + "Keypad MemSubtract", + "Keypad MemMultiply", + "Keypad MemDivide", + "Keypad +/-", + "Keypad Clear", + "Keypad ClearEntry", + "Keypad Binary", + "Keypad Octal", + "Keypad Decimal", + "Keypad Hexadecimal", + nullptr, + nullptr, + "Left Ctrl", + "Left Shift", + "Left Alt", + "Left GUI", + "Right Ctrl", + "Right Shift", + "Right Alt", + "Right GUI", + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + "ModeSwitch", + "AudioNext", + "AudioPrev", + "AudioStop", + "AudioPlay", + "AudioMute", + "MediaSelect", + "WWW", + "Mail", + "Calculator", + "Computer", + "AC Search", + "AC Home", + "AC Back", + "AC Forward", + "AC Stop", + "AC Refresh", + "AC Bookmarks", + "BrightnessDown", + "BrightnessUp", + "DisplaySwitch", + "KBDIllumToggle", + "KBDIllumDown", + "KBDIllumUp", + "Eject", + "Sleep", + "App1", + "App2", + "AudioRewind", + "AudioFastForward", +}; +#endif // BA_SDL2_BUILD || BA_MINSDL_BUILD + +Input::Input() { + // We're a singleton. + // assert(g_input == nullptr); + // g_input = this; + + assert(InGameThread()); + + // Config should have always been read by this point; right? + // assert(g_python); + // UpdateEnabledControllerSubsystems(); +} + +void Input::PushCreateKeyboardInputDevices() { + g_game->PushCall([this] { CreateKeyboardInputDevices(); }); +} + +void Input::CreateKeyboardInputDevices() { + assert(InGameThread()); + if (keyboard_input_ != nullptr || keyboard_input_2_ != nullptr) { + Log("Error: CreateKeyboardInputDevices called with existing kbs."); + return; + } + keyboard_input_ = Object::NewDeferred(nullptr); + AddInputDevice(keyboard_input_, false); + keyboard_input_2_ = Object::NewDeferred(keyboard_input_); + AddInputDevice(keyboard_input_2_, false); +} + +void Input::PushDestroyKeyboardInputDevices() { + g_game->PushCall([this] { DestroyKeyboardInputDevices(); }); +} + +void Input::DestroyKeyboardInputDevices() { + assert(InGameThread()); + if (keyboard_input_ == nullptr || keyboard_input_2_ == nullptr) { + Log("Error: DestroyKeyboardInputDevices called with null kb(s)."); + return; + } + RemoveInputDevice(keyboard_input_, false); + keyboard_input_ = nullptr; + RemoveInputDevice(keyboard_input_2_, false); + keyboard_input_2_ = nullptr; +} + +Input::~Input() = default; + +auto Input::GetInputDevice(int id) -> InputDevice* { + if (id < 0 || id >= static_cast(input_devices_.size())) { + return nullptr; + } + return input_devices_[id].get(); +} + +auto Input::GetInputDevice(const std::string& name, + const std::string& unique_id) -> InputDevice* { + assert(InGameThread()); + for (auto&& i : input_devices_) { + if (i.exists() && (i->GetDeviceName() == name) + && i->GetPersistentIdentifier() == unique_id) { + return i.get(); + } + } + return nullptr; +} + +auto Input::GetNewNumberedIdentifier(const std::string& name, + const std::string& identifier) -> int { + assert(InGameThread()); + + // Stuff like reserved_identifiers["JoyStickType"]["0x812312314"] = 2; + + // First off, if we came with an identifier, see if we've got a reserved + // number already. + if (!identifier.empty()) { + auto i = reserved_identifiers_.find(name); + if (i != reserved_identifiers_.end()) { + auto j = i->second.find(identifier); + if (j != i->second.end()) { + return j->second; + } + } + } + + int num = 1; + int full_id; + while (true) { + bool in_use = false; + + // Scan other devices with the same device-name and find the first number + // suffix that's not taken. + for (auto&& i : input_devices_) { + if (i.exists()) { + if ((i->GetRawDeviceName() == name) && i->number_ == num) { + in_use = true; + break; + } + } + } + if (!in_use) { + // Ok so far its unused.. however input devices that provide non-empty + // identifiers (serial number, usb-id, etc) reserve their number for the + // duration of the game, so we need to check against all reserved numbers + // so we don't steal someones... (so that if they disconnect and reconnect + // they'll get the same number and thus the same name, etc) + if (!identifier.empty()) { + auto i = reserved_identifiers_.find(name); + if (i != reserved_identifiers_.end()) { + for (auto&& j : i->second) { + if (j.second == num) { + in_use = true; + break; + } + } + } + } + + // If its *still* clear lets nab it. + if (!in_use) { + full_id = num; + + // If we have an identifier, reserve it. + if (!identifier.empty()) { + reserved_identifiers_[name][identifier] = num; + } + break; + } + } + num++; + } + return full_id; +} + +void Input::CreateTouchInput() { + assert(InMainThread()); + assert(touch_input_ == nullptr); + touch_input_ = Object::NewDeferred(); + PushAddInputDeviceCall(touch_input_, false); +} + +void Input::AnnounceConnects() { + static bool first_print = true; + + // For the first announcement just say "X controllers detected" and don't have + // a sound. + if (first_print && GetRealTime() < 10000) { + first_print = false; + + // Disabling this completely for now; being more lenient with devices + // allowed on android means this will often come back with large numbers. + bool do_print{false}; + + // If there's been several connected, just give a number. + if (explicit_bool(do_print)) { + if (newly_connected_controllers_.size() > 1) { + std::string s = g_game->GetResourceString("controllersDetectedText"); + Utils::StringReplaceOne( + &s, "${COUNT}", + std::to_string(newly_connected_controllers_.size())); + ScreenMessage(s); + } else { + ScreenMessage(g_game->GetResourceString("controllerDetectedText")); + } + } + } else { + // If there's been several connected, just give a number. + if (newly_connected_controllers_.size() > 1) { + std::string s = g_game->GetResourceString("controllersConnectedText"); + Utils::StringReplaceOne( + &s, "${COUNT}", std::to_string(newly_connected_controllers_.size())); + ScreenMessage(s); + } else { + // If its just one, name it. + std::string s = g_game->GetResourceString("controllerConnectedText"); + Utils::StringReplaceOne(&s, "${CONTROLLER}", + newly_connected_controllers_.front()); + ScreenMessage(s); + } + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kGunCock)); + } + + newly_connected_controllers_.clear(); +} + +void Input::AnnounceDisconnects() { + // If there's been several connected, just give a number. + if (newly_disconnected_controllers_.size() > 1) { + std::string s = g_game->GetResourceString("controllersDisconnectedText"); + Utils::StringReplaceOne( + &s, "${COUNT}", std::to_string(newly_disconnected_controllers_.size())); + ScreenMessage(s); + } else { + // If its just one, name it. + std::string s = g_game->GetResourceString("controllerDisconnectedText"); + Utils::StringReplaceOne(&s, "${CONTROLLER}", + newly_disconnected_controllers_.front()); + ScreenMessage(s); + } + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kCorkPop)); + + newly_disconnected_controllers_.clear(); +} + +void Input::ShowStandardInputDeviceConnectedMessage(InputDevice* j) { + assert(InGameThread()); + std::string suffix; + suffix += j->GetPersistentIdentifier(); + suffix += j->GetDeviceExtraDescription(); + if (!suffix.empty()) { + suffix = " " + suffix; + } + newly_connected_controllers_.push_back(j->GetDeviceName() + suffix); + + // Set a timer to go off and announce the accumulated additions. + if (connect_print_timer_id_ != 0) { + g_game->DeleteRealTimer(connect_print_timer_id_); + } + connect_print_timer_id_ = g_game->NewRealTimer( + 250, false, NewLambdaRunnable([this] { AnnounceConnects(); })); +} + +void Input::ShowStandardInputDeviceDisconnectedMessage(InputDevice* j) { + assert(InGameThread()); + + newly_disconnected_controllers_.push_back(j->GetDeviceName() + " " + + j->GetPersistentIdentifier() + + j->GetDeviceExtraDescription()); + + // Set a timer to go off and announce the accumulated additions. + if (disconnect_print_timer_id_ != 0) { + g_game->DeleteRealTimer(disconnect_print_timer_id_); + } + disconnect_print_timer_id_ = g_game->NewRealTimer( + 250, false, NewLambdaRunnable([this] { AnnounceDisconnects(); })); +} + +void Input::PushAddInputDeviceCall(InputDevice* input_device, + bool standard_message) { + g_game->PushCall([this, input_device, standard_message] { + AddInputDevice(input_device, standard_message); + }); +} + +void Input::AddInputDevice(InputDevice* input, bool standard_message) { + assert(InGameThread()); + + // Lets go through and find the first unused input-device id and use that + // (might as well keep our list small if we can). + int index = 0; + bool found_slot = false; + for (auto& input_device : input_devices_) { + if (!input_device.exists()) { + input_device = Object::MakeRefCounted(input); + found_slot = true; + input->set_index(index); + break; + } + index++; + } + if (!found_slot) { + input_devices_.push_back(Object::MakeRefCounted(input)); + input->set_index(static_cast(input_devices_.size() - 1)); + } + + // We also want to give this input-device as unique an identifier as + // possible. We ask it for its own string which hopefully includes a serial + // or something, but if it doesn't and thus matches an already-existing one, + // we tack an index on to it. that way we can at least uniquely address them + // based off how many are connected. + input->set_numbered_identifier(GetNewNumberedIdentifier( + input->GetRawDeviceName(), input->GetDeviceIdentifier())); + input->ConnectionComplete(); // Let it do any announcing it wants to. + + // Update controls for just this guy. + input->UpdateMapping(); + + // Need to do this after updating controls, as some control settings can + // affect things we count (such as whether start activates default button). + UpdateInputDeviceCounts(); + + if (g_buildconfig.ostype_macos()) { + // Special case: on mac, the first time a iOS/Mac controller is connected, + // let the user know they may want to enable them if they're currently set + // as ignored. (the default at the moment is to only use classic device + // support). + static bool printed_ios_mac_controller_warning = false; + if (!printed_ios_mac_controller_warning && ignore_mfi_controllers_ + && input->IsMFiController()) { + ScreenMessage(R"({"r":"macControllerSubsystemMFiNoteText"})", {1, 1, 0}); + printed_ios_mac_controller_warning = true; + } + } + + if (standard_message && !input->ShouldBeHiddenFromUser()) { + ShowStandardInputDeviceConnectedMessage(input); + } +} + +void Input::PushRemoveInputDeviceCall(InputDevice* input_device, + bool standard_message) { + g_game->PushCall([this, input_device, standard_message] { + RemoveInputDevice(input_device, standard_message); + }); +} + +void Input::RemoveInputDevice(InputDevice* input, bool standard_message) { + assert(InGameThread()); + + if (standard_message && !input->ShouldBeHiddenFromUser()) { + ShowStandardInputDeviceDisconnectedMessage(input); + } + + // Just look for it in our list.. if we find it, simply clear the ref + // (we need to keep the pointer around so our list indices don't change). + for (auto& input_device : input_devices_) { + if (input_device.exists() && (input_device.get() == input)) { + // Pull it off the list before killing it (in case it triggers another + // kill itself). + Object::Ref device = input_device; + + // Ok we cleared its slot in our vector; now we just have + // the local variable 'device' keeping it alive. + input_device.Clear(); + + // If we're attached to a local or remote player, kill the player. + if (input->attached_to_player()) { + if (input->GetPlayer() != nullptr) { + // NOTE: we now remove the player instantly instead of pushing + // a call to do it; otherwise its possible that someone tries + // to access the player's inputdevice before the call goes + // through which would lead to an exception. + g_game->RemovePlayer(input->GetPlayer()); + // g_game->PushRemovePlayerCall(input->GetPlayer()); + } + if (input->GetRemotePlayer() != nullptr) { + input->RemoveRemotePlayerFromGame(); + } + device->DetachFromPlayer(); + } + + // This should kill the device. + // FIXME: since many devices get allocated in the main thread, + // should we not kill it there too?... + device.Clear(); + UpdateInputDeviceCounts(); + return; + } + } + throw Exception("Input::RemoveInputDevice: invalid device provided"); +} + +void Input::UpdateInputDeviceCounts() { + assert(InGameThread()); + + have_button_using_inputs_ = false; + have_start_activated_default_button_inputs_ = false; + have_non_touch_inputs_ = false; + int total = 0; + int controller_count = 0; + for (auto& input_device : input_devices_) { + // Ok, we now limit non-keyboard non-touchscreen devices to ones that have + // been active recently.. (we're starting to get lots of virtual devices and + // other cruft on android; don't wanna show controller UIs just due to + // those) + if (input_device.exists() + && ((*input_device).IsTouchScreen() || (*input_device).IsKeyboard() + || ((*input_device).last_input_time() != 0 + && g_game->master_time() - (*input_device).last_input_time() + < 60000))) { + total++; + if (!(*input_device).IsTouchScreen()) { + have_non_touch_inputs_ = true; + } + if ((*input_device).start_button_activates_default_widget()) { + have_start_activated_default_button_inputs_ = true; + } + if ((*input_device).IsController()) { + have_button_using_inputs_ = true; + if (!(*input_device).IsUIOnly() && !(*input_device).IsTestInput()) { + controller_count++; + } + } + } + } + if (controller_count > max_controller_count_so_far_) { + max_controller_count_so_far_ = controller_count; + if (max_controller_count_so_far_ == 1) { + g_python->PushObjCall(Python::ObjID::kAwardInControlAchievementCall); + } else if (max_controller_count_so_far_ == 2) { + g_python->PushObjCall(Python::ObjID::kAwardDualWieldingAchievementCall); + } + } +} + +auto Input::GetLocalActiveInputDeviceCount() -> int { + assert(InGameThread()); + + // This can get called alot so lets cache the value. + millisecs_t current_time = g_game->master_time(); + if (current_time != last_have_many_local_active_input_devices_check_time_) { + last_have_many_local_active_input_devices_check_time_ = current_time; + + int count = 0; + for (auto& input_device : input_devices_) { + // Only count non-keyboard, non-touchscreen, local devices that have been + // used in the last minute. + if (input_device.exists() && !(*input_device).IsKeyboard() + && !(*input_device).IsTouchScreen() && !(*input_device).IsUIOnly() + && (*input_device).IsLocal() + && ((*input_device).last_input_time() != 0 + && g_game->master_time() - (*input_device).last_input_time() + < 60000)) { + count++; + } + } + local_active_input_device_count_ = count; + } + return local_active_input_device_count_; +} + +auto Input::HaveControllerWithPlayer() -> bool { + assert(InGameThread()); + for (auto& input_device : input_devices_) { + if (input_device.exists() && (*input_device).IsController() + && (*input_device).attached_to_player()) { + return true; + } + } + return false; +} + +auto Input::HaveRemoteAppController() -> bool { + assert(InGameThread()); + for (auto& input_device : input_devices_) { + if (input_device.exists() && (*input_device).IsRemoteApp()) { + return true; + } + } + return false; +} + +auto Input::GetInputDevicesWithName(const std::string& name) + -> std::vector { + std::vector vals; + if (!HeadlessMode()) { + for (auto& input_device : input_devices_) { + if (input_device.exists()) { + auto* js = dynamic_cast(input_device.get()); + if (js && js->GetDeviceName() == name) { + vals.push_back(js); + } + } + } + } + return vals; +} + +auto Input::GetConfigurableGamePads() -> std::vector { + assert(InGameThread()); + std::vector vals; + if (!HeadlessMode()) { + for (auto& input_device : input_devices_) { + if (input_device.exists()) { + auto* js = dynamic_cast(input_device.get()); + if (js && js->GetAllowsConfiguring() && !js->ShouldBeHiddenFromUser()) { + vals.push_back(js); + } + } + } + } + return vals; +} + +auto Input::ShouldCompletelyIgnoreInputDevice(InputDevice* input_device) + -> bool { + if (g_buildconfig.ostype_macos()) { + if (ignore_mfi_controllers_ && input_device->IsMFiController()) { + return true; + } + } + return ignore_sdl_controllers_ && input_device->IsSDLController(); +} + +auto Input::GetIdleTime() const -> millisecs_t { + return GetRealTime() - last_input_time_; +} + +void Input::UpdateEnabledControllerSubsystems() { + assert(IsBootstrapped()); + + // First off, on mac, let's update whether we want to completely ignore either + // the classic or the iOS/Mac controller subsystems. + if (g_buildconfig.ostype_macos()) { + std::string sys = + g_app_config->Resolve(AppConfig::StringID::kMacControllerSubsystem); + if (sys == "Classic") { + ignore_mfi_controllers_ = true; + ignore_sdl_controllers_ = false; + } else if (sys == "MFi") { + ignore_mfi_controllers_ = false; + ignore_sdl_controllers_ = true; + } else if (sys == "Both") { + ignore_mfi_controllers_ = false; + ignore_sdl_controllers_ = false; + } else { + BA_LOG_ONCE("Invalid mac-controller-subsystem value: '" + sys + "'"); + } + } +} + +// Tells all inputs to update their controls based on the app config. +void Input::ApplyAppConfig() { + assert(InGameThread()); + + UpdateEnabledControllerSubsystems(); + + // It's technically possible that updating these controls will add or remove + // devices, thus changing the input_devices_ list, so lets work with a copy of + // it. + std::vector > input_devices = input_devices_; + for (auto& input_device : input_devices) { + if (input_device.exists()) { + input_device->UpdateMapping(); + } + } +} + +void Input::Update() { + assert(InGameThread()); + + millisecs_t real_time = GetRealTime(); + + // If input has been locked an excessively long amount of time, unlock it. + if (input_lock_count_temp_) { + if (real_time - last_input_temp_lock_time_ > 10000) { + Log("Error: Input has been temp-locked for 10 seconds; unlocking."); + input_lock_count_temp_ = 0; + PrintLockLabels(); + input_lock_temp_labels_.clear(); + input_unlock_temp_labels_.clear(); + } + } + + // We now need to update our input-device numbers dynamically since they're + // based on recently-active devices. + // ..we do this much more often for the first few seconds to keep + // controller-usage from being as annoying. + // millisecs_t incr = (real_time > 10000) ? 468 : 98; + // Update: don't remember why that was annoying; trying a single value for + // now. + millisecs_t incr = 249; + if (real_time - last_input_device_count_update_time_ > incr) { + UpdateInputDeviceCounts(); + last_input_device_count_update_time_ = real_time; + } + + for (auto& input_device : input_devices_) { + if (input_device.exists()) { + (*input_device).Update(); + } + } +} + +void Input::Reset() { + assert(InGameThread()); + + // Detach all inputs from players. + for (auto& input_device : input_devices_) { + if (input_device.exists()) { + input_device->DetachFromPlayer(); + } + } +} + +void Input::LockAllInput(bool permanent, const std::string& label) { + assert(InGameThread()); + if (permanent) { + input_lock_count_permanent_++; + input_lock_permanent_labels_.push_back(label); + } else { + input_lock_count_temp_++; + if (input_lock_count_temp_ == 1) { + last_input_temp_lock_time_ = GetRealTime(); + } + input_lock_temp_labels_.push_back(label); + + recent_input_locks_unlocks_.push_back("temp lock: " + label + " time " + + std::to_string(GetRealTime())); + while (recent_input_locks_unlocks_.size() > 10) { + recent_input_locks_unlocks_.pop_front(); + } + } +} + +void Input::UnlockAllInput(bool permanent, const std::string& label) { + assert(InGameThread()); + + recent_input_locks_unlocks_.push_back( + permanent + ? "permanent unlock: " + : "temp unlock: " + label + " time " + std::to_string(GetRealTime())); + while (recent_input_locks_unlocks_.size() > 10) + recent_input_locks_unlocks_.pop_front(); + + if (permanent) { + input_lock_count_permanent_--; + input_unlock_permanent_labels_.push_back(label); + if (input_lock_count_permanent_ < 0) { + BA_LOG_PYTHON_TRACE_ONCE("lock-count-permanent < 0"); + PrintLockLabels(); + input_lock_count_permanent_ = 0; + } + + // When lock counts get back down to zero, clear our labels since all is + // well. + if (input_lock_count_permanent_ == 0) { + input_lock_permanent_labels_.clear(); + input_unlock_permanent_labels_.clear(); + } + } else { + input_lock_count_temp_--; + input_unlock_temp_labels_.push_back(label); + if (input_lock_count_temp_ < 0) { + Log("WARNING: temp input unlock at time " + std::to_string(GetRealTime()) + + " with no active lock: '" + label + "'"); + // This is to be expected since we can reset this to 0. + input_lock_count_temp_ = 0; + } + + // When lock counts get back down to zero, clear our labels since all is + // well. + if (input_lock_count_temp_ == 0) { + input_lock_temp_labels_.clear(); + input_unlock_temp_labels_.clear(); + } + } +} + +void Input::PrintLockLabels() { + std::string s = + "INPUT LOCK REPORT (time=" + std::to_string(GetRealTime()) + "):"; + int num; + + s += "\n " + std::to_string(input_lock_temp_labels_.size()) + " TEMP LOCKS:"; + num = 1; + for (auto& input_lock_temp_label : input_lock_temp_labels_) { + s += "\n " + std::to_string(num++) + ": " + input_lock_temp_label; + } + + s += "\n " + std::to_string(input_unlock_temp_labels_.size()) + + " TEMP UNLOCKS:"; + num = 1; + for (auto& input_unlock_temp_label : input_unlock_temp_labels_) { + s += "\n " + std::to_string(num++) + ": " + input_unlock_temp_label; + } + + s += "\n " + std::to_string(input_lock_permanent_labels_.size()) + + " PERMANENT LOCKS:"; + num = 1; + for (auto& input_lock_permanent_label : input_lock_permanent_labels_) { + s += "\n " + std::to_string(num++) + ": " + input_lock_permanent_label; + } + + s += "\n " + std::to_string(input_unlock_permanent_labels_.size()) + + " PERMANENT UNLOCKS:"; + num = 1; + for (auto& input_unlock_permanent_label : input_unlock_permanent_labels_) { + s += "\n " + std::to_string(num++) + ": " + input_unlock_permanent_label; + } + s += "\n " + std::to_string(recent_input_locks_unlocks_.size()) + + " MOST RECENT LOCKS:"; + num = 1; + for (auto& recent_input_locks_unlock : recent_input_locks_unlocks_) { + s += "\n " + std::to_string(num++) + ": " + recent_input_locks_unlock; + } + + Log(s); +} + +void Input::ProcessStressTesting(int player_count) { + assert(InMainThread()); + assert(player_count >= 0); + + millisecs_t time = GetRealTime(); + + // FIXME: If we don't check for stress_test_last_leave_time_ we totally + // confuse the game.. need to be able to survive that. + + // Kill some off if we have too many. + while (static_cast(test_inputs_.size()) > player_count) { + delete test_inputs_.front(); + test_inputs_.pop_front(); + } + + // If we have less than full test-inputs, add one randomly. + if (static_cast(test_inputs_.size()) < player_count + && ((rand() % 1000 < 10))) { // NOLINT + test_inputs_.push_back(new TestInput()); + } + + // Every so often lets kill the oldest one off. + if (explicit_bool(true)) { + if (test_inputs_.size() > 0 && (rand() % 2000 < 3)) { // NOLINT + stress_test_last_leave_time_ = time; + + // Usually do oldest; sometimes newest. + if (rand() % 5 == 0) { // NOLINT + delete test_inputs_.back(); + test_inputs_.pop_back(); + } else { + delete test_inputs_.front(); + test_inputs_.pop_front(); + } + } + } + + if (time - stress_test_time_ > 1000) { + stress_test_time_ = time; // reset.. + for (auto& test_input : test_inputs_) { + (*test_input).Reset(); + } + } + while (stress_test_time_ < time) { + stress_test_time_++; + for (auto& test_input : test_inputs_) { + (*test_input).Process(stress_test_time_); + } + } +} + +void Input::HandleBackPress(bool from_toolbar) { + assert(InGameThread()); + + // This can come through occasionally before our UI is up it seems? + // Just ignore in that case. + if (g_ui == nullptr || g_ui->screen_root_widget() == nullptr + || g_ui->overlay_root_widget() == nullptr + || g_ui->root_widget() == nullptr) { + // Log("HandleBackPress() called without main UI"); + return; + } + + // If there's no dialogs/windows up, ask for a menu (owned by the touch-input + // if available). + if (g_ui->screen_root_widget()->GetChildCount() == 0 + && g_ui->overlay_root_widget()->GetChildCount() == 0) { + g_game->PushMainMenuPressCall(touch_input_); + } else { + if (from_toolbar) { + // NOTE - this means the toolbar back button can never apply to overlay + // widgets; is that ok?... + g_ui->screen_root_widget()->HandleMessage( + WidgetMessage(WidgetMessage::Type::kCancel)); + } else { + g_ui->root_widget()->HandleMessage( + WidgetMessage(WidgetMessage::Type::kCancel)); + } + } +} + +void Input::PushTextInputEvent(const std::string& text) { + g_game->PushCall([this, text] { + ResetIdleTime(); + + // Ignore if input is locked. + if (IsInputLocked()) { + return; + } + if (g_app_globals->console != nullptr + && g_app_globals->console->HandleTextEditing(text)) { + return; + } + g_ui->SendWidgetMessage(WidgetMessage(WidgetMessage::Type::kTextInput, + nullptr, 0, 0, 0, 0, text.c_str())); + }); +} + +auto Input::PushJoystickEvent(const SDL_Event& event, InputDevice* input_device) + -> void { + g_game->PushCall([this, event, input_device] { + HandleJoystickEvent(event, input_device); + }); +} + +void Input::HandleJoystickEvent(const SDL_Event& event, + InputDevice* input_device) { + assert(InGameThread()); + assert(input_device); + + if (ShouldCompletelyIgnoreInputDevice(input_device)) { + return; + } + if (IsInputLocked()) { + return; + } + + // Make note that we're not idle. + ResetIdleTime(); + + // And that this particular device isn't idle either. + input_device->UpdateLastInputTime(); + + // Give Python a crack at it for captures, etc. + if (g_python->HandleJoystickEvent(event, input_device)) { + return; + } + + input_device->HandleSDLEvent(&event); +} + +void Input::PushKeyPressEvent(const SDL_Keysym& keysym) { + g_game->PushCall([this, keysym] { HandleKeyPress(&keysym); }); +} + +void Input::PushKeyReleaseEvent(const SDL_Keysym& keysym) { + g_game->PushCall([this, keysym] { HandleKeyRelease(&keysym); }); +} + +void Input::HandleKeyPress(const SDL_Keysym* keysym) { + assert(InGameThread()); + + ResetIdleTime(); + + // Ignore all key presses if input is locked. + if (IsInputLocked()) { + return; + } + + // Give Python a crack at it for captures, etc. + if (g_python->HandleKeyPressEvent(*keysym)) { + return; + } + + // Regardless of what else we do, keep track of mod key states. + // (for things like manual camera moves. For individual key presses + // ideally we should use the modifiers bundled with the key presses) + UpdateModKeyStates(keysym, true); + + bool repeat_press; + if (keys_held_.count(keysym->sym) != 0) { + repeat_press = true; + } else { + repeat_press = false; + keys_held_.insert(keysym->sym); + } + + // Mobile-specific stuff. + if (g_buildconfig.ostype_ios_tvos() || g_buildconfig.ostype_android()) { + switch (keysym->sym) { + // FIXME: See if this stuff is still necessary. Was this perhaps + // specifically to support the console? + case SDLK_DELETE: + case SDLK_RETURN: + case SDLK_KP_ENTER: + case SDLK_BACKSPACE: { + // FIXME: I don't remember what this was put here for, but now that we + // have hardware keyboards it crashes text fields by sending them a + // TEXT_INPUT message with no string.. I made them resistant to that + // case but wondering if we can take this out?... + g_ui->SendWidgetMessage( + WidgetMessage(WidgetMessage::Type::kTextInput, keysym)); + break; + } + default: + break; + } + } + + // A few things that apply only to non-mobile. + if (!g_buildconfig.ostype_ios_tvos() && !g_buildconfig.ostype_android()) { + // Command-F or Control-F toggles full-screen. + if (!repeat_press && keysym->sym == SDLK_f + && ((keysym->mod & KMOD_CTRL) || (keysym->mod & KMOD_GUI))) { // NOLINT + g_python->obj(Python::ObjID::kToggleFullscreenCall).Call(); + return; + } + + // Command-Q or Control-Q quits. + if (!repeat_press && keysym->sym == SDLK_q + && ((keysym->mod & KMOD_CTRL) || (keysym->mod & KMOD_GUI))) { // NOLINT + g_game->PushConfirmQuitCall(); + } + } + if (g_app_globals->console != nullptr + && g_app_globals->console->HandleKeyPress(keysym)) { + return; + } + + bool handled = false; + + // None of the following stuff accepts key repeats. + if (!repeat_press) { + switch (keysym->sym) { + // Menu button on android/etc. pops up the menu. + case SDLK_MENU: { + if (g_ui && g_ui->screen_root_widget()) { + // If there's no dialogs/windows up, ask for a menu (owned by the + // touch-screen if available). + if (g_ui->screen_root_widget()->GetChildCount() == 0) { + g_game->PushMainMenuPressCall(touch_input_); + } + } + handled = true; + break; + } + case SDLK_AC_BACK: { + HandleBackPress(false); + handled = true; + break; + } + + case SDLK_EQUALS: + case SDLK_PLUS: + g_game->ChangeGameSpeed(1); + handled = true; + break; + + case SDLK_MINUS: + g_game->ChangeGameSpeed(-1); + handled = true; + break; + + case SDLK_F5: { + g_ui->root_ui()->TogglePartyWindowKeyPress(); + handled = true; + break; + } + + case SDLK_F7: + g_game->PushToggleManualCameraCall(); + handled = true; + break; + + case SDLK_F8: + g_game->PushToggleDebugInfoDisplayCall(); + handled = true; + break; + + case SDLK_F9: + g_python->PushObjCall(Python::ObjID::kLanguageTestToggleCall); + handled = true; + break; + + case SDLK_F10: + g_game->PushToggleCollisionGeometryDisplayCall(); + handled = true; + break; + + case SDLK_ESCAPE: + + if (g_ui && g_ui->screen_root_widget() && g_ui->root_widget() + && g_ui->overlay_root_widget()) { + // If there's no dialogs/windows up, ask for a menu owned by the + // keyboard. + if (g_ui->screen_root_widget()->GetChildCount() == 0 + && g_ui->overlay_root_widget()->GetChildCount() == 0) { + if (keyboard_input_) { + g_game->PushMainMenuPressCall(keyboard_input_); + } + } else { + // Ok there's a UI up.. send along a cancel message. + g_ui->root_widget()->HandleMessage( + WidgetMessage(WidgetMessage::Type::kCancel)); + } + } + handled = true; + break; + + default: + break; + } + } + + // If we haven't claimed it, pass it along as potential player/widget input. + if (!handled) { + if (keyboard_input_) { + keyboard_input_->HandleKey(keysym, repeat_press, true); + } + } +} + +void Input::HandleKeyRelease(const SDL_Keysym* keysym) { + assert(InGameThread()); + + // Note: we want to let these through even if input is locked. + + ResetIdleTime(); + + // Give Python a crack at it for captures, etc. + if (g_python->HandleKeyReleaseEvent(*keysym)) { + return; + } + + // Regardless of what else we do, keep track of mod key states. + // (for things like manual camera moves. For individual key presses + // ideally we should use the modifiers bundled with the key presses) + UpdateModKeyStates(keysym, false); + + // In some cases we may receive duplicate key-release events + // (if a keyboard reset was run it deals out key releases but then the + // keyboard driver issues them as well) + if (keys_held_.count(keysym->sym) == 0) { + return; + } + + keys_held_.erase(keysym->sym); + + if (IsInputLocked()) { + return; + } + + bool handled = false; + + if (g_app_globals->console != nullptr + && g_app_globals->console->HandleKeyRelease(keysym)) { + handled = true; + } + + // If we haven't claimed it, pass it along as potential player input. + if (!handled) { + if (keyboard_input_) { + keyboard_input_->HandleKey(keysym, false, false); + } + } +} + +auto Input::UpdateModKeyStates(const SDL_Keysym* keysym, bool press) -> void { + switch (keysym->sym) { + case SDLK_LCTRL: + case SDLK_RCTRL: { + if (Camera* c = g_graphics->camera()) { + c->set_ctrl_down(press); + } + break; + } + case SDLK_LALT: + case SDLK_RALT: { + if (Camera* c = g_graphics->camera()) { + c->set_alt_down(press); + } + break; + } + case SDLK_LGUI: + case SDLK_RGUI: { + if (Camera* c = g_graphics->camera()) { + c->set_cmd_down(press); + } + break; + } + default: + break; + } +} + +auto Input::PushMouseScrollEvent(const Vector2f& amount) -> void { + g_game->PushCall([this, amount] { HandleMouseScroll(amount); }); +} + +auto Input::HandleMouseScroll(const Vector2f& amount) -> void { + assert(InGameThread()); + if (IsInputLocked()) { + return; + } + ResetIdleTime(); + + Widget* root_widget = g_ui->root_widget(); + if (std::abs(amount.y) > 0.0001f && root_widget) { + root_widget->HandleMessage(WidgetMessage(WidgetMessage::Type::kMouseWheel, + nullptr, cursor_pos_x_, + cursor_pos_y_, amount.y)); + } + if (std::abs(amount.x) > 0.0001f && root_widget) { + root_widget->HandleMessage(WidgetMessage(WidgetMessage::Type::kMouseWheelH, + nullptr, cursor_pos_x_, + cursor_pos_y_, amount.x)); + } + mouse_move_count_++; + + Camera* camera = g_graphics->camera(); + if (camera) { + if (camera->manual()) { + camera->ManualHandleMouseWheel(0.005f * amount.y); + } + } +} + +auto Input::PushSmoothMouseScrollEvent(const Vector2f& velocity, bool momentum) + -> void { + g_game->PushCall([this, velocity, momentum] { + HandleSmoothMouseScroll(velocity, momentum); + }); +} + +auto Input::HandleSmoothMouseScroll(const Vector2f& velocity, bool momentum) + -> void { + assert(InGameThread()); + if (IsInputLocked()) { + return; + } + ResetIdleTime(); + + bool handled = false; + Widget* root_widget = g_ui->root_widget(); + if (root_widget) { + handled = root_widget->HandleMessage( + WidgetMessage(WidgetMessage::Type::kMouseWheelVelocity, nullptr, + cursor_pos_x_, cursor_pos_y_, velocity.y, momentum)); + root_widget->HandleMessage( + WidgetMessage(WidgetMessage::Type::kMouseWheelVelocityH, nullptr, + cursor_pos_x_, cursor_pos_y_, velocity.x, momentum)); + } + last_mouse_move_time_ = GetRealTime(); + mouse_move_count_++; + + Camera* camera = g_graphics->camera(); + if (!handled && camera) { + if (camera->manual()) { + camera->ManualHandleMouseWheel(-0.25f * velocity.y); + } + } +} + +auto Input::PushMouseMotionEvent(const Vector2f& position) -> void { + g_game->PushCall([this, position] { HandleMouseMotion(position); }); +} + +auto Input::HandleMouseMotion(const Vector2f& position) -> void { + assert(g_graphics); + assert(InGameThread()); + ResetIdleTime(); + + float old_cursor_pos_x = cursor_pos_x_; + float old_cursor_pos_y = cursor_pos_y_; + + // Convert normalized view coords to our virtual ones. + cursor_pos_x_ = g_graphics->PixelToVirtualX( + position.x * g_graphics->screen_pixel_width()); + cursor_pos_y_ = g_graphics->PixelToVirtualY( + position.y * g_graphics->screen_pixel_height()); + + last_mouse_move_time_ = GetRealTime(); + mouse_move_count_++; + + bool handled2{}; + + // If we have a touch-input in editing mode, pass along events to it. + // (it usually handles its own events but here we want it to play nice + // with stuff under it by blocking touches, etc) + if (touch_input_ && touch_input_->editing()) { + touch_input_->HandleTouchMoved(reinterpret_cast(1), cursor_pos_x_, + cursor_pos_y_); + } + + // UI interaction. + Widget* root_widget = g_ui->root_widget(); + if (root_widget && !IsInputLocked()) + handled2 = root_widget->HandleMessage( + WidgetMessage(WidgetMessage::Type::kMouseMove, nullptr, cursor_pos_x_, + cursor_pos_y_)); + + // Manual camera motion. + Camera* camera = g_graphics->camera(); + if (!handled2 && camera && camera->manual()) { + float move_h = + (cursor_pos_x_ - old_cursor_pos_x) / g_graphics->screen_virtual_width(); + float move_v = + (cursor_pos_y_ - old_cursor_pos_y) / g_graphics->screen_virtual_width(); + camera->ManualHandleMouseMove(move_h, move_v); + } + + g_ui->root_ui()->HandleMouseMotion(cursor_pos_x_, cursor_pos_y_); +} + +auto Input::PushMouseDownEvent(int button, const Vector2f& position) -> void { + g_game->PushCall( + [this, button, position] { HandleMouseDown(button, position); }); +} + +auto Input::HandleMouseDown(int button, const Vector2f& position) -> void { + assert(g_graphics); + assert(InGameThread()); + + if (IsInputLocked()) { + return; + } + + if (g_ui == nullptr || g_ui->screen_root_widget() == nullptr) { + return; + } + + ResetIdleTime(); + + last_mouse_move_time_ = GetRealTime(); + mouse_move_count_++; + + // printf("Mouse down at %f %f\n", position.x, position.y); + + // Convert normalized view coords to our virtual ones. + cursor_pos_x_ = g_graphics->PixelToVirtualX( + position.x * g_graphics->screen_pixel_width()); + cursor_pos_y_ = g_graphics->PixelToVirtualY( + position.y * g_graphics->screen_pixel_height()); + + millisecs_t click_time = GetRealTime(); + bool double_click = (click_time - last_click_time_ <= double_click_time_); + last_click_time_ = click_time; + + bool handled2 = false; + Widget* root_widget = g_ui->root_widget(); + + // If we have a touch-input in editing mode, pass along events to it. + // (it usually handles its own events but here we want it to play nice + // with stuff under it by blocking touches, etc) + if (touch_input_ && touch_input_->editing()) { + handled2 = touch_input_->HandleTouchDown(reinterpret_cast(1), + cursor_pos_x_, cursor_pos_y_); + } + + if (!handled2) { + if (g_ui->root_ui()->HandleMouseButtonDown(cursor_pos_x_, cursor_pos_y_)) { + handled2 = true; + } + } + + if (root_widget && !handled2) { + handled2 = root_widget->HandleMessage( + WidgetMessage(WidgetMessage::Type::kMouseDown, nullptr, cursor_pos_x_, + cursor_pos_y_, double_click ? 2 : 1)); + } + + // Manual camera input. + Camera* camera = g_graphics->camera(); + if (!handled2 && camera) { + switch (button) { + case SDL_BUTTON_LEFT: + camera->set_mouse_left_down(true); + break; + case SDL_BUTTON_RIGHT: + camera->set_mouse_right_down(true); + break; + case SDL_BUTTON_MIDDLE: + camera->set_mouse_middle_down(true); + break; + default: + break; + } + camera->UpdateManualMode(); + } +} + +auto Input::PushMouseUpEvent(int button, const Vector2f& position) -> void { + g_game->PushCall( + [this, button, position] { HandleMouseUp(button, position); }); +} + +auto Input::HandleMouseUp(int button, const Vector2f& position) -> void { + assert(InGameThread()); + ResetIdleTime(); + + // Convert normalized view coords to our virtual ones. + cursor_pos_x_ = g_graphics->PixelToVirtualX( + position.x * g_graphics->screen_pixel_width()); + cursor_pos_y_ = g_graphics->PixelToVirtualY( + position.y * g_graphics->screen_pixel_height()); + + bool handled2{}; + + // If we have a touch-input in editing mode, pass along events to it. + // (it usually handles its own events but here we want it to play nice + // with stuff under it by blocking touches, etc) + if (touch_input_ && touch_input_->editing()) { + touch_input_->HandleTouchUp(reinterpret_cast(1), cursor_pos_x_, + cursor_pos_y_); + } + + Widget* root_widget = g_ui->root_widget(); + if (root_widget) + handled2 = root_widget->HandleMessage(WidgetMessage( + WidgetMessage::Type::kMouseUp, nullptr, cursor_pos_x_, cursor_pos_y_)); + Camera* camera = g_graphics->camera(); + if (!handled2 && camera) { + switch (button) { + case SDL_BUTTON_LEFT: + camera->set_mouse_left_down(false); + break; + case SDL_BUTTON_RIGHT: + camera->set_mouse_right_down(false); + break; + case SDL_BUTTON_MIDDLE: + camera->set_mouse_middle_down(false); + break; + default: + break; + } + camera->UpdateManualMode(); + } + g_ui->root_ui()->HandleMouseButtonUp(cursor_pos_x_, cursor_pos_y_); +} + +void Input::PushTouchEvent(const TouchEvent& e) { + g_game->PushCall([e, this] { HandleTouchEvent(e); }); +} + +void Input::HandleTouchEvent(const TouchEvent& e) { + assert(InGameThread()); + assert(g_graphics); + + if (IsInputLocked()) { + return; + } + + ResetIdleTime(); + + // float x = e.x; + // float y = e.y; + + if (g_buildconfig.ostype_ios_tvos()) { + printf("FIXME: update touch handling\n"); + } + + float x = g_graphics->PixelToVirtualX(e.x * g_graphics->screen_pixel_width()); + float y = + g_graphics->PixelToVirtualY(e.y * g_graphics->screen_pixel_height()); + + if (e.overall) { + // Sanity test: if the OS tells us that this is the beginning of an, + // overall multitouch gesture, it should always be winding up as our + // single_touch_. + if (e.type == TouchEvent::Type::kDown && single_touch_ != nullptr) { + BA_LOG_ONCE("Got touch labeled first but will not be our single."); + } + + // Also: if the OS tells us that this is the end of an overall multi-touch + // gesture, it should mean that our single_touch_ has ended or will be. + if ((e.type == TouchEvent::Type::kUp + || e.type == TouchEvent::Type::kCanceled) + && single_touch_ != nullptr && single_touch_ != e.touch) { + BA_LOG_ONCE("Last touch coming up is not single touch!"); + } + } + + // We keep track of one 'single' touch which we pass along as + // mouse events which covers most UI stuff. + if (e.type == TouchEvent::Type::kDown && single_touch_ == nullptr) { + single_touch_ = e.touch; + HandleMouseDown(SDL_BUTTON_LEFT, Vector2f(e.x, e.y)); + } + + if (e.type == TouchEvent::Type::kMoved && e.touch == single_touch_) { + HandleMouseMotion(Vector2f(e.x, e.y)); + } + + // Currently just applying touch-cancel the same as touch-up here; + // perhaps should be smarter in the future. + if ((e.type == TouchEvent::Type::kUp || e.type == TouchEvent::Type::kCanceled) + && (e.touch == single_touch_ || e.overall)) { + single_touch_ = nullptr; + HandleMouseUp(SDL_BUTTON_LEFT, Vector2f(e.x, e.y)); + } + + // If we've got a touch input device, forward events along to it. + if (touch_input_) { + touch_input_->HandleTouchEvent(e.type, e.touch, x, y); + } +} + +void Input::ResetJoyStickHeldButtons() { + for (auto&& i : input_devices_) { + if (i.exists()) { + i->ResetHeldStates(); + } + } +} + +// Send key-ups for any currently-held keys. +void Input::ResetKeyboardHeldKeys() { + assert(InGameThread()); + if (!HeadlessMode()) { + // Synthesize key-ups for all our held keys. + while (!keys_held_.empty()) { + SDL_Keysym k; + memset(&k, 0, sizeof(k)); + k.sym = (SDL_Keycode)(*keys_held_.begin()); + HandleKeyRelease(&k); + } + } +} + +void Input::Draw(FrameDef* frame_def) { + // Draw touch input visual guides. + if (touch_input_) { + touch_input_->Draw(frame_def); + } +} + +auto Input::IsCursorVisible() const -> bool { + assert(InGameThread()); + if (!g_ui) { + return false; + } + ContainerWidget* screen_root_widget = g_ui->screen_root_widget(); + + // Keeps mouse hidden to start with.. + if (mouse_move_count_ < 2) { + return false; + } + bool val; + + // Show our cursor if any dialogs/windows are up or else if its been + // moved very recently. + if (screen_root_widget && screen_root_widget->GetChildCount() > 0) { + val = (GetRealTime() - last_mouse_move_time_ < 5000); + } else { + val = (GetRealTime() - last_mouse_move_time_ < 1000); + } + return val; +} + +// The following was pulled from sdl2 +#if BA_SDL2_BUILD || BA_MINSDL_BUILD + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" + +static char* UCS4ToUTF8(uint32_t ch, char* dst) { + auto* p = reinterpret_cast(dst); + if (ch <= 0x7F) { + *p = static_cast(ch); + ++dst; + } else if (ch <= 0x7FF) { + p[0] = static_cast(0xC0 | static_cast((ch >> 6) & 0x1F)); + p[1] = static_cast(0x80 | static_cast(ch & 0x3F)); + dst += 2; + } else if (ch <= 0xFFFF) { + p[0] = static_cast(0xE0 | static_cast((ch >> 12) & 0x0F)); + p[1] = static_cast(0x80 | static_cast((ch >> 6) & 0x3F)); + p[2] = static_cast(0x80 | static_cast(ch & 0x3F)); + dst += 3; + } else if (ch <= 0x1FFFFF) { + p[0] = static_cast(0xF0 | static_cast((ch >> 18) & 0x07)); + p[1] = static_cast(0x80 | static_cast((ch >> 12) & 0x3F)); + p[2] = static_cast(0x80 | static_cast((ch >> 6) & 0x3F)); + p[3] = static_cast(0x80 | static_cast(ch & 0x3F)); + dst += 4; + } else if (ch <= 0x3FFFFFF) { + p[0] = static_cast(0xF8 | static_cast((ch >> 24) & 0x03)); + p[1] = static_cast(0x80 | static_cast((ch >> 18) & 0x3F)); + p[2] = static_cast(0x80 | static_cast((ch >> 12) & 0x3F)); + p[3] = static_cast(0x80 | static_cast((ch >> 6) & 0x3F)); + p[4] = static_cast(0x80 | static_cast(ch & 0x3F)); + dst += 5; + } else { + p[0] = static_cast(0xFC | static_cast((ch >> 30) & 0x01)); + p[1] = static_cast(0x80 | static_cast((ch >> 24) & 0x3F)); + p[2] = static_cast(0x80 | static_cast((ch >> 18) & 0x3F)); + p[3] = static_cast(0x80 | static_cast((ch >> 12) & 0x3F)); + p[4] = static_cast(0x80 | static_cast((ch >> 6) & 0x3F)); + p[5] = static_cast(0x80 | static_cast(ch & 0x3F)); + dst += 6; + } + return dst; +} +#pragma clang diagnostic pop + +const char* GetScancodeName(SDL_Scancode scancode) { + const char* name; + if (static_cast(scancode) < SDL_SCANCODE_UNKNOWN + || scancode >= SDL_NUM_SCANCODES) { + BA_LOG_ONCE("GetScancodeName passed invalid scancode " + + std::to_string(static_cast(scancode))); + return ""; + } + + name = scancode_names[scancode]; + if (name) + return name; + else + return ""; +} + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +auto Input::GetKeyName(int keycode) -> std::string { + SDL_Keycode key{keycode}; + static char name[8]; + char* end; + + if (key & SDLK_SCANCODE_MASK) { + return GetScancodeName((SDL_Scancode)(key & ~SDLK_SCANCODE_MASK)); + } + + switch (key) { + case SDLK_RETURN: + return GetScancodeName(SDL_SCANCODE_RETURN); + case SDLK_ESCAPE: + return GetScancodeName(SDL_SCANCODE_ESCAPE); + case SDLK_BACKSPACE: + return GetScancodeName(SDL_SCANCODE_BACKSPACE); + case SDLK_TAB: + return GetScancodeName(SDL_SCANCODE_TAB); + case SDLK_SPACE: + return GetScancodeName(SDL_SCANCODE_SPACE); + case SDLK_DELETE: + return GetScancodeName(SDL_SCANCODE_DELETE); + default: + /* Unaccented letter keys on latin keyboards are normally + labeled in upper case (and probably on others like Greek or + Cyrillic too, so if you happen to know for sure, please + adapt this). */ + if (key >= 'a' && key <= 'z') { + key -= 32; + } + + end = UCS4ToUTF8(static_cast(key), name); + *end = '\0'; + return name; + } +} +#pragma clang diagnostic pop +#endif // BA_SDL2_BUILD || BA_MINSDL_BUILD + +} // namespace ballistica diff --git a/src/ballistica/input/input.h b/src/ballistica/input/input.h new file mode 100644 index 00000000..c58ebd8e --- /dev/null +++ b/src/ballistica/input/input.h @@ -0,0 +1,195 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_INPUT_INPUT_H_ +#define BALLISTICA_INPUT_INPUT_H_ + +#include +#include +#include +#include +#include + +#include "ballistica/core/object.h" + +namespace ballistica { + +/// Class for managing input; owned and used by the game thread. +class Input { + public: + Input(); + virtual ~Input(); + + // Add an input device. Must be called from the game thread; otherwise use + // PushAddInputDeviceCall. + auto AddInputDevice(InputDevice* input, bool standard_message) -> void; + + // Removes a previously-added input-device. Must be called from the + // game thread; otherwise use PushRemoveInputDeviceCall. + auto RemoveInputDevice(InputDevice* input, bool standard_message) -> void; + + // Given a device name and persistent identifier for it, returns a device or + // nullptr note that this can return hidden devices (ones the user has flagged + // as totally-ignored, etc). + auto GetInputDevice(const std::string& name, const std::string& persistent_id) + -> InputDevice*; + + // Return a device by id. + // Note that this can return hidden devices (ones the user has flagged as + // totally-ignored, etc). + auto GetInputDevice(int id) -> InputDevice*; + + // Return all input devices with this name. + auto GetInputDevicesWithName(const std::string& name) + -> std::vector; + + auto Reset() -> void; + auto LockAllInput(bool permanent, const std::string& label) -> void; + auto UnlockAllInput(bool permanent, const std::string& label) -> void; + auto IsInputLocked() const -> bool { + return ((input_lock_count_temp_ > 0) || (input_lock_count_permanent_ > 0)); + } + auto cursor_pos_x() const -> float { return cursor_pos_x_; } + auto cursor_pos_y() const -> float { return cursor_pos_y_; } + + auto IsCursorVisible() const -> bool; + + // Return list of gamepads that are user-visible and able to be configured. + auto GetConfigurableGamePads() -> std::vector; + + // Reset all keyboard keys to a non-held state and deal out associated + // messages - used before switching keyboard focus to a new context + // so that the old one is not stuck with a held key forever. + auto ResetKeyboardHeldKeys() -> void; + + auto GetKeyName(int keycode) -> std::string; + + // Same idea but for joysticks. + auto ResetJoyStickHeldButtons() -> void; + auto ShouldCompletelyIgnoreInputDevice(InputDevice* input_device) -> bool; + auto ApplyAppConfig() -> void; + + auto touch_input() const -> TouchInput* { return touch_input_; } + auto have_non_touch_inputs() const -> bool { return have_non_touch_inputs_; } + auto have_button_using_inputs() const -> bool { + return have_button_using_inputs_; + } + auto have_start_activated_default_button_inputs() const -> bool { + return have_start_activated_default_button_inputs_; + } + auto Draw(FrameDef* frame_def) -> void; + + // Get the total idle time for the system. + // FIXME - should better coordinate this with InputDevice::getLastUsedTime(). + auto GetIdleTime() const -> millisecs_t; + + // Should be called whenever user-input of some form comes through. + auto ResetIdleTime() -> void { last_input_time_ = GetRealTime(); } + + // Should be called regularly to update button repeats, etc. + auto Update() -> void; + + // returns true if more than one non-keyboard device has been active recently + // ..this is used to determine whether we need to have strict menu ownership + // (otherwise menu use would be chaotic with 8 players connected) + auto HaveManyLocalActiveInputDevices() -> bool { + return GetLocalActiveInputDeviceCount() > 1; + } + auto GetLocalActiveInputDeviceCount() -> int; + + // Return true if there are any joysticks with players attached. + // The touch-input uses this to warn the user if it looks like they + // may have accidentally joined the game using a controller touchpad or + // something. + auto HaveControllerWithPlayer() -> bool; + auto HaveRemoteAppController() -> bool; + auto HandleBackPress(bool from_toolbar) -> void; + auto ProcessStressTesting(int player_count) -> void; + auto keyboard_input() const -> KeyboardInput* { return keyboard_input_; } + auto keyboard_input_2() const -> KeyboardInput* { return keyboard_input_2_; } + auto CreateTouchInput() -> void; + + auto PushTextInputEvent(const std::string& text) -> void; + auto PushKeyPressEvent(const SDL_Keysym& keysym) -> void; + auto PushKeyReleaseEvent(const SDL_Keysym& keysym) -> void; + auto PushMouseDownEvent(int button, const Vector2f& position) -> void; + auto PushMouseUpEvent(int button, const Vector2f& position) -> void; + auto PushMouseMotionEvent(const Vector2f& position) -> void; + auto PushSmoothMouseScrollEvent(const Vector2f& velocity, bool momentum) + -> void; + auto PushMouseScrollEvent(const Vector2f& amount) -> void; + auto PushJoystickEvent(const SDL_Event& event, InputDevice* input_device) + -> void; + auto PushAddInputDeviceCall(InputDevice* input_device, bool standard_message) + -> void; + auto PushRemoveInputDeviceCall(InputDevice* input_device, + bool standard_message) -> void; + auto PushTouchEvent(const TouchEvent& touch_event) -> void; + auto PushDestroyKeyboardInputDevices() -> void; + auto PushCreateKeyboardInputDevices() -> void; + + private: + auto UpdateInputDeviceCounts() -> void; + auto GetNewNumberedIdentifier(const std::string& name, + const std::string& identifier) -> int; + auto UpdateEnabledControllerSubsystems() -> void; + auto AnnounceConnects() -> void; + auto AnnounceDisconnects() -> void; + auto HandleKeyPress(const SDL_Keysym* keysym) -> void; + auto HandleKeyRelease(const SDL_Keysym* keysym) -> void; + auto HandleMouseMotion(const Vector2f& position) -> void; + auto HandleMouseDown(int button, const Vector2f& position) -> void; + auto HandleMouseUp(int button, const Vector2f& position) -> void; + auto HandleMouseScroll(const Vector2f& amount) -> void; + auto HandleSmoothMouseScroll(const Vector2f& velocity, bool momentum) -> void; + auto HandleJoystickEvent(const SDL_Event& event, InputDevice* input_device) + -> void; + auto HandleTouchEvent(const TouchEvent& e) -> void; + auto ShowStandardInputDeviceConnectedMessage(InputDevice* j) -> void; + auto ShowStandardInputDeviceDisconnectedMessage(InputDevice* j) -> void; + auto PrintLockLabels() -> void; + auto UpdateModKeyStates(const SDL_Keysym* keysym, bool press) -> void; + auto CreateKeyboardInputDevices() -> void; + auto DestroyKeyboardInputDevices() -> void; + int local_active_input_device_count_{}; + millisecs_t last_have_many_local_active_input_devices_check_time_{}; + std::map > reserved_identifiers_; + int max_controller_count_so_far_{}; + std::list newly_connected_controllers_; + std::list newly_disconnected_controllers_; + int connect_print_timer_id_{}; + int disconnect_print_timer_id_{}; + bool have_button_using_inputs_{}; + bool have_start_activated_default_button_inputs_{}; + bool have_non_touch_inputs_{}; + float cursor_pos_x_{}; + float cursor_pos_y_{}; + millisecs_t last_input_time_{}; + millisecs_t last_click_time_{}; + millisecs_t double_click_time_{200}; + millisecs_t last_mouse_move_time_{}; + int mouse_move_count_{}; + std::vector > input_devices_; + KeyboardInput* keyboard_input_ = nullptr; + KeyboardInput* keyboard_input_2_ = nullptr; + TouchInput* touch_input_ = nullptr; + int input_lock_count_temp_ = 0; + int input_lock_count_permanent_ = 0; + std::list input_lock_temp_labels_; + std::list input_unlock_temp_labels_; + std::list input_lock_permanent_labels_; + std::list input_unlock_permanent_labels_; + std::list recent_input_locks_unlocks_; + std::set keys_held_; + millisecs_t last_input_device_count_update_time_ = 0; + millisecs_t last_input_temp_lock_time_ = 0; + bool ignore_mfi_controllers_ = false; + bool ignore_sdl_controllers_ = false; + std::list test_inputs_; + millisecs_t stress_test_time_{}; + millisecs_t stress_test_last_leave_time_{}; + void* single_touch_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_INPUT_INPUT_H_ diff --git a/src/ballistica/input/remote_app.cc b/src/ballistica/input/remote_app.cc new file mode 100644 index 00000000..23a82b19 --- /dev/null +++ b/src/ballistica/input/remote_app.cc @@ -0,0 +1,547 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/input/remote_app.h" + +#if BA_OSTYPE_WINDOWS +#include +#pragma comment(lib, "ws2_32.lib") +#else +#include +#include +#include + +#include +#endif + +#include +#include + +#include "ballistica/app/app_globals.h" +#include "ballistica/ballistica.h" +#include "ballistica/game/game.h" +#include "ballistica/generic/utils.h" +#include "ballistica/input/input.h" +#include "ballistica/math/vector3f.h" +#include "ballistica/media/media.h" +#include "ballistica/networking/network_reader.h" +#include "ballistica/platform/min_sdl.h" +#include "ballistica/platform/platform.h" + +namespace ballistica { + +// Just used privately by the remote-server machinery. +enum class RemoteAppServer::RemoteEventType { + kDPadH, + kDPadV, + kPunchPress, + kPunchRelease, + kJumpPress, + kJumpRelease, + kThrowPress, + kThrowRelease, + kBombPress, + kBombRelease, + kMenu, // Old. + kMenuPress, + kMenuRelease, + kHoldPositionPress, + kHoldPositionRelease, + kRunPress, + kRunRelease +}; + +RemoteAppServer::RemoteAppServer() = default; + +RemoteAppServer::~RemoteAppServer() = default; + +void RemoteAppServer::HandleData(int socket, uint8_t* buffer, size_t amt, + struct sockaddr* addr, size_t addr_len) { + if (amt == 0) { + return; + } + switch (buffer[0]) { + case BA_PACKET_REMOTE_GAME_QUERY: { + // Ship them a response packet with our name. + char msg[256]; + std::string name = g_platform->GetDeviceName(); + msg[0] = BA_PACKET_REMOTE_GAME_RESPONSE; + strncpy(msg + 1, name.c_str(), sizeof(msg) - 1); + msg[255] = 0; + size_t msg_len = 1 + strlen(msg + 1); + + // This needs to be locked during any sd changes/writes. + std::lock_guard lock(g_network_reader->sd_mutex()); + sendto(socket, msg, static_cast_check_fit(msg_len), + 0, addr, static_cast(addr_len)); + + // if (result != msg_len) { + // Hmm; ive seen errno 64 (network down) and 65 (package not installed) + // here, but I don't know what we could do in response. Just gonna ignore + // them. + // } + break; + } + case BA_PACKET_REMOTE_ID_REQUEST: { + if (amt < 5 || amt > 127) { + BA_LOG_ONCE( + "Error: received invalid BA_PACKET_REMOTE_ID_REQUEST of length " + + std::to_string(amt)); + break; + } + + // Second byte is protocol version. + int protocol_version = buffer[1]; + + // Make sure we speak the same language. + if (protocol_version != kRemoteAppProtocolVersion) { + uint8_t data[2] = { + BA_PACKET_REMOTE_DISCONNECT, + static_cast_check_fit(RemoteError::kVersionMismatch)}; + + // This needs to be locked during any sd changes/writes. + std::lock_guard lock(g_network_reader->sd_mutex()); + sendto(socket, reinterpret_cast(data), sizeof(data), 0, addr, + static_cast(addr_len)); + break; + } + + // Third and fourth bytes are request id. + int16_t request_id; + memcpy(&request_id, buffer + 2, sizeof(request_id)); + + // This is now a protocol-version-request. It used to be an address index + // from the other end so probably will be a value between 1 and 5 or so on + // older builds. + int protocol_request = buffer[4]; + int protocol_response = + protocol_request; // Old default was to return same value. + + // If they sent 50, it means they want protocol v2 (24 bit states). + // In that case we return 100 to say 'ok, we support that version'. + // Note to self (years later): please explain to me why I did this. + bool using_v2 = (protocol_request == 50); + if (using_v2) { + protocol_response = 100; + } + + // Remaining bytes are name (up to 100 bytes). + char name[101]; + assert(amt >= 5); + size_t name_len = amt - 5; + if (name_len > 100) { + name_len = 100; + } + strncpy(name, reinterpret_cast(buffer) + 5, name_len); + name[name_len] = 0; + int client_id = GetClient(request_id, addr, static_cast(addr_len), + name, using_v2); + + // If we've got a slot for this client, tell them what their id is. + if (client_id != -1) { + uint8_t data[3] = {BA_PACKET_REMOTE_ID_RESPONSE, + static_cast(client_id), + static_cast(protocol_response)}; + + // This needs to be locked during any sd changes/writes. + std::lock_guard lock(g_network_reader->sd_mutex()); + sendto(socket, reinterpret_cast(data), sizeof(data), 0, addr, + static_cast(addr_len)); + } else { + // No room. + uint8_t data[2] = {BA_PACKET_REMOTE_DISCONNECT, + static_cast_check_fit( + RemoteError::kNotAcceptingConnections)}; + + // This needs to be locked during any sd changes/writes. + std::lock_guard lock(g_network_reader->sd_mutex()); + sendto(socket, reinterpret_cast(data), sizeof(data), 0, addr, + static_cast(addr_len)); + } + break; + } + case BA_PACKET_REMOTE_DISCONNECT: { + // They told us they're leaving.. free up their slot. + if (amt == 2 && buffer[1] < kMaxRemoteAppClients) { + int joystickID = buffer[1]; + + // Tell our delegate to kill its local joystick. + RemoteAppClient* client = clients_ + joystickID; + if (clients_[joystickID].in_use) { + // Print 'Billy Bob's iPhone Disconnected'. + char m[256]; + snprintf(m, sizeof(m), "%s", client->display_name); + + // Replace ${CONTROLLER} with it in our message. + std::string s = + g_game->GetResourceString("controllerDisconnectedText"); + Utils::StringReplaceOne(&s, "${CONTROLLER}", m); + g_game->PushScreenMessage(s, Vector3f(1, 1, 1)); + g_game->PushPlaySoundCall(SystemSoundID::kCorkPop); + g_input->PushRemoveInputDeviceCall(client->joystick_, false); + client->joystick_ = nullptr; + client->in_use = false; + client->name[0] = 0; + } + + // Send an ack. + uint8_t data[1] = {BA_PACKET_REMOTE_DISCONNECT_ACK}; + + // This needs to be locked during any sd changes/writes. + std::lock_guard lock(g_network_reader->sd_mutex()); + sendto(socket, reinterpret_cast(data), 1, 0, addr, + static_cast(addr_len)); + } + break; + } + case BA_PACKET_REMOTE_STATE2: { + // Has to be at least 4 bytes. + // (msg-type, joystick-id, state-count, starting-state-id) + if (amt < 4) { + break; + } + + uint8_t joystick_id = buffer[1]; + uint8_t state_count = buffer[2]; + uint8_t state_id = buffer[3]; + + // If its not an active joystick, let them know they're not playing + // (this can happen if they time-out but still try to keep talking to us). + if (!clients_[joystick_id].in_use) { + uint8_t data[2] = { + BA_PACKET_REMOTE_DISCONNECT, + static_cast_check_fit(RemoteError::kNotConnected)}; + + // This needs to be locked during any sd changes/writes. + std::lock_guard lock(g_network_reader->sd_mutex()); + sendto(socket, reinterpret_cast(data), sizeof(data), 0, addr, + static_cast(addr_len)); + break; + } + + // Each state is 2 bytes. So make sure our length adds up. + if (amt != 4 + state_count * 3) { + BA_LOG_ONCE("Error: Invalid state packet"); + return; + } + RemoteAppClient* client = clients_ + joystick_id; + + // Take note that we heard from them. + client->last_contact_time = GetRealTime(); + + // Ok now iterate. + uint8_t* val = buffer + 4; + for (int i = 0; i < state_count; i++) { + // If we're behind enough, just skip ahead to here + uint8_t diff = state_id - client->next_state_id; + + // Diffs close to 255 are probably just retransmitted states we just + // looked at. + if (diff > 10 && diff < 200) { + client->next_state_id = state_id; + } + + // If this is the next state we're looking for, apply it. + if (client->next_state_id == state_id) { + uint32_t last_state = client->state; + uint32_t state = val[0] + (val[1] << 8u) + (val[2] << 16u); + uint32_t h_raw = (state >> 8u) & 0xFFu; + uint32_t v_raw = (state >> 16u) & 0xFFu; + uint32_t h_raw_last = (last_state >> 8u) & 0xFFu; + uint32_t v_raw_last = (last_state >> 16u) & 0xFFu; + float dpad_h, dpad_v; + dpad_h = -1.0f + 2.0f * (h_raw / 255.0f); + dpad_v = -1.0f + 2.0f * (v_raw / 255.0f); + float last_dpad_h, last_dpad_v; + last_dpad_h = -1.0f + 2.0f * (h_raw_last / 255.0f); + last_dpad_v = -1.0f + 2.0f * (v_raw_last / 255.0f); + + // Process this first since it can affect how other events are + // handled. + if ((last_state & kRemoteStateHoldPosition) + && !(state & kRemoteStateHoldPosition)) { + HandleRemoteEvent(client, RemoteEventType::kHoldPositionRelease); + } else if (!(last_state & kRemoteStateHoldPosition) + && (state & kRemoteStateHoldPosition)) { + HandleRemoteEvent(client, RemoteEventType::kHoldPositionPress); + } + if (dpad_h != last_dpad_h) { + HandleRemoteFloatEvent(client, RemoteEventType::kDPadH, dpad_h); + } + if (dpad_v != last_dpad_v) { + HandleRemoteFloatEvent(client, RemoteEventType::kDPadV, dpad_v); + } + if ((last_state & kRemoteStateBomb) && !(state & kRemoteStateBomb)) { + HandleRemoteEvent(client, RemoteEventType::kBombRelease); + } else if (!(last_state & kRemoteStateBomb) + && (state & kRemoteStateBomb)) { + HandleRemoteEvent(client, RemoteEventType::kBombPress); + } + if ((last_state & kRemoteStateJump) && !(state & kRemoteStateJump)) { + HandleRemoteEvent(client, RemoteEventType::kJumpRelease); + } else if (!(last_state & kRemoteStateJump) + && (state & kRemoteStateJump)) { + HandleRemoteEvent(client, RemoteEventType::kJumpPress); + } + if ((last_state & kRemoteStatePunch) + && !(state & kRemoteStatePunch)) { + HandleRemoteEvent(client, RemoteEventType::kPunchRelease); + } else if (!(last_state & kRemoteStatePunch) + && (state & kRemoteStatePunch)) { + HandleRemoteEvent(client, RemoteEventType::kPunchPress); + } + if ((last_state & kRemoteStateThrow) + && !(state & kRemoteStateThrow)) { + HandleRemoteEvent(client, RemoteEventType::kThrowRelease); + } else if (!(last_state & kRemoteStateThrow) + && (state & kRemoteStateThrow)) { + HandleRemoteEvent(client, RemoteEventType::kThrowPress); + } + if ((last_state & kRemoteStateMenu) && !(state & kRemoteStateMenu)) { + HandleRemoteEvent(client, RemoteEventType::kMenuRelease); + } else if (!(last_state & kRemoteStateMenu) + && (state & kRemoteStateMenu)) { + HandleRemoteEvent(client, RemoteEventType::kMenuPress); + } + if ((last_state & kRemoteStateRun) && !(state & kRemoteStateRun)) { + HandleRemoteEvent(client, RemoteEventType::kRunRelease); + } else if (!(last_state & kRemoteStateRun) + && (state & kRemoteStateRun)) { + HandleRemoteEvent(client, RemoteEventType::kRunPress); + } + client->state = state; + client->next_state_id++; + } + state_id++; + val += 3; + } + + // Ok now send an ack with the state ID we're looking for next. + uint8_t data[2] = {BA_PACKET_REMOTE_STATE_ACK, client->next_state_id}; + + // This needs to be locked during any sd changes/writes. + std::lock_guard lock(g_network_reader->sd_mutex()); + sendto(socket, reinterpret_cast(data), 2, 0, addr, + static_cast(addr_len)); + + break; + } + case BA_PACKET_REMOTE_STATE: { + // Has to be at least 4 bytes. + // (msg-type, joystick-id, state-count, starting-state-id) + if (amt < 4) break; + + // This was used on older versions of the remote app; no longer supported. + { + uint8_t data[2] = { + BA_PACKET_REMOTE_DISCONNECT, + static_cast_check_fit(RemoteError::kVersionMismatch)}; + + // This needs to be locked during any sd changes/writes. + std::lock_guard lock(g_network_reader->sd_mutex()); + sendto(socket, reinterpret_cast(data), sizeof(data), 0, addr, + static_cast(addr_len)); + break; + } + } + default: + break; + } +} + +auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr, + size_t addr_len, const char* name, + bool using_v2) -> int { + // If we're not accepting connections at all, reject 'em. + if (!g_app_globals->remote_server_accepting_connections) { + return -1; + } + + // First see if we have an id for this name. (we no longer care about + // request-id). + for (int i = 0; i < kMaxRemoteAppClients; i++) { + // We now have clients include unique IDs in their name so we simply compare + // to name. + // This allows re-establishing connections and whatnot. + if (strcmp(name, "") != 0 && !strcmp(name, clients_[i].name)) { + // If the request id has changed it means that they rebooted their remote + // or something; lets take note of that. + if (clients_[i].request_id != request_id) { + clients_[i].request_id = request_id; + + // Print 'Billy Bob's iPhone Reconnected'. + char m[256]; + snprintf(m, sizeof(m), "%s", clients_[i].display_name); + + // Replace ${CONTROLLER} with it in our message. + std::string s = g_game->GetResourceString("controllerReconnectedText"); + Utils::StringReplaceOne(&s, "${CONTROLLER}", m); + g_game->PushScreenMessage(s, Vector3f(1, 1, 1)); + g_game->PushPlaySoundCall(SystemSoundID::kGunCock); + } + clients_[i].in_use = true; + return i; + } + } + + // Don't reuse a slot for 5 seconds. + millisecs_t cooldown_time = GetRealTime() - 5000; + + // Ok, not there already.. now look for a non-taken one and return that. + for (int i = 0; i < kMaxRemoteAppClients; i++) { + if (!clients_[i].in_use && clients_[i].last_contact_time < cooldown_time) { + // Ok lets fill out the client. + clients_[i].in_use = true; + clients_[i].next_state_id = 0; + clients_[i].state = 0; + BA_PRECONDITION(addr_len <= sizeof(clients_[i].address)); + memcpy(&clients_[i].address, addr, addr_len); + clients_[i].address_size = addr_len; + strncpy(clients_[i].name, name, sizeof(clients_[i].name)); + clients_[i].name[sizeof(clients_[i].name) - 1] = + 0; // in case we overflowed + + // Display-name is simply name with everything after '#' removed (which is + // only used as a unique ID). + strcpy(clients_[i].display_name, clients_[i].name); // NOLINT + char* c = strchr(clients_[i].display_name, '#'); + if (c) *c = 0; + clients_[i].last_contact_time = GetRealTime(); + clients_[i].request_id = request_id; + char m[256]; + + // Print 'Billy Bob's iPhone Connected' + snprintf(m, sizeof(m), "%s", clients_[i].display_name); + + // Replace ${CONTROLLER} with it in our message. + std::string s = g_game->GetResourceString("controllerConnectedText"); + Utils::StringReplaceOne(&s, "${CONTROLLER}", m); + g_game->PushScreenMessage(s, Vector3f(1, 1, 1)); + g_game->PushPlaySoundCall(SystemSoundID::kGunCock); + std::string utf8 = Utils::GetValidUTF8(clients_[i].display_name, "rsgc1"); + clients_[i].joystick_ = Object::NewDeferred( + -1, // not an sdl joystick + "RemoteApp: " + + utf8, // device name (we now incorporate the name they send us) + false, // don't allow configuring + using_v2); // calibrate in v2; not v1 + clients_[i].joystick_->set_is_remote_app(true); + + // If they name they supplied was <= 10 characters, use it as our default + // character name. + if (Utils::UTF8StringLength(utf8.c_str()) <= 10) { + clients_[i].joystick_->set_custom_default_player_name(utf8); + } + assert(g_game); + g_input->PushAddInputDeviceCall(clients_[i].joystick_, false); + return i; + } + } + // Sorry no room. + return -1; +} + +void RemoteAppServer::HandleRemoteEvent(RemoteAppClient* client, + RemoteEventType b) { + // Ok we got some data from the remote. + // All we have to do is translate it into an SDL event and feed it to our + // manual joystick we made. + SDL_Event e{}; + bool send = true; + switch (b) { + case RemoteEventType::kBombPress: + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 2; + break; + case RemoteEventType::kBombRelease: + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 2; + break; + + // Could actually call the menu func directly, + // but it should be fine to just emulate it via the button-press. + case RemoteEventType::kMenu: + case RemoteEventType::kMenuPress: + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 5; + break; + case RemoteEventType::kMenuRelease: + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 5; + break; + case RemoteEventType::kJumpPress: + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 0; + break; + case RemoteEventType::kJumpRelease: + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 0; + break; + case RemoteEventType::kThrowPress: + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 3; + break; + case RemoteEventType::kThrowRelease: + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 3; + break; + case RemoteEventType::kPunchPress: + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 1; + break; + case RemoteEventType::kPunchRelease: + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 1; + break; + case RemoteEventType::kHoldPositionPress: + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 25; + break; + case RemoteEventType::kHoldPositionRelease: + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 25; + break; + case RemoteEventType::kRunPress: + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 64; + break; + case RemoteEventType::kRunRelease: + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 64; + break; + + default: + send = false; + break; + } + if (send) { + assert(g_game); + g_input->PushJoystickEvent(e, client->joystick_); + } +} + +void RemoteAppServer::HandleRemoteFloatEvent(RemoteAppClient* client, + RemoteEventType b, float val) { + SDL_Event e{}; + bool send = true; + switch (b) { + case RemoteEventType::kDPadH: + e.type = SDL_JOYAXISMOTION; + e.jaxis.axis = 0; + e.jaxis.value = static_cast(32767 * val); + break; + case RemoteEventType::kDPadV: + e.type = SDL_JOYAXISMOTION; + e.jaxis.axis = 1; + e.jaxis.value = static_cast(32767 * val); + break; + default: + send = false; + break; + } + if (send) { + assert(g_game); + g_input->PushJoystickEvent(e, client->joystick_); + } +} + +} // namespace ballistica diff --git a/src/ballistica/input/remote_app.h b/src/ballistica/input/remote_app.h new file mode 100644 index 00000000..62a48625 --- /dev/null +++ b/src/ballistica/input/remote_app.h @@ -0,0 +1,67 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_INPUT_REMOTE_APP_H_ +#define BALLISTICA_INPUT_REMOTE_APP_H_ + +#include "ballistica/input/device/joystick.h" +#include "ballistica/networking/networking.h" +#include "ballistica/networking/networking_sys.h" + +namespace ballistica { + +constexpr int kRemoteAppProtocolVersion = 121; +constexpr int kMaxRemoteAppClients = 24; + +enum class RemoteError { + kVersionMismatch, + kGameShuttingDown, + kNotAcceptingConnections, + kNotConnected +}; + +enum RemoteState { + kRemoteStateMenu = 1u << 0u, + kRemoteStateJump = 1u << 1u, + kRemoteStatePunch = 1u << 2u, + kRemoteStateThrow = 1u << 3u, + kRemoteStateBomb = 1u << 4u, + kRemoteStateRun = 1u << 5u, + kRemoteStateFly = 1u << 6u, + kRemoteStateHoldPosition = 1u << 7u, + // Second byte is d-pad h-value and third byte is d-pad v-value. +}; + +class RemoteAppServer { + public: + RemoteAppServer(); + ~RemoteAppServer(); + + // Feed the remote-server with data coming in to a listening udp socket. + void HandleData(int sd, uint8_t* data, size_t data_size, + struct sockaddr* from, size_t from_size); + + private: + auto GetClient(int request_id, struct sockaddr* addr, size_t addr_len, + const char* name, bool using_v2) -> int; + struct RemoteAppClient { + bool in_use{}; + int request_id{}; + char name[101]{}; + char display_name[101]{}; + struct sockaddr_storage address {}; + size_t address_size{}; + millisecs_t last_contact_time{}; + uint8_t next_state_id{}; + uint32_t state{}; + Joystick* joystick_{}; + }; + RemoteAppClient clients_[kMaxRemoteAppClients]{}; + enum class RemoteEventType; + void HandleRemoteEvent(RemoteAppClient* client, RemoteEventType msg); + void HandleRemoteFloatEvent(RemoteAppClient* client, RemoteEventType msg, + float val); +}; + +} // namespace ballistica + +#endif // BALLISTICA_INPUT_REMOTE_APP_H_ diff --git a/src/ballistica/input/std_input_module.cc b/src/ballistica/input/std_input_module.cc new file mode 100644 index 00000000..19ac6e39 --- /dev/null +++ b/src/ballistica/input/std_input_module.cc @@ -0,0 +1,82 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/input/std_input_module.h" + +#include +#include + +#include "ballistica/app/app_globals.h" +#include "ballistica/game/game.h" +#include "ballistica/platform/platform.h" + +namespace ballistica { + +StdInputModule::StdInputModule(Thread* thread) : Module("stdin", thread) { + assert(g_std_input_module == nullptr); + g_std_input_module = this; +} + +StdInputModule::~StdInputModule() = default; + +void StdInputModule::PushBeginReadCall() { + PushCall([this] { + bool stdin_is_terminal = IsStdinATerminal(); + + while (true) { + // Print a prompt if we're a tty. + // We send this to the game thread so it happens AFTER the + // results of the last script-command message we may have just sent. + if (stdin_is_terminal) { + g_game->PushCall([] { + if (!g_app_globals->shutting_down) { + printf(">>> "); + fflush(stdout); + } + }); + } + + // Was using getline, but switched to + // new fgets based approach (more portable). + // Ideally at some point we can wire up to the Python api to get behavior + // more like the actual Python command line. + char buffer[4096]; + char* val = fgets(buffer, sizeof(buffer), stdin); + if (val) { + int last_char = static_cast(strlen(buffer) - 1); + + // Clip off our last char if its a newline (just to keep things tidier). + if (last_char >= 0 && buffer[last_char] == '\n') { + buffer[last_char] = 0; + } + g_game->PushStdinScriptCommand(buffer); + } else { + // At the moment we bail on any read error. + if (feof(stdin)) { + if (stdin_is_terminal) { + // Ok this is strange: on windows consoles, it seems that Ctrl-C in + // a terminal immediately closes our stdin even if we catch the + // interrupt, and then our python interrupt handler runs a moment + // later. This means we wind up telling the user that EOF was + // reached and they should Ctrl-C to quit right after they've hit + // Ctrl-C to quit. To hopefully avoid this, let's hold off on the + // print for a second and see if a shutdown has begun first. + // (or, more likely, just never print because the app has exited). + if (g_buildconfig.windows_console_build()) { + Platform::SleepMS(250); + } + if (!g_app_globals->shutting_down) { + printf("Stdin EOF reached. Use Ctrl-C to quit.\n"); + fflush(stdout); + } + } + } else { + Log("StdInputModule got non-eof error reading stdin: " + + std::to_string(ferror(stdin))); + } + break; + } + } + }); +} + +} // namespace ballistica diff --git a/src/ballistica/input/std_input_module.h b/src/ballistica/input/std_input_module.h new file mode 100644 index 00000000..74a1e66d --- /dev/null +++ b/src/ballistica/input/std_input_module.h @@ -0,0 +1,19 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_INPUT_STD_INPUT_MODULE_H_ +#define BALLISTICA_INPUT_STD_INPUT_MODULE_H_ + +#include "ballistica/core/module.h" + +namespace ballistica { + +class StdInputModule : public Module { + public: + explicit StdInputModule(Thread* thread); + ~StdInputModule() override; + void PushBeginReadCall(); +}; + +} // namespace ballistica + +#endif // BALLISTICA_INPUT_STD_INPUT_MODULE_H_ diff --git a/src/ballistica/math/matrix44f.cc b/src/ballistica/math/matrix44f.cc new file mode 100644 index 00000000..fcdb3e10 --- /dev/null +++ b/src/ballistica/math/matrix44f.cc @@ -0,0 +1,340 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/math/matrix44f.h" + +namespace ballistica { + +auto Matrix44fRotate(const Vector3f& axis, float angle) -> Matrix44f { + // Page 466, Graphics Gems + + Matrix44f rotate{kMatrix44fIdentity}; + + float s = sinf(-angle * kPiDeg); + float c = cosf(-angle * kPiDeg); + float t = 1 - c; + + Vector3f ax = axis / sqrtf(axis.LengthSquared()); + + float x = ax.x; + float y = ax.y; + float z = ax.z; + + rotate.set(0, 0, t * x * x + c); + rotate.set(1, 0, t * y * x + s * z); + rotate.set(2, 0, t * z * x - s * y); + + rotate.set(0, 1, t * x * y - s * z); + rotate.set(1, 1, t * y * y + c); + rotate.set(2, 1, t * z * y + s * x); + + rotate.set(0, 2, t * x * z + s * y); + rotate.set(1, 2, t * y * z - s * x); + rotate.set(2, 2, t * z * z + c); + + return rotate; +} + +auto Matrix44fRotate(float azimuth, float elevation) -> Matrix44f { + Matrix44f rotate{kMatrix44fIdentity}; + + float ca = cosf(azimuth * kPiDeg); + float sa = sinf(azimuth * kPiDeg); + float cb = cosf(elevation * kPiDeg); + float sb = sinf(elevation * kPiDeg); + + rotate.set(0, 0, cb); + rotate.set(1, 0, 0); + rotate.set(2, 0, -sb); + + rotate.set(0, 1, -sa * sb); + rotate.set(1, 1, ca); + rotate.set(2, 1, -sa * cb); + + rotate.set(0, 2, ca * sb); + rotate.set(1, 2, sa); + rotate.set(2, 2, ca * cb); + + return rotate; +} + +auto Matrix44fOrient(const Vector3f& x, const Vector3f& y, const Vector3f& z) + -> Matrix44f { + Matrix44f orient{kMatrix44fIdentity}; + + orient.set(0, 0, x.x); + orient.set(0, 1, x.y); + orient.set(0, 2, x.z); + + orient.set(1, 0, y.x); + orient.set(1, 1, y.y); + orient.set(1, 2, y.z); + + orient.set(2, 0, z.x); + orient.set(2, 1, z.y); + orient.set(2, 2, z.z); + + return orient; +} + +auto Matrix44fOrient(const Vector3f& direction, const Vector3f& up) + -> Matrix44f { + assert(direction.LengthSquared() > 0.0f); + assert(up.LengthSquared() > 0.0f); + + Vector3f d(direction); + d.Normalize(); + + Vector3f u(up); + u.Normalize(); + + return Matrix44fOrient(Vector3f::Cross(u, d), u, d); +} + +auto Matrix44fFrustum(float left, float right, float bottom, float top, + float nearval, float farval) -> Matrix44f { + float m_persp[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -1, 0, 0, 0, 0}; + m_persp[0] = (2.0f * nearval) / (right - left); + m_persp[5] = (2.0f * nearval) / (top - bottom); + m_persp[10] = -(farval + nearval) / (farval - nearval); + m_persp[8] = -(right + left) / (right - left); + m_persp[9] = (top + bottom) / (top - bottom); + m_persp[10] = -(farval + nearval) / (farval - nearval); + m_persp[14] = -2 * farval * nearval / (farval - nearval); + return Matrix44f(m_persp); +} + +auto Matrix44f::Transpose() const -> Matrix44f { + Matrix44f tmp; // NOLINT: uninitialized on purpose. + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + tmp.set(j, i, get(i, j)); + } + } + return tmp; +} + +// +// From Mesa-2.2\src\glu\project.c +// + +// +// Compute the inverse of a 4x4 matrix. Contributed by scotter@lafn.org +// + +static void InvertMatrixGeneral(const float* mat, float* out) { +/* NB. OpenGL Matrices are COLUMN major. */ +#define MAT(m, r, c) (mat)[(c)*4 + (r)] + +/* Here's some shorthand converting standard (row,column) to index. */ +#define m11 MAT(mat, 0, 0) +#define m12 MAT(mat, 0, 1) +#define m13 MAT(mat, 0, 2) +#define m14 MAT(mat, 0, 3) +#define m21 MAT(mat, 1, 0) +#define m22 MAT(mat, 1, 1) +#define m23 MAT(mat, 1, 2) +#define m24 MAT(mat, 1, 3) +#define m31 MAT(mat, 2, 0) +#define m32 MAT(mat, 2, 1) +#define m33 MAT(mat, 2, 2) +#define m34 MAT(mat, 2, 3) +#define m41 MAT(mat, 3, 0) +#define m42 MAT(mat, 3, 1) +#define m43 MAT(mat, 3, 2) +#define m44 MAT(mat, 3, 3) + + float det; + float d12, d13, d23, d24, d34, d41; + float tmp[16]; /* Allow out == in. */ + + /* Inverse = adjoint / det. (See linear algebra texts.)*/ + + /* pre-compute 2x2 dets for last two rows when computing */ + /* cofnodes of first two rows. */ + d12 = (m31 * m42 - m41 * m32); + d13 = (m31 * m43 - m41 * m33); + d23 = (m32 * m43 - m42 * m33); + d24 = (m32 * m44 - m42 * m34); + d34 = (m33 * m44 - m43 * m34); + d41 = (m34 * m41 - m44 * m31); + + tmp[0] = (m22 * d34 - m23 * d24 + m24 * d23); + tmp[1] = -(m21 * d34 + m23 * d41 + m24 * d13); + tmp[2] = (m21 * d24 + m22 * d41 + m24 * d12); + tmp[3] = -(m21 * d23 - m22 * d13 + m23 * d12); + + /* Compute determinant as early as possible using these cofnodes. */ + det = m11 * tmp[0] + m12 * tmp[1] + m13 * tmp[2] + m14 * tmp[3]; + + /* Run singularity test. */ + if (det == 0.0f) { + memcpy(out, kMatrix44fIdentity.m, 16 * sizeof(float)); + } else { + float invDet = 1.0f / det; + /* Compute rest of inverse. */ + tmp[0] *= invDet; + tmp[1] *= invDet; + tmp[2] *= invDet; + tmp[3] *= invDet; + + tmp[4] = -(m12 * d34 - m13 * d24 + m14 * d23) * invDet; + tmp[5] = (m11 * d34 + m13 * d41 + m14 * d13) * invDet; + tmp[6] = -(m11 * d24 + m12 * d41 + m14 * d12) * invDet; + tmp[7] = (m11 * d23 - m12 * d13 + m13 * d12) * invDet; + + /* Pre-compute 2x2 dets for first two rows when computing */ + /* cofnodes of last two rows. */ + d12 = m11 * m22 - m21 * m12; + d13 = m11 * m23 - m21 * m13; + d23 = m12 * m23 - m22 * m13; + d24 = m12 * m24 - m22 * m14; + d34 = m13 * m24 - m23 * m14; + d41 = m14 * m21 - m24 * m11; + + tmp[8] = (m42 * d34 - m43 * d24 + m44 * d23) * invDet; + tmp[9] = -(m41 * d34 + m43 * d41 + m44 * d13) * invDet; + tmp[10] = (m41 * d24 + m42 * d41 + m44 * d12) * invDet; + tmp[11] = -(m41 * d23 - m42 * d13 + m43 * d12) * invDet; + tmp[12] = -(m32 * d34 - m33 * d24 + m34 * d23) * invDet; + tmp[13] = (m31 * d34 + m33 * d41 + m34 * d13) * invDet; + tmp[14] = -(m31 * d24 + m32 * d41 + m34 * d12) * invDet; + tmp[15] = (m31 * d23 - m32 * d13 + m33 * d12) * invDet; + + memcpy(out, tmp, 16 * sizeof(float)); + } + +#undef m11 +#undef m12 +#undef m13 +#undef m14 +#undef m21 +#undef m22 +#undef m23 +#undef m24 +#undef m31 +#undef m32 +#undef m33 +#undef m34 +#undef m41 +#undef m42 +#undef m43 +#undef m44 +#undef MAT +} + +// +// Invert matrix mat. This algorithm contributed by Stephane Rehel +// +// + +static void InvertMatrix(const float* mat, float* out) { +/* NB. OpenGL Matrices are COLUMN major. */ +#define MAT(mat, r, c) (mat)[(c)*4 + (r)] + +/* Here's some shorthand converting standard (row,column) to index. */ +#define m11 MAT(mat, 0, 0) +#define m12 MAT(mat, 0, 1) +#define m13 MAT(mat, 0, 2) +#define m14 MAT(mat, 0, 3) +#define m21 MAT(mat, 1, 0) +#define m22 MAT(mat, 1, 1) +#define m23 MAT(mat, 1, 2) +#define m24 MAT(mat, 1, 3) +#define m31 MAT(mat, 2, 0) +#define m32 MAT(mat, 2, 1) +#define m33 MAT(mat, 2, 2) +#define m34 MAT(mat, 2, 3) +#define m41 MAT(mat, 3, 0) +#define m42 MAT(mat, 3, 1) +#define m43 MAT(mat, 3, 2) +#define m44 MAT(mat, 3, 3) + + float det; + float tmp[16]; /* Allow out == in. */ + + if (m41 != 0.f || m42 != 0.f || m43 != 0.f || m44 != 1.f) { + InvertMatrixGeneral(mat, out); + return; + } + + /* Inverse = adjoint / det. (See linear algebra texts.)*/ + + tmp[0] = m22 * m33 - m23 * m32; + tmp[1] = m23 * m31 - m21 * m33; + tmp[2] = m21 * m32 - m22 * m31; + + /* Compute determinant as early as possible using these cofnodes. */ + det = m11 * tmp[0] + m12 * tmp[1] + m13 * tmp[2]; + + /* Run singularity test. */ + if (det == 0.0f) { + memcpy(out, kMatrix44fIdentity.m, 16 * sizeof(float)); + } else { + float d12, d13, d23, d24, d34, d41; + float im11, im12, im13, im14; + + det = 1.f / det; + + /* Compute rest of inverse. */ + tmp[0] *= det; + tmp[1] *= det; + tmp[2] *= det; + tmp[3] = 0.f; + + im11 = m11 * det; + im12 = m12 * det; + im13 = m13 * det; + im14 = m14 * det; + tmp[4] = im13 * m32 - im12 * m33; + tmp[5] = im11 * m33 - im13 * m31; + tmp[6] = im12 * m31 - im11 * m32; + tmp[7] = 0.f; + + /* Pre-compute 2x2 dets for first two rows when computing */ + /* cofnodes of last two rows. */ + d12 = im11 * m22 - m21 * im12; + d13 = im11 * m23 - m21 * im13; + d23 = im12 * m23 - m22 * im13; + d24 = im12 * m24 - m22 * im14; + d34 = im13 * m24 - m23 * im14; + d41 = im14 * m21 - m24 * im11; + + tmp[8] = d23; + tmp[9] = -d13; + tmp[10] = d12; + tmp[11] = 0.f; + + tmp[12] = -(m32 * d34 - m33 * d24 + m34 * d23); + tmp[13] = (m31 * d34 + m33 * d41 + m34 * d13); + tmp[14] = -(m31 * d24 + m32 * d41 + m34 * d12); + tmp[15] = 1.f; + + memcpy(out, tmp, 16 * sizeof(float)); + } + +#undef m11 +#undef m12 +#undef m13 +#undef m14 +#undef m21 +#undef m22 +#undef m23 +#undef m24 +#undef m31 +#undef m32 +#undef m33 +#undef m34 +#undef m41 +#undef m42 +#undef m43 +#undef m44 +#undef MAT +} + +auto Matrix44f::Inverse() const -> Matrix44f { + Matrix44f inv; // NOLINT: uninitialized on purpose + InvertMatrix(m, inv.m); + return inv; +} + +} // namespace ballistica diff --git a/src/ballistica/math/matrix44f.h b/src/ballistica/math/matrix44f.h new file mode 100644 index 00000000..b90d2f2e --- /dev/null +++ b/src/ballistica/math/matrix44f.h @@ -0,0 +1,201 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MATH_MATRIX44F_H_ +#define BALLISTICA_MATH_MATRIX44F_H_ + +#include // for memcpy + +#include "ballistica/math/vector3f.h" + +namespace ballistica { + +class Matrix44f { + public: + // Stop linter complaints about uninitialized members. + // (It seems our union might be confusing it, and in some cases + // we want to leave things uninited). +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-use-equals-default" +#pragma ide diagnostic ignored "cppcoreguidelines-pro-type-member-init" + + // Default constructor (leaves uninitialized) + Matrix44f() = default; + + Matrix44f(float m00, float m01, float m02, float m03, float m10, float m11, + float m12, float m13, float m20, float m21, float m22, float m23, + float m30, float m31, float m32, float m33) + : m00(m00), + m01(m01), + m02(m02), + m03(m03), + m10(m10), + m11(m11), + m12(m12), + m13(m13), + m20(m20), + m21(m21), + m22(m22), + m23(m23), + m30(m30), + m31(m31), + m32(m32), + m33(m33) {} + + // Construct from array. + explicit Matrix44f(const float* matrix) { memcpy(m, matrix, sizeof(m)); } + + // Construct from array. + explicit Matrix44f(const double* matrix) { + float* i = m; + const double* j = matrix; + for (int k = 0; k < 16; i++, j++, k++) { + *i = static_cast(*j); + } + } + +#pragma clang diagnostic pop + + // Matrix multiplication. + auto operator*(const Matrix44f& other) const -> Matrix44f { + Matrix44f prod; // NOLINT: uninitialized on purpose. + for (int c = 0; c < 4; c++) { + for (int r = 0; r < 4; r++) { + prod.set(c, r, + get(c, 0) * other.get(0, r) + get(c, 1) * other.get(1, r) + + get(c, 2) * other.get(2, r) + + get(c, 3) * other.get(3, r)); + } + } + return prod; + } + + auto tx() const -> float { return m[12]; } + auto ty() const -> float { return m[13]; } + auto tz() const -> float { return m[14]; } + + void set_tx(float v) { m[12] = v; } + void set_ty(float v) { m[13] = v; } + void set_tz(float v) { m[14] = v; } + + auto GetTranslate() const -> Vector3f { return {tx(), ty(), tz()}; } + + auto LocalXAxis() const -> Vector3f { return {m[0], m[1], m[2]}; } + auto LocalYAxis() const -> Vector3f { return {m[4], m[5], m[6]}; } + auto LocalZAxis() const -> Vector3f { return {m[8], m[9], m[10]}; } + + // In-place matrix multiplication. + auto operator*=(const Matrix44f& other) -> Matrix44f& { + return (*this) = (*this) * other; + } + + // Matrix transformation of 3D vector. + auto operator*(const Vector3f& vec) const -> Vector3f { + float prod[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + for (int r = 0; r < 4; r++) { + for (int c = 0; c < 3; c++) prod[r] += vec.v[c] * get(c, r); + prod[r] += get(3, r); + } + float div = 1.0f / prod[3]; + return {prod[0] * div, prod[1] * div, prod[2] * div}; + } + + // Rotate/scale a 3d vector. + auto TransformAsNormal(const Vector3f& val) const -> Vector3f { + // There's probably a smarter way to do this via 3x3 matrices?.. + Matrix44f m2{*this}; + m2.set_tx(0); + m2.set_ty(0); + m2.set_tz(0); + return m2 * val; + } + + // Equality operator. + auto operator==(const Matrix44f& other) const -> bool { + return !memcmp(m, other.m, sizeof(m)); + } + + // Not-equal operator. + auto operator!=(const Matrix44f& other) const -> bool { + return memcmp(m, other.m, sizeof(m)) != 0; + } + + // Calculate matrix inverse. + auto Inverse() const -> Matrix44f; + + // Calculate matrix transpose. + auto Transpose() const -> Matrix44f; + + union { + struct { + float m00, m10, m20, m30; + float m01, m11, m21, m31; + float m02, m12, m22, m32; + float m03, m13, m23, m33; + }; + float m[16]; + }; + + void set(const int col, const int row, const float val) { + m[col * 4 + row] = val; + } + + auto get(const int col, const int row) const -> float { + return m[col * 4 + row]; + } + + auto element(const int col, const int row) -> float& { + return m[col * 4 + row]; + } +}; + +const Matrix44f kMatrix44fIdentity{1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f}; + +inline auto Matrix44fTranslate(const Vector3f& trans) -> Matrix44f { + Matrix44f translate{kMatrix44fIdentity}; + translate.set(3, 0, trans.v[0]); + translate.set(3, 1, trans.v[1]); + translate.set(3, 2, trans.v[2]); + return translate; +} + +inline auto Matrix44fTranslate(const float x, const float y, const float z) + -> Matrix44f { + Matrix44f translate{kMatrix44fIdentity}; + translate.set(3, 0, x); + translate.set(3, 1, y); + translate.set(3, 2, z); + return translate; +} + +inline auto Matrix44fScale(const float sf) -> Matrix44f { + Matrix44f mat{kMatrix44fIdentity}; + mat.set(0, 0, sf); + mat.set(1, 1, sf); + mat.set(2, 2, sf); + return mat; +} + +inline auto Matrix44fScale(const Vector3f& sf) -> Matrix44f { + Matrix44f scale{kMatrix44fIdentity}; + scale.set(0, 0, sf.v[0]); + scale.set(1, 1, sf.v[1]); + scale.set(2, 2, sf.v[2]); + return scale; +} + +auto Matrix44fRotate(const Vector3f& axis, float angle) -> Matrix44f; +auto Matrix44fRotate(float azimuth, float elevation) -> Matrix44f; +auto Matrix44fOrient(const Vector3f& x, const Vector3f& y, const Vector3f& z) + -> Matrix44f; + +// Note: direction and up need to be perpendicular and normalized here. +auto Matrix44fOrient(const Vector3f& direction, const Vector3f& up) + -> Matrix44f; +auto Matrix44fFrustum(float left, float right, float bottom, float top, + float near, float far) -> Matrix44f; + +} // namespace ballistica + +#endif // BALLISTICA_MATH_MATRIX44F_H_ diff --git a/src/ballistica/math/point2d.h b/src/ballistica/math/point2d.h new file mode 100644 index 00000000..4a0cacb4 --- /dev/null +++ b/src/ballistica/math/point2d.h @@ -0,0 +1,17 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MATH_POINT2D_H_ +#define BALLISTICA_MATH_POINT2D_H_ + +namespace ballistica { + +// 2d point; pretty barebones at the moment.. +struct Point2D { + float x, y; + Point2D() : x(0), y(0) {} + Point2D(float x_in, float y_in) : x(x_in), y(y_in) {} +}; + +} // namespace ballistica + +#endif // BALLISTICA_MATH_POINT2D_H_ diff --git a/src/ballistica/math/random.cc b/src/ballistica/math/random.cc new file mode 100644 index 00000000..5f9e531b --- /dev/null +++ b/src/ballistica/math/random.cc @@ -0,0 +1,544 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/math/random.h" + +#include +#include +#include +#include + +namespace ballistica { + +#define RAND_RANGE(min, max) \ + (min) + (static_cast(rand()) / RAND_MAX) * ((max) - (min)) // NOLINT + +class SmoothGen1D { + public: + SmoothGen1D() = default; + ~SmoothGen1D() = default; + auto GetX(int index) -> float { + Expand(static_cast(index)); + return vals_x[index]; + } + + private: + void Expand(uint32_t index) { + if (index >= vals_x.size()) { + if (vals_x.empty()) { + float initial_x = RAND_RANGE(0, 1); + root = std::make_unique(0, 1, initial_x); + vals_x.push_back(initial_x); + } + for (auto i = static_cast(vals_x.size()); i <= index; i++) { + float x; + root->GetNewValue(&x); + vals_x.push_back(x); + } + } + } + + class Node { + public: + Node(float min_x_in, float max_x_in, float first_val_x) + : min_x(min_x_in), max_x(max_x_in) { + // Store our initial value in the right section. + assert(first_val_x >= min_x && first_val_x <= max_x); + + float mid_x = min_x + (max_x - min_x) * 0.5f; + + Section q; + if (first_val_x < mid_x) { + q = kx; + } else { + q = kX; + } + + initial_x[q] = first_val_x; + int stored = 0; + for (int i = 0; i < 2; i++) { + if (i != q) { + sections[stored] = (Section)i; + stored++; + } + } + val_count = 1; + } + ~Node() = default; + void GetNewValue(float* x) { + if (val_count % 2 == 0) ResetSections(); + Section q = PullRandomSection(); + assert(val_count != 0); + + // We gen the rest of our first 2 values ourself. + if (val_count < 2) { + switch (q) { + case kx: + (*x) = initial_x[q] = + RAND_RANGE(min_x, min_x + (max_x - min_x) * 0.5f); + break; + case kX: + (*x) = initial_x[q] = + RAND_RANGE(min_x + (max_x - min_x) * 0.5f, max_x); + } + } else { + if (val_count == 2) { + // Make child nodes and feed them their initial values. + float mid_x = min_x + (max_x - min_x) * 0.5f; + children[kx] = std::make_unique(min_x, mid_x, initial_x[kx]); + children[kX] = std::make_unique(mid_x, max_x, initial_x[kX]); + } + + // After this point we let our children do the work. + children[q]->GetNewValue(x); + } + val_count++; + } + + private: + enum Section { kx, kX }; + + std::unique_ptr children[2]; + + // Pull a section and remove it from our list. + auto PullRandomSection() -> Section { + int remaining_sections = 2 - val_count % 2; + int q_picked = rand() % remaining_sections; // NOLINT + Section q_val = sections[q_picked]; + int pos_new = 0; + for (int pos_old = 0; pos_old < remaining_sections; pos_old++) { + if (pos_old != q_picked) { + sections[pos_new] = sections[pos_old]; + pos_new++; + } + } + return q_val; + } + void ResetSections() { + sections[0] = kx; + sections[1] = kX; + } + Section sections[2]{}; + float initial_x[2]{}; + float min_x, max_x; + int val_count; + }; + std::unique_ptr root; + std::vector vals_x; +}; + +class SmoothGen2D { + public: + SmoothGen2D() = default; + ~SmoothGen2D() = default; + auto GetX(int index) -> float { + Expand(static_cast(index)); + return vals_x[index]; + } + auto GetY(int index) -> float { + Expand(static_cast(index)); + return vals_y[index]; + } + + private: + void Expand(uint32_t index) { + if (index >= vals_x.size()) { + if (vals_x.empty()) { + float initial_x = RAND_RANGE(0, 1); + float initial_y = RAND_RANGE(0, 1); + root = std::make_unique(0, 0, 1, 1, initial_x, initial_y); + vals_x.push_back(initial_x); + vals_y.push_back(initial_y); + } + for (auto i = static_cast(vals_x.size()); i <= index; i++) { + float x, y; + root->GetNewValue(&x, &y); + vals_x.push_back(x); + vals_y.push_back(y); + } + } + } + + class Node { + public: + Node(float min_x_in, float min_y_in, float max_x_in, float max_y_in, + float first_val_x, float first_val_y) + : min_x(min_x_in), max_x(max_x_in), min_y(min_y_in), max_y(max_y_in) { + // Store our initial value in the right section. + assert(first_val_x >= min_x && first_val_x <= max_x); + assert(first_val_y >= min_y && first_val_y <= max_y); + + float mid_x = min_x + (max_x - min_x) * 0.5f; + float mid_y = min_y + (max_y - min_y) * 0.5f; + + Section q; + if (first_val_x < mid_x) { + if (first_val_y < mid_y) { + q = kxy; + } else { + q = kxY; + } + } else { + if (first_val_y < mid_y) { + q = kXy; + } else { + q = kXY; + } + } + initial_x[q] = first_val_x; + initial_y[q] = first_val_y; + int stored = 0; + for (int i = 0; i < 4; i++) { + if (i != q) { + sections[stored] = (Section)i; + stored++; + } + } + val_count = 1; + } + ~Node() = default; + void GetNewValue(float* x, float* y) { + if (val_count % 4 == 0) ResetSections(); + Section q = PullRandomSection(); + + assert(val_count != 0); + + // We gen the rest of our first 4 values ourself. + if (val_count < 4) { + switch (q) { + case kxy: + case kXy: + (*y) = initial_y[q] = + RAND_RANGE(min_y, min_y + (max_y - min_y) * 0.5f); + break; + case kxY: + case kXY: + (*y) = initial_y[q] = + RAND_RANGE(min_y + (max_y - min_y) * 0.5f, max_y); + } + switch (q) { + case kxy: + case kxY: + (*x) = initial_x[q] = + RAND_RANGE(min_x, min_x + (max_x - min_x) * 0.5f); + break; + case kXy: + case kXY: + (*x) = initial_x[q] = + RAND_RANGE(min_x + (max_x - min_x) * 0.5f, max_x); + } + } else { + if (val_count == 4) { + // Make child nodes and feed them their initial values. + float mid_x = min_x + (max_x - min_x) * 0.5f; + float mid_y = min_y + (max_y - min_y) * 0.5f; + children[kxy] = std::make_unique( + min_x, min_y, mid_x, mid_y, initial_x[kxy], initial_y[kxy]); + children[kXy] = std::make_unique( + mid_x, min_y, max_x, mid_y, initial_x[kXy], initial_y[kXy]); + children[kXY] = std::make_unique( + mid_x, mid_y, max_x, max_y, initial_x[kXY], initial_y[kXY]); + children[kxY] = std::make_unique( + min_x, mid_y, mid_x, max_y, initial_x[kxY], initial_y[kxY]); + } + + // After this point we let our children do the work. + children[q]->GetNewValue(x, y); + } + val_count++; + } + + private: + enum Section { kxy, kXy, kxY, kXY }; + + std::unique_ptr children[4]; + + // Pull a section and remove it from our list. + auto PullRandomSection() -> Section { + int remaining_sections = 4 - val_count % 4; + int q_picked = rand() % remaining_sections; // NOLINT + Section q_val = sections[q_picked]; + int pos_new = 0; + for (int pos_old = 0; pos_old < remaining_sections; pos_old++) { + if (pos_old != q_picked) { + sections[pos_new] = sections[pos_old]; + pos_new++; + } + } + return q_val; + } + void ResetSections() { + sections[0] = kxy; + sections[1] = kXy; + sections[2] = kXY; + sections[3] = kxY; + } + Section sections[4]{}; + float initial_x[4]{}; + float initial_y[4]{}; + float min_x, min_y, max_x, max_y; + int val_count; + }; + std::unique_ptr root; + std::vector vals_x; + std::vector vals_y; +}; + +class SmoothGen3D { + public: + SmoothGen3D() = default; + ~SmoothGen3D() = default; + auto GetX(int index) -> float { + Expand(static_cast(index)); + return vals_x[index]; + } + auto GetY(int index) -> float { + Expand(static_cast(index)); + return vals_y[index]; + } + auto GetZ(int index) -> float { + Expand(static_cast(index)); + return vals_z[index]; + } + + private: + void Expand(uint32_t index) { + if (index >= vals_x.size()) { + if (vals_x.empty()) { + float initial_x = RAND_RANGE(0, 1); + float initial_y = RAND_RANGE(0, 1); + float initial_z = RAND_RANGE(0, 1); + root = std::make_unique(0, 0, 0, 1, 1, 1, initial_x, initial_y, + initial_z); + vals_x.push_back(initial_x); + vals_y.push_back(initial_y); + vals_z.push_back(initial_z); + } + for (auto i = static_cast(vals_x.size()); i <= index; i++) { + float x, y, z; + root->GetNewValue(&x, &y, &z); + vals_x.push_back(x); + vals_y.push_back(y); + vals_z.push_back(z); + } + } + } + + class Node { + public: + Node(float min_x_in, float min_y_in, float min_z_in, float max_x_in, + float max_y_in, float max_z_in, float first_val_x, float first_val_y, + float first_val_z) + : min_x(min_x_in), + max_x(max_x_in), + min_y(min_y_in), + max_y(max_y_in), + min_z(min_z_in), + max_z(max_z_in) { + // store our initial value in the right section... + assert(first_val_x >= min_x && first_val_x <= max_x); + assert(first_val_y >= min_y && first_val_y <= max_y); + assert(first_val_z >= min_z && first_val_z <= max_z); + + float mid_x = min_x + (max_x - min_x) * 0.5f; + float mid_y = min_y + (max_y - min_y) * 0.5f; + float mid_z = min_z + (max_z - min_z) * 0.5f; + + Section q; + if (first_val_x < mid_x) { + if (first_val_y < mid_y) { + if (first_val_z < mid_z) { + q = kxyz; + } else { + q = kxyZ; + } + } else { + if (first_val_z < mid_z) { + q = kxYz; + } else { + q = kxYZ; + } + } + } else { + if (first_val_y < mid_y) { + if (first_val_z < mid_z) { + q = kXyz; + } else { + q = kXyZ; + } + } else { + if (first_val_z < mid_z) { + q = kXYz; + } else { + q = kXYZ; + } + } + } + + initial_x[q] = first_val_x; + initial_y[q] = first_val_y; + initial_z[q] = first_val_z; + int stored = 0; + for (int i = 0; i < 8; i++) { + if (i != q) { + sections[stored] = (Section)i; + stored++; + } + } + val_count = 1; + } + ~Node() = default; + void GetNewValue(float* x, float* y, float* z) { + if (val_count % 8 == 0) ResetSections(); + + Section q = PullRandomSection(); + + assert(val_count != 0); + + // We gen the rest of our first 8 values ourself. + if (val_count < 8) { + switch (q) { + case kxyz: + case kxyZ: + case kxYz: + case kxYZ: + (*x) = initial_x[q] = + RAND_RANGE(min_x, min_x + (max_x - min_x) * 0.5f); + break; + case kXyz: + case kXyZ: + case kXYz: + case kXYZ: + (*x) = initial_x[q] = + RAND_RANGE(min_x + (max_x - min_x) * 0.5f, max_x); + } + switch (q) { + case kxyz: + case kxyZ: + case kXyz: + case kXyZ: + (*y) = initial_y[q] = + RAND_RANGE(min_y, min_y + (max_y - min_y) * 0.5f); + break; + case kxYz: + case kxYZ: + case kXYz: + case kXYZ: + (*y) = initial_y[q] = + RAND_RANGE(min_y + (max_y - min_y) * 0.5f, max_y); + } + switch (q) { + case kxyz: + case kXyz: + case kXYz: + case kxYz: + (*z) = initial_z[q] = + RAND_RANGE(min_z, min_z + (max_z - min_z) * 0.5f); + break; + case kxyZ: + case kXyZ: + case kXYZ: + case kxYZ: + (*z) = initial_z[q] = + RAND_RANGE(min_z + (max_z - min_z) * 0.5f, max_z); + } + } else { + if (val_count == 8) { + // make child nodes and feed them their initial values... + float mid_x = min_x + (max_x - min_x) * 0.5f; + float mid_y = min_y + (max_y - min_y) * 0.5f; + float mid_z = min_z + (max_z - min_z) * 0.5f; + children[kxyz] = std::make_unique( + min_x, min_y, min_z, mid_x, mid_y, mid_z, initial_x[kxyz], + initial_y[kxyz], initial_z[kxyz]); + children[kxyZ] = std::make_unique( + min_x, min_y, mid_z, mid_x, mid_y, max_z, initial_x[kxyZ], + initial_y[kxyZ], initial_z[kxyZ]); + children[kXyz] = std::make_unique( + mid_x, min_y, min_z, max_x, mid_y, mid_z, initial_x[kXyz], + initial_y[kXyz], initial_z[kXyz]); + children[kXyZ] = std::make_unique( + mid_x, min_y, mid_z, max_x, mid_y, max_z, initial_x[kXyZ], + initial_y[kXyZ], initial_z[kXyZ]); + children[kXYz] = std::make_unique( + mid_x, mid_y, min_z, max_x, max_y, mid_z, initial_x[kXYz], + initial_y[kXYz], initial_z[kXYz]); + children[kXYZ] = std::make_unique( + mid_x, mid_y, mid_z, max_x, max_y, max_z, initial_x[kXYZ], + initial_y[kXYZ], initial_z[kXYZ]); + children[kxYz] = std::make_unique( + min_x, mid_y, min_z, mid_x, max_y, mid_z, initial_x[kxYz], + initial_y[kxYz], initial_z[kxYz]); + children[kxYZ] = std::make_unique( + min_x, mid_y, mid_z, mid_x, max_y, max_z, initial_x[kxYZ], + initial_y[kxYZ], initial_z[kxYZ]); + } + + // After this point we let our children do the work. + children[q]->GetNewValue(x, y, z); + } + val_count++; + } + + private: + enum Section { kxyz, kxyZ, kXyz, kXyZ, kxYz, kxYZ, kXYz, kXYZ }; + + std::unique_ptr children[8]; + + // Pull a section and remove it from our list. + auto PullRandomSection() -> Section { + int remaining_sections = 8 - val_count % 8; + int q_picked = rand() % remaining_sections; // NOLINT + Section q_val = sections[q_picked]; + int pos_new = 0; + for (int pos_old = 0; pos_old < remaining_sections; pos_old++) { + if (pos_old != q_picked) { + sections[pos_new] = sections[pos_old]; + pos_new++; + } + } + return q_val; + } + void ResetSections() { + for (int i = 0; i < 8; i++) sections[i] = (Section)i; + } + Section sections[8]{}; + float initial_x[8]{}; + float initial_y[8]{}; + float initial_z[8]{}; + float min_x, min_y, min_z, max_x, max_y, max_z; + int val_count; + }; + std::unique_ptr root; + std::vector vals_x; + std::vector vals_y; + std::vector vals_z; +}; + +void Random::GenList1D(float* list, int size) { + SmoothGen1D gen; + gen.GetX(size - 1); // Expand it in one fell swoop + for (int i = 0; i < size; i++) { + list[i] = gen.GetX(i); + } +} + +void Random::GenList2D(float (*list)[2], int size) { + SmoothGen2D gen; + gen.GetX(size - 1); // Expand it in one fell swoop + for (int i = 0; i < size; i++) { + list[i][0] = gen.GetX(i); + list[i][1] = gen.GetY(i); + } +} + +void Random::GenList3D(float (*list)[3], int size) { + SmoothGen3D gen; + gen.GetX(size - 1); // Expand it in one fell swoop + for (int i = 0; i < size; i++) { + list[i][0] = gen.GetX(i); + list[i][1] = gen.GetY(i); + list[i][2] = gen.GetZ(i); + } +} + +} // namespace ballistica diff --git a/src/ballistica/math/random.h b/src/ballistica/math/random.h new file mode 100644 index 00000000..4418dfd0 --- /dev/null +++ b/src/ballistica/math/random.h @@ -0,0 +1,17 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MATH_RANDOM_H_ +#define BALLISTICA_MATH_RANDOM_H_ + +namespace ballistica { + +class Random { + public: + static void GenList1D(float* list, int size); + static void GenList2D(float (*list)[2], int size); + static void GenList3D(float (*list)[3], int size); +}; + +} // namespace ballistica + +#endif // BALLISTICA_MATH_RANDOM_H_ diff --git a/src/ballistica/math/rect.h b/src/ballistica/math/rect.h new file mode 100644 index 00000000..bd7778cc --- /dev/null +++ b/src/ballistica/math/rect.h @@ -0,0 +1,21 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MATH_RECT_H_ +#define BALLISTICA_MATH_RECT_H_ + +// A Generic 2d rect. +namespace ballistica { + +class Rect { + public: + float l{}, r{}, b{}, t{}; + Rect() = default; + Rect(float l_in, float b_in, float r_in, float t_in) + : l(l_in), r(r_in), b(b_in), t(t_in) {} + auto width() const -> float { return r - l; } + auto height() const -> float { return t - b; } +}; + +} // namespace ballistica + +#endif // BALLISTICA_MATH_RECT_H_ diff --git a/src/ballistica/math/vector2f.h b/src/ballistica/math/vector2f.h new file mode 100644 index 00000000..e9ad463f --- /dev/null +++ b/src/ballistica/math/vector2f.h @@ -0,0 +1,27 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MATH_VECTOR2F_H_ +#define BALLISTICA_MATH_VECTOR2F_H_ + +namespace ballistica { + +class Vector2f { + public: + // Leaves uninitialized. + Vector2f() {} // NOLINT + Vector2f(float x, float y) : x(x), y(y) {} // NOLINT + + union { + struct { + float x; + float y; + }; + float v[2]; + }; +}; + +const Vector2f kVector2f0{0.0f, 0.0f}; + +} // namespace ballistica + +#endif // BALLISTICA_MATH_VECTOR2F_H_ diff --git a/src/ballistica/math/vector3f.cc b/src/ballistica/math/vector3f.cc new file mode 100644 index 00000000..fa49eb60 --- /dev/null +++ b/src/ballistica/math/vector3f.cc @@ -0,0 +1,51 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/math/vector3f.h" + +namespace ballistica { + +auto Vector3f::Dominant() const -> int { + const float x = std::abs(v[0]); + const float y = std::abs(v[1]); + const float z = std::abs(v[2]); + if (x > y && x > z) { + return 0; + } else { + if (y > z) { + return 1; + } else { + return 2; + } + } +} + +auto Vector3f::Angle(const Vector3f& v1, const Vector3f& v2) -> float { + float s = sqrtf(v1.LengthSquared() * v2.LengthSquared()); + assert(s != 0.0f); + return (360.0f / (2.0f * kPi)) * acosf(Dot(v1, v2) / s); +} + +auto Vector3f::PlaneNormal(const Vector3f& v1, const Vector3f& v2, + const Vector3f& v3) -> Vector3f { + return Cross(v2 - v1, v3 - v1); +} + +auto Vector3f::Polar(float lat, float longitude) -> Vector3f { + return {cosf(lat * kPiDeg) * cosf(longitude * kPiDeg), sinf(lat * kPiDeg), + cosf(lat * kPiDeg) * sinf(longitude * kPiDeg)}; +} + +void Vector3f::OrthogonalSystem(Vector3f* a, Vector3f* b, Vector3f* c) { + a->Normalize(); + if (std::abs(a->z) > 0.8f) { + *b = Cross(*a, kVector3fY); + *c = Cross(*a, *b); + } else { + *b = Cross(*a, kVector3fZ); + *c = Cross(*a, *b); + } + b->Normalize(); + c->Normalize(); +} + +} // namespace ballistica diff --git a/src/ballistica/math/vector3f.h b/src/ballistica/math/vector3f.h new file mode 100644 index 00000000..afc402bf --- /dev/null +++ b/src/ballistica/math/vector3f.h @@ -0,0 +1,200 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MATH_VECTOR3F_H_ +#define BALLISTICA_MATH_VECTOR3F_H_ + +#include +#include // for memcpy +#include + +#include "ballistica/ballistica.h" + +namespace ballistica { + +class Vector3f { + public: + // Default constructor (leaves uninitialized) + Vector3f() = default; + + // Constructor. + Vector3f(float x, float y, float z) : x(x), y(y), z(z) {} // NOLINT + + // Constructor. + explicit Vector3f(const float* vals) { // NOLINT + memcpy(v, vals, sizeof(v)); + } // NOLINT + + // Constructor. + explicit Vector3f(const std::vector& vals) { // NOLINT + assert(vals.size() == 3); + memcpy(v, vals.data(), sizeof(v)); + } + + auto Normalized() const -> Vector3f { + Vector3f v2(*this); + v2.Normalize(); + return v2; + } + + // Equality operator. + auto operator==(const Vector3f& other) const -> bool { + return x == other.x && y == other.y && z == other.z; + } + + // Inequality operator. + auto operator!=(const Vector3f& other) const -> bool { + return x != other.x || y != other.y || z != other.z; + } + + // Equality operator: x==a && y==a && z==a/ + auto operator==(const float& a) const -> bool { + return x == a && y == a && z == a; + } + + // Less-than comparison. + auto operator<(const Vector3f& other) const -> bool { + if (x != other.x) return x < other.x; + if (y != other.y) return y < other.y; + return z < other.z; + } + + // Greater-than comparison. + auto operator>(const Vector3f& other) const -> bool { + if (x != other.x) return x > other.x; + if (y != other.y) return y > other.y; + return z > other.z; + } + + // Assignment operator. + auto operator=(const float* vals) -> Vector3f& { + memcpy(v, vals, sizeof(v)); + return *this; + } + + // Assignment operator. + auto operator=(const double* vals) -> Vector3f& { + x = static_cast(vals[0]); + y = static_cast(vals[1]); + z = static_cast(vals[2]); + return *this; + } + + // Addition in place. + auto operator+=(const Vector3f& other) -> Vector3f& { + x += other.x; + y += other.y; + z += other.z; + return *this; + } + + // Subtraction in place. + auto operator-=(const Vector3f& other) -> Vector3f& { + x -= other.x; + y -= other.y; + z -= other.z; + return *this; + } + + auto Dot(const Vector3f& other) const -> float { + return x * other.x + y * other.y + z * other.z; + } + + // Multiply in place. + auto operator*=(float val) -> Vector3f& { + x *= val; + y *= val; + z *= val; + return *this; + } + + // Negative. + auto operator-() const -> Vector3f { return {-x, -y, -z}; } + + auto operator/(float val) const -> Vector3f { + assert(val != 0.0f); + float inv = 1.0f / val; + return {x * inv, y * inv, z * inv}; + } + + auto operator*(float val) const -> Vector3f { + return {val * x, val * y, val * z}; + } + // (allow NUM * VEC order) + friend auto operator*(float val, const Vector3f& vec) -> Vector3f { + return vec * val; + } + auto operator+(const Vector3f& other) const -> Vector3f { + return {x + other.x, y + other.y, z + other.z}; + } + auto operator-(const Vector3f& other) const -> Vector3f { + return {x - other.x, y - other.y, z - other.z}; + } + + void Scale(const Vector3f& val) { + x *= val.x; + y *= val.y; + z *= val.z; + } + + // Normalise the vector: |x| = 1.0. + void Normalize() { + const float mag = sqrtf(LengthSquared()); + if (mag == 0.0f) return; + const float mag_inv = 1.0f / mag; + x *= mag_inv; + y *= mag_inv; + z *= mag_inv; + } + + // Make x, y and z positive. + void MakeAbs() { + x = std::abs(x); + y = std::abs(y); + z = std::abs(z); + } + + // Find the dominant component: x, y or z. + auto Dominant() const -> int; + + // Squared length of vector. + auto LengthSquared() const -> float { return ((*this).Dot(*this)); } + + // Length of vector. + auto Length() const -> float { return sqrtf((*this).Dot(*this)); } + + union { + struct { + float x; + float y; + float z; + }; + float v[3]; + }; + + static auto Cross(const Vector3f& v1, const Vector3f& v2) -> Vector3f { + return {v1.v[1] * v2.v[2] - v1.v[2] * v2.v[1], + v1.v[2] * v2.v[0] - v1.v[0] * v2.v[2], + v1.v[0] * v2.v[1] - v1.v[1] * v2.v[0]}; + } + + static auto PlaneNormal(const Vector3f& v1, const Vector3f& v2, + const Vector3f& v3) -> Vector3f; + static auto Polar(float lat, float longitude) -> Vector3f; + + static void OrthogonalSystem(Vector3f* a, Vector3f* b, Vector3f* c); + + static auto Dot(const Vector3f& v1, const Vector3f& v2) -> float { + return (v1.x * v2.x + v1.y * v2.y + v1.z * v2.z); + } + static auto Angle(const Vector3f& v1, const Vector3f& v2) -> float; +}; + +const Vector3f kVector3fX{1.0f, 0.0f, 0.0f}; +const Vector3f kVector3fY{0.0f, 1.0f, 0.0f}; +const Vector3f kVector3fZ{0.0f, 0.0f, 1.0f}; +const Vector3f kVector3f0{0.0f, 0.0f, 0.0f}; +const Vector3f kVector3f1{1.0f, 1.0f, 1.0f}; + +} // namespace ballistica + +#endif // BALLISTICA_MATH_VECTOR3F_H_ diff --git a/src/ballistica/math/vector4f.h b/src/ballistica/math/vector4f.h new file mode 100644 index 00000000..c66f46c5 --- /dev/null +++ b/src/ballistica/math/vector4f.h @@ -0,0 +1,33 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MATH_VECTOR4F_H_ +#define BALLISTICA_MATH_VECTOR4F_H_ + +#include "ballistica/math/vector3f.h" + +namespace ballistica { + +class Vector4f { + public: + Vector4f() = default; + // NOLINTNEXTLINE saying we don't init v but in effect we do. + Vector4f(float x, float y, float z, float w) : x(x), y(y), z(z), w(w) {} + + auto xyz() const -> Vector3f { return {x, y, z}; } + + union { + struct { + float x; + float y; + float z; + float w; + }; + float v[4]; + }; +}; + +const Vector4f kVector4f0{0.0f, 0.0f, 0.0f, 0.0f}; + +} // namespace ballistica + +#endif // BALLISTICA_MATH_VECTOR4F_H_ diff --git a/src/ballistica/media/component/collide_model.cc b/src/ballistica/media/component/collide_model.cc new file mode 100644 index 00000000..23c39486 --- /dev/null +++ b/src/ballistica/media/component/collide_model.cc @@ -0,0 +1,46 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/component/collide_model.h" + +#include + +#include "ballistica/game/game_stream.h" +#include "ballistica/python/class/python_class_collide_model.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +CollideModel::CollideModel(const std::string& name, Scene* scene) + : MediaComponent(name, scene), dead_(false) { + assert(InGameThread()); + if (scene) { + if (GameStream* os = scene->GetGameStream()) { + os->AddCollideModel(this); + } + } + { + Media::MediaListsLock lock; + collide_model_data_ = g_media->GetCollideModelData(name); + } + assert(collide_model_data_.exists()); +} + +CollideModel::~CollideModel() { MarkDead(); } + +void CollideModel::MarkDead() { + if (dead_) { + return; + } + if (Scene* s = scene()) { + if (GameStream* os = s->GetGameStream()) { + os->RemoveCollideModel(this); + } + } + dead_ = true; +} + +auto CollideModel::CreatePyObject() -> PyObject* { + return PythonClassCollideModel::Create(this); +} + +} // namespace ballistica diff --git a/src/ballistica/media/component/collide_model.h b/src/ballistica/media/component/collide_model.h new file mode 100644 index 00000000..7b8fa1e8 --- /dev/null +++ b/src/ballistica/media/component/collide_model.h @@ -0,0 +1,41 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_COMPONENT_COLLIDE_MODEL_H_ +#define BALLISTICA_MEDIA_COMPONENT_COLLIDE_MODEL_H_ + +#include + +#include "ballistica/media/component/media_component.h" +#include "ballistica/media/data/collide_model_data.h" +#include "ballistica/media/media.h" + +namespace ballistica { + +// user-facing collide_model class +class CollideModel : public MediaComponent { + public: + CollideModel(const std::string& name, Scene* scene); + ~CollideModel() override; + + // return the CollideModelData currently associated with this collide_model + // note that a collide_model's data can change over time as different + // versions are spooled in/out/etc + auto collide_model_data() const -> CollideModelData* { + return collide_model_data_.get(); + } + auto GetMediaComponentTypeName() const -> std::string override { + return "CollideModel"; + } + void MarkDead(); + + protected: + auto CreatePyObject() -> PyObject* override; + + private: + bool dead_; + Object::Ref collide_model_data_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_COMPONENT_COLLIDE_MODEL_H_ diff --git a/src/ballistica/media/component/cube_map_texture.cc b/src/ballistica/media/component/cube_map_texture.cc new file mode 100644 index 00000000..384cb43e --- /dev/null +++ b/src/ballistica/media/component/cube_map_texture.cc @@ -0,0 +1,21 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/component/cube_map_texture.h" + +#include "ballistica/media/media.h" + +namespace ballistica { + +CubeMapTexture::CubeMapTexture(const std::string& name, Scene* scene) + : MediaComponent(name, scene) { + assert(InGameThread()); + + // cant currently add these to scenes so nothing to do here.. + { + Media::MediaListsLock lock; + texture_data_ = g_media->GetCubeMapTextureData(name); + } + assert(texture_data_.exists()); +} + +} // namespace ballistica diff --git a/src/ballistica/media/component/cube_map_texture.h b/src/ballistica/media/component/cube_map_texture.h new file mode 100644 index 00000000..c72e4623 --- /dev/null +++ b/src/ballistica/media/component/cube_map_texture.h @@ -0,0 +1,32 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_COMPONENT_CUBE_MAP_TEXTURE_H_ +#define BALLISTICA_MEDIA_COMPONENT_CUBE_MAP_TEXTURE_H_ + +#include + +#include "ballistica/media/component/media_component.h" +#include "ballistica/media/data/texture_data.h" + +namespace ballistica { + +// user-facing texture class +class CubeMapTexture : public MediaComponent { + public: + CubeMapTexture(const std::string& name, Scene* s); + + // return the TextureData currently associated with this texture + // note that a texture's data can change over time as different + // versions are spooled in/out/etc + auto GetTextureData() const -> TextureData* { return texture_data_.get(); } + auto GetMediaComponentTypeName() const -> std::string override { + return "CubeMapTexture"; + } + + private: + Object::Ref texture_data_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_COMPONENT_CUBE_MAP_TEXTURE_H_ diff --git a/src/ballistica/media/component/data.cc b/src/ballistica/media/component/data.cc new file mode 100644 index 00000000..8e1361a2 --- /dev/null +++ b/src/ballistica/media/component/data.cc @@ -0,0 +1,45 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/component/data.h" + +#include "ballistica/game/game_stream.h" +#include "ballistica/python/class/python_class_data.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +Data::Data(const std::string& name, Scene* scene) + : MediaComponent(name, scene), dead_(false) { + assert(InGameThread()); + + if (scene) { + if (GameStream* os = scene->GetGameStream()) { + os->AddData(this); + } + } + { + Media::MediaListsLock lock; + data_data_ = g_media->GetDataData(name); + } + assert(data_data_.exists()); +} + +Data::~Data() { MarkDead(); } + +void Data::MarkDead() { + if (dead_) { + return; + } + if (Scene* s = scene()) { + if (GameStream* os = s->GetGameStream()) { + os->RemoveData(this); + } + } + dead_ = true; +} + +auto Data::CreatePyObject() -> PyObject* { + return PythonClassData::Create(this); +} + +} // namespace ballistica diff --git a/src/ballistica/media/component/data.h b/src/ballistica/media/component/data.h new file mode 100644 index 00000000..30e926a2 --- /dev/null +++ b/src/ballistica/media/component/data.h @@ -0,0 +1,43 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_COMPONENT_DATA_H_ +#define BALLISTICA_MEDIA_COMPONENT_DATA_H_ + +#include +#include + +#include "ballistica/ballistica.h" +#include "ballistica/core/object.h" +#include "ballistica/media/component/media_component.h" +#include "ballistica/media/data/data_data.h" +#include "ballistica/media/data/media_component_data.h" +#include "ballistica/media/media.h" + +namespace ballistica { + +// user-facing data class +class Data : public MediaComponent { + public: + Data(const std::string& name, Scene* scene); + ~Data() override; + + // return the DataData currently associated with this data + // note that a data's data can change over time as different + // versions are spooled in/out/etc. + auto data_data() const -> DataData* { return data_data_.get(); } + auto GetMediaComponentTypeName() const -> std::string override { + return "Data"; + } + void MarkDead(); + + protected: + auto CreatePyObject() -> PyObject* override; + + private: + bool dead_; + Object::Ref data_data_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_COMPONENT_DATA_H_ diff --git a/src/ballistica/media/component/media_component.cc b/src/ballistica/media/component/media_component.cc new file mode 100644 index 00000000..4fe11860 --- /dev/null +++ b/src/ballistica/media/component/media_component.cc @@ -0,0 +1,37 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/component/media_component.h" + +#include +#include + +#include "ballistica/python/python_sys.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +MediaComponent::MediaComponent(std::string name, Scene* scene) + : name_(std::move(name)), scene_(scene) {} + +auto MediaComponent::GetPyRef(bool new_ref) -> PyObject* { + if (!py_object_) { + // if we have no python object, create it + py_object_ = CreatePyObject(); + assert(py_object_ != nullptr); + } + if (new_ref) { + Py_INCREF(py_object_); + } + return py_object_; +} + +auto MediaComponent::GetObjectDescription() const -> std::string { + return ""; +} + +void MediaComponent::ClearPyObject() { + assert(py_object_ != nullptr); + py_object_ = nullptr; +} + +} // namespace ballistica diff --git a/src/ballistica/media/component/media_component.h b/src/ballistica/media/component/media_component.h new file mode 100644 index 00000000..84887de1 --- /dev/null +++ b/src/ballistica/media/component/media_component.h @@ -0,0 +1,63 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_COMPONENT_MEDIA_COMPONENT_H_ +#define BALLISTICA_MEDIA_COMPONENT_MEDIA_COMPONENT_H_ + +#include + +#include "ballistica/core/context.h" +#include "ballistica/core/object.h" + +namespace ballistica { + +class MediaComponent : public Object { + public: + MediaComponent(std::string name, Scene* scene); + auto name() const -> std::string { return name_; } + + // Returns true if this texture was created in the UI context. + // UI stuff should check this before accepting a texture. + auto IsFromUIContext() const -> bool { + return (context_.GetUIContext() != nullptr); + } + auto has_py_object() const -> bool { return (py_object_ != nullptr); } + auto NewPyRef() -> PyObject* { return GetPyRef(true); } + auto BorrowPyRef() -> PyObject* { return GetPyRef(false); } + auto GetObjectDescription() const -> std::string override; + auto scene() const -> Scene* { return scene_.get(); } + + // Called by python wrapper objs when they are dying. + void ClearPyObject(); + + auto stream_id() const -> int64_t { return stream_id_; } + void set_stream_id(int64_t val) { + assert(stream_id_ == -1); + stream_id_ = val; + } + + void clear_stream_id() { + assert(stream_id_ != -1); + stream_id_ = -1; + } + + protected: + virtual auto GetMediaComponentTypeName() const -> std::string = 0; + + // Create a python representation of this object. + virtual auto CreatePyObject() -> PyObject* = 0; + + private: + int64_t stream_id_{-1}; + Object::WeakRef scene_; + PyObject* py_object_{}; + + // Return a python reference to the object, (creating python obj if needed). + auto GetPyRef(bool new_ref = true) -> PyObject*; + std::string name_; + Context context_; + friend class ClientSession; +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_COMPONENT_MEDIA_COMPONENT_H_ diff --git a/src/ballistica/media/component/model.cc b/src/ballistica/media/component/model.cc new file mode 100644 index 00000000..e5060444 --- /dev/null +++ b/src/ballistica/media/component/model.cc @@ -0,0 +1,45 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/component/model.h" + +#include "ballistica/game/game_stream.h" +#include "ballistica/python/class/python_class_model.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +Model::Model(const std::string& name, Scene* scene) + : MediaComponent(name, scene), dead_(false) { + assert(InGameThread()); + + if (scene) { + if (GameStream* os = scene->GetGameStream()) { + os->AddModel(this); + } + } + { + Media::MediaListsLock lock; + model_data_ = g_media->GetModelData(name); + } + assert(model_data_.exists()); +} + +Model::~Model() { MarkDead(); } + +void Model::MarkDead() { + if (dead_) { + return; + } + if (Scene* s = scene()) { + if (GameStream* os = s->GetGameStream()) { + os->RemoveModel(this); + } + } + dead_ = true; +} + +auto Model::CreatePyObject() -> PyObject* { + return PythonClassModel::Create(this); +} + +} // namespace ballistica diff --git a/src/ballistica/media/component/model.h b/src/ballistica/media/component/model.h new file mode 100644 index 00000000..c7a8968b --- /dev/null +++ b/src/ballistica/media/component/model.h @@ -0,0 +1,44 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_COMPONENT_MODEL_H_ +#define BALLISTICA_MEDIA_COMPONENT_MODEL_H_ + +#include +#include + +#include "ballistica/ballistica.h" +#include "ballistica/core/object.h" +#include "ballistica/media/component/media_component.h" +#include "ballistica/media/data/media_component_data.h" +#include "ballistica/media/data/model_data.h" +#include "ballistica/media/data/model_renderer_data.h" +#include "ballistica/media/media.h" + +namespace ballistica { + +// user-facing model class +class Model : public MediaComponent { + public: + Model(const std::string& name, Scene* scene); + ~Model() override; + + // return the ModelData currently associated with this model + // note that a model's data can change over time as different + // versions are spooled in/out/etc + auto model_data() const -> ModelData* { return model_data_.get(); } + auto GetMediaComponentTypeName() const -> std::string override { + return "Model"; + } + void MarkDead(); + + protected: + auto CreatePyObject() -> PyObject* override; + + private: + bool dead_; + Object::Ref model_data_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_COMPONENT_MODEL_H_ diff --git a/src/ballistica/media/component/sound.cc b/src/ballistica/media/component/sound.cc new file mode 100644 index 00000000..218146cf --- /dev/null +++ b/src/ballistica/media/component/sound.cc @@ -0,0 +1,44 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/component/sound.h" + +#include "ballistica/game/game_stream.h" +#include "ballistica/media/data/sound_data.h" +#include "ballistica/media/media.h" +#include "ballistica/python/class/python_class_sound.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +Sound::Sound(const std::string& name, Scene* scene) + : MediaComponent(name, scene) { + assert(InGameThread()); + if (scene) { + if (GameStream* os = scene->GetGameStream()) { + os->AddSound(this); + } + } + { + Media::MediaListsLock lock; + sound_data_ = g_media->GetSoundData(name); + } + assert(sound_data_.exists()); +} + +Sound::~Sound() { MarkDead(); } + +void Sound::MarkDead() { + if (dead_) return; + if (Scene* s = scene()) { + if (GameStream* os = s->GetGameStream()) { + os->RemoveSound(this); + } + } + dead_ = true; +} + +auto Sound::CreatePyObject() -> PyObject* { + return PythonClassSound::Create(this); +} + +} // namespace ballistica diff --git a/src/ballistica/media/component/sound.h b/src/ballistica/media/component/sound.h new file mode 100644 index 00000000..e712cced --- /dev/null +++ b/src/ballistica/media/component/sound.h @@ -0,0 +1,37 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_COMPONENT_SOUND_H_ +#define BALLISTICA_MEDIA_COMPONENT_SOUND_H_ + +#include +#include + +#include "ballistica/media/component/media_component.h" + +namespace ballistica { + +class Sound : public MediaComponent { + public: + Sound(const std::string& name, Scene* scene); + ~Sound() override; + + // Return the SoundData currently associated with this sound. + // Note that a sound's data can change over time as different + // versions are spooled in/out/etc. + auto GetSoundData() const -> SoundData* { return sound_data_.get(); } + auto GetMediaComponentTypeName() const -> std::string override { + return "Sound"; + } + void MarkDead(); + + protected: + auto CreatePyObject() -> PyObject* override; + + private: + bool dead_{}; + Object::Ref sound_data_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_COMPONENT_SOUND_H_ diff --git a/src/ballistica/media/component/texture.cc b/src/ballistica/media/component/texture.cc new file mode 100644 index 00000000..3948927a --- /dev/null +++ b/src/ballistica/media/component/texture.cc @@ -0,0 +1,59 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/component/texture.h" + +#include + +#include "ballistica/game/game_stream.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/python/class/python_class_texture.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +Texture::Texture(const std::string& name, Scene* scene) + : MediaComponent(name, scene), dead_(false) { + assert(InGameThread()); + + // Add to the provided scene to get a numeric ID. + if (scene) { + if (GameStream* os = scene->GetGameStream()) { + os->AddTexture(this); + } + } + { + Media::MediaListsLock lock; + texture_data_ = g_media->GetTextureData(name); + } + assert(texture_data_.exists()); +} + +// qrcode version +Texture::Texture(const std::string& qr_url) : MediaComponent(qr_url, nullptr) { + assert(InGameThread()); + { + Media::MediaListsLock lock; + texture_data_ = g_media->GetTextureDataQRCode(qr_url); + } + assert(texture_data_.exists()); +} + +Texture::~Texture() { MarkDead(); } + +void Texture::MarkDead() { + if (dead_) { + return; + } + if (Scene* s = scene()) { + if (GameStream* os = s->GetGameStream()) { + os->RemoveTexture(this); + } + } + dead_ = true; +} + +auto Texture::CreatePyObject() -> PyObject* { + return PythonClassTexture::Create(this); +} + +} // namespace ballistica diff --git a/src/ballistica/media/component/texture.h b/src/ballistica/media/component/texture.h new file mode 100644 index 00000000..d03ed2e5 --- /dev/null +++ b/src/ballistica/media/component/texture.h @@ -0,0 +1,39 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_COMPONENT_TEXTURE_H_ +#define BALLISTICA_MEDIA_COMPONENT_TEXTURE_H_ + +#include + +#include "ballistica/media/component/media_component.h" +#include "ballistica/media/data/texture_data.h" + +namespace ballistica { + +// User-facing texture class. +class Texture : public MediaComponent { + public: + Texture(const std::string& name, Scene* scene); + explicit Texture(const std::string& qr_url); + ~Texture() override; + + // Return the TextureData currently associated with this texture. + // Note that a texture's data can change over time as different + // versions are spooled in/out/etc. + auto texture_data() const -> TextureData* { return texture_data_.get(); } + auto GetMediaComponentTypeName() const -> std::string override { + return "Texture"; + } + void MarkDead(); + + protected: + auto CreatePyObject() -> PyObject* override; + + private: + bool dead_ = false; + Object::Ref texture_data_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_COMPONENT_TEXTURE_H_ diff --git a/src/ballistica/media/data/collide_model_data.cc b/src/ballistica/media/data/collide_model_data.cc new file mode 100644 index 00000000..2486c341 --- /dev/null +++ b/src/ballistica/media/data/collide_model_data.cc @@ -0,0 +1,134 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/data/collide_model_data.h" + +#include + +#include "ballistica/media/media.h" + +namespace ballistica { + +CollideModelData::CollideModelData(const std::string& file_name_in) + : file_name_(file_name_in) { + file_name_full_ = + g_media->FindMediaFile(Media::FileType::kCollisionModel, file_name_in); + valid_ = true; +} + +void CollideModelData::DoPreload() { + assert(!file_name_.empty()); + + FILE* f = g_platform->FOpen(file_name_full_.c_str(), "rb"); + uint32_t i_vals[2]; + if (!f) { + throw Exception("Can't open collide model: '" + file_name_full_ + "'"); + } + + uint32_t version; + if (fread(&version, sizeof(version), 1, f) != 1) { + throw Exception("Error reading file header for '" + file_name_full_ + "'"); + } + + if (version != kCobFileID) { + throw Exception("File '" + file_name_full_ + + " is in an old format or not a cob file (got id " + + std::to_string(version) + ", " + + std::to_string(kCobFileID) + ")"); + } + + // Read the vertex count and face count. + if (fread(i_vals, sizeof(i_vals), 1, f) != 1) { + throw Exception("Read failed for " + file_name_full_); + } + + size_t vertex_count = i_vals[0]; + size_t tri_count = i_vals[1]; + + // Need 3 floats per vertex. + vertices_.resize(vertex_count * 3); + + // Need 3 indices per face. + indices_.resize(tri_count * 3); + + // Need 3 floats per face-normal. + normals_.resize(tri_count * 3); + + if (fread(&(vertices_[0]), vertices_.size() * sizeof(dReal), 1, f) != 1) { + throw Exception("Read failed for " + file_name_full_); + } + if (fread(&(indices_[0]), indices_.size() * sizeof(uint32_t), 1, f) != 1) { + throw Exception("Read failed for " + file_name_full_); + } + if (fread(&(normals_[0]), normals_.size() * sizeof(dReal), 1, f) != 1) { + throw Exception("Read failed for " + file_name_full_); + } + + fclose(f); + + tri_mesh_data_ = dGeomTriMeshDataCreate(); + BA_PRECONDITION(tri_mesh_data_); + + if (!HeadlessMode()) { + tri_mesh_data_bg_ = dGeomTriMeshDataCreate(); + BA_PRECONDITION(tri_mesh_data_bg_); + } + +#ifdef dSINGLE + dGeomTriMeshDataBuildSingle1( + tri_mesh_data_, &(vertices_[0]), 3 * sizeof(dReal), + static_cast_check_fit(vertex_count), &(indices_[0]), + static_cast(indices_.size()), 3 * sizeof(uint32_t), &(normals_[0])); + if (!HeadlessMode()) { + dGeomTriMeshDataBuildSingle1(tri_mesh_data_bg_, &(vertices_[0]), + 3 * sizeof(dReal), i_vals[0], &(indices_[0]), + static_cast(indices_.size()), + 3 * sizeof(uint32_t), &(normals_[0])); + } +#else +#ifndef dDOUBLE +#error single or double precition not defined +#endif + dGeomTriMeshDataBuildDouble1( + tri_mesh_data_, &(vertices_[0]), 3 * sizeof(dReal), vertex_count, + &(indices_[0]), indices_.size(), 3 * sizeof(uint32_t), &(normals_[0])); + if (!HeadlessMode()) { + dGeomTriMeshDataBuildDouble1( + tri_mesh_data_bg_, &(vertices_[0]), 3 * sizeof(dReal), i_vals[0], + &(indices_[0]), indices_.size(), 3 * sizeof(uint32_t), &(normals_[0])); + } +#endif // dSINGLE +} // namespace ballistica + +void CollideModelData::DoLoad() { assert(InGameThread()); } + +void CollideModelData::DoUnload() { + // TODO(ericf): if we want to support in-game reloading we need + // to keep track of what ODE trimeshes are using our data and update + // them all accordingly on unload/loads... + + // we should still be fine for regular pruning unloads though; + // if there are no references remaining to us then nothing in the + // game should be using us. + + if (!valid_) { + return; + } + + dGeomTriMeshDataDestroy(tri_mesh_data_); + if (tri_mesh_data_bg_) { + dGeomTriMeshDataDestroy(tri_mesh_data_bg_); + } +} + +auto CollideModelData::GetMeshData() -> dTriMeshDataID { + assert(tri_mesh_data_); + return tri_mesh_data_; +} + +auto CollideModelData::GetBGMeshData() -> dTriMeshDataID { + assert(loaded()); + assert(!HeadlessMode()); + return tri_mesh_data_bg_; +} + +} // namespace ballistica diff --git a/src/ballistica/media/data/collide_model_data.h b/src/ballistica/media/data/collide_model_data.h new file mode 100644 index 00000000..3e3223d3 --- /dev/null +++ b/src/ballistica/media/data/collide_model_data.h @@ -0,0 +1,47 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_DATA_COLLIDE_MODEL_DATA_H_ +#define BALLISTICA_MEDIA_DATA_COLLIDE_MODEL_DATA_H_ + +#include +#include + +#include "ballistica/media/data/media_component_data.h" +#include "ode/ode.h" + +namespace ballistica { + +// Loadable model for collision detection. +class CollideModelData : public MediaComponentData { + public: + CollideModelData() = default; + explicit CollideModelData(const std::string& file_name_in); + void DoPreload() override; + void DoLoad() override; + void DoUnload() override; + auto GetMediaType() const -> MediaType override { + return MediaType::kCollideModel; + } + auto GetName() const -> std::string override { + if (!file_name_full_.empty()) { + return file_name_full_; + } else { + return "invalid CollideModel"; + } + } + auto GetMeshData() -> dTriMeshDataID; + auto GetBGMeshData() -> dTriMeshDataID; + + private: + std::string file_name_; + std::string file_name_full_; + std::vector vertices_; + std::vector indices_; + std::vector normals_; + dTriMeshDataID tri_mesh_data_{}; + dTriMeshDataID tri_mesh_data_bg_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_DATA_COLLIDE_MODEL_DATA_H_ diff --git a/src/ballistica/media/data/data_data.cc b/src/ballistica/media/data/data_data.cc new file mode 100644 index 00000000..05555b03 --- /dev/null +++ b/src/ballistica/media/data/data_data.cc @@ -0,0 +1,50 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/data/data_data.h" + +#include "ballistica/media/media.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_sys.h" + +namespace ballistica { + +DataData::DataData(const std::string& file_name_in) : file_name_(file_name_in) { + file_name_full_ = + g_media->FindMediaFile(Media::FileType::kData, file_name_in); + valid_ = true; +} + +void DataData::DoPreload() { + // NOTE TO SELF: originally I tried to grab the GIL here and do our actual + // Python loading in Preload(). However this resulted in deadlock + // in the following case: + // - asset thread grabs payload lock for Preload() + // - asset thread tries to grab GIL in Preload(); spins. + // - meanwhile, something in game thread has called Load() + // - game thread holds GIL by default and now spins waiting on payload lock. + // - deadlock :-( + + // ...so the new plan is to simply load the file into a string in Preload() + // and then do the Python work in Load(). This should still avoid the nastiest + // IO-related hitches at least.. + + raw_input_ = Utils::FileToString(file_name_full_); +} + +void DataData::DoLoad() { + assert(InGameThread()); + assert(valid_); + PythonRef args(Py_BuildValue("(s)", raw_input_.c_str()), PythonRef::kSteal); + object_ = g_python->obj(Python::ObjID::kJsonLoadsCall).Call(args); + if (!object_.exists()) { + throw Exception("Unable to load data: '" + file_name_ + "'."); + } +} + +void DataData::DoUnload() { + assert(InGameThread()); + assert(valid_); + object_.Release(); +} + +} // namespace ballistica diff --git a/src/ballistica/media/data/data_data.h b/src/ballistica/media/data/data_data.h new file mode 100644 index 00000000..d6b2d64f --- /dev/null +++ b/src/ballistica/media/data/data_data.h @@ -0,0 +1,47 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_DATA_DATA_DATA_H_ +#define BALLISTICA_MEDIA_DATA_DATA_DATA_H_ + +#include + +#include "ballistica/media/data/media_component_data.h" +#include "ballistica/python/python_ref.h" + +namespace ballistica { + +class DataData : public MediaComponentData { + public: + DataData() = default; + explicit DataData(const std::string& file_name_in); + + void DoPreload() override; + void DoLoad() override; + void DoUnload() override; + + auto GetMediaType() const -> MediaType override { return MediaType::kData; } + auto GetName() const -> std::string override { + if (!file_name_full_.empty()) { + return file_name_full_; + } else { + return "invalid data"; + } + } + auto object() -> const PythonRef& { + assert(InGameThread()); + assert(loaded()); + return object_; + } + auto file_name() const -> const std::string& { return file_name_; } + auto file_name_full() const -> const std::string& { return file_name_full_; } + + private: + PythonRef object_; + std::string file_name_; + std::string file_name_full_; + std::string raw_input_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_DATA_DATA_DATA_H_ diff --git a/src/ballistica/media/data/media_component_data.cc b/src/ballistica/media/data/media_component_data.cc new file mode 100644 index 00000000..9d7fc411 --- /dev/null +++ b/src/ballistica/media/data/media_component_data.cc @@ -0,0 +1,108 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/data/media_component_data.h" + +namespace ballistica { + +MediaComponentData::MediaComponentData() { + assert(InGameThread()); + assert(g_media); + last_used_time_ = GetRealTime(); +} + +MediaComponentData::~MediaComponentData() { + // at the moment whoever owns the last reference to us + // needs to make sure to unload us before we die.. + // I feel like there should be a more elegant solution to that. + assert(g_media); + assert(!locked()); + assert(!loaded()); +} + +void MediaComponentData::Preload(bool already_locked) { + LockGuard lock(this, already_locked ? LockGuard::Type::kDontLock + : LockGuard::Type::kLock); + if (!preloaded_) { + assert(!loaded_); +#if BA_SHOW_LOADS_UNLOADS + printf("pre-loading %s\n", GetName().c_str()); +#endif + BA_PRECONDITION(locked()); + preload_start_time_ = GetRealTime(); + DoPreload(); + preload_end_time_ = GetRealTime(); + preloaded_ = true; + } +} + +void MediaComponentData::Load(bool already_locked) { + LockGuard lock(this, already_locked ? LockGuard::Type::kDontLock + : LockGuard::Type::kLock); + if (!preloaded_) { + Preload(true); + } + + if (!loaded_) { +#if BA_SHOW_LOADS_UNLOADS + printf("loading %s\n", GetName().c_str()); +#endif + assert(preloaded_ && !loaded_); + BA_DEBUG_FUNCTION_TIMER_BEGIN(); + BA_PRECONDITION(locked()); + load_start_time_ = GetRealTime(); + DoLoad(); + load_end_time_ = GetRealTime(); + BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(50, GetName()); + loaded_ = true; + } +} + +void MediaComponentData::Unload(bool already_locked) { + LockGuard lock(this, already_locked ? LockGuard::Type::kDontLock + : LockGuard::Type::kLock); + + // if somehow we're told to unload after we've preloaded but before load, + // finish the load first... (don't wanna worry about guarding against that + // case) + // UPDATE: is this still necessary? It's a holdover from when we had + // potentially-multi-stage loads... now we just have a single load always. + if (preloaded_ && !loaded_) { + Load(true); + } + if (loaded_ && preloaded_) { +#if BA_SHOW_LOADS_UNLOADS + printf("unloading %s\n", GetName().c_str()); +#endif + BA_PRECONDITION(locked()); + DoUnload(); + preloaded_ = false; + loaded_ = false; + } +} + +MediaComponentData::LockGuard::LockGuard(MediaComponentData* data, Type type) + : data_(data) { + switch (type) { + case kLock: { + BA_DEBUG_FUNCTION_TIMER_BEGIN(); + data_->Lock(); + holds_lock_ = true; + BA_DEBUG_FUNCTION_TIMER_END_THREAD(20); + break; + } + case kInheritLock: + holds_lock_ = true; + break; + case kDontLock: + break; + default: + throw Exception(); + } +} + +MediaComponentData::LockGuard::~LockGuard() { + if (holds_lock_) { + data_->Unlock(); + } +} +} // namespace ballistica diff --git a/src/ballistica/media/data/media_component_data.h b/src/ballistica/media/data/media_component_data.h new file mode 100644 index 00000000..7ab99e25 --- /dev/null +++ b/src/ballistica/media/data/media_component_data.h @@ -0,0 +1,136 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_DATA_MEDIA_COMPONENT_DATA_H_ +#define BALLISTICA_MEDIA_DATA_MEDIA_COMPONENT_DATA_H_ + +#include +#include + +#include "ballistica/core/object.h" +#include "ballistica/platform/platform.h" + +namespace ballistica { + +/// Base class for loadable media components. +class MediaComponentData : public Object { + public: + MediaComponentData(); + ~MediaComponentData() override; + void Preload(bool already_locked = false); + void Load(bool already_locked = false); + void Unload(bool already_locked = false); + auto preloaded() const -> bool { return preloaded_; } + auto loaded() const -> bool { return preloaded_ && loaded_; } + virtual auto GetMediaType() const -> MediaType = 0; + + // Return name or another identifier. For debugging purposes. + virtual auto GetName() const -> std::string { return "invalid"; } + virtual auto GetNameFull() const -> std::string { return GetName(); } + + // Used to lock asset payloads for modification in a RAII manner. + // FIXME - need to better define the times when payloads need to + // be locked. For instance, we ensure everything is loaded at the + // beginning of drawing a frame, but technically is anything preventing + // it from being unloaded during the draw?.. + class LockGuard { + public: + enum Type { kLock, kInheritLock, kDontLock }; + explicit LockGuard(MediaComponentData* data, Type type = kLock); + ~LockGuard(); + + // Does this guard hold a lock? + auto holds_lock() const -> bool { return holds_lock_; } + + private: + MediaComponentData* data_ = nullptr; + bool holds_lock_ = false; + }; + + // Attempt to lock the component without blocking. returns true if + // successful. In the case of success, use a LockGuard with + // kInheritLock to release the lock. + auto TryLock() -> bool { + bool val = mutex_.try_lock(); + if (val) { + assert(!locked_); + locked_ = true; + } + return val; + } + + auto locked() const -> bool { return locked_; } + auto last_used_time() const -> millisecs_t { return last_used_time_; } + void set_last_used_time(millisecs_t val) { last_used_time_ = val; } + + // Used by the renderer when adding component refs to frame_defs. + auto last_frame_def_num() const -> int64_t { return last_frame_def_num_; } + void set_last_frame_def_num(int64_t last) { last_frame_def_num_ = last; } + auto preload_time() const -> millisecs_t { + return preload_end_time_ - preload_start_time_; + } + auto load_time() const -> millisecs_t { + return load_end_time_ - load_start_time_; + } + + // Sanity testing. + auto valid() const -> bool { return valid_; } + + protected: + // Preload the component's data. This may be called from any thread so must + // be safe regardless (ie: just load data into the component; don't make GL + // calls, etc). + virtual void DoPreload() = 0; + + // This is always called by the main thread that uses the component to finish + // loading. ie: whatever thread is running opengl will call this for textures, + // audio thread for sounds, etc as much heavy lifting as possible should be + // done in DoPreload but interaction with the corresponding api (gl, al, etc) + // is done here. + virtual void DoLoad() = 0; + + // Unload the component. This is always called by the main component thread + // (same as DoLoad). + virtual void DoUnload() = 0; + + // Do we still use/need this? + bool valid_ = false; + + private: + // Lock the component - components must be locked whenever using them. + void Lock() { + BA_DEBUG_FUNCTION_TIMER_BEGIN(); + mutex_.lock(); + assert(!locked_); + locked_ = true; + BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(20, GetName()); + } + + // Unlock the component. each call to lock must be accompanied by one of + // these. + void Unlock() { + BA_DEBUG_FUNCTION_TIMER_BEGIN(); + assert(locked_); + locked_ = false; + mutex_.unlock(); + BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(20, GetName()); + } + + bool locked_ = false; + millisecs_t preload_start_time_ = 0; + millisecs_t preload_end_time_ = 0; + millisecs_t load_start_time_ = 0; + millisecs_t load_end_time_ = 0; + + // We keep track of what frame_def we've been added to so + // we only include a single reference to ourself in it. + int64_t last_frame_def_num_ = 0; + millisecs_t last_used_time_ = 0; + bool preloaded_ = false; + bool loaded_ = false; + std::mutex mutex_; + BA_DISALLOW_CLASS_COPIES(MediaComponentData); +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_DATA_MEDIA_COMPONENT_DATA_H_ diff --git a/src/ballistica/media/data/model_data.cc b/src/ballistica/media/data/model_data.cc new file mode 100644 index 00000000..c0081f02 --- /dev/null +++ b/src/ballistica/media/data/model_data.cc @@ -0,0 +1,128 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/data/model_data.h" + +#include +#include + +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/media/media.h" + +namespace ballistica { + +ModelData::ModelData(const std::string& file_name_in) + : file_name_(file_name_in) { + file_name_full_ = + g_media->FindMediaFile(Media::FileType::kModel, file_name_in); + valid_ = true; +} + +void ModelData::DoPreload() { + // In headless, don't load anything. +#if !BA_HEADLESS_BUILD + + assert(!file_name_.empty()); + FILE* f = g_platform->FOpen(file_name_full_.c_str(), "rb"); + if (!f) { + throw Exception("Can't open model: '" + file_name_full_ + "'"); + } + + // We currently read/write in little-endian since that's all we run on at the + // moment. +#if WORDS_BIGENDIAN +#error FIX THIS FOR BIG ENDIAN +#endif + + uint32_t version; + if (fread(&version, sizeof(version), 1, f) != 1) { + throw Exception("Error reading file header for '" + file_name_full_ + "'"); + } + if (version != kBobFileID) { + throw Exception("File: '" + file_name_full_ + + "' is an old format or not a bob file (got id " + + std::to_string(version) + ", " + + std::to_string(kBobFileID) + ")"); + } + + uint32_t mesh_format; + if (fread(&mesh_format, sizeof(mesh_format), 1, f) != 1) { + throw Exception("Error reading mesh_format for '" + file_name_full_ + "'"); + } + format_ = static_cast(mesh_format); + BA_PRECONDITION((format_ == MeshFormat::kUV16N8Index8) + || (format_ == MeshFormat::kUV16N8Index16) + || (format_ == MeshFormat::kUV16N8Index32)); + + uint32_t vertex_count; + if (fread(&vertex_count, sizeof(vertex_count), 1, f) != 1) { + throw Exception("Error reading vertex_count for '" + file_name_full_ + "'"); + } + + uint32_t face_count; + if (fread(&face_count, sizeof(face_count), 1, f) != 1) { + throw Exception("Error reading face_count for '" + file_name_full_ + "'"); + } + + vertices_.resize(vertex_count); + if (fread(&(vertices_[0]), vertices_.size() * sizeof(VertexObjectFull), 1, f) + != 1) { + throw Exception("Read failed for " + file_name_full_); + } + switch (GetIndexSize()) { + case 1: { + indices8_.resize(face_count * 3); + if (fread(indices8_.data(), indices8_.size() * sizeof(uint8_t), 1, f) + != 1) { + throw Exception("Read failed for " + file_name_full_); + } + break; + } + case 2: { + indices16_.resize(face_count * 3); + if (fread(indices16_.data(), indices16_.size() * sizeof(uint16_t), 1, f) + != 1) { + throw Exception("Read failed for " + file_name_full_); + } + break; + } + case 4: { + indices32_.resize(face_count * 3); + if (fread(indices32_.data(), indices32_.size() * sizeof(uint32_t), 1, f) + != 1) { + throw Exception("Read failed for " + file_name_full_); + } + break; + } + default: + throw Exception(); + } + + fclose(f); + +#endif // BA_HEADLESS_BUILD +} + +void ModelData::DoLoad() { + assert(!renderer_data_.exists()); + renderer_data_ = Object::MakeRefCounted( + g_graphics_server->renderer()->NewModelData(*this)); + + // once we're loaded lets free up our vert data memory + std::vector().swap(vertices_); + std::vector().swap(indices8_); + std::vector().swap(indices16_); + std::vector().swap(indices32_); +} + +void ModelData::DoUnload() { + assert(valid_); + assert(renderer_data_.exists()); + std::vector().swap(vertices_); + std::vector().swap(indices8_); + std::vector().swap(indices16_); + std::vector().swap(indices32_); + renderer_data_.Clear(); +} + +} // namespace ballistica diff --git a/src/ballistica/media/data/model_data.h b/src/ballistica/media/data/model_data.h new file mode 100644 index 00000000..cb155c6c --- /dev/null +++ b/src/ballistica/media/data/model_data.h @@ -0,0 +1,67 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_DATA_MODEL_DATA_H_ +#define BALLISTICA_MEDIA_DATA_MODEL_DATA_H_ + +#include +#include + +#include "ballistica/media/data/media_component_data.h" +#include "ballistica/media/data/model_renderer_data.h" + +namespace ballistica { + +class ModelData : public MediaComponentData { + public: + ModelData() = default; + explicit ModelData(const std::string& file_name_in); + void DoPreload() override; + void DoLoad() override; + void DoUnload() override; + auto GetMediaType() const -> MediaType override { return MediaType::kModel; } + auto GetName() const -> std::string override { + if (!file_name_full_.empty()) { + return file_name_full_; + } else { + return "invalid Model"; + } + } + auto renderer_data() const -> ModelRendererData* { + assert(renderer_data_.exists()); + return renderer_data_.get(); + } + auto vertices() const -> const std::vector& { + return vertices_; + } + auto indices8() const -> const std::vector& { return indices8_; } + auto indices16() const -> const std::vector& { return indices16_; } + auto indices32() const -> const std::vector& { return indices32_; } + auto GetIndexSize() const -> int { + switch (format_) { + case MeshFormat::kUV16N8Index8: + return 1; + case MeshFormat::kUV16N8Index16: + return 2; + case MeshFormat::kUV16N8Index32: + return 4; + default: + throw Exception(); + } + } + + private: + Object::Ref renderer_data_; + std::string file_name_; + std::string file_name_full_; + MeshFormat format_{}; + std::vector vertices_; + std::vector indices8_; + std::vector indices16_; + std::vector indices32_; + friend class ModelRendererData; + BA_DISALLOW_CLASS_COPIES(ModelData); +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_DATA_MODEL_DATA_H_ diff --git a/src/ballistica/media/data/model_renderer_data.h b/src/ballistica/media/data/model_renderer_data.h new file mode 100644 index 00000000..ad23080a --- /dev/null +++ b/src/ballistica/media/data/model_renderer_data.h @@ -0,0 +1,21 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_DATA_MODEL_RENDERER_DATA_H_ +#define BALLISTICA_MEDIA_DATA_MODEL_RENDERER_DATA_H_ + +#include "ballistica/core/object.h" + +namespace ballistica { + +// Renderer-specific data (gl display list, etc) +// this is provided by the renderer +class ModelRendererData : public Object { + public: + auto GetDefaultOwnerThread() const -> ThreadIdentifier override { + return ThreadIdentifier::kMain; + } +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_DATA_MODEL_RENDERER_DATA_H_ diff --git a/src/ballistica/media/data/sound_data.cc b/src/ballistica/media/data/sound_data.cc new file mode 100644 index 00000000..a9048843 --- /dev/null +++ b/src/ballistica/media/data/sound_data.cc @@ -0,0 +1,322 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/data/sound_data.h" + +#include + +#include + +#if BA_ENABLE_AUDIO +#if BA_USE_TREMOR_VORBIS +#include "ivorbisfile.h" // NOLINT +#else +#include +#endif +#endif // BA_ENABLE_AUDIO + +#include +#include + +#include "ballistica/audio/audio_server.h" +#include "ballistica/media/media.h" +#include "ballistica/platform/platform.h" +#include "ballistica/python/python.h" + +// Need to move away from OpenAL on Apple stuff. +#if __clang__ +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +namespace ballistica { + +#if BA_ENABLE_AUDIO + +const int kReadBufferSize = 32768; // 32 KB buffers + +static auto CallbackRead(void* ptr, size_t size, size_t nmemb, + void* data_source) -> size_t { + return fread(ptr, size, nmemb, static_cast(data_source)); +} +static auto CallbackSeek(void* data_source, ogg_int64_t offset, int whence) + -> int { + return fseek(static_cast(data_source), + static_cast_check_fit(offset), whence); // NOLINT +} +static auto CallbackClose(void* data_source) -> int { + return fclose(static_cast(data_source)); +} +static long CallbackTell(void* data_source) { // NOLINT (vorbis uses long) + return ftell(static_cast(data_source)); +} + +// This function loads a .ogg file into a memory buffer and returns +// the format and frequency. return value is true on success or false if a +// fallback was used +static auto LoadOgg(const char* file_name, std::vector* buffer, + ALenum* format, ALsizei* freq) -> bool { + int bit_stream; + int bytes; + char array[kReadBufferSize]; // Local fixed size array + FILE* f; + bool fallback = false; + + // Open for binary reading. + f = g_platform->FOpen(file_name, "rb"); + if (f == nullptr) { + fallback = true; + Log(std::string("Error: Can't open sound file '") + file_name + + "' for reading..."); + + // Attempt a fallback standin; if that doesn't work, throw in the towel. + file_name = "data/global/audio/blank.ogg"; + f = g_platform->FOpen(file_name, "rb"); + if (f == nullptr) + throw Exception(std::string("Can't open fallback sound file '") + + file_name + "' for reading..."); + } + + vorbis_info* p_info; + OggVorbis_File ogg_file; + ov_callbacks callbacks; + callbacks.read_func = CallbackRead; + callbacks.seek_func = CallbackSeek; + callbacks.close_func = CallbackClose; + callbacks.tell_func = CallbackTell; + + // Try opening the given file + if (ov_open_callbacks(f, &ogg_file, nullptr, 0, callbacks) != 0) { + Log(std::string("Error decoding sound file '") + file_name + "'"); + + fclose(f); + + // Attempt fallback. + file_name = "data/global/audio/blank.ogg"; + f = g_platform->FOpen(file_name, "rb"); + + // If fallback doesn't work, throw in the towel. + if (f == nullptr) + throw Exception(std::string("Can't open fallback sound file '") + + file_name + "' for reading..."); + if (ov_open_callbacks(f, &ogg_file, nullptr, 0, callbacks) != 0) + throw Exception(std::string("Error decoding fallback sound file '") + + file_name + "'"); + } + + // Get some information about the OGG file. + p_info = ov_info(&ogg_file, -1); + + // Check the number of channels. Always use 16-bit samples. + if (p_info->channels == 1) { + (*format) = AL_FORMAT_MONO16; + } else { + (*format) = AL_FORMAT_STEREO16; + } + + // The frequency of the sampling rate. + (*freq) = static_cast(p_info->rate); + + bool corrupt = false; + + // Keep reading until all is read. + do { + // Read up to a buffer's worth of decoded sound data. +#if BA_USE_TREMOR_VORBIS + bytes = static_cast( + ov_read(&ogg_file, array, kReadBufferSize, &bit_stream)); +#else + bytes = static_cast( + ov_read(&ogg_file, array, kReadBufferSize, 0, 2, 1, &bit_stream)); +#endif + + // If something went wrong in the decode, just spit out an empty sound and + // an error message that the user should re-install. + if (bytes < 0) { + corrupt = true; + ov_clear(&ogg_file); + break; + } + + // Append to end of buffer + buffer->insert(buffer->end(), array, array + bytes); + } while (bytes > 0); + + // Clean up! + ov_clear(&ogg_file); + + if (corrupt) { + static bool reported_corrupt = false; + if (!reported_corrupt) { + reported_corrupt = true; + g_python->PushObjCall(Python::ObjID::kPrintCorruptFileErrorCall); + } + (*buffer) = std::vector(32 * 100, 0); + } + + if ((*buffer).empty()) { + throw Exception(std::string("Error: got zero-length buffer from ogg-file '") + + file_name + "'"); + } + return !fallback; +} + +static void LoadCachedOgg(const char* file_name, std::vector* buffer, + ALenum* format, ALsizei* freq) { + std::string sound_cache_dir = + g_platform->GetConfigDirectory() + "/audiocache"; + static bool made_sound_cache_dir = false; + if (!made_sound_cache_dir) { + g_platform->MakeDir(sound_cache_dir); + made_sound_cache_dir = true; + } + std::vector b(strlen(file_name) + 1); + memcpy(b.data(), file_name, b.size()); + for (char* c = &b[0]; *c != 0; c++) { + if ((*c) == '/') *c = '_'; + } + std::string cache_file_name = sound_cache_dir + "/" + &b[0] + ".cache"; + + // If we have a cache file and it matches the mod time on the ogg, attempt to + // load it. + struct BA_STAT stat_ogg {}; + time_t ogg_mod_time = 0; + if (g_platform->Stat(file_name, &stat_ogg) == 0) { + ogg_mod_time = stat_ogg.st_mtime; + } + FILE* f_cache = g_platform->FOpen(cache_file_name.c_str(), "rb"); + if (f_cache && ogg_mod_time != 0) { + bool got_cache = false; + time_t cache_mod_time; + if (fread(&cache_mod_time, sizeof(cache_mod_time), 1, f_cache) == 1) { + if (cache_mod_time == ogg_mod_time) { + if (fread(&(*format), sizeof((*format)), 1, f_cache) == 1) { + if (fread(&(*freq), sizeof((*freq)), 1, f_cache) == 1) { + uint32_t buffer_size; + if (fread(&buffer_size, sizeof(buffer_size), 1, f_cache) == 1) { + (*buffer).resize(buffer_size); + if (fread(&(*buffer)[0], buffer_size, 1, f_cache) == 1) { + got_cache = true; + } + } + } + } + } + } + fclose(f_cache); + if (got_cache) { + // At a loss for how this happened, but wound up loading cache files + // with invalid formats of 0 once. Report and ignore if we see + // something like that. + if (*format != AL_FORMAT_MONO16 && *format != AL_FORMAT_STEREO16) { + Log(std::string("Ignoring invalid audio cache of ") + file_name + + " with format " + std::to_string(*format)); + } else { + return; // SUCCESS!!!! + } + } + } + + // Ok that didn't work. Load the actual ogg. + (*buffer).clear(); + bool success = LoadOgg(file_name, buffer, format, freq); + + // If the load went cleanly, attempt to write a cache file. + if (success) { + FILE* f = g_platform->FOpen(cache_file_name.c_str(), "wb"); + bool success2 = false; + if (f) { + if (fwrite(&ogg_mod_time, sizeof(ogg_mod_time), 1, f) == 1) { + if (fwrite(&(*format), sizeof((*format)), 1, f) == 1) { + if (fwrite(&(*freq), sizeof((*freq)), 1, f) == 1) { + auto buffer_size = static_cast((*buffer).size()); + if (fwrite(&buffer_size, sizeof(buffer_size), 1, f) == 1) { + if (fwrite(&(*buffer)[0], buffer_size, 1, f) == 1) { + success2 = true; + } + } + } + } + } + fclose(f); + + // Attempt to clean up if it looks like something went wrong. + if (!success2) { + g_platform->Unlink(cache_file_name.c_str()); + } + } + } +} + +#endif // BA_ENABLE_AUDIO + +SoundData::SoundData(const std::string& file_name_in) + : file_name_(file_name_in), + is_streamed_(false), +#if BA_ENABLE_AUDIO + buffer_(0), +#endif // BA_ENABLE_AUDIO + last_play_time_(0) { + file_name_full_ = + g_media->FindMediaFile(Media::FileType::kSound, file_name_in); + valid_ = true; +} + +void SoundData::DoPreload() { +#if BA_ENABLE_AUDIO + + // Its an ogg sound file. + // if it has 'music' in its name, we'll stream it; + // otherwise we load it in its entirety into our load-buffer. + if (strstr(file_name_full_.c_str(), "Music.ogg")) { + is_streamed_ = true; + } else if (strstr(file_name_full_.c_str(), ".ogg")) { + is_streamed_ = false; + LoadCachedOgg(file_name_full_.c_str(), &load_buffer_, &format_, &freq_); + } else { + throw Exception("Unsupported sound file (needs to end in .ogg): '" + + file_name_full_ + "'"); + } +#endif // BA_ENABLE_AUDIO +} + +void SoundData::DoLoad() { + assert(InAudioThread()); + assert(valid_); + +#if BA_ENABLE_AUDIO + assert(!g_audio_server->paused()); + + // Note: streamed sources create buffers as they're used; not here. + if (!is_streamed_) { + // Generate our buffer. + CHECK_AL_ERROR; + alGenBuffers(1, &buffer_); + CHECK_AL_ERROR; + + // Preload pulled data into our load-buffer, and send that along to openal. + alBufferData(buffer_, format_, &load_buffer_[0], + static_cast(load_buffer_.size()), freq_); + + CHECK_AL_ERROR; + + // Done with load buffer; clear its used memory. + std::vector().swap(load_buffer_); + } + + CHECK_AL_ERROR; +#endif // BA_ENABLE_AUDIO +} + +void SoundData::DoUnload() { + assert(valid_); + assert(InAudioThread()); +#if BA_ENABLE_AUDIO + if (!is_streamed_) { + assert(buffer_); + CHECK_AL_ERROR; + alDeleteBuffers(1, &buffer_); + CHECK_AL_ERROR; + } +#endif // BA_ENABLE_AUDIO +} + +} // namespace ballistica diff --git a/src/ballistica/media/data/sound_data.h b/src/ballistica/media/data/sound_data.h new file mode 100644 index 00000000..f1bc3092 --- /dev/null +++ b/src/ballistica/media/data/sound_data.h @@ -0,0 +1,58 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_DATA_SOUND_DATA_H_ +#define BALLISTICA_MEDIA_DATA_SOUND_DATA_H_ + +#include +#include + +#include "ballistica/audio/al_sys.h" +#include "ballistica/media/data/media_component_data.h" + +namespace ballistica { + +class SoundData : public MediaComponentData { + public: + SoundData() = default; + explicit SoundData(const std::string& file_name_in); + void DoPreload() override; + void DoLoad() override; + + // FIXME: Should make sure the sound_data isn't in use before unloading it. + void DoUnload() override; + auto GetMediaType() const -> MediaType override { return MediaType::kSound; } + auto GetName() const -> std::string override { + if (!file_name_full_.empty()) + return file_name_full_; + else + return "invalid sound"; + } +#if BA_ENABLE_AUDIO + auto format() const -> ALenum { return format_; } + auto buffer() const -> ALuint { + assert(!is_streamed_); + return buffer_; + } +#endif // BA_ENABLE_AUDIO + auto is_streamed() const -> bool { return is_streamed_; } + auto file_name() const -> const std::string& { return file_name_; } + auto file_name_full() const -> const std::string& { return file_name_full_; } + void UpdatePlayTime() { last_play_time_ = GetRealTime(); } + auto last_play_time() const -> millisecs_t { return last_play_time_; } + + private: + std::string file_name_; + std::string file_name_full_; + bool is_streamed_{}; +#if BA_ENABLE_AUDIO + ALuint buffer_{}; + ALenum format_{}; + ALsizei freq_{}; +#endif // BA_ENABLE_AUDIO + std::vector load_buffer_; + millisecs_t last_play_time_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_DATA_SOUND_DATA_H_ diff --git a/src/ballistica/media/data/texture_data.cc b/src/ballistica/media/data/texture_data.cc new file mode 100644 index 00000000..0b43a6e1 --- /dev/null +++ b/src/ballistica/media/data/texture_data.cc @@ -0,0 +1,450 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/data/texture_data.h" + +#include +#include +#include +#include + +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/graphics/renderer.h" +#include "ballistica/graphics/text/text_packer.h" +#include "ballistica/graphics/texture/dds.h" +#include "ballistica/graphics/texture/ktx.h" +#include "ballistica/graphics/texture/pvr.h" +#include "ballistica/media/data/texture_preload_data.h" +#include "ballistica/media/data/texture_renderer_data.h" +#include "ballistica/media/media.h" +#include "external/qr_code_generator/QrCode.hpp" + +namespace ballistica { + +static void rgba8888_unpremultiply_in_place(void* src, size_t cb) { + // Compute the actual number of pixel elements in the buffer. + size_t cpel = cb / 4; + auto* psrc = static_cast(src); + auto* pdst = static_cast(src); + for (size_t i = 0; i < cpel; i++) { + int r = *psrc++; + int g = *psrc++; + int b = *psrc++; + int a = *psrc++; + if (a == 0) { + *pdst++ = 255; + *pdst++ = 255; + *pdst++ = 255; + *pdst++ = 0; + } else { + *pdst++ = static_cast_check_fit(std::min(255, r * 255 / a)); + *pdst++ = static_cast_check_fit(std::min(255, g * 255 / a)); + *pdst++ = static_cast_check_fit(std::min(255, b * 255 / a)); + *pdst++ = static_cast_check_fit(a); + } + } +} + +TextureData::TextureData() = default; +TextureData::TextureData(const std::string& file_in, TextureType type_in, + TextureMinQuality min_quality_in) + : file_name_(file_in), type_(type_in), min_quality_(min_quality_in) { + file_name_full_ = g_media->FindMediaFile(Media::FileType::kTexture, file_in); + valid_ = true; +} + +TextureData::TextureData(TextPacker* packer) : packer_(packer) { + file_name_ = packer->hash(); + valid_ = true; +} + +TextureData::TextureData(const std::string& qr_url) : is_qr_code_(true) { + file_name_ = qr_url; + valid_ = true; +} + +TextureData::~TextureData() = default; + +void TextureData::DoPreload() { + assert(valid_); + + assert(g_graphics_server + && g_graphics_server->texture_compression_types_are_set()); + + // We figure out which LOD should be our base level based on quality. + TextureQuality texture_quality = g_graphics_server->texture_quality(); + + // If we're a text-texture. + if (packer_.exists()) { + assert(type_ == TextureType::k2D); + + int width = packer_->texture_width(); + int height = packer_->texture_height(); + float quality_scale = 1.0f; + + if (texture_quality == TextureQuality::kMedium) { + width /= 2; + height /= 2; + quality_scale *= 0.5f; + } else if (texture_quality == TextureQuality::kLow) { + width /= 4; + height /= 4; + quality_scale *= 0.25f; + } + float scale = packer_->text_scale() * quality_scale; + + std::vector strings; + std::vector positions; + std::vector visible_widths; + + int index = 0; + const std::list& spans = packer_->spans(); + for (const auto& span : spans) { + strings.push_back(span.string); + positions.push_back(span.tex_x * quality_scale); + positions.push_back(span.tex_y * quality_scale); + visible_widths.push_back((span.bounds.r - span.bounds.l)); + index++; + } + + assert(!strings.empty()); + assert(strings.size() * 2 == positions.size()); + + void* tex_ref{g_platform->CreateTextTexture( + width, height, strings, positions, visible_widths, scale)}; + uint8_t* pixels{g_platform->GetTextTextureData(tex_ref)}; + + assert(pixels); + assert(tex_ref); + + // For now just copy it over to our local 32 bit buffer. + // As an optimization we could convert it to RGBA4444 on the fly or perhaps + // even just alpha if there's no non-white colors present. + // NOTE: This data is also coming in premultiplied (on apple at least) so we + // need to take care of that. + preload_datas_.resize(1); + assert(width >= 0 && height >= 0); + size_t buffer_size = + static_cast(width) * static_cast(height) * 4u; + auto* buffer = static_cast(malloc(buffer_size)); + preload_datas_[0].buffers[0] = buffer; + memcpy(buffer, pixels, buffer_size); + rgba8888_unpremultiply_in_place(buffer, buffer_size); + preload_datas_[0].widths[0] = width; + preload_datas_[0].heights[0] = height; + preload_datas_[0].formats[0] = TextureFormat::kRGBA_8888; + preload_datas_[0].base_level = 0; + + g_platform->FreeTextTexture(tex_ref); + + // Downsample this down to rgba4444 in-place. + TexturePreloadData::rgba8888_to_rgba4444_in_place(buffer, buffer_size); + preload_datas_[0].formats[0] = TextureFormat::kRGBA_4444; + + } else if (is_qr_code_) { + const qrcodegen::QrCode qr2{qrcodegen::QrCode::encodeText( + file_name_.c_str(), qrcodegen::QrCode::Ecc::HIGH)}; + int qr_size = qr2.getSize(); + + int width = 512; + int height = 512; + preload_datas_.resize(1); + assert(width >= 0 && height >= 0); + size_t buffer_size = + static_cast(width) * static_cast(height) * 2u; + auto* buffer = static_cast(malloc(buffer_size)); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + float xf = static_cast(x) / static_cast(width); + float yf = static_cast(y) / static_cast(height); + uint16_t* dst = buffer + width * y + x; + int x2 = static_cast( + floor((xf - 0.05f) * (static_cast(qr_size) * 1.1f))); + int y2 = static_cast( + floor((yf - 0.05f) * (static_cast(qr_size) * 1.1f))); + if (x2 >= 0 && x2 < qr_size && y2 >= 0 && y2 < qr_size + && qr2.getModule(x2, y2)) { + *dst = 0; + } else { + *dst = 0xffff; + } + } + } + preload_datas_[0].buffers[0] = reinterpret_cast(buffer); + preload_datas_[0].widths[0] = width; + preload_datas_[0].heights[0] = height; + preload_datas_[0].formats[0] = TextureFormat::kRGB_565; + preload_datas_[0].base_level = 0; + } else { + if (type_ == TextureType::k2D) { + preload_datas_.resize(1); + + int file_name_size = static_cast(file_name_full_.size()); + BA_PRECONDITION(file_name_size > 4); + + // Etc1 or dxt3 for non-alpha and dxt5 for alpha (.android_dds files). + if (file_name_size > 12 + && !strcmp(file_name_full_.c_str() + file_name_size - 12, + ".android_dds")) { +#if BA_ENABLE_OPENGL + LoadDDS(file_name_full_, preload_datas_[0].buffers, + preload_datas_[0].widths, preload_datas_[0].heights, + preload_datas_[0].formats, preload_datas_[0].sizes, + texture_quality, static_cast(min_quality_), + &preload_datas_[0].base_level); +#else + throw Exception(); +#endif + + // We should only be loading this if we support etc1 in hardware. + assert(g_graphics_server->SupportsTextureCompressionType( + TextureCompressionType::kETC1)); + + // Decompress dxt1/dxt5 ones if we don't natively support S3TC. + if (!g_graphics_server->SupportsTextureCompressionType( + TextureCompressionType::kS3TC)) { + if ((preload_datas_[0].formats[preload_datas_[0].base_level] + == TextureFormat::kDXT5) + || (preload_datas_[0].formats[preload_datas_[0].base_level] + == TextureFormat::kDXT1)) { + preload_datas_[0].ConvertToUncompressed(this); + } + } + } else if (!strcmp(file_name_full_.c_str() + file_name_size - 4, + ".dds")) { + // Dxt1 for non-alpha and dxt5 for alpha (.dds files). +#if BA_ENABLE_OPENGL + LoadDDS(file_name_full_, preload_datas_[0].buffers, + preload_datas_[0].widths, preload_datas_[0].heights, + preload_datas_[0].formats, preload_datas_[0].sizes, + texture_quality, static_cast(min_quality_), + &preload_datas_[0].base_level); +#else + throw Exception(); +#endif + + // Decompress dxt1/dxt5 if we don't natively support it. + if (!g_graphics_server->SupportsTextureCompressionType( + TextureCompressionType::kS3TC)) { + preload_datas_[0].ConvertToUncompressed(this); + } + } else if (!strcmp(file_name_full_.c_str() + file_name_size - 4, + ".ktx")) { + // Etc2 or etc1 for non-alpha and etc2 for alpha (.ktx files). + try { +#if BA_ENABLE_OPENGL + LoadKTX(file_name_full_, preload_datas_[0].buffers, + preload_datas_[0].widths, preload_datas_[0].heights, + preload_datas_[0].formats, preload_datas_[0].sizes, + texture_quality, static_cast(min_quality_), + &preload_datas_[0].base_level); +#else + throw Exception(); +#endif + } catch (const std::exception& e) { + throw Exception("Error loading file '" + file_name_full_ + + "': " + e.what()); + } + + // Decompress etc2 if we don't natively support it. + if (((preload_datas_[0].formats[preload_datas_[0].base_level] + == TextureFormat::kETC2_RGB) + || (preload_datas_[0].formats[preload_datas_[0].base_level] + == TextureFormat::kETC2_RGBA)) + && (!g_graphics_server->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_graphics_server->SupportsTextureCompressionType( + TextureCompressionType::kETC1))) { + preload_datas_[0].ConvertToUncompressed(this); + } + + } else if (!strcmp(file_name_full_.c_str() + file_name_size - 4, + ".pvr")) { + // Pvr for all (.pvr files). + LoadPVR(file_name_full_, preload_datas_[0].buffers, + preload_datas_[0].widths, preload_datas_[0].heights, + preload_datas_[0].formats, preload_datas_[0].sizes, + texture_quality, static_cast(min_quality_), + &preload_datas_[0].base_level); + + // We should only be loading this if we support pvr in hardware. + assert(g_graphics_server->SupportsTextureCompressionType( + TextureCompressionType::kPVR)); + } else if (!strcmp(file_name_full_.c_str() + file_name_size - 4, + ".nop")) { + // Dummy path for headless; nothing to do here. + } else { + throw Exception("Invalid texture file name: '" + file_name_full_ + "'"); + } + + } else if (type_ == TextureType::kCubeMap) { + preload_datas_.resize(6); + std::string name; + int file_name_size = static_cast(file_name_full_.size()); + BA_PRECONDITION(file_name_size > 4); + for (int d = 0; d < 6; d++) { + name = file_name_full_; + switch (d) { + case 0: + name.replace(name.find('#'), 1, "_+x"); + break; + case 1: + name.replace(name.find('#'), 1, "_-x"); + break; + case 2: + name.replace(name.find('#'), 1, "_+y"); + break; + case 3: + name.replace(name.find('#'), 1, "_-y"); + break; + case 4: + name.replace(name.find('#'), 1, "_+z"); + break; + case 5: + name.replace(name.find('#'), 1, "_-z"); + break; + default: + throw Exception(); + } + + // Etc1 or dxt3 for non-alpha and dxt5 for alpha (.android_dds files). + if (file_name_size > 12 + && !strcmp(file_name_full_.c_str() + file_name_size - 12, + ".android_dds")) { + try { +#if BA_ENABLE_OPENGL + LoadDDS(name, preload_datas_[d].buffers, preload_datas_[d].widths, + preload_datas_[d].heights, preload_datas_[d].formats, + preload_datas_[d].sizes, texture_quality, + static_cast(min_quality_), + &preload_datas_[d].base_level); +#else + throw Exception(); +#endif + } catch (const std::exception& e) { + throw Exception("Error loading file '" + file_name_full_ + + "': " + e.what()); + } + + // We should only be loading this if we support etc1 in hardware. + assert(g_graphics_server->SupportsTextureCompressionType( + TextureCompressionType::kETC1)); + + // Decompress dxt1/dxt5 ones if we don't natively support S3TC. + if (!g_graphics_server->SupportsTextureCompressionType( + TextureCompressionType::kS3TC)) { + if ((preload_datas_[d].formats[preload_datas_[d].base_level] + == TextureFormat::kDXT5) + || (preload_datas_[d].formats[preload_datas_[d].base_level] + == TextureFormat::kDXT1)) { + preload_datas_[d].ConvertToUncompressed(this); + } + } + } else if (!strcmp(file_name_full_.c_str() + file_name_size - 4, + ".dds")) { +#if BA_ENABLE_OPENGL + // Dxt1 for non-alpha and dxt5 for alpha (.dds files). + LoadDDS(name, preload_datas_[d].buffers, preload_datas_[d].widths, + preload_datas_[d].heights, preload_datas_[d].formats, + preload_datas_[d].sizes, texture_quality, + static_cast(min_quality_), + &preload_datas_[d].base_level); +#else + throw Exception(); +#endif + + // Decompress dxt1/dxt5 if we don't natively support it. + if (!g_graphics_server->SupportsTextureCompressionType( + TextureCompressionType::kS3TC)) { + preload_datas_[d].ConvertToUncompressed(this); + } + } else if (!strcmp(file_name_full_.c_str() + file_name_size - 4, + ".ktx")) { + // Etc2 or etc1 for non-alpha and etc2 for alpha (.ktx files) +#if BA_ENABLE_OPENGL + LoadKTX(name, preload_datas_[d].buffers, preload_datas_[d].widths, + preload_datas_[d].heights, preload_datas_[d].formats, + preload_datas_[d].sizes, texture_quality, + static_cast(min_quality_), + &preload_datas_[d].base_level); +#else + throw Exception(); +#endif + + // Decompress etc2 ones if we don't natively support them. + if (((preload_datas_[d].formats[preload_datas_[d].base_level] + == TextureFormat::kETC2_RGB) + || (preload_datas_[d].formats[preload_datas_[d].base_level] + == TextureFormat::kETC2_RGBA)) + && (!g_graphics_server->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_graphics_server->SupportsTextureCompressionType( + TextureCompressionType::kETC1))) { + preload_datas_[d].ConvertToUncompressed(this); + } + + } else if (!strcmp(file_name_full_.c_str() + file_name_size - 4, + ".pvr")) { + // Pvr for both non-alpha and alpha (.pvr files). + try { + LoadPVR(name, preload_datas_[d].buffers, preload_datas_[d].widths, + preload_datas_[d].heights, preload_datas_[d].formats, + preload_datas_[d].sizes, texture_quality, + static_cast(min_quality_), + &preload_datas_[d].base_level); + } catch (const std::exception& e) { + throw Exception("Error loading file '" + file_name_full_ + + "': " + e.what()); + } + } else if (!strcmp(file_name_full_.c_str() + file_name_size - 4, + ".nop")) { + // Dummy path for headless; nothing to do here. + } else { + throw Exception("Invalid texture file name: '" + file_name_full_ + + "'"); + } + } + } else { + throw Exception("unknown texture type"); + } + } +} + +void TextureData::DoLoad() { + assert(InGraphicsThread()); + assert(!renderer_data_.exists()); + renderer_data_ = Object::MakeRefCounted( + g_graphics_server->renderer()->NewTextureData(*this)); + assert(renderer_data_.exists()); + renderer_data_->Load(); + + // Store our base-level from the preload-data so we know if we're lower than + // full quality. + assert(!preload_datas_.empty()); + base_level_ = preload_datas_[0].base_level; + + // If we're done, kill our preload data. + preload_datas_.clear(); +} + +void TextureData::DoUnload() { + assert(InGraphicsThread()); + assert(valid_); + assert(renderer_data_.exists()); + renderer_data_.Clear(); + base_level_ = 0; +} + +} // namespace ballistica diff --git a/src/ballistica/media/data/texture_data.h b/src/ballistica/media/data/texture_data.h new file mode 100644 index 00000000..1083f101 --- /dev/null +++ b/src/ballistica/media/data/texture_data.h @@ -0,0 +1,62 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_DATA_TEXTURE_DATA_H_ +#define BALLISTICA_MEDIA_DATA_TEXTURE_DATA_H_ + +#include +#include + +#include "ballistica/media/data/media_component_data.h" + +namespace ballistica { + +// Loadable texture media component. +class TextureData : public MediaComponentData { + public: + TextureData(); + ~TextureData() override; + + // pass a newly allocated TextPacker pointer here; TextureData takes ownership + // and handles cleaning it up + explicit TextureData(TextPacker* packer); + explicit TextureData(const std::string& file_in, TextureType type_in, + TextureMinQuality min_quality_in); + explicit TextureData(const std::string& qr_url); + auto GetName() const -> std::string override { + return (!file_name_.empty()) ? file_name_ : "invalid texture"; + } + auto GetNameFull() const -> std::string override { return file_name_full(); } + auto file_name() const -> const std::string& { return file_name_; } + auto file_name_full() const -> const std::string& { return file_name_full_; } + auto GetMediaType() const -> MediaType override { + return MediaType::kTexture; + } + void DoPreload() override; + void DoLoad() override; + void DoUnload() override; + auto texture_type() const -> TextureType { return type_; } + auto is_qr_code() const -> bool { return is_qr_code_; } + auto preload_datas() const -> const std::vector& { + return preload_datas_; + } + auto renderer_data() const -> TextureRendererData* { + assert(renderer_data_.exists()); + return renderer_data_.get(); + } + auto base_level() const -> int { return base_level_; } + + private: + Object::Ref packer_; + bool is_qr_code_ = false; + std::string file_name_; + std::string file_name_full_; + std::vector preload_datas_; + TextureType type_ = TextureType::k2D; + TextureMinQuality min_quality_ = TextureMinQuality::kLow; + Object::Ref renderer_data_; + int base_level_ = 0; +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_DATA_TEXTURE_DATA_H_ diff --git a/src/ballistica/media/data/texture_preload_data.cc b/src/ballistica/media/data/texture_preload_data.cc new file mode 100644 index 00000000..aa78d528 --- /dev/null +++ b/src/ballistica/media/data/texture_preload_data.cc @@ -0,0 +1,577 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/data/texture_preload_data.h" + +#include +#include + +#include "ballistica/graphics/texture/ktx.h" +#include "ballistica/media/component/texture.h" + +namespace ballistica { + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#pragma ide diagnostic ignored "bugprone-narrowing-conversions" + +#ifndef GL_COMPRESSED_RGB8_ETC2 +#define GL_COMPRESSED_RGB8_ETC2 0x9274 +#endif +#ifndef GL_COMPRESSED_RGBA8_ETC2_EAC +#define GL_COMPRESSED_RGBA8_ETC2_EAC 0x9278 +#endif +#ifndef GL_ETC1_RGB8_OES +#define GL_ETC1_RGB8_OES 0x8D64 +#endif + +void TexturePreloadData::rgba8888_to_rgba4444_in_place(void* src, size_t cb) { + // Compute the actual number of pixel elements in the buffer. + size_t cpel = cb / 4; + auto* psrc = static_cast(src); + auto* pdst = static_cast(src); + + int r_dither = 0; + int g_dither = 0; + int b_dither = 0; + int a_dither = 0; + + // reset our dithering slightly randomly to reduce + // patterns (might be a smarter way to do this) + int d_reset = rand() % 100; // NOLINT + + // Convert every pixel. + for (size_t i = 0; i < cpel; i++) { + // Read a source pixel. + int pel = psrc[i]; // NOLINT + + // Unpack the source data as 8 bit values. + int r = pel & 0xff; + int g = (pel >> 8) & 0xff; + int b = (pel >> 16) & 0xff; + int a = (pel >> 24) & 0xff; + r = std::min(255, std::max(0, r + r_dither)); + g = std::min(255, std::max(0, g + g_dither)); + b = std::min(255, std::max(0, b + b_dither)); + a = std::min(255, std::max(0, a + a_dither)); + // convert to 4 bit values + int r2 = r >> 4; + int g2 = g >> 4; + int b2 = b >> 4; + int a2 = a >> 4; + + r_dither = r - (r2 << 4); + g_dither = g - (g2 << 4); + b_dither = b - (b2 << 4); + a_dither = a - (a2 << 4); + + d_reset--; + if (d_reset <= 0) { + r_dither = g_dither = b_dither = a_dither = 0; + d_reset = rand() % 100; // NOLINT + } + + pdst[i] = + static_cast_check_fit(a2 | b2 << 4 | g2 << 8 | r2 << 12); + } +} + +static void rgba8888_to_rgb888_in_place(void* src, int cb) { + int i; + + // Compute the actual number of pixel elements in the buffer. + int cpel = cb / 4; + auto* psrc = static_cast(src); + auto* pdst = static_cast(src); + for (i = 0; i < cpel; i++) { + *pdst++ = *psrc++; // NOLINT + *pdst++ = *psrc++; + *pdst++ = *psrc++; + psrc++; + } +} + +static void rgb888_to_rgb565_in_place(void* src, size_t cb) { + // compute the actual number of pixel elements in the buffer. + size_t cpel = cb / 3; + auto* psrc = static_cast(src); + auto* pdst = static_cast(src); + + int r_dither = 0; + int g_dither = 0; + int b_dither = 0; + + // reset our dithering slightly randomly to reduce + // patterns (might be a smarter way to do this) + int d_reset = rand() % 100; // NOLINT + + // convert every pixel + for (size_t i = 0; i < cpel; i++) { + // read a source pixel + int r = *psrc++; // NOLINT + int g = *psrc++; + int b = *psrc++; + // unpack the source data as 8 bit values + r = std::min(255, std::max(0, r + r_dither)); + g = std::min(255, std::max(0, g + g_dither)); + b = std::min(255, std::max(0, b + b_dither)); + + // convert to 565 + int r2 = r >> 3; + int g2 = g >> 2; + int b2 = b >> 3; + + r_dither = r - (r2 << 3); + g_dither = g - (g2 << 2); + b_dither = b - (b2 << 3); + + d_reset--; + if (d_reset <= 0) { + r_dither = g_dither = b_dither = 0; + d_reset = rand() % 100; // NOLINT + } + + *pdst++ = static_cast_check_fit(b2 | g2 << 5 | r2 << 11); + } +} + +// ----------------------------------------------------------------------------- +// S3TC DXT1 / DXT5 Texture Decompression Routines +// Author: Benjamin Dobell - http://www.glassechidna.com.au +// +// Feel free to use these methods in any open-source, freeware or commercial +// projects. It's not necessary to credit me however I would be grateful if you +// chose to do so. I'll also be very interested to hear what projects make use +// of this code. Feel free to drop me a line via the contact form on the Glass +// Echidna website. +// +// NOTE: The code was written for a little endian system where sizeof(int32_t) +// == 4. +// ----------------------------------------------------------------------------- + +// uint32_t PackRGBA(): Helper method that packs RGBA channels into a single 4 +// byte pixel. +// +// unsigned char r: red channel. +// unsigned char g: green channel. +// unsigned char b: blue channel. +// unsigned char a: alpha channel. + +static auto PackRGBA(unsigned char r, unsigned char g, unsigned char b, + unsigned char a) -> uint32_t { + return ((a << 24) | (b << 16) | (g << 8) | r); +} + +// void DecompressBlockDXT1(): Decompresses one block of a DXT1 texture and +// stores the resulting pixels at the appropriate offset in 'image'. +// +// uint32_t x: x-coordinate of the +// first pixel in the block. uint32_t y: y-coordinate of the first pixel in the +// block. uint32_t width: width of the texture being decompressed. uint32_t +// height: height of the texture being decompressed. const unsigned char +// *blockStorage: pointer to the block to decompress. uint32_t *image: +// pointer to image where the decompressed pixel data should be stored. +static void DecompressBlockDXT1(uint32_t x, uint32_t y, uint32_t width, + uint32_t height, + const unsigned char* block_storage, + uint32_t* image) { + uint16_t color0, color1; + memcpy(&color0, block_storage, sizeof(color0)); + memcpy(&color1, block_storage + 2, sizeof(color1)); + + uint32_t temp; + + temp = (color0 >> 11u) * 255u + 16u; + auto r0 = (unsigned char)((temp / 32u + temp) / 32u); + temp = ((color0 & 0x07E0u) >> 5u) * 255u + 32u; + auto g0 = (unsigned char)((temp / 64u + temp) / 64u); + temp = (color0 & 0x001Fu) * 255u + 16u; + auto b0 = (unsigned char)((temp / 32 + temp) / 32); + + temp = (color1 >> 11u) * 255u + 16u; + auto r1 = (unsigned char)((temp / 32u + temp) / 32u); + temp = ((color1 & 0x07E0u) >> 5u) * 255u + 32u; + auto g1 = (unsigned char)((temp / 64u + temp) / 64u); + temp = (color1 & 0x001Fu) * 255u + 16u; + auto b1 = (unsigned char)((temp / 32u + temp) / 32u); + + uint32_t code = *reinterpret_cast(block_storage + 4); + + for (int j = 0; j < 4; j++) { + for (int i = 0; i < 4; i++) { + uint32_t final_color = 0; + auto positionCode = + static_cast((code >> 2 * (4 * j + i)) & 0x03); + + if (color0 > color1) { + switch (positionCode) { + case 0: + final_color = PackRGBA(r0, g0, b0, 255); + break; + case 1: + final_color = PackRGBA(r1, g1, b1, 255); + break; + case 2: + final_color = + PackRGBA(static_cast((2 * r0 + r1) / 3), + static_cast((2 * g0 + g1) / 3), + static_cast((2 * b0 + b1) / 3), 255); + break; + case 3: + final_color = + PackRGBA(static_cast((r0 + 2 * r1) / 3), + static_cast((g0 + 2 * g1) / 3), + static_cast((b0 + 2 * b1) / 3), 255); + break; + default: + break; + } + } else { + switch (positionCode) { + case 0: + final_color = PackRGBA(r0, g0, b0, 255); + break; + case 1: + final_color = PackRGBA(r1, g1, b1, 255); + break; + case 2: + final_color = PackRGBA(static_cast((r0 + r1) / 2), + static_cast((g0 + g1) / 2), + static_cast((b0 + b1) / 2), 255); + break; + case 3: + final_color = PackRGBA(0, 0, 0, 255); + break; + default: + break; + } + } + if ((x + i < width) && (y + j < height)) { + image[(y + j) * width + (x + i)] = final_color; + } + } + } +} + +// void BlockDecompressImageDXT1(): Decompresses all the blocks of a DXT1 +// compressed texture and stores the resulting pixels in 'image'. +// +// uint32_t width: Texture width. +// uint32_t height: Texture height. +// const unsigned char *block_storage: pointer to compressed DXT1 blocks. +// uint32_t *image: pointer to the image where the +// decompressed pixels will be stored. + +static void BlockDecompressImageDXT1(uint32_t width, uint32_t height, + const unsigned char* block_storage, + uint32_t* image) { + uint32_t block_count_x = (width + 3) / 4; + uint32_t block_count_y = (height + 3) / 4; + // uint32_t blockWidth = (width < 4) ? width : 4; + // uint32_t blockHeight = (height < 4) ? height : 4; + + for (uint32_t j = 0; j < block_count_y; j++) { + for (uint32_t i = 0; i < block_count_x; i++) { + DecompressBlockDXT1(i * 4, j * 4, width, height, block_storage + i * 8, + image); + } + block_storage += block_count_x * 8; + } +} + +// void DecompressBlockDXT5(): Decompresses one block of a DXT5 texture and +// stores the resulting pixels at the appropriate offset in 'image'. +// +// uint32_t x: x-coordinate of the +// first pixel in the block. uint32_t y: y-coordinate of the first pixel in the +// block. uint32_t width: width of the texture being decompressed. uint32_t +// height: height of the texture being decompressed. const unsigned char +// *block_storage: pointer to the block to decompress. uint32_t *image: +// pointer to image where the decompressed pixel data should be stored. + +static void DecompressBlockDXT5(uint32_t x, uint32_t y, uint32_t width, + uint32_t height, const uint8_t* block_storage, + uint32_t* image) { + uint8_t alpha0 = *block_storage; + uint8_t alpha1 = *(block_storage + 1); + + const uint8_t* bits = block_storage + 2; + uint32_t alpha_code_1 = + bits[2] | (bits[3] << 8) | (bits[4] << 16) | (bits[5] << 24); + uint16_t alpha_code_2 = bits[0] | (bits[1] << 8); + + uint16_t color0, color1; + memcpy(&color0, block_storage + 8, sizeof(color0)); + memcpy(&color1, block_storage + 10, sizeof(color1)); + + uint32_t temp; + + temp = (color0 >> 11u) * 255u + 16u; + auto r0 = (uint8_t)((temp / 32u + temp) / 32u); + temp = ((color0 & 0x07E0u) >> 5u) * 255u + 32u; + auto g0 = (uint8_t)((temp / 64u + temp) / 64u); + temp = (color0 & 0x001Fu) * 255u + 16u; + auto b0 = (uint8_t)((temp / 32u + temp) / 32u); + + temp = (color1 >> 11u) * 255u + 16u; + auto r1 = (uint8_t)((temp / 32u + temp) / 32u); + temp = ((color1 & 0x07E0u) >> 5u) * 255u + 32u; + auto g1 = (uint8_t)((temp / 64u + temp) / 64u); + temp = (color1 & 0x001Fu) * 255u + 16u; + auto b1 = (uint8_t)((temp / 32u + temp) / 32u); + + uint32_t code = *reinterpret_cast(block_storage + 12); + + for (int j = 0; j < 4; j++) { + for (int i = 0; i < 4; i++) { + int alpha_code_index = 3 * (4 * j + i); + int alpha_code; + + if (alpha_code_index <= 12) { + alpha_code = (alpha_code_2 >> alpha_code_index) & 0x07; + } else if (alpha_code_index == 15) { + // NOLINTNEXTLINE + alpha_code = (alpha_code_2 >> 15) | ((alpha_code_1 << 1) & 0x06); + } else { + // NOLINTNEXTLINE + alpha_code = (alpha_code_1 >> (alpha_code_index - 16)) & 0x07; + } + + uint8_t final_alpha; + if (alpha_code == 0) { + final_alpha = alpha0; + } else if (alpha_code == 1) { + final_alpha = alpha1; + } else { + if (alpha0 > alpha1) { + final_alpha = static_cast( + ((8 - alpha_code) * alpha0 + (alpha_code - 1) * alpha1) / 7); + } else { + if (alpha_code == 6) { + final_alpha = 0; + } else if (alpha_code == 7) { + final_alpha = 255; + } else { + final_alpha = static_cast( + ((6 - alpha_code) * alpha0 + (alpha_code - 1) * alpha1) / 5); + } + } + } + + auto colorCode = static_cast((code >> 2 * (4 * j + i)) & 0x03); + + uint32_t final_color = 0; + switch (colorCode) { + case 0: + final_color = PackRGBA(r0, g0, b0, final_alpha); + break; + case 1: + final_color = PackRGBA(r1, g1, b1, final_alpha); + break; + case 2: + final_color = + PackRGBA(static_cast((2 * r0 + r1) / 3), + static_cast((2 * g0 + g1) / 3), + static_cast((2 * b0 + b1) / 3), final_alpha); + break; + case 3: + final_color = + PackRGBA(static_cast((r0 + 2 * r1) / 3), + static_cast((g0 + 2 * g1) / 3), + static_cast((b0 + 2 * b1) / 3), final_alpha); + break; + default: + break; + } + + if ((x + i < width) && (y + j < height)) { + image[(y + j) * width + (x + i)] = final_color; + } + } + } +} + +static void BlockDecompressImageDXT5(uint32_t width, uint32_t height, + const uint8_t* block_storage, + uint32_t* image) { + uint32_t block_count_x = (width + 3) / 4; + uint32_t block_count_y = (height + 3) / 4; + for (uint32_t j = 0; j < block_count_y; j++) { + for (uint32_t i = 0; i < block_count_x; i++) + DecompressBlockDXT5(i * 4, j * 4, width, height, block_storage + i * 16, + image); + block_storage += block_count_x * 16; + } +} + +void TexturePreloadData::ConvertToUncompressed(TextureData* texture) { + // FIXME; we could technically get better quality on our + // lower mip levels by dynamically generating them in this + // case instead of decompressing each level. + for (int i = 0; i < kMaxTextureLevels; i++) { + // Convert all non-empty texture slots. + if (formats[i] != TextureFormat::kNone) { + if (formats[i] == TextureFormat::kDXT1) { + // Lets go 32 bit for now. + uint8_t* old_buffer = buffers[i]; + assert(widths[i] >= 0 && heights[i] >= 0); + size_t b_size = static_cast(widths[i]) + * static_cast(heights[i]) * 4u; + auto* new_buffer = static_cast(malloc(b_size)); + assert(new_buffer); + buffers[i] = new_buffer; + formats[i] = TextureFormat::kRGBA_8888; + BlockDecompressImageDXT1(static_cast(widths[i]), + static_cast(heights[i]), old_buffer, + reinterpret_cast(new_buffer)); + free(reinterpret_cast(old_buffer)); + + // Ok; this gave us RGBA data, but we don't need the A since DXT1 has no + // alpha.. + rgba8888_to_rgb888_in_place(buffers[i], widths[i] * heights[i] * 4); + formats[i] = TextureFormat::kRGB_888; + } else if (formats[i] == TextureFormat::kDXT5) { + // lets go 32 bit for now + uint8_t* old_buffer = buffers[i]; + assert(widths[i] >= 0 && heights[i] >= 0); + size_t b_size = static_cast(widths[i]) + * static_cast(heights[i]) * 4u; + auto* new_buffer = static_cast(malloc(b_size)); + assert(new_buffer); + buffers[i] = new_buffer; + formats[i] = TextureFormat::kRGBA_8888; + BlockDecompressImageDXT5(static_cast(widths[i]), + static_cast(heights[i]), old_buffer, + reinterpret_cast(new_buffer)); + free(reinterpret_cast(old_buffer)); + } else if (formats[i] == TextureFormat::kETC2_RGBA) { + // Let's go 32 bit for now. + uint8_t* old_buffer = buffers[i]; + uint8_t* new_buffer = nullptr; + + if (explicit_bool(true)) { +#if BA_ENABLE_OPENGL + unsigned int format; + unsigned int internal_format; + unsigned int type; + KTXUnpackETC(old_buffer, GL_COMPRESSED_RGBA8_ETC2_EAC, + static_cast(widths[i]), + static_cast(heights[i]), &new_buffer, &format, + &internal_format, &type, 0, false); +#else + throw Exception(); +#endif // BA_ENABLE_OPENGL + } else { + assert(widths[i] >= 0 && heights[i] >= 0); + size_t b_size = static_cast(widths[i]) + * static_cast(heights[i]) * 4u; + new_buffer = static_cast(malloc(b_size)); + } + BA_PRECONDITION(new_buffer); + buffers[i] = new_buffer; + formats[i] = TextureFormat::kRGBA_8888; + free(reinterpret_cast(old_buffer)); + } else if (formats[i] == TextureFormat::kETC2_RGB) { + // lets go 32 bit for now + uint8_t* old_buffer = buffers[i]; + uint8_t* new_buffer = nullptr; +#if BA_ENABLE_OPENGL + unsigned int format; + unsigned int internal_format; + unsigned int type; + if (explicit_bool(true)) { + KTXUnpackETC(old_buffer, GL_COMPRESSED_RGB8_ETC2, + static_cast(widths[i]), + static_cast(heights[i]), &new_buffer, &format, + &internal_format, &type, 0, false); + } else { + assert(widths[i] >= 0 && heights[i] >= 0); + size_t b_size = static_cast(widths[i]) + * static_cast(heights[i]) * 3u; + new_buffer = static_cast(malloc(b_size)); + } +#else + throw Exception(); +#endif // BA_ENABLE_OPENGL + BA_PRECONDITION(new_buffer); + buffers[i] = new_buffer; + formats[i] = TextureFormat::kRGB_888; + free(reinterpret_cast(old_buffer)); + } else if (formats[i] == TextureFormat::kETC1) { + // lets go 32 bit for now + uint8_t* old_buffer = buffers[i]; + uint8_t* new_buffer = nullptr; +#if BA_ENABLE_OPENGL + unsigned int format; + unsigned int internal_format; + unsigned int type; + if (explicit_bool(true)) { + KTXUnpackETC(old_buffer, GL_ETC1_RGB8_OES, + static_cast(widths[i]), + static_cast(heights[i]), &new_buffer, &format, + &internal_format, &type, 0, false); + } else { + assert(widths[i] >= 0 && heights[i] >= 0); + size_t b_size = static_cast(widths[i]) + * static_cast(heights[i]) * 3u; + new_buffer = static_cast(malloc(b_size)); + memset(new_buffer, 128, b_size); + } +#else + throw Exception(); +#endif + BA_PRECONDITION(new_buffer); + buffers[i] = new_buffer; + formats[i] = TextureFormat::kRGB_888; + free(reinterpret_cast(old_buffer)); + } else { + throw Exception("Can't convert tex format " + + std::to_string(static_cast(formats[i])) + + " to uncompressed"); + } + + // ok, for RGBA stuff let's go ahead and convert to dithered 4444 instead + // of 8888 (the exception is cube-maps; we want to keep those as high + // bitdepth as possible since dithering is quite noticeable in + // reflections) + if (formats[i] == TextureFormat::kRGBA_8888 + && texture->texture_type() != TextureType::kCubeMap) { + assert(widths[i] >= 0 && heights[i] >= 0); + size_t buffer_size = static_cast(widths[i]) + * static_cast(heights[i]) * 4u; + rgba8888_to_rgba4444_in_place(buffers[i], buffer_size); + formats[i] = TextureFormat::kRGBA_4444; + } + + // convert RGB 888 to RGB 565 to get our sizes down a bit + // (again, make an exception for cube-maps) + if (formats[i] == TextureFormat::kRGB_888 + && texture->texture_type() != TextureType::kCubeMap) { + assert(widths[i] >= 0 && heights[i] >= 0); + size_t buffer_size = static_cast(widths[i]) + * static_cast(heights[i]) * 3u; + rgb888_to_rgb565_in_place(buffers[i], buffer_size); + formats[i] = TextureFormat::kRGB_565; + } + + // ok; nowadays for uncompressed stuff we just load the top level + // and generate the rest on the gpu. This should give us nicer + // quality than decompressed lower-level mip images would + // and is hopefully faster too.. + // HMMM actually the quality argument may be iffy in cases where + // we're dithering.. (or maybe not?) + break; + } + } +} + +TexturePreloadData::~TexturePreloadData() { + for (auto& buffer : buffers) { + if (buffer) { + free(buffer); + } + } +} + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/media/data/texture_preload_data.h b/src/ballistica/media/data/texture_preload_data.h new file mode 100644 index 00000000..db0454df --- /dev/null +++ b/src/ballistica/media/data/texture_preload_data.h @@ -0,0 +1,40 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_DATA_TEXTURE_PRELOAD_DATA_H_ +#define BALLISTICA_MEDIA_DATA_TEXTURE_PRELOAD_DATA_H_ + +#include "ballistica/ballistica.h" + +namespace ballistica { + +// Determined by the biggest tex dimension we support (current 4096). +// FIXME: Should define that dimension as a constant somewhere. +const int kMaxTextureLevels = 14; + +// Temporary data that is passed along to the renderer when creating +// rendererdata. This may include sdl surfaces and/or compressed buffers. +class TexturePreloadData { + public: + static void rgba8888_to_rgba4444_in_place(void* src, size_t cb); + + TexturePreloadData() { + // There isn't a way to do this in bracket-init, is there? + // (aside from writing out all values manually I mean). + for (auto& format : formats) { + format = TextureFormat::kNone; + } + } + ~TexturePreloadData(); + void ConvertToUncompressed(TextureData* texture); + + uint8_t* buffers[kMaxTextureLevels]{}; + size_t sizes[kMaxTextureLevels]{}; + TextureFormat formats[kMaxTextureLevels]{}; + int widths[kMaxTextureLevels]{}; + int heights[kMaxTextureLevels]{}; + int base_level{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_DATA_TEXTURE_PRELOAD_DATA_H_ diff --git a/src/ballistica/media/data/texture_renderer_data.h b/src/ballistica/media/data/texture_renderer_data.h new file mode 100644 index 00000000..ca7a71de --- /dev/null +++ b/src/ballistica/media/data/texture_renderer_data.h @@ -0,0 +1,27 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_DATA_TEXTURE_RENDERER_DATA_H_ +#define BALLISTICA_MEDIA_DATA_TEXTURE_RENDERER_DATA_H_ + +namespace ballistica { + +// Renderer-specific data (gl tex, etc) +// this is extended by the renderer +class TextureRendererData : public Object { + public: + auto GetDefaultOwnerThread() const -> ThreadIdentifier override { + return ThreadIdentifier::kMain; + } + + // Create the renderer data but don't load it in yet. + TextureRendererData() = default; + + // load the data. + // if incremental is true, return whether the load was completed + // (non-incremental loads should always complete) + virtual void Load() = 0; +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_DATA_TEXTURE_RENDERER_DATA_H_ diff --git a/src/ballistica/media/media.cc b/src/ballistica/media/media.cc new file mode 100644 index 00000000..0f80f5f9 --- /dev/null +++ b/src/ballistica/media/media.cc @@ -0,0 +1,1251 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/media.h" + +#if !BA_OSTYPE_WINDOWS +#include +#endif + +#include +#include + +#include "ballistica/audio/audio_server.h" +#include "ballistica/game/game.h" +#include "ballistica/generic/timer.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/graphics/text/text_packer.h" +#include "ballistica/media/component/collide_model.h" +#include "ballistica/media/component/data.h" +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/media/data/data_data.h" +#include "ballistica/media/data/sound_data.h" +#include "ballistica/media/media_server.h" +#include "ballistica/python/python_sys.h" + +namespace ballistica { + +// Debug printing. +#define BA_SHOW_LOADS_UNLOADS 0 +#define SHOW_PRUNING_INFO 0 + +// Standard prune time for unused media: 10 minutes (1000ms * 60 * 10). +#define STANDARD_MEDIA_PRUNE_TIME 600000 + +// More aggressive prune time for dynamically-generated text-textures: 10 +// seconds. +#define TEXT_TEXTURE_PRUNE_TIME 10000 + +#define QR_TEXTURE_PRUNE_TIME 10000 + +// How long we should spend loading media in each runPendingLoads() call. +#define PENDING_LOAD_PROCESS_TIME 5 + +void Media::Init() { + // Just create our singleton. + assert(g_media == nullptr); + g_media = new Media(); +} + +Media::Media() { + media_paths_.emplace_back("ba_data"); + for (bool& have_pending_load : have_pending_loads_) { + have_pending_load = false; + } +} + +void Media::LoadSystemTexture(SystemTextureID id, const char* name) { + assert(media_lists_locked_); + system_textures_.push_back(GetTextureData(name)); + assert(system_textures_.size() == static_cast(id) + 1); +} + +void Media::LoadSystemCubeMapTexture(SystemCubeMapTextureID id, + const char* name) { + assert(media_lists_locked_); + system_cube_map_textures_.push_back(GetCubeMapTextureData(name)); + assert(system_cube_map_textures_.size() == static_cast(id) + 1); +} + +void Media::LoadSystemSound(SystemSoundID id, const char* name) { + system_sounds_.push_back(GetSoundData(name)); + assert(system_sounds_.size() == static_cast(id) + 1); +} + +void Media::LoadSystemData(SystemDataID id, const char* name) { + system_datas_.push_back(GetDataData(name)); + assert(system_datas_.size() == static_cast(id) + 1); +} + +void Media::LoadSystemModel(SystemModelID id, const char* name) { + system_models_.push_back(GetModelData(name)); + assert(system_models_.size() == static_cast(id) + 1); +} + +void Media::LoadSystemMedia() { + assert(InGameThread()); + assert(g_audio_server && g_media_server && g_graphics_server); + assert(g_graphics_server + && g_graphics_server->texture_compression_types_are_set()); + assert(g_graphics && g_graphics_server->texture_quality_set()); + + // Just grab the lock once for all this stuff for efficiency. + MediaListsLock lock; + + // System textures: + LoadSystemTexture(SystemTextureID::kUIAtlas, "uiAtlas"); + LoadSystemTexture(SystemTextureID::kButtonSquare, "buttonSquare"); + LoadSystemTexture(SystemTextureID::kWhite, "white"); + LoadSystemTexture(SystemTextureID::kFontSmall0, "fontSmall0"); + LoadSystemTexture(SystemTextureID::kFontBig, "fontBig"); + LoadSystemTexture(SystemTextureID::kCursor, "cursor"); + LoadSystemTexture(SystemTextureID::kBoxingGlove, "boxingGlovesColor"); + LoadSystemTexture(SystemTextureID::kShield, "shield"); + LoadSystemTexture(SystemTextureID::kExplosion, "explosion"); + LoadSystemTexture(SystemTextureID::kTextClearButton, "textClearButton"); + LoadSystemTexture(SystemTextureID::kWindowHSmallVMed, "windowHSmallVMed"); + LoadSystemTexture(SystemTextureID::kWindowHSmallVSmall, "windowHSmallVSmall"); + LoadSystemTexture(SystemTextureID::kGlow, "glow"); + LoadSystemTexture(SystemTextureID::kScrollWidget, "scrollWidget"); + LoadSystemTexture(SystemTextureID::kScrollWidgetGlow, "scrollWidgetGlow"); + LoadSystemTexture(SystemTextureID::kFlagPole, "flagPoleColor"); + LoadSystemTexture(SystemTextureID::kScorch, "scorch"); + LoadSystemTexture(SystemTextureID::kScorchBig, "scorchBig"); + LoadSystemTexture(SystemTextureID::kShadow, "shadow"); + LoadSystemTexture(SystemTextureID::kLight, "light"); + LoadSystemTexture(SystemTextureID::kShadowSharp, "shadowSharp"); + LoadSystemTexture(SystemTextureID::kLightSharp, "lightSharp"); + LoadSystemTexture(SystemTextureID::kShadowSoft, "shadowSoft"); + LoadSystemTexture(SystemTextureID::kLightSoft, "lightSoft"); + LoadSystemTexture(SystemTextureID::kSparks, "sparks"); + LoadSystemTexture(SystemTextureID::kEye, "eyeColor"); + LoadSystemTexture(SystemTextureID::kEyeTint, "eyeColorTintMask"); + LoadSystemTexture(SystemTextureID::kFuse, "fuse"); + LoadSystemTexture(SystemTextureID::kShrapnel1, "shrapnel1Color"); + LoadSystemTexture(SystemTextureID::kSmoke, "smoke"); + LoadSystemTexture(SystemTextureID::kCircle, "circle"); + LoadSystemTexture(SystemTextureID::kCircleOutline, "circleOutline"); + LoadSystemTexture(SystemTextureID::kCircleNoAlpha, "circleNoAlpha"); + LoadSystemTexture(SystemTextureID::kCircleOutlineNoAlpha, + "circleOutlineNoAlpha"); + LoadSystemTexture(SystemTextureID::kCircleShadow, "circleShadow"); + LoadSystemTexture(SystemTextureID::kSoftRect, "softRect"); + LoadSystemTexture(SystemTextureID::kSoftRect2, "softRect2"); + LoadSystemTexture(SystemTextureID::kSoftRectVertical, "softRectVertical"); + LoadSystemTexture(SystemTextureID::kStartButton, "startButton"); + LoadSystemTexture(SystemTextureID::kBombButton, "bombButton"); + LoadSystemTexture(SystemTextureID::kOuyaAButton, "ouyaAButton"); + LoadSystemTexture(SystemTextureID::kBackIcon, "backIcon"); + LoadSystemTexture(SystemTextureID::kNub, "nub"); + LoadSystemTexture(SystemTextureID::kArrow, "arrow"); + LoadSystemTexture(SystemTextureID::kMenuButton, "menuButton"); + LoadSystemTexture(SystemTextureID::kUsersButton, "usersButton"); + LoadSystemTexture(SystemTextureID::kActionButtons, "actionButtons"); + LoadSystemTexture(SystemTextureID::kTouchArrows, "touchArrows"); + LoadSystemTexture(SystemTextureID::kTouchArrowsActions, "touchArrowsActions"); + LoadSystemTexture(SystemTextureID::kRGBStripes, "rgbStripes"); + LoadSystemTexture(SystemTextureID::kUIAtlas2, "uiAtlas2"); + LoadSystemTexture(SystemTextureID::kFontSmall1, "fontSmall1"); + LoadSystemTexture(SystemTextureID::kFontSmall2, "fontSmall2"); + LoadSystemTexture(SystemTextureID::kFontSmall3, "fontSmall3"); + LoadSystemTexture(SystemTextureID::kFontSmall4, "fontSmall4"); + LoadSystemTexture(SystemTextureID::kFontSmall5, "fontSmall5"); + LoadSystemTexture(SystemTextureID::kFontSmall6, "fontSmall6"); + LoadSystemTexture(SystemTextureID::kFontSmall7, "fontSmall7"); + LoadSystemTexture(SystemTextureID::kFontExtras, "fontExtras"); + LoadSystemTexture(SystemTextureID::kFontExtras2, "fontExtras2"); + LoadSystemTexture(SystemTextureID::kFontExtras3, "fontExtras3"); + LoadSystemTexture(SystemTextureID::kFontExtras4, "fontExtras4"); + LoadSystemTexture(SystemTextureID::kCharacterIconMask, "characterIconMask"); + LoadSystemTexture(SystemTextureID::kBlack, "black"); + LoadSystemTexture(SystemTextureID::kWings, "wings"); + + // System cube map textures: + LoadSystemCubeMapTexture(SystemCubeMapTextureID::kReflectionChar, + "reflectionChar#"); + LoadSystemCubeMapTexture(SystemCubeMapTextureID::kReflectionPowerup, + "reflectionPowerup#"); + LoadSystemCubeMapTexture(SystemCubeMapTextureID::kReflectionSoft, + "reflectionSoft#"); + LoadSystemCubeMapTexture(SystemCubeMapTextureID::kReflectionSharp, + "reflectionSharp#"); + LoadSystemCubeMapTexture(SystemCubeMapTextureID::kReflectionSharper, + "reflectionSharper#"); + LoadSystemCubeMapTexture(SystemCubeMapTextureID::kReflectionSharpest, + "reflectionSharpest#"); + + // System sounds: + LoadSystemSound(SystemSoundID::kDeek, "deek"); + LoadSystemSound(SystemSoundID::kBlip, "blip"); + LoadSystemSound(SystemSoundID::kBlank, "blank"); + LoadSystemSound(SystemSoundID::kPunch, "punch01"); + LoadSystemSound(SystemSoundID::kClick, "click01"); + LoadSystemSound(SystemSoundID::kErrorBeep, "error"); + LoadSystemSound(SystemSoundID::kSwish, "swish"); + LoadSystemSound(SystemSoundID::kSwish2, "swish2"); + LoadSystemSound(SystemSoundID::kSwish3, "swish3"); + LoadSystemSound(SystemSoundID::kTap, "tap"); + LoadSystemSound(SystemSoundID::kCorkPop, "corkPop"); + LoadSystemSound(SystemSoundID::kGunCock, "gunCocking"); + LoadSystemSound(SystemSoundID::kTickingCrazy, "tickingCrazy"); + LoadSystemSound(SystemSoundID::kSparkle, "sparkle01"); + LoadSystemSound(SystemSoundID::kSparkle2, "sparkle02"); + LoadSystemSound(SystemSoundID::kSparkle3, "sparkle03"); + + // System datas: + // (crickets) + + // System models: + LoadSystemModel(SystemModelID::kButtonSmallTransparent, + "buttonSmallTransparent"); + LoadSystemModel(SystemModelID::kButtonSmallOpaque, "buttonSmallOpaque"); + LoadSystemModel(SystemModelID::kButtonMediumTransparent, + "buttonMediumTransparent"); + LoadSystemModel(SystemModelID::kButtonMediumOpaque, "buttonMediumOpaque"); + LoadSystemModel(SystemModelID::kButtonBackTransparent, + "buttonBackTransparent"); + LoadSystemModel(SystemModelID::kButtonBackOpaque, "buttonBackOpaque"); + LoadSystemModel(SystemModelID::kButtonBackSmallTransparent, + "buttonBackSmallTransparent"); + LoadSystemModel(SystemModelID::kButtonBackSmallOpaque, + "buttonBackSmallOpaque"); + LoadSystemModel(SystemModelID::kButtonTabTransparent, "buttonTabTransparent"); + LoadSystemModel(SystemModelID::kButtonTabOpaque, "buttonTabOpaque"); + LoadSystemModel(SystemModelID::kButtonLargeTransparent, + "buttonLargeTransparent"); + LoadSystemModel(SystemModelID::kButtonLargeOpaque, "buttonLargeOpaque"); + LoadSystemModel(SystemModelID::kButtonLargerTransparent, + "buttonLargerTransparent"); + LoadSystemModel(SystemModelID::kButtonLargerOpaque, "buttonLargerOpaque"); + LoadSystemModel(SystemModelID::kButtonSquareTransparent, + "buttonSquareTransparent"); + LoadSystemModel(SystemModelID::kButtonSquareOpaque, "buttonSquareOpaque"); + LoadSystemModel(SystemModelID::kCheckTransparent, "checkTransparent"); + LoadSystemModel(SystemModelID::kScrollBarThumbTransparent, + "scrollBarThumbTransparent"); + LoadSystemModel(SystemModelID::kScrollBarThumbOpaque, "scrollBarThumbOpaque"); + LoadSystemModel(SystemModelID::kScrollBarThumbSimple, "scrollBarThumbSimple"); + LoadSystemModel(SystemModelID::kScrollBarThumbShortTransparent, + "scrollBarThumbShortTransparent"); + LoadSystemModel(SystemModelID::kScrollBarThumbShortOpaque, + "scrollBarThumbShortOpaque"); + LoadSystemModel(SystemModelID::kScrollBarThumbShortSimple, + "scrollBarThumbShortSimple"); + LoadSystemModel(SystemModelID::kScrollBarTroughTransparent, + "scrollBarTroughTransparent"); + LoadSystemModel(SystemModelID::kTextBoxTransparent, "textBoxTransparent"); + LoadSystemModel(SystemModelID::kImage1x1, "image1x1"); + LoadSystemModel(SystemModelID::kImage1x1FullScreen, "image1x1FullScreen"); + LoadSystemModel(SystemModelID::kImage2x1, "image2x1"); + LoadSystemModel(SystemModelID::kImage4x1, "image4x1"); + LoadSystemModel(SystemModelID::kImage16x1, "image16x1"); +#if BA_VR_BUILD + LoadSystemModel(SystemModelID::kImage1x1VRFullScreen, "image1x1VRFullScreen"); + LoadSystemModel(SystemModelID::kVROverlay, "vrOverlay"); + LoadSystemModel(SystemModelID::kVRFade, "vrFade"); +#endif // BA_VR_BUILD + LoadSystemModel(SystemModelID::kOverlayGuide, "overlayGuide"); + LoadSystemModel(SystemModelID::kWindowHSmallVMedTransparent, + "windowHSmallVMedTransparent"); + LoadSystemModel(SystemModelID::kWindowHSmallVMedOpaque, + "windowHSmallVMedOpaque"); + LoadSystemModel(SystemModelID::kWindowHSmallVSmallTransparent, + "windowHSmallVSmallTransparent"); + LoadSystemModel(SystemModelID::kWindowHSmallVSmallOpaque, + "windowHSmallVSmallOpaque"); + LoadSystemModel(SystemModelID::kSoftEdgeOutside, "softEdgeOutside"); + LoadSystemModel(SystemModelID::kSoftEdgeInside, "softEdgeInside"); + LoadSystemModel(SystemModelID::kBoxingGlove, "boxingGlove"); + LoadSystemModel(SystemModelID::kShield, "shield"); + LoadSystemModel(SystemModelID::kFlagPole, "flagPole"); + LoadSystemModel(SystemModelID::kFlagStand, "flagStand"); + LoadSystemModel(SystemModelID::kScorch, "scorch"); + LoadSystemModel(SystemModelID::kEyeBall, "eyeBall"); + LoadSystemModel(SystemModelID::kEyeBallIris, "eyeBallIris"); + LoadSystemModel(SystemModelID::kEyeLid, "eyeLid"); + LoadSystemModel(SystemModelID::kHairTuft1, "hairTuft1"); + LoadSystemModel(SystemModelID::kHairTuft1b, "hairTuft1b"); + LoadSystemModel(SystemModelID::kHairTuft2, "hairTuft2"); + LoadSystemModel(SystemModelID::kHairTuft3, "hairTuft3"); + LoadSystemModel(SystemModelID::kHairTuft4, "hairTuft4"); + LoadSystemModel(SystemModelID::kShrapnel1, "shrapnel1"); + LoadSystemModel(SystemModelID::kShrapnelSlime, "shrapnelSlime"); + LoadSystemModel(SystemModelID::kShrapnelBoard, "shrapnelBoard"); + LoadSystemModel(SystemModelID::kShockWave, "shockWave"); + LoadSystemModel(SystemModelID::kFlash, "flash"); + LoadSystemModel(SystemModelID::kCylinder, "cylinder"); + LoadSystemModel(SystemModelID::kArrowFront, "arrowFront"); + LoadSystemModel(SystemModelID::kArrowBack, "arrowBack"); + LoadSystemModel(SystemModelID::kActionButtonLeft, "actionButtonLeft"); + LoadSystemModel(SystemModelID::kActionButtonTop, "actionButtonTop"); + LoadSystemModel(SystemModelID::kActionButtonRight, "actionButtonRight"); + LoadSystemModel(SystemModelID::kActionButtonBottom, "actionButtonBottom"); + LoadSystemModel(SystemModelID::kBox, "box"); + LoadSystemModel(SystemModelID::kLocator, "locator"); + LoadSystemModel(SystemModelID::kLocatorBox, "locatorBox"); + LoadSystemModel(SystemModelID::kLocatorCircle, "locatorCircle"); + LoadSystemModel(SystemModelID::kLocatorCircleOutline, "locatorCircleOutline"); + LoadSystemModel(SystemModelID::kCrossOut, "crossOut"); + LoadSystemModel(SystemModelID::kWing, "wing"); + + // Hooray! + system_media_loaded_ = true; +} + +Media::~Media() = default; + +void Media::PrintLoadInfo() { + std::string s; + char buffer[256]; + int num = 1; + + // Need to lock lists while iterating over them. + MediaListsLock lock; + s = "Media load results: (all times in milliseconds):\n"; + snprintf(buffer, sizeof(buffer), " %-50s %10s %10s", "FILE", + "PRELOAD_TIME", "LOAD_TIME"); + s += buffer; + Log(s, true, false); + millisecs_t total_preload_time = 0; + millisecs_t total_load_time = 0; + assert(media_lists_locked_); + for (auto&& i : models_) { + millisecs_t preload_time = i.second->preload_time(); + millisecs_t load_time = i.second->load_time(); + total_preload_time += preload_time; + total_load_time += load_time; + snprintf(buffer, sizeof(buffer), "%-3d %-50s %10d %10d", num, + i.second->GetName().c_str(), + static_cast_check_fit(preload_time), + static_cast_check_fit(load_time)); + Log(buffer, true, false); + num++; + } + assert(media_lists_locked_); + for (auto&& i : collide_models_) { + millisecs_t preload_time = i.second->preload_time(); + millisecs_t load_time = i.second->load_time(); + total_preload_time += preload_time; + total_load_time += load_time; + snprintf(buffer, sizeof(buffer), "%-3d %-50s %10d %10d", num, + i.second->GetName().c_str(), + static_cast_check_fit(preload_time), + static_cast_check_fit(load_time)); + Log(buffer, true, false); + num++; + } + assert(media_lists_locked_); + for (auto&& i : sounds_) { + millisecs_t preload_time = i.second->preload_time(); + millisecs_t load_time = i.second->load_time(); + total_preload_time += preload_time; + total_load_time += load_time; + snprintf(buffer, sizeof(buffer), "%-3d %-50s %10d %10d", num, + i.second->GetName().c_str(), + static_cast_check_fit(preload_time), + static_cast_check_fit(load_time)); + Log(buffer, true, false); + num++; + } + assert(media_lists_locked_); + for (auto&& i : datas_) { + millisecs_t preload_time = i.second->preload_time(); + millisecs_t load_time = i.second->load_time(); + total_preload_time += preload_time; + total_load_time += load_time; + snprintf(buffer, sizeof(buffer), "%-3d %-50s %10d %10d", num, + i.second->GetName().c_str(), + static_cast_check_fit(preload_time), + static_cast_check_fit(load_time)); + Log(buffer, true, false); + num++; + } + assert(media_lists_locked_); + for (auto&& i : textures_) { + millisecs_t preload_time = i.second->preload_time(); + millisecs_t load_time = i.second->load_time(); + total_preload_time += preload_time; + total_load_time += load_time; + snprintf(buffer, sizeof(buffer), "%-3d %-50s %10d %10d", num, + i.second->file_name_full().c_str(), + static_cast_check_fit(preload_time), + static_cast_check_fit(load_time)); + Log(buffer, true, false); + num++; + } + snprintf(buffer, sizeof(buffer), + "Total preload time (loading data from disk): %i\nTotal load time " + "(feeding data to OpenGL, etc): %i", + static_cast(total_preload_time), + static_cast(total_load_time)); + Log(buffer, true, false); +} + +void Media::MarkAllMediaForLoad() { + assert(InGameThread()); + + // Need to keep lists locked while iterating over them. + MediaListsLock m_lock; + for (auto&& i : textures_) { + if (!i.second->preloaded()) { + MediaComponentData::LockGuard lock(i.second.get()); + have_pending_loads_[static_cast(MediaType::kTexture)] = true; + MarkComponentForLoad(i.second.get()); + } + } + for (auto&& i : text_textures_) { + if (!i.second->preloaded()) { + MediaComponentData::LockGuard lock(i.second.get()); + have_pending_loads_[static_cast(MediaType::kTexture)] = true; + MarkComponentForLoad(i.second.get()); + } + } + for (auto&& i : qr_textures_) { + if (!i.second->preloaded()) { + MediaComponentData::LockGuard lock(i.second.get()); + have_pending_loads_[static_cast(MediaType::kTexture)] = true; + MarkComponentForLoad(i.second.get()); + } + } + for (auto&& i : models_) { + if (!i.second->preloaded()) { + MediaComponentData::LockGuard lock(i.second.get()); + have_pending_loads_[static_cast(MediaType::kModel)] = true; + MarkComponentForLoad(i.second.get()); + } + } +} + +// Call this from the graphics thread to immediately unload all +// media used by it. (for when GL context gets lost, etc). +void Media::UnloadRendererBits(bool do_textures, bool do_models) { + assert(InGraphicsThread()); + // need to keep lists locked while iterating over them.. + MediaListsLock m_lock; + if (do_textures) { + assert(media_lists_locked_); + for (auto&& i : textures_) { + MediaComponentData::LockGuard lock(i.second.get()); + i.second->Unload(true); + } + for (auto&& i : text_textures_) { + MediaComponentData::LockGuard lock(i.second.get()); + i.second->Unload(true); + } + for (auto&& i : qr_textures_) { + MediaComponentData::LockGuard lock(i.second.get()); + i.second->Unload(true); + } + } + if (do_models) { + for (auto&& i : models_) { + MediaComponentData::LockGuard lock(i.second.get()); + i.second->Unload(true); + } + } +} + +auto Media::GetModelData(const std::string& file_name) + -> Object::Ref { + return GetComponentData(file_name, &models_); +} + +auto Media::GetSoundData(const std::string& file_name) + -> Object::Ref { + return GetComponentData(file_name, &sounds_); +} + +auto Media::GetDataData(const std::string& file_name) -> Object::Ref { + return GetComponentData(file_name, &datas_); +} + +auto Media::GetCollideModelData(const std::string& file_name) + -> Object::Ref { + return GetComponentData(file_name, &collide_models_); +} + +template +auto Media::GetComponentData(const std::string& file_name, + std::map >* c_list) + -> Object::Ref { + assert(InGameThread()); + assert(media_lists_locked_); + auto i = c_list->find(file_name); + if (i != c_list->end()) { + return Object::Ref(i->second.get()); + } else { + auto d(Object::New(file_name)); + (*c_list)[file_name] = d; + { + MediaComponentData::LockGuard lock(d.get()); + have_pending_loads_[static_cast(d->GetMediaType())] = true; + MarkComponentForLoad(d.get()); + } + d->set_last_used_time(GetRealTime()); + return d; + } +} + +auto Media::GetTextureData(TextPacker* packer) -> Object::Ref { + assert(InGameThread()); + assert(media_lists_locked_); + const std::string& hash(packer->hash()); + auto i = text_textures_.find(hash); + if (i != text_textures_.end()) { + return Object::Ref(i->second.get()); + } else { + auto d(Object::New(packer)); + text_textures_[hash] = d; + { + MediaComponentData::LockGuard lock(d.get()); + have_pending_loads_[static_cast(d->GetMediaType())] = true; + MarkComponentForLoad(d.get()); + } + d->set_last_used_time(GetRealTime()); + return d; + } +} + +auto Media::GetTextureDataQRCode(const std::string& url) + -> Object::Ref { + assert(InGameThread()); + assert(media_lists_locked_); + auto i = qr_textures_.find(url); + if (i != qr_textures_.end()) { + return Object::Ref(i->second.get()); + } else { + auto d(Object::New(url)); + qr_textures_[url] = d; + { + MediaComponentData::LockGuard lock(d.get()); + have_pending_loads_[static_cast(d->GetMediaType())] = true; + MarkComponentForLoad(d.get()); + } + d->set_last_used_time(GetRealTime()); + return d; + } +} + +// Eww can't recycle GetComponent here since we need extra stuff (tex-type arg) +// ..should fix. +auto Media::GetCubeMapTextureData(const std::string& file_name) + -> Object::Ref { + assert(InGameThread()); + assert(media_lists_locked_); + auto i = textures_.find(file_name); + if (i != textures_.end()) { + return Object::Ref(i->second.get()); + } else { + auto d(Object::New(file_name, TextureType::kCubeMap, + TextureMinQuality::kLow)); + textures_[file_name] = d; + { + MediaComponentData::LockGuard lock(d.get()); + have_pending_loads_[static_cast(d->GetMediaType())] = true; + MarkComponentForLoad(d.get()); + } + d->set_last_used_time(GetRealTime()); + return d; + } +} + +// Eww; can't recycle GetComponent here since we need extra stuff (quality +// settings, etc). Should fix. +auto Media::GetTextureData(const std::string& file_name) + -> Object::Ref { + assert(InGameThread()); + assert(media_lists_locked_); + auto i = textures_.find(file_name); + if (i != textures_.end()) { + return Object::Ref(i->second.get()); + } else { + static std::set* quality_map_medium = nullptr; + static std::set* quality_map_high = nullptr; + static bool quality_maps_inited = false; + + // TEMP - we currently set min quality based on filename; + // in the future this will be stored with the texture package or whatnot + if (!quality_maps_inited) { + quality_maps_inited = true; + quality_map_medium = new std::set(); + quality_map_high = new std::set(); + const char* vals_med[] = { + "fontSmall0", "fontSmall1", "fontSmall2", "fontSmall3", "fontSmall4", + "fontSmall5", "fontSmall6", "fontSmall7", "fontExtras", nullptr}; + + const char* vals_high[] = {"frostyIcon", "jackIcon", "melIcon", + "santaIcon", "ninjaIcon", "neoSpazIcon", + "zoeIcon", "kronkIcon", "scrollWidgetGlow", + "glow", nullptr}; + + for (const char** val3 = vals_med; *val3 != nullptr; val3++) { + quality_map_medium->insert(*val3); + } + for (const char** val2 = vals_high; *val2 != nullptr; val2++) { + quality_map_high->insert(*val2); + } + } + + TextureMinQuality min_quality = TextureMinQuality::kLow; + if (quality_map_medium->find(file_name) != quality_map_medium->end()) { + min_quality = TextureMinQuality::kMedium; + } else if (quality_map_high->find(file_name) != quality_map_high->end()) { + min_quality = TextureMinQuality::kHigh; + } + + auto d(Object::New(file_name, TextureType::k2D, min_quality)); + textures_[file_name] = d; + { + MediaComponentData::LockGuard lock(d.get()); + have_pending_loads_[static_cast(d->GetMediaType())] = true; + MarkComponentForLoad(d.get()); + } + d->set_last_used_time(GetRealTime()); + return d; + } +} + +void Media::MarkComponentForLoad(MediaComponentData* c) { + assert(InGameThread()); + + assert(c->locked()); + + // *allocate* a reference as a standalone pointer so we can be + // sure this guy sticks around until it's been sent all the way + // through the preload/load cycle. (since other threads will be touching it) + // once it makes it back to us we can delete the ref (in + // ClearPendingLoadsDoneList) + + auto media_ptr = new Object::Ref(c); + g_media_server->PushRunnable(Object::NewDeferred(media_ptr)); +} + +auto Media::GetModelPendingLoadCount() -> int { + if (!have_pending_loads_[static_cast(MediaType::kModel)]) { + return 0; + } + MediaListsLock lock; + int total = GetComponentPendingLoadCount(&models_, MediaType::kModel); + if (total == 0) { + // When fully loaded, stop counting. + have_pending_loads_[static_cast(MediaType::kModel)] = false; + } + return total; +} + +auto Media::GetTexturePendingLoadCount() -> int { + if (!have_pending_loads_[static_cast(MediaType::kTexture)]) { + return 0; + } + MediaListsLock lock; + int total = + (GetComponentPendingLoadCount(&textures_, MediaType::kTexture) + + GetComponentPendingLoadCount(&text_textures_, MediaType::kTexture) + + GetComponentPendingLoadCount(&qr_textures_, MediaType::kTexture)); + if (total == 0) { + // When fully loaded, stop counting. + have_pending_loads_[static_cast(MediaType::kTexture)] = false; + } + return total; +} + +auto Media::GetSoundPendingLoadCount() -> int { + if (!have_pending_loads_[static_cast(MediaType::kSound)]) { + return 0; + } + MediaListsLock lock; + int total = GetComponentPendingLoadCount(&sounds_, MediaType::kSound); + if (total == 0) { + // When fully loaded, stop counting. + have_pending_loads_[static_cast(MediaType::kSound)] = false; + } + return total; +} + +auto Media::GetDataPendingLoadCount() -> int { + if (!have_pending_loads_[static_cast(MediaType::kData)]) { + return 0; + } + MediaListsLock lock; + int total = GetComponentPendingLoadCount(&datas_, MediaType::kData); + if (total == 0) { + // When fully loaded, stop counting. + have_pending_loads_[static_cast(MediaType::kData)] = false; + } + return total; +} + +auto Media::GetCollideModelPendingLoadCount() -> int { + if (!have_pending_loads_[static_cast(MediaType::kCollideModel)]) { + return 0; + } + MediaListsLock lock; + int total = + GetComponentPendingLoadCount(&collide_models_, MediaType::kCollideModel); + if (total == 0) { + // When fully loaded, stop counting. + have_pending_loads_[static_cast(MediaType::kCollideModel)] = false; + } + return total; +} + +auto Media::GetGraphicalPendingLoadCount() -> int { + // Each of these calls lock the media-lists so we don't. + return GetModelPendingLoadCount() + GetTexturePendingLoadCount(); +} + +auto Media::GetPendingLoadCount() -> int { + // Each of these calls lock the media-lists so we don't. + return GetModelPendingLoadCount() + GetTexturePendingLoadCount() + + GetDataPendingLoadCount() + GetSoundPendingLoadCount() + + GetCollideModelPendingLoadCount(); +} + +template +auto Media::GetComponentPendingLoadCount( + std::map >* t_list, MediaType type) -> int { + assert(InGameThread()); + assert(media_lists_locked_); + + int c = 0; + for (auto&& i : (*t_list)) { + if (i.second.exists()) { + if (i.second->TryLock()) { + MediaComponentData::LockGuard lock( + i.second.get(), MediaComponentData::LockGuard::Type::kInheritLock); + if (!i.second->loaded()) { + c++; + } + } else { + c++; + } + } + } + return c; +} + +// Runs the pending loads that need to run from the audio thread. +auto Media::RunPendingAudioLoads() -> bool { + assert(InAudioThread()); + return RunPendingLoadList(&pending_loads_sounds_); +} + +// Runs the pending loads that need to run from the graphics thread. +auto Media::RunPendingGraphicsLoads() -> bool { + assert(InGraphicsThread()); + return RunPendingLoadList(&pending_loads_graphics_); +} + +// Runs the pending loads that run in the main thread. Also clears the list of +// done loads. +auto Media::RunPendingLoadsGameThread() -> bool { + assert(InGameThread()); + return RunPendingLoadList(&pending_loads_other_); +} + +template +auto Media::RunPendingLoadList(std::vector*>* c_list) -> bool { + bool flush = false; + millisecs_t starttime = GetRealTime(); + + std::vector*> l; + std::vector*> l_unfinished; + std::vector*> l_finished; + { + std::lock_guard lock(pending_load_list_mutex_); + + // If we're already out of time. + if (!flush && GetRealTime() - starttime > PENDING_LOAD_PROCESS_TIME) { + bool return_val = (!c_list->empty()); + return return_val; + } + + // Save time if there's nothing to load. + if (c_list->empty()) { + return false; + } + + // Pull the contents of c_list and set it to empty. + l.swap(*c_list); + } + + // Run loads on our list until either the list is empty or we're out of time + // (don't want to block here for very long...) + // We should also think about the fact that even if a load is quick here it + // may add work on the graphics thread/etc so maybe we should add other + // restrictions. + bool out_of_time = false; + if (!l.empty()) { + while (true) { + for (auto i = l.begin(); i != l.end(); i++) { + if (!out_of_time) { + (***i).Load(false); + + // If the load finished, pop it on our "done-loading" list.. otherwise + // keep it around. + l_finished.push_back(*i); // else l_unfinished.push_back(*i); + if (GetRealTime() - starttime > PENDING_LOAD_PROCESS_TIME && !flush) { + out_of_time = true; + } + } else { + // Already out of time - just save this one for later. + l_unfinished.push_back(*i); + } + } + l = l_unfinished; + l_unfinished.clear(); + if (l.empty() || out_of_time) { + break; + } + } + } + + // Now add unfinished ones back onto the original list and finished ones into + // the done list. + { + std::lock_guard lock(pending_load_list_mutex_); + for (auto&& i : l) { + c_list->push_back(i); + } + for (auto&& i : l_finished) { + pending_loads_done_.push_back(i); + } + } + + // if we dumped anything on the pending loads done list, shake the game thread + // to tell it to kill the reference.. + if (!l_finished.empty()) { + assert(g_game); + g_game->PushHavePendingLoadsDoneCall(); + } + return (!l.empty()); +} + +void Media::Prune(int level) { + assert(InGameThread()); + millisecs_t current_time = GetRealTime(); + + // need lists locked while accessing/modifying them + MediaListsLock lock; + + // we can specify level for more aggressive pruning (during memory warnings + // and whatnot) + millisecs_t standard_media_prune_time = STANDARD_MEDIA_PRUNE_TIME; + millisecs_t text_texture_prune_time = TEXT_TEXTURE_PRUNE_TIME; + millisecs_t qr_texture_prune_time = QR_TEXTURE_PRUNE_TIME; + switch (level) { + case 1: + standard_media_prune_time = 120000; // 2 min + text_texture_prune_time = 1000; // 1 sec + qr_texture_prune_time = 1000; // 1 sec + break; + case 2: + standard_media_prune_time = 30000; // 30 sec + text_texture_prune_time = 1000; // 1 sec + qr_texture_prune_time = 1000; // 1 sec + break; + case 3: + standard_media_prune_time = 5000; // 5 sec + text_texture_prune_time = 1000; // 1 sec + qr_texture_prune_time = 1000; // 1 sec + break; + default: + break; + } + + std::vector*> graphics_thread_unloads; + std::vector*> audio_thread_unloads; + +#if SHOW_PRUNING_INFO + assert(media_lists_locked_); + int old_texture_count = textures_.size(); + int old_text_texture_count = text_textures_.size(); + int old_qr_texture_count = qr_textures_.size(); + int old_model_count = models_.size(); + int old_collide_model_count = collide_models_.size(); + int old_sound_count = sounds_.size(); +#endif // SHOW_PRUNING_INFO + + // prune textures.. + assert(media_lists_locked_); + for (auto i = textures_.begin(); i != textures_.end();) { + TextureData* texture_data = i->second.get(); + // attempt to prune if there are no references remaining except our own and + // its been a while since it was used + if (current_time - texture_data->last_used_time() + > standard_media_prune_time + && (texture_data->object_strong_ref_count() <= 1)) { + // if its preloaded/loaded we need to ask the graphics thread to unload it + // first + if (texture_data->preloaded()) { + // allocate a reference to keep this texture_data alive while the unload + // is happening + graphics_thread_unloads.push_back( + new Object::Ref(texture_data)); + auto i_next = i; + i_next++; + textures_.erase(i); + i = i_next; + } + } else { + i++; + } + } + + // prune text-textures more aggressively since we may generate lots of them + // FIXME - we may want to prune based on total number of these instead of + // time.. + assert(media_lists_locked_); + for (auto i = text_textures_.begin(); i != text_textures_.end();) { + TextureData* texture_data = i->second.get(); + // attempt to prune if there are no references remaining except our own and + // its been a while since it was used + if (current_time - texture_data->last_used_time() > text_texture_prune_time + && (texture_data->object_strong_ref_count() <= 1)) { + // if its preloaded/loaded we need to ask the graphics thread to unload it + // first + if (texture_data->preloaded()) { + // allocate a reference to keep this texture_data alive while the unload + // is happening + graphics_thread_unloads.push_back( + new Object::Ref(texture_data)); + auto i_next = i; + i_next++; + text_textures_.erase(i); + i = i_next; + } + } else { + i++; + } + } + + // prune textures + assert(media_lists_locked_); + for (auto i = qr_textures_.begin(); i != qr_textures_.end();) { + TextureData* texture_data = i->second.get(); + // attempt to prune if there are no references remaining except our own and + // its been a while since it was used + if (current_time - texture_data->last_used_time() > qr_texture_prune_time + && (texture_data->object_strong_ref_count() <= 1)) { + // if its preloaded/loaded we need to ask the graphics thread to unload it + // first + if (texture_data->preloaded()) { + // allocate a reference to keep this texture_data alive while the unload + // is happening + graphics_thread_unloads.push_back( + new Object::Ref(texture_data)); + auto i_next = i; + i_next++; + qr_textures_.erase(i); + i = i_next; + } + } else { + i++; + } + } + + // prune models.. + assert(media_lists_locked_); + for (auto i = models_.begin(); i != models_.end();) { + ModelData* model_data = i->second.get(); + // attempt to prune if there are no references remaining except our own and + // its been a while since it was used + if (current_time - model_data->last_used_time() > standard_media_prune_time + && (model_data->object_strong_ref_count() <= 1)) { + // if its preloaded/loaded we need to ask the graphics thread to unload it + // first + if (model_data->preloaded()) { + // allocate a reference to keep this model_data alive while the unload + // is happening + graphics_thread_unloads.push_back( + new Object::Ref(model_data)); + auto i_next = i; + i_next++; + models_.erase(i); + i = i_next; + } + } else { + i++; + } + } + + // Prune collide-models. + assert(media_lists_locked_); + for (auto i = collide_models_.begin(); i != collide_models_.end();) { + CollideModelData* collide_model_data = i->second.get(); + // attempt to prune if there are no references remaining except our own and + // its been a while since it was used (unlike other media we never prune + // these if there's still references to them + if (current_time - collide_model_data->last_used_time() + > standard_media_prune_time + && (collide_model_data->object_strong_ref_count() <= 1)) { + // we can unload it immediately since that happens in the game thread... + collide_model_data->Unload(); + auto i_next = i; + ++i_next; + collide_models_.erase(i); + i = i_next; + } else { + i++; + } + } + + // Prune sounds. + // (DISABLED FOR NOW - getting AL errors; need to better determine which + // sounds are still in active use by OpenAL and ensure references exist for + // them somewhere while that is the case + if (explicit_bool(false)) { + assert(media_lists_locked_); + for (auto i = sounds_.begin(); i != sounds_.end();) { + SoundData* sound_data = i->second.get(); + // Attempt to prune if there are no references remaining except our own + // and its been a while since it was used. + if (current_time - sound_data->last_used_time() + > standard_media_prune_time + && (sound_data->object_strong_ref_count() <= 1)) { + // If its preloaded/loaded we need to ask the graphics thread to unload + // it first. + if (sound_data->preloaded()) { + // Allocate a reference to keep this sound_data alive while the unload + // is happening. + audio_thread_unloads.push_back( + new Object::Ref(sound_data)); + auto i_next = i; + i_next++; + sounds_.erase(i); + i = i_next; + } + } else { + i++; + } + } + } + + if (!graphics_thread_unloads.empty()) { + g_graphics_server->PushComponentUnloadCall(graphics_thread_unloads); + } + if (!audio_thread_unloads.empty()) { + g_audio_server->PushComponentUnloadCall(audio_thread_unloads); + } + +#if SHOW_PRUNING_INFO + assert(media_lists_locked_); + if (textures_.size() != old_texture_count) { + Log("Textures pruned from " + std::to_string(old_texture_count) + " to " + + std::to_string(textures_.size())); + } + if (text_textures_.size() != old_text_texture_count) { + Log("TextTextures pruned from " + std::to_string(old_text_texture_count) + + " to " + std::to_string(text_textures_.size())); + } + if (qr_textures_.size() != old_qr_texture_count) { + Log("QrTextures pruned from " + std::to_string(old_qr_texture_count) + + " to " + std::to_string(qr_textures_.size())); + } + if (models_.size() != old_model_count) { + Log("Models pruned from " + std::to_string(old_model_count) + " to " + + std::to_string(models_.size())); + } + if (collide_models_.size() != old_collide_model_count) { + Log("CollideModels pruned from " + std::to_string(old_collide_model_count) + + " to " + std::to_string(collide_models_.size())); + } + if (sounds_.size() != old_sound_count) { + Log("Sounds pruned from " + std::to_string(old_sound_count) + " to " + + std::to_string(sounds_.size())); + } +#endif // SHOW_PRUNING_INFO +} + +auto Media::FindMediaFile(FileType type, const std::string& name) + -> std::string { + std::string file_out; + + // We don't protect package-path access so make sure its always from here. + assert(InGameThread()); + + const char* ext = ""; + const char* prefix = ""; + + switch (type) { + case FileType::kSound: +#if BA_HEADLESS_BUILD + return "headless_dummy_path.sound"; +#else // BA_HEADLESS_BUILD + prefix = "audio/"; + ext = ".ogg"; + break; +#endif // BA_HEADLESS_BUILD + + case FileType::kModel: +#if BA_HEADLESS_BUILD + return "headless_dummy_path.model"; +#else // BA_HEADLESS_BUILD + prefix = "models/"; + ext = ".bob"; + break; +#endif // BA_HEADLESS_BUILD + + case FileType::kCollisionModel: + prefix = "models/"; + ext = ".cob"; + break; + + case FileType::kData: + prefix = "data/"; + ext = ".json"; + break; + + case FileType::kTexture: { +#if BA_HEADLESS_BUILD + if (strchr(name.c_str(), '#')) { + return "headless_dummy_path#.nop"; + } else { + return "headless_dummy_path.nop"; + } +#else // BA_HEADLESS_BUILD + + assert(g_graphics_server + && g_graphics_server->texture_compression_types_are_set()); + assert(g_graphics_server && g_graphics_server->texture_quality_set()); + prefix = "textures/"; + +#if BA_OSTYPE_ANDROID && !BA_ANDROID_DDS_BUILD + // On most android builds we go for .kvm, which contains etc2 and etc1. + ext = ".ktx"; +#elif BA_OSTYPE_IOS_TVOS + // On iOS we use pvr. + ext = ".pvr"; +#else + // all else defaults to dds + ext = ".dds"; +#endif +#endif // BA_HEADLESS_BUILD + break; + } + default: + break; + } + + const std::vector& media_paths_used = media_paths_; + + for (auto&& i : media_paths_used) { + struct BA_STAT stats {}; + file_out = i + "/" + prefix + name + ext; // NOLINT + int result; + + // '#' denotes a cube map texture, which is actually 6 files. + if (strchr(file_out.c_str(), '#')) { + std::string tmp_name = file_out; + tmp_name.replace(tmp_name.find('#'), 1, "_+x"); + + // Just look for one of them i guess. + result = g_platform->Stat(tmp_name.c_str(), &stats); + } else { + result = g_platform->Stat(file_out.c_str(), &stats); + } + if (result == 0) { + if (S_ISREG(stats.st_mode)) { // NOLINT + return file_out; + } + } + } + + // We wanna fail gracefully for some types. + if (type == FileType::kSound && name != "blank") { + Log("Unable to load audio: '" + name + "'; trying fallback..."); + return FindMediaFile(type, "blank"); + } else if (type == FileType::kTexture && name != "white") { + Log("Unable to load texture: '" + name + "'; trying fallback..."); + return FindMediaFile(type, "white"); + } + + throw Exception("Can't find media: \"" + name + "\""); + // return file_out; +} + +void Media::AddPendingLoad(Object::Ref* c) { + switch ((**c).GetMediaType()) { + case MediaType::kTexture: + case MediaType::kModel: { + // Tell the graphics thread there's pending loads... + std::lock_guard lock(pending_load_list_mutex_); + pending_loads_graphics_.push_back(c); + break; + } + case MediaType::kSound: { + // Tell the audio thread there's pending loads. + { + std::lock_guard lock(pending_load_list_mutex_); + pending_loads_sounds_.push_back(c); + } + g_audio_server->PushHavePendingLoadsCall(); + break; + } + default: { + // Tell the game thread there's pending loads. + { + std::lock_guard lock(pending_load_list_mutex_); + pending_loads_other_.push_back(c); + } + g_game->PushHavePendingLoadsCall(); + break; + } + } +} + +void Media::ClearPendingLoadsDoneList() { + assert(InGameThread()); + + std::lock_guard lock(pending_load_list_mutex_); + + // Our explicitly-allocated reference pointer has made it back to us here in + // the game thread. + // We can now kill the reference knowing that it's safe for this component + // to die at any time (anyone needing it to be alive now should be holding a + // reference themselves). + for (Object::Ref* i : pending_loads_done_) { + delete i; + } + pending_loads_done_.clear(); +} + +void Media::PreloadRunnable::Run() { + assert(InMediaThread()); + + // add our pointer to one of the preload lists and shake our preload thread to + // wake it up + if ((**c).GetMediaType() == MediaType::kSound) { + g_media_server->pending_preloads_audio_.push_back(c); + } else { + g_media_server->pending_preloads_.push_back(c); + } + g_media_server->process_timer_->SetLength(0); +} + +void Media::AddPackage(const std::string& name, const std::string& path) { + // we don't protect package-path access so make sure its always from here.. + assert(InGameThread()); +#if BA_DEBUG_BUILD + if (packages_.find(name) != packages_.end()) { + Log("WARNING: adding duplicate package: '" + name + "'"); + } +#endif // BA_DEBUG_BUILD + packages_[name] = path; +} + +Media::MediaListsLock::MediaListsLock() { + BA_DEBUG_FUNCTION_TIMER_BEGIN(); + g_media->media_lists_mutex_.lock(); + assert(!g_media->media_lists_locked_); + g_media->media_lists_locked_ = true; + BA_DEBUG_FUNCTION_TIMER_END_THREAD(20); +} + +Media::MediaListsLock::~MediaListsLock() { + assert(g_media->media_lists_locked_); + g_media->media_lists_locked_ = false; + g_media->media_lists_mutex_.unlock(); +} + +} // namespace ballistica diff --git a/src/ballistica/media/media.h b/src/ballistica/media/media.h new file mode 100644 index 00000000..54d49e49 --- /dev/null +++ b/src/ballistica/media/media.h @@ -0,0 +1,215 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_MEDIA_H_ +#define BALLISTICA_MEDIA_MEDIA_H_ + +#include +#include +#include + +#include "ballistica/core/context.h" +#include "ballistica/core/module.h" +#include "ballistica/core/object.h" + +namespace ballistica { + +/// Global media wrangling class. +class Media { + public: + static void Init(); + ~Media(); + + /// Handy function to try to return a bit of media from a std::map + /// of weak-refs, loading/adding it if need be. + template + static auto GetMedia(std::map >* list, + const std::string& name, Scene* scene) + -> Object::Ref { + assert(InGameThread()); + assert(list); + auto i = list->find(name); + + // If we have an entry pointing to a live component, just return a new ref + // to it. + if (i != list->end() && i->second.exists()) { + return Object::Ref(i->second.get()); + } else { + // Otherwise make a new one, pop a weak-ref on our list, and return a + // strong-ref to it. + auto t(Object::New(name, scene)); + (*list)[name] = t; + return t; + } + } + + void AddPackage(const std::string& name, const std::string& path); + void Prune(int level = 0); + + /// Finish loading any media that has been preloaded but still needs to be + /// loaded by the proper thread. + auto RunPendingLoadsGameThread() -> bool; + + /// Return true if audio loads remain to be done. + auto RunPendingAudioLoads() -> bool; + + /// Return true if graphics loads remain to be done. + auto RunPendingGraphicsLoads() -> bool; + void ClearPendingLoadsDoneList(); + template + auto RunPendingLoadList(std::vector*>* cList) -> bool; + + /// This function takes a newly allocated pointer which + /// is deleted once the load is completed. + void AddPendingLoad(Object::Ref* c); + struct PreloadRunnable; + enum class FileType { kModel, kCollisionModel, kTexture, kSound, kData }; + auto FindMediaFile(FileType fileType, const std::string& file_in) + -> std::string; + + /// Unload renderer-specific bits only (gl display lists, etc) - used when + /// recreating/adjusting the renderer. + void UnloadRendererBits(bool textures, bool models); + + /// Should be called from the game thread after UnloadRendererBits(); + /// kicks off bg loads for all existing unloaded media. + void MarkAllMediaForLoad(); + void PrintLoadInfo(); + + auto GetModelPendingLoadCount() -> int; + auto GetTexturePendingLoadCount() -> int; + auto GetSoundPendingLoadCount() -> int; + auto GetDataPendingLoadCount() -> int; + auto GetCollideModelPendingLoadCount() -> int; + + /// Return the total number of graphics related pending loads. + auto GetGraphicalPendingLoadCount() -> int; + + /// Return the total number of pending loads. + auto GetPendingLoadCount() -> int; + + /// You must hold one of these locks while calling Get*Data() below. + class MediaListsLock { + public: + MediaListsLock(); + ~MediaListsLock(); + }; + + /// Load/cache media (make sure you hold a MediaListsLock). + auto GetTextureData(const std::string& file_name) -> Object::Ref; + auto GetTextureData(TextPacker* packer) -> Object::Ref; + auto GetTextureDataQRCode(const std::string& file_name) + -> Object::Ref; + auto GetCubeMapTextureData(const std::string& file_name) + -> Object::Ref; + auto GetModelData(const std::string& file_name) -> Object::Ref; + auto GetSoundData(const std::string& file_name) -> Object::Ref; + auto GetDataData(const std::string& file_name) -> Object::Ref; + auto GetCollideModelData(const std::string& file_name) + -> Object::Ref; + + // Get system assets. + auto GetTexture(SystemTextureID id) -> TextureData* { + BA_PRECONDITION_FATAL(system_media_loaded_); // Revert to assert later. + assert(InGameThread()); + assert(static_cast(id) < system_textures_.size()); + return system_textures_[static_cast(id)].get(); + } + auto GetCubeMapTexture(SystemCubeMapTextureID id) -> TextureData* { + BA_PRECONDITION_FATAL(system_media_loaded_); // Revert to assert later. + assert(InGameThread()); + assert(static_cast(id) < system_cube_map_textures_.size()); + return system_cube_map_textures_[static_cast(id)].get(); + } + auto GetSound(SystemSoundID id) -> SoundData* { + BA_PRECONDITION_FATAL(system_media_loaded_); // Revert to assert later. + assert(InGameThread()); + assert(static_cast(id) < system_sounds_.size()); + return system_sounds_[static_cast(id)].get(); + } + auto GetModel(SystemModelID id) -> ModelData* { + BA_PRECONDITION_FATAL(system_media_loaded_); // Revert to assert later. + assert(InGameThread()); + assert(static_cast(id) < system_models_.size()); + return system_models_[static_cast(id)].get(); + } + + /// Load up hard-coded media for interface, etc. + void LoadSystemMedia(); + + auto total_model_count() const -> uint32_t { + return static_cast(models_.size()); + } + auto total_texture_count() const -> uint32_t { + return static_cast(textures_.size() + text_textures_.size() + + qr_textures_.size()); + } + auto total_sound_count() const -> uint32_t { + return static_cast(sounds_.size()); + } + auto total_collide_model_count() const -> uint32_t { + return static_cast(collide_models_.size()); + } + struct PreloadRunnable : public Runnable { + explicit PreloadRunnable(Object::Ref* c_in) : c(c_in) {} + void Run() override; + Object::Ref* c; + }; + + private: + Media(); + static void MarkComponentForLoad(MediaComponentData* c); + void LoadSystemTexture(SystemTextureID id, const char* name); + void LoadSystemCubeMapTexture(SystemCubeMapTextureID id, const char* name); + void LoadSystemSound(SystemSoundID id, const char* name); + void LoadSystemData(SystemDataID id, const char* name); + void LoadSystemModel(SystemModelID id, const char* name); + + template + auto GetComponentPendingLoadCount( + std::map >* t_list, MediaType type) -> int; + + template + auto GetComponentData(const std::string& file_name, + std::map >* c_list) + -> Object::Ref; + + std::vector media_paths_; + bool have_pending_loads_[static_cast(MediaType::kLast)]{}; + std::map packages_; + + // For use by MediaListsLock; don't manually acquire + std::mutex media_lists_mutex_; + + // Will be true while a MediaListsLock exists. Good to debug-verify this + // during any media list access. + bool media_lists_locked_{}; + + // 'hard-wired' internal media + bool system_media_loaded_{}; + std::vector > system_textures_; + std::vector > system_cube_map_textures_; + std::vector > system_sounds_; + std::vector > system_datas_; + std::vector > system_models_; + + // All existing media by filename (including internal). + std::map > textures_; + std::map > text_textures_; + std::map > qr_textures_; + std::map > models_; + std::map > sounds_; + std::map > datas_; + std::map > collide_models_; + + // Components that have been preloaded but need to be loaded. + std::mutex pending_load_list_mutex_; + std::vector*> pending_loads_graphics_; + std::vector*> pending_loads_sounds_; + std::vector*> pending_loads_datas_; + std::vector*> pending_loads_other_; + std::vector*> pending_loads_done_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_MEDIA_H_ diff --git a/src/ballistica/media/media_server.cc b/src/ballistica/media/media_server.cc new file mode 100644 index 00000000..e869506e --- /dev/null +++ b/src/ballistica/media/media_server.cc @@ -0,0 +1,240 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/media/media_server.h" + +#include +#include + +#include "ballistica/generic/huffman.h" +#include "ballistica/generic/timer.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/media/data/media_component_data.h" +#include "ballistica/media/media.h" +#include "ballistica/platform/platform.h" + +namespace ballistica { + +MediaServer::MediaServer(Thread* thread) + : Module("media", thread), + writing_replay_(false), + replay_message_bytes_(0), + replays_broken_(false), + replay_out_file_(nullptr) { + assert(g_media_server == nullptr); + g_media_server = this; + + // get our thread to give us periodic processing time... + process_timer_ = + NewThreadTimer(1000, true, NewLambdaRunnable([this] { Process(); })); +} + +MediaServer::~MediaServer() = default; + +void MediaServer::PushBeginWriteReplayCall() { + PushCall([this] { + if (replays_broken_) { + return; + } + + // we only allow writing one replay at once; make sure that's actually the + // case + if (writing_replay_) { + Log("MediaServer got BeginWriteReplayCall while already writing"); + WriteReplayMessages(); + if (replay_out_file_) { + fclose(replay_out_file_); + } + replay_out_file_ = nullptr; + replays_broken_ = true; + return; + } + writing_replay_ = true; + + std::string f_name = "__lastReplay"; + assert(g_platform); + std::string file_path = + g_platform->GetReplaysDir() + BA_DIRSLASH + f_name + ".brp"; + replay_out_file_ = g_platform->FOpen(file_path.c_str(), "wb"); + replay_bytes_written_ = 0; + + if (!replay_out_file_) { + Log("ERROR: unable to open output-stream file: '" + file_path + "'"); + } else { + // write file id and protocol-version + // NOTE - we always write replays in our host protocol version + // no matter what the client stream is + uint32_t file_id = kBrpFileID; + uint16_t version = kProtocolVersion; + if ((fwrite(&file_id, sizeof(file_id), 1, replay_out_file_) != 1) + || (fwrite(&version, sizeof(version), 1, replay_out_file_) != 1)) { + fclose(replay_out_file_); + replay_out_file_ = nullptr; + Log("error writing replay file header: " + + g_platform->GetErrnoString()); + } + replay_bytes_written_ = 5; + } + + // trigger our process timer to go off immediately + // (we may need to wake it up) + g_media_server->process_timer_->SetLength(0); + }); +} + +void MediaServer::PushAddMessageToReplayCall(const std::vector& data) { + PushCall([this, data] { + if (replays_broken_) { + return; + } + + // sanity check.. + if (!writing_replay_) { + Log("MediaServer got AddMessageToReplayCall while not writing replay"); + replays_broken_ = true; + return; + } + + // just add it to our list + if (replay_out_file_) { + // if we've got too much data built up (lets go with 10 megs for now), + // abort + if (replay_message_bytes_ > 10000000) { + Log("replay output buffer exceeded 10 megs; aborting replay"); + fclose(replay_out_file_); + replay_out_file_ = nullptr; + replay_message_bytes_ = 0; + replay_messages_.clear(); + return; + } + replay_message_bytes_ += data.size(); + replay_messages_.push_back(data); + } + }); +} + +void MediaServer::PushEndWriteReplayCall() { + PushCall([this] { + if (replays_broken_) { + return; + } + + // sanity check.. + if (!writing_replay_) { + Log("_finishWritingReplay called while not writing"); + replays_broken_ = true; + return; + } + WriteReplayMessages(); + + // whether or not we actually have a file has no impact on our + // writing_replay_ status.. + if (replay_out_file_) { + fclose(replay_out_file_); + replay_out_file_ = nullptr; + } + writing_replay_ = false; + }); +} + +void MediaServer::WriteReplayMessages() { + if (replay_out_file_) { + for (auto&& i : replay_messages_) { + std::vector data_compressed = g_utils->huffman()->compress(i); + + // If message length is < 254, write length as one byte. + // If its between 254 and 65535, write 254 and then 2 length bytes + // otherwise write 255 and then 4 length bytes. + auto len32 = static_cast(data_compressed.size()); + { + uint8_t len8; + if (len32 < 254) { + len8 = static_cast_check_fit(len32); + } else if (len32 < 65535) { + len8 = 254; + } else { + len8 = 255; + } + if (fwrite(&len8, 1, 1, replay_out_file_) != 1) { + fclose(replay_out_file_); + replay_out_file_ = nullptr; + Log("error writing replay file: " + g_platform->GetErrnoString()); + return; + } + } + // write 16 bit val if need be.. + if (len32 >= 254) { + if (len32 <= 65535) { + auto len16 = static_cast_check_fit(len32); + if (fwrite(&len16, 2, 1, replay_out_file_) != 1) { + fclose(replay_out_file_); + replay_out_file_ = nullptr; + Log("error writing replay file: " + g_platform->GetErrnoString()); + return; + } + } else { + if (fwrite(&len32, 4, 1, replay_out_file_) != 1) { + fclose(replay_out_file_); + replay_out_file_ = nullptr; + Log("error writing replay file: " + g_platform->GetErrnoString()); + return; + } + } + } + // write buffer + size_t result = fwrite(&(data_compressed[0]), data_compressed.size(), 1, + replay_out_file_); + if (result != 1) { + fclose(replay_out_file_); + replay_out_file_ = nullptr; + Log("error writing replay file: " + g_platform->GetErrnoString()); + return; + } + replay_bytes_written_ += data_compressed.size() + 2; + } + replay_messages_.clear(); + replay_message_bytes_ = 0; + } +} + +void MediaServer::Process() { + // make sure we don't do any loading until we know what kind/quality of + // textures we'll be loading + if (!g_media || !g_graphics_server + || !g_graphics_server->texture_compression_types_are_set() // NOLINT + || !g_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 + // on-demand, so that's a lower priority for us) + if (!pending_preloads_.empty()) { + (**pending_preloads_.back()).Preload(); + // pass the ref-pointer along to the load queue + g_media->AddPendingLoad(pending_preloads_.back()); + pending_preloads_.pop_back(); + } else if (!pending_preloads_audio_.empty()) { + (**pending_preloads_audio_.back()).Preload(); + // pass the ref-pointer along to the load queue + g_media->AddPendingLoad(pending_preloads_audio_.back()); + pending_preloads_audio_.pop_back(); + } + + // if we're writing a replay, dump anything we've got built up.. + if (writing_replay_) { + WriteReplayMessages(); + } + + // if we've got nothing left, set our timer to go off every now and then if + // we're writing a replay.. otherwise just sleep indefinitely. + if (pending_preloads_.empty() && pending_preloads_audio_.empty()) { + if (writing_replay_) { + process_timer_->SetLength(1000); + } else { + process_timer_->SetLength(-1); + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/media/media_server.h b/src/ballistica/media/media_server.h new file mode 100644 index 00000000..b52b1cd0 --- /dev/null +++ b/src/ballistica/media/media_server.h @@ -0,0 +1,39 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_MEDIA_MEDIA_SERVER_H_ +#define BALLISTICA_MEDIA_MEDIA_SERVER_H_ + +#include +#include + +#include "ballistica/core/module.h" + +namespace ballistica { + +class MediaServer : public Module { + public: + explicit MediaServer(Thread* thread); + ~MediaServer() override; + void PushBeginWriteReplayCall(); + void PushEndWriteReplayCall(); + void PushAddMessageToReplayCall(const std::vector& data); + + private: + void Process(); + void WriteReplayMessages(); + FILE* replay_out_file_{}; + size_t replay_bytes_written_{}; + bool writing_replay_{}; + bool replays_broken_{}; + std::list > replay_messages_; + size_t replay_message_bytes_{}; + Timer* process_timer_{}; + std::vector*> pending_preloads_; + std::vector*> pending_preloads_audio_; + friend struct PreloadRunnable; + friend class Media; +}; + +} // namespace ballistica + +#endif // BALLISTICA_MEDIA_MEDIA_SERVER_H_ diff --git a/src/ballistica/networking/network_reader.h b/src/ballistica/networking/network_reader.h new file mode 100644 index 00000000..e6ff0bd5 --- /dev/null +++ b/src/ballistica/networking/network_reader.h @@ -0,0 +1,60 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_NETWORKING_NETWORK_READER_H_ +#define BALLISTICA_NETWORKING_NETWORK_READER_H_ + +#include +#include +#include +#include +#include + +#include "ballistica/ballistica.h" + +namespace ballistica { + +// This is a special thread that manages the game's main network sockets; +// it handles creating/destroying them as well as listening for incoming +// packets. it is not a normal BA thread so doesn't have the ability to receive +// messages (it generally sits blocked in a select() call). Writing to these +// sockets takes place in other threads; just make sure to lock the mutex and +// ensure the sockets exist before doing the actual write. +class NetworkReader { + public: + explicit NetworkReader(int port); + ~NetworkReader(); + auto Pause() -> void; + auto Resume() -> void; + auto port4() const { return port4_; } + auto port6() const { return port6_; } + auto sd_mutex() -> std::mutex& { return sd_mutex_; } + auto sd4() const { return sd4_; } + auto sd6() const { return sd6_; } + + private: + auto HandleJSONPing(const std::string& data) -> std::string; + auto PokeSelf() -> void; + auto RunThread() -> int; + static auto RunThreadStatic(void* self) -> int { + return static_cast(self)->RunThread(); + } + std::unique_ptr remote_server_; + int sd4_{-1}; + int sd6_{-1}; + std::mutex sd_mutex_; + + // This needs to be locked while modifying or writing to either the ipv4 or + // ipv6 socket. The one exception is when the network-reader thread is reading + // from them, since there is no chance of anyone else reading or modifying + // them. (that is all handled by the net-reader thread). + int port4_{-1}; + int port6_{-1}; + std::thread* thread_{}; + bool paused_{}; + std::mutex paused_mutex_; + std::condition_variable paused_cv_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_NETWORKING_NETWORK_READER_H_ diff --git a/src/ballistica/networking/network_write_module.h b/src/ballistica/networking/network_write_module.h new file mode 100644 index 00000000..821d3e77 --- /dev/null +++ b/src/ballistica/networking/network_write_module.h @@ -0,0 +1,21 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_NETWORKING_NETWORK_WRITE_MODULE_H_ +#define BALLISTICA_NETWORKING_NETWORK_WRITE_MODULE_H_ + +#include + +#include "ballistica/core/module.h" + +namespace ballistica { + +// this thread handles network output and whatnot +class NetworkWriteModule : public Module { + public: + void PushSendToCall(const std::vector& msg, const SockAddr& addr); + explicit NetworkWriteModule(Thread* thread); +}; + +} // namespace ballistica + +#endif // BALLISTICA_NETWORKING_NETWORK_WRITE_MODULE_H_ diff --git a/src/ballistica/networking/networking.h b/src/ballistica/networking/networking.h new file mode 100644 index 00000000..fa4c8816 --- /dev/null +++ b/src/ballistica/networking/networking.h @@ -0,0 +1,168 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_NETWORKING_NETWORKING_H_ +#define BALLISTICA_NETWORKING_NETWORKING_H_ + +#include +#include +#include +#include + +#include "ballistica/ballistica.h" + +namespace ballistica { + +// Packet types (first byte of raw udp packet). +// These packets can apply to our UDP connection layer, Remote App, etc. +// and don't exist for other connection mechanisms (GPGS, etc). +#define BA_PACKET_REMOTE_PING 0 +#define BA_PACKET_REMOTE_PONG 1 +#define BA_PACKET_REMOTE_ID_REQUEST 2 +#define BA_PACKET_REMOTE_ID_RESPONSE 3 +#define BA_PACKET_REMOTE_DISCONNECT 4 +#define BA_PACKET_REMOTE_STATE 5 +#define BA_PACKET_REMOTE_STATE_ACK 6 +#define BA_PACKET_REMOTE_DISCONNECT_ACK 7 +#define BA_PACKET_REMOTE_GAME_QUERY 8 +#define BA_PACKET_REMOTE_GAME_RESPONSE 9 +#define BA_PACKET_REMOTE_STATE2 10 + +// Very simple 1 byte packet/response used to test accessibility. +#define BA_PACKET_SIMPLE_PING 11 +#define BA_PACKET_SIMPLE_PONG 12 + +// Fancier ping packet that can contain arbitrary data snippets. +// (so we can include stuff like current player counts, etc. in our response) +#define BA_PACKET_JSON_PING 13 +#define BA_PACKET_JSON_PONG 14 + +// Used on android to wake our socket up so we can kill it. +#define BA_PACKET_POKE 21 + +// Local network game scanning. +#define BA_PACKET_GAME_QUERY 22 +#define BA_PACKET_GAME_QUERY_RESPONSE 23 +#define BA_PACKET_CLIENT_REQUEST 24 +#define BA_PACKET_CLIENT_ACCEPT 25 +#define BA_PACKET_CLIENT_DENY 26 +#define BA_PACKET_CLIENT_DENY_VERSION_MISMATCH 27 +#define BA_PACKET_CLIENT_DENY_ALREADY_IN_PARTY 28 +#define BA_PACKET_CLIENT_DENY_PARTY_FULL 29 +#define BA_PACKET_DISCONNECT_FROM_CLIENT_REQUEST 32 +#define BA_PACKET_DISCONNECT_FROM_CLIENT_ACK 33 +#define BA_PACKET_DISCONNECT_FROM_HOST_REQUEST 34 +#define BA_PACKET_DISCONNECT_FROM_HOST_ACK 35 +#define BA_PACKET_CLIENT_GAMEPACKET_COMPRESSED 36 +#define BA_PACKET_HOST_GAMEPACKET_COMPRESSED 37 + +// Gamepackets are chunks of compressed data that apply specifically to a +// ballistica game connection. These packets can be provided over the UDP +// connection layer or by any other transport layer. When decompressed they have +// the following types as their first byte. NOTE - these originally shared a +// domain with BA_PACKET, but now they're independent... so need to avoid value +// clashes.. (hmm did i mean to say NO need?) +#define BA_GAMEPACKET_HANDSHAKE 15 +#define BA_GAMEPACKET_HANDSHAKE_RESPONSE 16 +#define BA_GAMEPACKET_MESSAGE 17 +#define BA_GAMEPACKET_MESSAGE_UNRELIABLE 18 +#define BA_GAMEPACKET_DISCONNECT 19 +#define BA_GAMEPACKET_KEEPALIVE 20 + +// Messages is our high level layer that sits on top of gamepackets. +// They can be any size and will always arrive in the order they were sent +// (though ones marked unreliable may be dropped). +#define BA_MESSAGE_SESSION_RESET 0 +#define BA_MESSAGE_SESSION_COMMANDS 1 +#define BA_MESSAGE_SESSION_DYNAMICS_CORRECTION 2 +#define BA_MESSAGE_REQUEST_REMOTE_PLAYER 4 +#define BA_MESSAGE_ATTACH_REMOTE_PLAYER 5 // OBSOLETE (use the _2 version) +#define BA_MESSAGE_DETACH_REMOTE_PLAYER 6 +#define BA_MESSAGE_REMOTE_PLAYER_INPUT_COMMANDS 7 +#define BA_MESSAGE_REMOVE_REMOTE_PLAYER 8 +#define BA_MESSAGE_PARTY_ROSTER 9 +#define BA_MESSAGE_CHAT 10 +#define BA_MESSAGE_PARTY_MEMBER_JOINED 11 +#define BA_MESSAGE_PARTY_MEMBER_LEFT 12 + +// Hmmm; should multipart logic exist at the gamepacket layer instead?... +// A: that would require the re-send logic to be aware of multi-packet messages +// so maybe this is best. +#define BA_MESSAGE_MULTIPART 13 +#define BA_MESSAGE_MULTIPART_END 14 +#define BA_MESSAGE_CLIENT_PLAYER_PROFILES 15 +#define BA_MESSAGE_ATTACH_REMOTE_PLAYER_2 16 +#define BA_MESSAGE_HOST_INFO 17 +#define BA_MESSAGE_CLIENT_INFO 18 +#define BA_MESSAGE_KICK_VOTE 19 + +// General purpose json message type; its "t" entry is is an int corresponding +// to the BA_JMESSAGE types below. +#define BA_MESSAGE_JMESSAGE 20 +#define BA_MESSAGE_CLIENT_PLAYER_PROFILES_JSON 21 + +#define BA_JMESSAGE_SCREEN_MESSAGE 0 + +// Enable huffman compression for all net packets? +#define BA_HUFFMAN_NET_COMPRESSION 1 + +// Enable training mode to build the huffman tree. +// This will spit a C array of ints to stdout based on net data. +// we currently hard code our tree. +#if !BA_HUFFMAN_NET_COMPRESSION +#define HUFFMAN_TRAINING_MODE 0 +#endif + +// Bits used by the game thread for network communication. +class Networking { + public: + // Send a message to an address. This may block for a brief moment, so it can + // be more efficient to send a SendToMessage to the NetworkWrite thread which + // will do this there. + static void SendTo(const std::vector& buffer, const SockAddr& addr); + Networking(); + ~Networking(); + + // Run a cycle of host scanning (basically sending out a broadcast packet to + // see who's out there). + void HostScanCycle(); + void EndHostScanning(); + + // Called on mobile platforms when going into the background, etc + // (when all networking should be shut down) + void Pause(); + void Resume(); + struct ScanResultsEntry { + std::string display_string; + std::string address; + }; + auto GetScanResults() -> std::vector; + + /// Sends a POST request to the master server and returns the response. + /// path should be something like "/mystatspage". + /// Throws std::exceptions (NOT ballistica Exceptions) if something goes + /// wrong. + static auto MasterServerPost( + const std::string& path, + const std::map& parameters, + bool use_fallback_addr = false) -> std::string; + + /// Sends a GET request to the master server and returns the response. + /// path should be something like "/mystatspage". + /// Throws std::exceptions (NOT ballistica Exceptions) if something goes + /// wrong. + static auto MasterServerGet(const std::string& path, + bool use_fallback_addr = false) -> std::string; + + private: + void PruneScanResults(); + struct ScanResultsEntryPriv; + std::map scan_results_; + std::mutex scan_results_mutex_; + uint32_t next_scan_query_id_{}; + int scan_socket_{-1}; + bool running_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_NETWORKING_NETWORKING_H_ diff --git a/src/ballistica/networking/networking_sys.h b/src/ballistica/networking/networking_sys.h new file mode 100644 index 00000000..b21e9e65 --- /dev/null +++ b/src/ballistica/networking/networking_sys.h @@ -0,0 +1,30 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_NETWORKING_NETWORKING_SYS_H_ +#define BALLISTICA_NETWORKING_NETWORKING_SYS_H_ + +// Include everything needed for standard sockets api usage. + +#if BA_OSTYPE_WINDOWS +// (need includes to stay in this order to disabling formatting) +// clang-format off +#include +#include +// clang-format on +#else +#include +#include +#include +#include +#include +#include +#if BA_OSTYPE_ANDROID +#include "ballistica/platform/android/ifaddrs_android_ext.h" +#else +#include +#endif +#endif +#include +#include + +#endif // BALLISTICA_NETWORKING_NETWORKING_SYS_H_ diff --git a/src/ballistica/networking/sockaddr.h b/src/ballistica/networking/sockaddr.h new file mode 100644 index 00000000..25916e00 --- /dev/null +++ b/src/ballistica/networking/sockaddr.h @@ -0,0 +1,55 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_NETWORKING_SOCKADDR_H_ +#define BALLISTICA_NETWORKING_SOCKADDR_H_ + +#include +#include + +#include "ballistica/ballistica.h" +#include "ballistica/networking/networking_sys.h" + +namespace ballistica { + +class SockAddr { + public: + SockAddr() { memset(&addr_, 0, sizeof(addr_)); } + + // Creates from an ipv4 or ipv6 address string; + // throws an exception on error. + SockAddr(const std::string& addr, int port); + explicit SockAddr(const sockaddr_storage& addr_in) { + addr_ = addr_in; + assert(addr_.ss_family == AF_INET || addr_.ss_family == AF_INET6); + } + auto GetSockAddr() const -> const sockaddr* { + return reinterpret_cast(&addr_); + } + auto GetSockAddrLen() const -> socklen_t { + switch (addr_.ss_family) { + case AF_INET: + return sizeof(sockaddr_in); + case AF_INET6: + return sizeof(sockaddr_in6); + default: + throw Exception(); + } + } + auto IsV6() const -> bool { + switch (addr_.ss_family) { + case AF_INET: + return false; + case AF_INET6: + return true; + default: + throw Exception(); + } + } + + private: + sockaddr_storage addr_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_NETWORKING_SOCKADDR_H_ diff --git a/src/ballistica/networking/telnet_server.cc b/src/ballistica/networking/telnet_server.cc new file mode 100644 index 00000000..39d4be31 --- /dev/null +++ b/src/ballistica/networking/telnet_server.cc @@ -0,0 +1,241 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/networking/telnet_server.h" + +#include + +#include "ballistica/app/app_globals.h" +#include "ballistica/ballistica.h" +#include "ballistica/core/context.h" +#include "ballistica/game/game.h" +#include "ballistica/networking/networking.h" +#include "ballistica/networking/networking_sys.h" +#include "ballistica/platform/platform.h" +#include "ballistica/python/python_command.h" +#include "ballistica/python/python_sys.h" + +namespace ballistica { + +TelnetServer::TelnetServer(int port) : port_(port) { + thread_ = new std::thread(RunThreadStatic, this); + assert(g_app_globals->telnet_server == nullptr); + g_app_globals->telnet_server = this; + + // NOTE: we consider access implicitly granted on headless builds + // since we can't pop up the request dialog. + // There is still password protection and we now don't even spin + // up the telnet socket by default on servers. + if (HeadlessMode()) { + user_has_granted_access_ = true; + } +} + +void TelnetServer::Pause() { + assert(InMainThread()); + assert(!paused_); + { + std::unique_lock lock(paused_mutex_); + paused_ = true; + } + + // FIXME - need a way to kill these sockets; + // On iOS they die automagically but not android. + // attempted to force-close at some point but it didn't work (on android at + // least) +} + +void TelnetServer::Resume() { + assert(InMainThread()); + assert(paused_); + { + std::unique_lock lock(paused_mutex_); + paused_ = false; + } + + // Poke our thread so it can go on its way. + paused_cv_.notify_all(); +} + +auto TelnetServer::RunThread() -> int { + // Do this whole thing in a loop. + // If we get put to sleep we just start over. + while (true) { + // Sleep until we're unpaused. + if (paused_) { + std::unique_lock lock(paused_mutex_); + paused_cv_.wait(lock, [this] { return (!paused_); }); + } + + sd_ = socket(AF_INET, SOCK_STREAM, 0); + if (sd_ < 0) { + Log("Error: Unable to open host socket; errno " + std::to_string(errno)); + return 0; + } + + // Make it reusable. + int on = 1; + int status = + setsockopt(sd_, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on)); + if (-1 == status) Log("Error setting SO_REUSEADDR on telnet server"); + + // Bind to local server port. + struct sockaddr_in serv_addr {}; + serv_addr.sin_family = AF_INET; + serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // NOLINT + int result; + serv_addr.sin_port = htons(port_); // NOLINT + result = ::bind(sd_, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); + if (result != 0) { + return 0; + } + char buffer[10000]; + const char* prompt = "ballisticacore> "; + const char* password_prompt = "password:"; + + // Now just listen and forward msg along to people. + while (true) { + struct sockaddr_storage from {}; + socklen_t from_size = sizeof(from); + if (listen(sd_, 0) == 0) { + client_sd_ = accept(sd_, (struct sockaddr*)&from, &from_size); + if (client_sd_ < 0) { + break; + } + + // If we dont have access and havnt asked the user for it yet, ask them. + if (!user_has_granted_access_ && g_game + && !have_asked_user_for_access_) { + g_game->PushAskUserForTelnetAccessCall(); + have_asked_user_for_access_ = true; + } + + // Require password for each connection if we have one + reading_password_ = require_password_; + + if (g_game) { + if (reading_password_) { + PushPrint(password_prompt); + } else { + PushPrint(prompt); + } + } + while (true) { + result = + static_cast(recv(client_sd_, buffer, sizeof(buffer) - 1, 0)); + + // Socket closed/disconnected. + if (result == 0 || result == -1) { + // We got closed for whatever reason. + if (client_sd_ != -1) { + g_platform->CloseSocket(client_sd_); + } + client_sd_ = -1; + break; + } else { + buffer[result] = 0; + + // Looks like these come in with '\r\n' at the end.. lets strip + // that. + if (result > 0 && (buffer[result - 1] == '\n')) { + buffer[result - 1] = 0; + if (result > 1 && (buffer[result - 2] == '\r')) + buffer[result - 2] = 0; + } + if (g_game) { + if (user_has_granted_access_) { + if (reading_password_) { + if (GetRealTime() - last_try_time_ < 2000) { + PushPrint( + std::string("retried too soon; please wait a moment " + "and try again.\n") + + password_prompt); + } else if (buffer == password_) { + reading_password_ = false; + PushPrint(prompt); + } else { + last_try_time_ = GetRealTime(); + PushPrint(std::string("incorrect.\n") + password_prompt); + } + } else { + PushTelnetScriptCommand(buffer); + } + } else { + PushPrint(g_game->GetResourceString("telnetAccessDeniedText")); + } + } + } + } + } else { + // Listening failed; abort. + if (sd_ != -1) { + g_platform->CloseSocket(sd_); + } + break; + } + } + + // Sleep for a moment to keep us from running wild if we're unable to block. + Platform::SleepMS(1000); + } +} + +void TelnetServer::PushTelnetScriptCommand(const std::string& command) { + assert(g_game); + if (g_game == nullptr) { + return; + } + g_game->PushCall([this, command] { + // These are always run in whichever context is 'visible'. + ScopedSetContext cp(g_game->GetForegroundContext()); + if (!g_app_globals->user_ran_commands) { + g_app_globals->user_ran_commands = true; + } + PythonCommand cmd(command, ""); + if (cmd.CanEval()) { + PyObject* obj = cmd.RunReturnObj(true); + if (obj && obj != Py_None) { + PyObject* s = PyObject_Repr(obj); + if (s) { + const char* c = PyUnicode_AsUTF8(s); + PushPrint(std::string(c) + "\n"); + Py_DECREF(s); + } + Py_DECREF(obj); + } + } else { + // Not eval-able; just run it. + cmd.Run(); + } + PushPrint("ballisticacore> "); + }); +} + +void TelnetServer::PushPrint(const std::string& s) { + assert(g_game); + g_game->PushCall([this, s] { Print(s); }); +} + +void TelnetServer::Print(const std::string& s) { + // Currently we make the assumption that *only* the game thread writes to our + // socket. + assert(InGameThread()); + if (client_sd_ != -1) { + send(client_sd_, s.c_str(), + static_cast_check_fit(s.size()), 0); + } +} + +TelnetServer::~TelnetServer() = default; + +void TelnetServer::SetAccessEnabled(bool v) { user_has_granted_access_ = v; } + +void TelnetServer::SetPassword(const char* password) { + if (password != nullptr) { + password_ = password; + require_password_ = true; + } else { + require_password_ = false; + } +} + +} // namespace ballistica diff --git a/src/ballistica/networking/telnet_server.h b/src/ballistica/networking/telnet_server.h new file mode 100644 index 00000000..fd0b0d09 --- /dev/null +++ b/src/ballistica/networking/telnet_server.h @@ -0,0 +1,49 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_NETWORKING_TELNET_SERVER_H_ +#define BALLISTICA_NETWORKING_TELNET_SERVER_H_ + +#include +#include +#include +#include + +#include "ballistica/ballistica.h" + +namespace ballistica { + +class TelnetServer { + public: + explicit TelnetServer(int port); + ~TelnetServer(); + auto Pause() -> void; + auto Resume() -> void; + auto PushTelnetScriptCommand(const std::string& command) -> void; + auto PushPrint(const std::string& s) -> void; + auto SetAccessEnabled(bool v) -> void; + auto SetPassword(const char* password) -> void; // nullptr == no password + + private: + auto RunThread() -> int; + auto Print(const std::string& s) -> void; + static auto RunThreadStatic(void* self) -> int { + return static_cast(self)->RunThread(); + } + int sd_{-1}; + int client_sd_{-1}; + int port_{}; + std::thread* thread_{}; + bool have_asked_user_for_access_{}; + bool user_has_granted_access_{}; + bool paused_{}; + bool reading_password_{}; + bool require_password_{}; + millisecs_t last_try_time_{}; + std::string password_; + std::mutex paused_mutex_; + std::condition_variable paused_cv_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_NETWORKING_TELNET_SERVER_H_ diff --git a/src/ballistica/platform/linux/platform_linux.cc b/src/ballistica/platform/linux/platform_linux.cc new file mode 100644 index 00000000..47550ef9 --- /dev/null +++ b/src/ballistica/platform/linux/platform_linux.cc @@ -0,0 +1,72 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#if BA_OSTYPE_LINUX +#include "ballistica/platform/linux/platform_linux.h" + +#include + +#include + +namespace ballistica { + +PlatformLinux::PlatformLinux() {} + +std::string PlatformLinux::GenerateUUID() { + std::string val; + char buffer[100]; + FILE* fd_out = popen("cat /proc/sys/kernel/random/uuid", "r"); + if (fd_out) { + int size = fread(buffer, 1, 99, fd_out); + fclose(fd_out); + if (size == 37) { + buffer[size - 1] = 0; // chop off trailing newline + val = buffer; + } + } + if (val == "") { + throw Exception("kernel uuid not available"); + } + return val; +} + +bool PlatformLinux::DoHasTouchScreen() { return false; } + +void PlatformLinux::DoOpenURL(const std::string& url) { + // hmmm is there a more universal option than this?... + int result = system((std::string("xdg-open \"") + url + "\"").c_str()); + if (result != 0) { + ScreenMessage("error on xdg-open"); + } +} + +void PlatformLinux::OpenFileExternally(const std::string& path) { + std::string cmd = std::string("xdg-open \"") + path + "\""; + int result = system(cmd.c_str()); + if (result != 0) { + Log("Error: Got return value " + std::to_string(result) + + " on xdg-open cmd '" + cmd + "'"); + } +} + +void PlatformLinux::OpenDirExternally(const std::string& path) { + std::string cmd = std::string("xdg-open \"") + path + "\""; + int result = system(cmd.c_str()); + if (result != 0) { + Log("Error: Got return value " + std::to_string(result) + + " on xdg-open cmd '" + cmd + "'"); + } +} + +std::string PlatformLinux::GetPlatformName() { return "linux"; } + +std::string PlatformLinux::GetSubplatformName() { +#if BA_TEST_BUILD + return "test"; +#else + return ""; +#endif +} + +} // namespace ballistica + +#endif // BA_OSTYPE_LINUX diff --git a/src/ballistica/platform/linux/platform_linux.h b/src/ballistica/platform/linux/platform_linux.h new file mode 100644 index 00000000..bebeefc7 --- /dev/null +++ b/src/ballistica/platform/linux/platform_linux.h @@ -0,0 +1,29 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PLATFORM_LINUX_PLATFORM_LINUX_H_ +#define BALLISTICA_PLATFORM_LINUX_PLATFORM_LINUX_H_ +#if BA_OSTYPE_LINUX + +#include + +#include "ballistica/platform/platform.h" + +namespace ballistica { + +class PlatformLinux : public Platform { + public: + PlatformLinux(); + std::string GetDeviceUUIDPrefix() override { return "l"; } + std::string GenerateUUID() override; + bool DoHasTouchScreen() override; + void DoOpenURL(const std::string& url) override; + void OpenFileExternally(const std::string& path) override; + void OpenDirExternally(const std::string& path) override; + std::string GetPlatformName() override; + std::string GetSubplatformName() override; +}; + +} // namespace ballistica + +#endif // BA_OSTYPE_LINUX +#endif // BALLISTICA_PLATFORM_LINUX_PLATFORM_LINUX_H_ diff --git a/src/ballistica/platform/min_sdl.h b/src/ballistica/platform/min_sdl.h new file mode 100644 index 00000000..8b6abfb3 --- /dev/null +++ b/src/ballistica/platform/min_sdl.h @@ -0,0 +1,792 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PLATFORM_MIN_SDL_H_ +#define BALLISTICA_PLATFORM_MIN_SDL_H_ + +// A bit of history: +// +// Ballistica originally used SDL as its sole library for events, +// window-management, etc. on all platforms. This means a lot of the low level +// event handling code was written with SDL types. +// +// Over time, for various reasons, I started converting bits of functionality +// over to native platform APIs, to the point where nowadays SDL's role is +// largely vestigial in some builds; SDL types are getting passed around but +// not actually being supplied by SDL. +// +// Moving forward, my plan has been to clean things up so that SDL can be +// completely removed from platforms that don't actually use it (though +// still fully supported for platforms where it makes sense). That's where +// this header comes in. +// +// The minimum bits of SDL still needed to compile the game have been copied +// here for use by 'non-sdl' platforms. This mainly includes things like event +// types and keysyms. +// +// On platforms using 'full' SDL, this header simply includes the full sdl +// headers. +// +// The idea is that, over time, the SDL types contained here can be replaced +// with ballistica-specific types added to types.h. The 'full' SDL platform +// layer can then translate its SDL types to ballistica types in the same way +// that other platform code translates their native types, and eventually SDL +// usage should be nicely contained to platform/sdl/* for the platforms that +// want it. + +/* + Simple DirectMedia Layer + Copyright (C) 1997-2012 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#if BA_SDL_BUILD +#include "SDL.h" +#else + +typedef int32_t SDL_Keycode; + +typedef enum { + SDL_SCANCODE_UNKNOWN = 0, + + SDL_SCANCODE_A = 4, + SDL_SCANCODE_B = 5, + SDL_SCANCODE_C = 6, + SDL_SCANCODE_D = 7, + SDL_SCANCODE_E = 8, + SDL_SCANCODE_F = 9, + SDL_SCANCODE_G = 10, + SDL_SCANCODE_H = 11, + SDL_SCANCODE_I = 12, + SDL_SCANCODE_J = 13, + SDL_SCANCODE_K = 14, + SDL_SCANCODE_L = 15, + SDL_SCANCODE_M = 16, + SDL_SCANCODE_N = 17, + SDL_SCANCODE_O = 18, + SDL_SCANCODE_P = 19, + SDL_SCANCODE_Q = 20, + SDL_SCANCODE_R = 21, + SDL_SCANCODE_S = 22, + SDL_SCANCODE_T = 23, + SDL_SCANCODE_U = 24, + SDL_SCANCODE_V = 25, + SDL_SCANCODE_W = 26, + SDL_SCANCODE_X = 27, + SDL_SCANCODE_Y = 28, + SDL_SCANCODE_Z = 29, + + SDL_SCANCODE_1 = 30, + SDL_SCANCODE_2 = 31, + SDL_SCANCODE_3 = 32, + SDL_SCANCODE_4 = 33, + SDL_SCANCODE_5 = 34, + SDL_SCANCODE_6 = 35, + SDL_SCANCODE_7 = 36, + SDL_SCANCODE_8 = 37, + SDL_SCANCODE_9 = 38, + SDL_SCANCODE_0 = 39, + + SDL_SCANCODE_RETURN = 40, + SDL_SCANCODE_ESCAPE = 41, + SDL_SCANCODE_BACKSPACE = 42, + SDL_SCANCODE_TAB = 43, + SDL_SCANCODE_SPACE = 44, + + SDL_SCANCODE_MINUS = 45, + SDL_SCANCODE_EQUALS = 46, + SDL_SCANCODE_LEFTBRACKET = 47, + SDL_SCANCODE_RIGHTBRACKET = 48, + SDL_SCANCODE_BACKSLASH = 49, + SDL_SCANCODE_NONUSHASH = 50, + SDL_SCANCODE_SEMICOLON = 51, + SDL_SCANCODE_APOSTROPHE = 52, + SDL_SCANCODE_GRAVE = 53, + SDL_SCANCODE_COMMA = 54, + SDL_SCANCODE_PERIOD = 55, + SDL_SCANCODE_SLASH = 56, + + SDL_SCANCODE_CAPSLOCK = 57, + + SDL_SCANCODE_F1 = 58, + SDL_SCANCODE_F2 = 59, + SDL_SCANCODE_F3 = 60, + SDL_SCANCODE_F4 = 61, + SDL_SCANCODE_F5 = 62, + SDL_SCANCODE_F6 = 63, + SDL_SCANCODE_F7 = 64, + SDL_SCANCODE_F8 = 65, + SDL_SCANCODE_F9 = 66, + SDL_SCANCODE_F10 = 67, + SDL_SCANCODE_F11 = 68, + SDL_SCANCODE_F12 = 69, + + SDL_SCANCODE_PRINTSCREEN = 70, + SDL_SCANCODE_SCROLLLOCK = 71, + SDL_SCANCODE_PAUSE = 72, + SDL_SCANCODE_INSERT = 73, + SDL_SCANCODE_HOME = 74, + SDL_SCANCODE_PAGEUP = 75, + SDL_SCANCODE_DELETE = 76, + SDL_SCANCODE_END = 77, + SDL_SCANCODE_PAGEDOWN = 78, + SDL_SCANCODE_RIGHT = 79, + SDL_SCANCODE_LEFT = 80, + SDL_SCANCODE_DOWN = 81, + SDL_SCANCODE_UP = 82, + + SDL_SCANCODE_NUMLOCKCLEAR = 83, + SDL_SCANCODE_KP_DIVIDE = 84, + SDL_SCANCODE_KP_MULTIPLY = 85, + SDL_SCANCODE_KP_MINUS = 86, + SDL_SCANCODE_KP_PLUS = 87, + SDL_SCANCODE_KP_ENTER = 88, + SDL_SCANCODE_KP_1 = 89, + SDL_SCANCODE_KP_2 = 90, + SDL_SCANCODE_KP_3 = 91, + SDL_SCANCODE_KP_4 = 92, + SDL_SCANCODE_KP_5 = 93, + SDL_SCANCODE_KP_6 = 94, + SDL_SCANCODE_KP_7 = 95, + SDL_SCANCODE_KP_8 = 96, + SDL_SCANCODE_KP_9 = 97, + SDL_SCANCODE_KP_0 = 98, + SDL_SCANCODE_KP_PERIOD = 99, + + SDL_SCANCODE_NONUSBACKSLASH = 100, + SDL_SCANCODE_APPLICATION = 101, + SDL_SCANCODE_POWER = 102, + SDL_SCANCODE_KP_EQUALS = 103, + SDL_SCANCODE_F13 = 104, + SDL_SCANCODE_F14 = 105, + SDL_SCANCODE_F15 = 106, + SDL_SCANCODE_F16 = 107, + SDL_SCANCODE_F17 = 108, + SDL_SCANCODE_F18 = 109, + SDL_SCANCODE_F19 = 110, + SDL_SCANCODE_F20 = 111, + SDL_SCANCODE_F21 = 112, + SDL_SCANCODE_F22 = 113, + SDL_SCANCODE_F23 = 114, + SDL_SCANCODE_F24 = 115, + SDL_SCANCODE_EXECUTE = 116, + SDL_SCANCODE_HELP = 117, + SDL_SCANCODE_MENU = 118, + SDL_SCANCODE_SELECT = 119, + SDL_SCANCODE_STOP = 120, + SDL_SCANCODE_AGAIN = 121, + SDL_SCANCODE_UNDO = 122, + SDL_SCANCODE_CUT = 123, + SDL_SCANCODE_COPY = 124, + SDL_SCANCODE_PASTE = 125, + SDL_SCANCODE_FIND = 126, + SDL_SCANCODE_MUTE = 127, + SDL_SCANCODE_VOLUMEUP = 128, + SDL_SCANCODE_VOLUMEDOWN = 129, + + SDL_SCANCODE_KP_COMMA = 133, + SDL_SCANCODE_KP_EQUALSAS400 = 134, + + SDL_SCANCODE_INTERNATIONAL1 = 135, + SDL_SCANCODE_INTERNATIONAL2 = 136, + SDL_SCANCODE_INTERNATIONAL3 = 137, + SDL_SCANCODE_INTERNATIONAL4 = 138, + SDL_SCANCODE_INTERNATIONAL5 = 139, + SDL_SCANCODE_INTERNATIONAL6 = 140, + SDL_SCANCODE_INTERNATIONAL7 = 141, + SDL_SCANCODE_INTERNATIONAL8 = 142, + SDL_SCANCODE_INTERNATIONAL9 = 143, + SDL_SCANCODE_LANG1 = 144, + SDL_SCANCODE_LANG2 = 145, + SDL_SCANCODE_LANG3 = 146, + SDL_SCANCODE_LANG4 = 147, + SDL_SCANCODE_LANG5 = 148, + SDL_SCANCODE_LANG6 = 149, + SDL_SCANCODE_LANG7 = 150, + SDL_SCANCODE_LANG8 = 151, + SDL_SCANCODE_LANG9 = 152, + + SDL_SCANCODE_ALTERASE = 153, + SDL_SCANCODE_SYSREQ = 154, + SDL_SCANCODE_CANCEL = 155, + SDL_SCANCODE_CLEAR = 156, + SDL_SCANCODE_PRIOR = 157, + SDL_SCANCODE_RETURN2 = 158, + SDL_SCANCODE_SEPARATOR = 159, + SDL_SCANCODE_OUT = 160, + SDL_SCANCODE_OPER = 161, + SDL_SCANCODE_CLEARAGAIN = 162, + SDL_SCANCODE_CRSEL = 163, + SDL_SCANCODE_EXSEL = 164, + + SDL_SCANCODE_KP_00 = 176, + SDL_SCANCODE_KP_000 = 177, + SDL_SCANCODE_THOUSANDSSEPARATOR = 178, + SDL_SCANCODE_DECIMALSEPARATOR = 179, + SDL_SCANCODE_CURRENCYUNIT = 180, + SDL_SCANCODE_CURRENCYSUBUNIT = 181, + SDL_SCANCODE_KP_LEFTPAREN = 182, + SDL_SCANCODE_KP_RIGHTPAREN = 183, + SDL_SCANCODE_KP_LEFTBRACE = 184, + SDL_SCANCODE_KP_RIGHTBRACE = 185, + SDL_SCANCODE_KP_TAB = 186, + SDL_SCANCODE_KP_BACKSPACE = 187, + SDL_SCANCODE_KP_A = 188, + SDL_SCANCODE_KP_B = 189, + SDL_SCANCODE_KP_C = 190, + SDL_SCANCODE_KP_D = 191, + SDL_SCANCODE_KP_E = 192, + SDL_SCANCODE_KP_F = 193, + SDL_SCANCODE_KP_XOR = 194, + SDL_SCANCODE_KP_POWER = 195, + SDL_SCANCODE_KP_PERCENT = 196, + SDL_SCANCODE_KP_LESS = 197, + SDL_SCANCODE_KP_GREATER = 198, + SDL_SCANCODE_KP_AMPERSAND = 199, + SDL_SCANCODE_KP_DBLAMPERSAND = 200, + SDL_SCANCODE_KP_VERTICALBAR = 201, + SDL_SCANCODE_KP_DBLVERTICALBAR = 202, + SDL_SCANCODE_KP_COLON = 203, + SDL_SCANCODE_KP_HASH = 204, + SDL_SCANCODE_KP_SPACE = 205, + SDL_SCANCODE_KP_AT = 206, + SDL_SCANCODE_KP_EXCLAM = 207, + SDL_SCANCODE_KP_MEMSTORE = 208, + SDL_SCANCODE_KP_MEMRECALL = 209, + SDL_SCANCODE_KP_MEMCLEAR = 210, + SDL_SCANCODE_KP_MEMADD = 211, + SDL_SCANCODE_KP_MEMSUBTRACT = 212, + SDL_SCANCODE_KP_MEMMULTIPLY = 213, + SDL_SCANCODE_KP_MEMDIVIDE = 214, + SDL_SCANCODE_KP_PLUSMINUS = 215, + SDL_SCANCODE_KP_CLEAR = 216, + SDL_SCANCODE_KP_CLEARENTRY = 217, + SDL_SCANCODE_KP_BINARY = 218, + SDL_SCANCODE_KP_OCTAL = 219, + SDL_SCANCODE_KP_DECIMAL = 220, + SDL_SCANCODE_KP_HEXADECIMAL = 221, + + SDL_SCANCODE_LCTRL = 224, + SDL_SCANCODE_LSHIFT = 225, + SDL_SCANCODE_LALT = 226, + SDL_SCANCODE_LGUI = 227, + SDL_SCANCODE_RCTRL = 228, + SDL_SCANCODE_RSHIFT = 229, + SDL_SCANCODE_RALT = 230, + SDL_SCANCODE_RGUI = 231, + + SDL_SCANCODE_MODE = 257, + + SDL_SCANCODE_AUDIONEXT = 258, + SDL_SCANCODE_AUDIOPREV = 259, + SDL_SCANCODE_AUDIOSTOP = 260, + SDL_SCANCODE_AUDIOPLAY = 261, + SDL_SCANCODE_AUDIOMUTE = 262, + SDL_SCANCODE_MEDIASELECT = 263, + SDL_SCANCODE_WWW = 264, + SDL_SCANCODE_MAIL = 265, + SDL_SCANCODE_CALCULATOR = 266, + SDL_SCANCODE_COMPUTER = 267, + SDL_SCANCODE_AC_SEARCH = 268, + SDL_SCANCODE_AC_HOME = 269, + SDL_SCANCODE_AC_BACK = 270, + SDL_SCANCODE_AC_FORWARD = 271, + SDL_SCANCODE_AC_STOP = 272, + SDL_SCANCODE_AC_REFRESH = 273, + SDL_SCANCODE_AC_BOOKMARKS = 274, + + SDL_SCANCODE_BRIGHTNESSDOWN = 275, + SDL_SCANCODE_BRIGHTNESSUP = 276, + SDL_SCANCODE_DISPLAYSWITCH = 277, + SDL_SCANCODE_KBDILLUMTOGGLE = 278, + SDL_SCANCODE_KBDILLUMDOWN = 279, + SDL_SCANCODE_KBDILLUMUP = 280, + SDL_SCANCODE_EJECT = 281, + SDL_SCANCODE_SLEEP = 282, + + SDL_NUM_SCANCODES = 512 +} SDL_Scancode; + +#define SDLK_SCANCODE_MASK (1 << 30) +#define SDL_SCANCODE_TO_KEYCODE(X) (X | SDLK_SCANCODE_MASK) + +enum { + SDLK_UNKNOWN = 0, + + SDLK_RETURN = '\r', + SDLK_ESCAPE = '\033', + SDLK_BACKSPACE = '\b', + SDLK_TAB = '\t', + SDLK_SPACE = ' ', + SDLK_EXCLAIM = '!', + SDLK_QUOTEDBL = '"', + SDLK_HASH = '#', + SDLK_PERCENT = '%', + SDLK_DOLLAR = '$', + SDLK_AMPERSAND = '&', + SDLK_QUOTE = '\'', + SDLK_LEFTPAREN = '(', + SDLK_RIGHTPAREN = ')', + SDLK_ASTERISK = '*', + SDLK_PLUS = '+', + SDLK_COMMA = ',', + SDLK_MINUS = '-', + SDLK_PERIOD = '.', + SDLK_SLASH = '/', + SDLK_0 = '0', + SDLK_1 = '1', + SDLK_2 = '2', + SDLK_3 = '3', + SDLK_4 = '4', + SDLK_5 = '5', + SDLK_6 = '6', + SDLK_7 = '7', + SDLK_8 = '8', + SDLK_9 = '9', + SDLK_COLON = ':', + SDLK_SEMICOLON = ';', + SDLK_LESS = '<', + SDLK_EQUALS = '=', + SDLK_GREATER = '>', + SDLK_QUESTION = '?', + SDLK_AT = '@', + /* + Skip uppercase letters + */ + SDLK_LEFTBRACKET = '[', + SDLK_BACKSLASH = '\\', + SDLK_RIGHTBRACKET = ']', + SDLK_CARET = '^', + SDLK_UNDERSCORE = '_', + SDLK_BACKQUOTE = '`', + SDLK_a = 'a', + SDLK_b = 'b', + SDLK_c = 'c', + SDLK_d = 'd', + SDLK_e = 'e', + SDLK_f = 'f', + SDLK_g = 'g', + SDLK_h = 'h', + SDLK_i = 'i', + SDLK_j = 'j', + SDLK_k = 'k', + SDLK_l = 'l', + SDLK_m = 'm', + SDLK_n = 'n', + SDLK_o = 'o', + SDLK_p = 'p', + SDLK_q = 'q', + SDLK_r = 'r', + SDLK_s = 's', + SDLK_t = 't', + SDLK_u = 'u', + SDLK_v = 'v', + SDLK_w = 'w', + SDLK_x = 'x', + SDLK_y = 'y', + SDLK_z = 'z', + + SDLK_CAPSLOCK = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CAPSLOCK), + + SDLK_F1 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F1), + SDLK_F2 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F2), + SDLK_F3 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F3), + SDLK_F4 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F4), + SDLK_F5 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F5), + SDLK_F6 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F6), + SDLK_F7 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F7), + SDLK_F8 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F8), + SDLK_F9 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F9), + SDLK_F10 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F10), + SDLK_F11 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F11), + SDLK_F12 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F12), + + SDLK_PRINTSCREEN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_PRINTSCREEN), + SDLK_SCROLLLOCK = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_SCROLLLOCK), + SDLK_PAUSE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_PAUSE), + SDLK_INSERT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_INSERT), + SDLK_HOME = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_HOME), + SDLK_PAGEUP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_PAGEUP), + SDLK_DELETE = '\177', + SDLK_END = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_END), + SDLK_PAGEDOWN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_PAGEDOWN), + SDLK_RIGHT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_RIGHT), + SDLK_LEFT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_LEFT), + SDLK_DOWN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_DOWN), + SDLK_UP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_UP), + + SDLK_NUMLOCKCLEAR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_NUMLOCKCLEAR), + SDLK_KP_DIVIDE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_DIVIDE), + SDLK_KP_MULTIPLY = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MULTIPLY), + SDLK_KP_MINUS = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MINUS), + SDLK_KP_PLUS = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_PLUS), + SDLK_KP_ENTER = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_ENTER), + SDLK_KP_1 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_1), + SDLK_KP_2 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_2), + SDLK_KP_3 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_3), + SDLK_KP_4 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_4), + SDLK_KP_5 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_5), + SDLK_KP_6 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_6), + SDLK_KP_7 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_7), + SDLK_KP_8 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_8), + SDLK_KP_9 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_9), + SDLK_KP_0 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_0), + SDLK_KP_PERIOD = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_PERIOD), + + SDLK_APPLICATION = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_APPLICATION), + SDLK_POWER = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_POWER), + SDLK_KP_EQUALS = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_EQUALS), + SDLK_F13 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F13), + SDLK_F14 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F14), + SDLK_F15 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F15), + SDLK_F16 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F16), + SDLK_F17 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F17), + SDLK_F18 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F18), + SDLK_F19 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F19), + SDLK_F20 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F20), + SDLK_F21 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F21), + SDLK_F22 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F22), + SDLK_F23 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F23), + SDLK_F24 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F24), + SDLK_EXECUTE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_EXECUTE), + SDLK_HELP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_HELP), + SDLK_MENU = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_MENU), + SDLK_SELECT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_SELECT), + SDLK_STOP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_STOP), + SDLK_AGAIN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AGAIN), + SDLK_UNDO = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_UNDO), + SDLK_CUT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CUT), + SDLK_COPY = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_COPY), + SDLK_PASTE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_PASTE), + SDLK_FIND = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_FIND), + SDLK_MUTE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_MUTE), + SDLK_VOLUMEUP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_VOLUMEUP), + SDLK_VOLUMEDOWN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_VOLUMEDOWN), + SDLK_KP_COMMA = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_COMMA), + SDLK_KP_EQUALSAS400 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_EQUALSAS400), + + SDLK_ALTERASE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_ALTERASE), + SDLK_SYSREQ = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_SYSREQ), + SDLK_CANCEL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CANCEL), + SDLK_CLEAR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CLEAR), + SDLK_PRIOR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_PRIOR), + SDLK_RETURN2 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_RETURN2), + SDLK_SEPARATOR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_SEPARATOR), + SDLK_OUT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_OUT), + SDLK_OPER = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_OPER), + SDLK_CLEARAGAIN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CLEARAGAIN), + SDLK_CRSEL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CRSEL), + SDLK_EXSEL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_EXSEL), + + SDLK_KP_00 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_00), + SDLK_KP_000 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_000), + SDLK_THOUSANDSSEPARATOR = + SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_THOUSANDSSEPARATOR), + SDLK_DECIMALSEPARATOR = + SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_DECIMALSEPARATOR), + SDLK_CURRENCYUNIT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CURRENCYUNIT), + SDLK_CURRENCYSUBUNIT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CURRENCYSUBUNIT), + SDLK_KP_LEFTPAREN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_LEFTPAREN), + SDLK_KP_RIGHTPAREN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_RIGHTPAREN), + SDLK_KP_LEFTBRACE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_LEFTBRACE), + SDLK_KP_RIGHTBRACE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_RIGHTBRACE), + SDLK_KP_TAB = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_TAB), + SDLK_KP_BACKSPACE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_BACKSPACE), + SDLK_KP_A = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_A), + SDLK_KP_B = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_B), + SDLK_KP_C = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_C), + SDLK_KP_D = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_D), + SDLK_KP_E = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_E), + SDLK_KP_F = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_F), + SDLK_KP_XOR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_XOR), + SDLK_KP_POWER = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_POWER), + SDLK_KP_PERCENT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_PERCENT), + SDLK_KP_LESS = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_LESS), + SDLK_KP_GREATER = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_GREATER), + SDLK_KP_AMPERSAND = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_AMPERSAND), + SDLK_KP_DBLAMPERSAND = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_DBLAMPERSAND), + SDLK_KP_VERTICALBAR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_VERTICALBAR), + SDLK_KP_DBLVERTICALBAR = + SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_DBLVERTICALBAR), + SDLK_KP_COLON = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_COLON), + SDLK_KP_HASH = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_HASH), + SDLK_KP_SPACE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_SPACE), + SDLK_KP_AT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_AT), + SDLK_KP_EXCLAM = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_EXCLAM), + SDLK_KP_MEMSTORE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MEMSTORE), + SDLK_KP_MEMRECALL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MEMRECALL), + SDLK_KP_MEMCLEAR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MEMCLEAR), + SDLK_KP_MEMADD = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MEMADD), + SDLK_KP_MEMSUBTRACT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MEMSUBTRACT), + SDLK_KP_MEMMULTIPLY = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MEMMULTIPLY), + SDLK_KP_MEMDIVIDE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MEMDIVIDE), + SDLK_KP_PLUSMINUS = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_PLUSMINUS), + SDLK_KP_CLEAR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_CLEAR), + SDLK_KP_CLEARENTRY = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_CLEARENTRY), + SDLK_KP_BINARY = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_BINARY), + SDLK_KP_OCTAL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_OCTAL), + SDLK_KP_DECIMAL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_DECIMAL), + SDLK_KP_HEXADECIMAL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_HEXADECIMAL), + + SDLK_LCTRL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_LCTRL), + SDLK_LSHIFT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_LSHIFT), + SDLK_LALT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_LALT), + SDLK_LGUI = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_LGUI), + SDLK_RCTRL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_RCTRL), + SDLK_RSHIFT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_RSHIFT), + SDLK_RALT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_RALT), + SDLK_RGUI = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_RGUI), + + SDLK_MODE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_MODE), + + SDLK_AUDIONEXT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AUDIONEXT), + SDLK_AUDIOPREV = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AUDIOPREV), + SDLK_AUDIOSTOP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AUDIOSTOP), + SDLK_AUDIOPLAY = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AUDIOPLAY), + SDLK_AUDIOMUTE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AUDIOMUTE), + SDLK_MEDIASELECT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_MEDIASELECT), + SDLK_WWW = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_WWW), + SDLK_MAIL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_MAIL), + SDLK_CALCULATOR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CALCULATOR), + SDLK_COMPUTER = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_COMPUTER), + SDLK_AC_SEARCH = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AC_SEARCH), + SDLK_AC_HOME = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AC_HOME), + SDLK_AC_BACK = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AC_BACK), + SDLK_AC_FORWARD = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AC_FORWARD), + SDLK_AC_STOP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AC_STOP), + SDLK_AC_REFRESH = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AC_REFRESH), + SDLK_AC_BOOKMARKS = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AC_BOOKMARKS), + + SDLK_BRIGHTNESSDOWN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_BRIGHTNESSDOWN), + SDLK_BRIGHTNESSUP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_BRIGHTNESSUP), + SDLK_DISPLAYSWITCH = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_DISPLAYSWITCH), + SDLK_KBDILLUMTOGGLE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KBDILLUMTOGGLE), + SDLK_KBDILLUMDOWN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KBDILLUMDOWN), + SDLK_KBDILLUMUP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KBDILLUMUP), + SDLK_EJECT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_EJECT), + SDLK_SLEEP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_SLEEP) +}; + +typedef enum { + KMOD_NONE = 0x0000, + KMOD_LSHIFT = 0x0001, + KMOD_RSHIFT = 0x0002, + KMOD_LCTRL = 0x0040, + KMOD_RCTRL = 0x0080, + KMOD_LALT = 0x0100, + KMOD_RALT = 0x0200, + KMOD_LGUI = 0x0400, + KMOD_RGUI = 0x0800, + KMOD_NUM = 0x1000, + KMOD_CAPS = 0x2000, + KMOD_MODE = 0x4000, + KMOD_RESERVED = 0x8000 +} SDL_Keymod; + +#define KMOD_CTRL (KMOD_LCTRL | KMOD_RCTRL) +#define KMOD_SHIFT (KMOD_LSHIFT | KMOD_RSHIFT) +#define KMOD_ALT (KMOD_LALT | KMOD_RALT) +#define KMOD_GUI (KMOD_LGUI | KMOD_RGUI) + +typedef struct SDL_Keysym { + int scancode; + SDL_Keycode sym; + uint16_t mod; + uint32_t unicode; +} SDL_Keysym; + +typedef enum { + SDL_FIRSTEVENT = 0, + + SDL_QUIT = 0x100, + + SDL_WINDOWEVENT = 0x200, + SDL_SYSWMEVENT, + + SDL_KEYDOWN = 0x300, + SDL_KEYUP, + SDL_TEXTEDITING, + SDL_TEXTINPUT, + + SDL_MOUSEMOTION = 0x400, + SDL_MOUSEBUTTONDOWN, + SDL_MOUSEBUTTONUP, + SDL_MOUSEWHEEL, + + SDL_INPUTMOTION = 0x500, + SDL_INPUTBUTTONDOWN, + SDL_INPUTBUTTONUP, + SDL_INPUTWHEEL, + SDL_INPUTPROXIMITYIN, + SDL_INPUTPROXIMITYOUT, + + SDL_JOYAXISMOTION = 0x600, + SDL_JOYBALLMOTION, + SDL_JOYHATMOTION, + SDL_JOYBUTTONDOWN, + SDL_JOYBUTTONUP, + SDL_JOYDEVICEADDED, + SDL_JOYDEVICEREMOVED, + + SDL_CONTROLLERAXISMOTION = 0x650, + SDL_CONTROLLERBUTTONDOWN, + SDL_CONTROLLERBUTTONUP, + SDL_CONTROLLERDEVICEADDED, + SDL_CONTROLLERDEVICEREMOVED, + + SDL_FINGERDOWN = 0x700, + SDL_FINGERUP, + SDL_FINGERMOTION, + SDL_TOUCHBUTTONDOWN, + SDL_TOUCHBUTTONUP, + + SDL_DOLLARGESTURE = 0x800, + SDL_DOLLARRECORD, + SDL_MULTIGESTURE, + + SDL_CLIPBOARDUPDATE = 0x900, + + SDL_DROPFILE = 0x1000, + + // ericf additions + SDL_DRAWEVENT = 0x1100, + SDL_RESIZEDRAWEVENT, + SDL_CONTEXTLOSTEVENT, + + SDL_USEREVENT = 0x8000, + + SDL_LASTEVENT = 0xFFFF +} SDL_EventType; + +typedef struct SDL_JoyAxisEvent { + uint32_t type; + uint32_t timestamp; + uint8_t which; + uint8_t axis; + uint8_t padding1; + uint8_t padding2; + int value; +} SDL_JoyAxisEvent; + +typedef struct SDL_JoyHatEvent { + uint32_t type; + uint32_t timestamp; + uint8_t which; + uint8_t hat; + uint8_t value; + uint8_t padding1; +} SDL_JoyHatEvent; + +#define SDL_HAT_CENTERED 0x00 +#define SDL_HAT_UP 0x01 +#define SDL_HAT_RIGHT 0x02 +#define SDL_HAT_DOWN 0x04 +#define SDL_HAT_LEFT 0x08 +#define SDL_HAT_RIGHTUP (SDL_HAT_RIGHT | SDL_HAT_UP) +#define SDL_HAT_RIGHTDOWN (SDL_HAT_RIGHT | SDL_HAT_DOWN) +#define SDL_HAT_LEFTUP (SDL_HAT_LEFT | SDL_HAT_UP) +#define SDL_HAT_LEFTDOWN (SDL_HAT_LEFT | SDL_HAT_DOWN) + +typedef struct SDL_MouseMotionEvent { + uint32_t type; + uint32_t timestamp; + uint32_t windowID; + uint8_t state; + uint8_t padding1; + uint8_t padding2; + uint8_t padding3; + int x; + int y; + int xrel; + int yrel; +} SDL_MouseMotionEvent; + +typedef struct SDL_JoyButtonEvent { + uint32_t type; + uint32_t timestamp; + uint8_t which; + uint8_t button; + uint8_t state; + uint8_t padding1; +} SDL_JoyButtonEvent; + +typedef struct SDL_MouseWheelEvent { + uint32_t type; + uint32_t timestamp; + uint32_t windowID; + int x; + int y; +} SDL_MouseWheelEvent; + +typedef struct SDL_MouseButtonEvent { + uint32_t type; + uint32_t timestamp; + uint32_t windowID; + uint8_t button; + uint8_t state; + uint8_t padding1; + uint8_t padding2; + int x; + int y; +} SDL_MouseButtonEvent; + +#define SDL_BUTTON(X) (1 << ((X)-1)) +#define SDL_BUTTON_LEFT 1 +#define SDL_BUTTON_MIDDLE 2 +#define SDL_BUTTON_RIGHT 3 +#define SDL_BUTTON_X1 4 +#define SDL_BUTTON_X2 5 +#define SDL_BUTTON_LMASK SDL_BUTTON(SDL_BUTTON_LEFT) +#define SDL_BUTTON_MMASK SDL_BUTTON(SDL_BUTTON_MIDDLE) +#define SDL_BUTTON_RMASK SDL_BUTTON(SDL_BUTTON_RIGHT) +#define SDL_BUTTON_X1MASK SDL_BUTTON(SDL_BUTTON_X1) +#define SDL_BUTTON_X2MASK SDL_BUTTON(SDL_BUTTON_X2) + +#define SDL_TEXTINPUTEVENT_TEXT_SIZE (32) + +typedef struct SDL_TextInputEvent { + uint32_t type; + uint32_t timestamp; + uint32_t windowID; + char text[SDL_TEXTINPUTEVENT_TEXT_SIZE]; +} SDL_TextInputEvent; + +typedef struct SDL_KeyboardEvent { + uint32_t type; + uint32_t timestamp; + uint32_t windowID; + uint8_t state; + uint8_t repeat; + uint8_t padding2; + uint8_t padding3; + SDL_Keysym keysym; +} SDL_KeyboardEvent; + +typedef union SDL_Event { + uint32_t type; + SDL_KeyboardEvent key; + SDL_TextInputEvent text; + SDL_MouseMotionEvent motion; + SDL_MouseButtonEvent button; + SDL_MouseWheelEvent wheel; + SDL_JoyAxisEvent jaxis; + SDL_JoyHatEvent jhat; + SDL_JoyButtonEvent jbutton; +} SDL_Event; + +#endif // BA_SDL_BUILD +#endif // BALLISTICA_PLATFORM_MIN_SDL_H_ diff --git a/src/ballistica/platform/platform.cc b/src/ballistica/platform/platform.cc new file mode 100644 index 00000000..3f5293ff --- /dev/null +++ b/src/ballistica/platform/platform.cc @@ -0,0 +1,1364 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/platform/platform.h" + +#if !BA_OSTYPE_WINDOWS +#include +#endif +#include + +// Trying to avoid platform-specific headers here except for +// a few mostly-cross-platform bits where its worth the mess. +#if !BA_OSTYPE_WINDOWS +#if BA_ENABLE_EXECINFO_BACKTRACES +#include +#endif +#include +#include +#include +#include +#endif + +#include +#include +#include +#include + +#include "ballistica/app/app.h" +#include "ballistica/app/headless_app.h" +#include "ballistica/app/vr_app.h" +#include "ballistica/core/thread.h" +#include "ballistica/dynamics/bg/bg_dynamics_server.h" +#include "ballistica/game/friend_score_set.h" +#include "ballistica/game/game.h" +#include "ballistica/game/score_to_beat.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/camera.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/mesh/sprite_mesh.h" +#include "ballistica/graphics/vr_graphics.h" +#include "ballistica/input/input.h" +#include "ballistica/input/std_input_module.h" +#include "ballistica/networking/networking_sys.h" +#include "ballistica/platform/sdl/sdl_app.h" +#include "ballistica/python/python.h" + +// ------------------------- PLATFORM SELECTION -------------------------------- + +// This ugly chunk of macros simply pulls in the correct platform class header +// for each platform and defines the actual class g_platform will be. + +// Android --------------------------------------------------------------------- + +#if BA_OSTYPE_ANDROID +#if BA_GOOGLE_BUILD +#include "ballistica/platform/android/google/platform_android_google.h" +#define BA_PLATFORM_CLASS PlatformAndroidGoogle +#elif BA_AMAZON_BUILD +#include "ballistica/platform/android/amazon/platform_android_amazon.h" +#define BA_PLATFORM_CLASS PlatformAndroidAmazon +#elif BA_CARDBOARD_BUILD +#include "ballistica/platform/android/cardboard/platform_android_cardboard.h" +#define BA_PLATFORM_CLASS PlatformAndroidCardboard +#else // Generic android. +#include "ballistica/platform/android/platform_android.h" +#define BA_PLATFORM_CLASS PlatformAndroid +#endif // (Android subplatform) + +// Apple ----------------------------------------------------------------------- + +#elif BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS +#include "ballistica/platform/apple/platform_apple.h" +#define BA_PLATFORM_CLASS PlatformApple + +// Windows --------------------------------------------------------------------- + +#elif BA_OSTYPE_WINDOWS +#if BA_RIFT_BUILD +#include "ballistica/platform/windows/platform_windows_oculus.h" +#define BA_PLATFORM_CLASS PlatformWindowsOculus +#else // generic windows +#include "ballistica/platform/windows/platform_windows.h" +#define BA_PLATFORM_CLASS PlatformWindows +#endif // windows subtype + +// Linux ----------------------------------------------------------------------- + +#elif BA_OSTYPE_LINUX +#include "ballistica/platform/linux/platform_linux.h" +#define BA_PLATFORM_CLASS PlatformLinux +#else + +// Generic --------------------------------------------------------------------- + +#define BA_PLATFORM_CLASS Platform + +#endif + +// ----------------------- END PLATFORM SELECTION ------------------------------ + +#ifndef BA_PLATFORM_CLASS +#error no BA_PLATFORM_CLASS defined for this platform +#endif + +namespace ballistica { + +auto Platform::Create() -> Platform* { + auto platform = new BA_PLATFORM_CLASS(); + return platform; +} + +void Platform::FinalCleanup() { + if (g_app_globals->temp_cleanup_callback) { + g_app_globals->temp_cleanup_callback(); + } +} + +Platform::Platform() : starttime_(GetCurrentMilliseconds()) {} + +auto Platform::PostInit() -> void {} + +Platform::~Platform() = default; + +auto Platform::GetUniqueDeviceIdentifier() -> const std::string& { + if (!have_device_uuid_) { + device_uuid_ = GetDeviceUUIDPrefix(); + + std::string real_unique_uuid; + bool have_real_unique_uuid = GetRealDeviceUUID(&real_unique_uuid); + if (have_real_unique_uuid) { + device_uuid_ += real_unique_uuid; + } + + // Keep demo/arcade uuids unique. + if (g_buildconfig.demo_build()) { + device_uuid_ += "_d"; + } else if (g_buildconfig.arcade_build()) { + device_uuid_ += "_a"; + } + + // Ok, as a fallback on platforms where we don't yet have a way to get a + // real UUID, lets do our best to generate one and stuff it in a file + // in our config dir. This should be globally-unique, but the downside is + // the user can tamper with it. + if (!have_real_unique_uuid) { + std::string path = GetConfigDirectory() + BA_DIRSLASH + ".bsuuid"; + + if (FILE* f = FOpen(path.c_str(), "rb")) { + // There's an existing one; read it. + char buffer[100]; + size_t size = fread(buffer, 1, 99, f); + if (size >= 0) { + assert(size < 100); + buffer[size] = 0; + device_uuid_ += buffer; + } + fclose(f); + } else { + // No existing one; generate it. + std::string val = GenerateUUID(); + device_uuid_ += val; + if (FILE* f2 = FOpen(path.c_str(), "wb")) { + size_t result = fwrite(val.c_str(), val.size(), 1, f2); + if (result != 1) Log("unable to write bsuuid file."); + fclose(f2); + } else { + Log("unable to open bsuuid file for writing: '" + path + "'"); + } + } + } + have_device_uuid_ = true; + } + return device_uuid_; +} + +auto Platform::GetDeviceUUIDPrefix() -> std::string { + Log("GetDeviceUUIDPrefix() unimplemented"); + return "u"; +} + +auto Platform::GetRealDeviceUUID(std::string* uuid) -> bool { return false; } + +auto Platform::GenerateUUID() -> std::string { + throw Exception("GenerateUUID() unimplemented"); +} + +auto Platform::GetDefaultConfigDir() -> std::string { + std::string config_dir; + // As a default, look for a HOME env var and use that if present + // this will cover linux and command-line macOS. + char* home = getenv("HOME"); + if (home) { + config_dir = std::string(home) + "/.ballisticacore"; + } else { + printf("GetDefaultConfigDir: can't get env var \"HOME\"\n"); + fflush(stdout); + throw Exception(); + } + return config_dir; +} + +auto Platform::GetConfigFilePath() -> std::string { + return GetConfigDirectory() + BA_DIRSLASH + "config.json"; +} + +auto Platform::GetLowLevelConfigValue(const char* key, int default_value) + -> int { + std::string path = GetConfigDirectory() + BA_DIRSLASH + ".cvar_" + key; + int val = default_value; + FILE* f = FOpen(path.c_str(), "r"); + if (f) { + int val2; + int result = fscanf(f, "%d", &val2); // NOLINT + if (result == 1) { + // I'm guessing scanned val is probably untouched on failure + // but why risk it? Let's only copy it in if it looks successful. + val = val2; + } + fclose(f); + } + return val; +} + +void Platform::SetLowLevelConfigValue(const char* key, int value) { + std::string path = GetConfigDirectory() + BA_DIRSLASH + ".cvar_" + key; + std::string out = std::to_string(value); + FILE* f = FOpen(path.c_str(), "w"); + if (f) { + size_t result = fwrite(out.c_str(), out.size(), 1, f); + if (result != 1) Log("unable to write low level config file."); + fclose(f); + } else { + Log("unable to open low level config file for writing."); + } +} + +auto Platform::GetUserPythonDirectory() -> std::string { + // Make sure it exists the first time we run. + static bool attempted_to_make_user_scripts_dir = false; + + if (!attempted_to_make_user_scripts_dir) { + user_scripts_dir_ = DoGetUserPythonDirectory(); + + // Attempt to make it. (it's ok if this fails) + MakeDir(user_scripts_dir_, true); + attempted_to_make_user_scripts_dir = true; + } + return user_scripts_dir_; +} + +auto Platform::GetAppPythonDirectory() -> std::string { + static bool checked_dir = false; + if (!checked_dir) { + checked_dir = true; + + // If there is a sys/VERSION in the user-python dir we use that. + app_python_dir_ = GetUserPythonDirectory() + BA_DIRSLASH + "sys" + + BA_DIRSLASH + kAppVersion; + + // Fall back to our default if that doesn't exist. + if (FilePathExists(app_python_dir_)) { + using_custom_app_python_dir_ = true; + Log("Using custom app Python path: '" + + (GetUserPythonDirectory() + BA_DIRSLASH + "sys" + BA_DIRSLASH + + kAppVersion) + + "'.", + true, false); + + } else { + // Going with relative paths for cleaner tracebacks... + app_python_dir_ = std::string("ba_data") + BA_DIRSLASH + "python"; + } + } + return app_python_dir_; +} + +auto Platform::GetSitePythonDirectory() -> std::string { + static bool checked_dir = false; + if (!checked_dir) { + checked_dir = true; + + if (!FilePathExists(site_python_dir_)) { + // Going with relative paths for cleaner tracebacks... + site_python_dir_ = + std::string("ba_data") + BA_DIRSLASH + "python-site-packages"; + } + } + return site_python_dir_; +} + +auto Platform::GetReplaysDir() -> std::string { + static bool made_dir = false; + if (!made_dir) { + replays_dir_ = GetConfigDirectory() + BA_DIRSLASH + "replays"; + MakeDir(replays_dir_); + made_dir = true; + } + return replays_dir_; +} + +// rename() supporting UTF8 strings. +auto Platform::Rename(const char* oldname, const char* newname) -> int { + // this covers non-windows platforms +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + return rename(oldname, newname); +#endif +} + +auto Platform::Remove(const char* path) -> int { + // this covers non-windows platforms +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + return remove(path); +#endif +} + +// stat() supporting UTF8 strings. +auto Platform::Stat(const char* path, struct BA_STAT* buffer) -> int { + // this covers non-windows platforms +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + return stat(path, buffer); +#endif +} + +// fopen() supporting UTF8 strings. +auto Platform::FOpen(const char* path, const char* mode) -> FILE* { + // this covers non-windows platforms +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + return fopen(path, mode); +#endif +} + +auto Platform::FilePathExists(const std::string& name) -> bool { + struct BA_STAT buffer {}; + return (Stat(name.c_str(), &buffer) == 0); +} + +auto Platform::GetSocketErrorString() -> std::string { + // On default platforms we just look at errno. + return GetErrnoString(); +} + +auto Platform::GetSocketError() -> int { + // by default this is simply errno + return errno; +} + +auto Platform::GetErrnoString() -> std::string { + // this covers non-windows platforms +#if BA_OSTYPE_WINDOWS + throw Exception(); +#elif BA_OSTYPE_LINUX + // We seem to be getting a gnu-specific version on linux + // which returns a char pointer that doesn't always point + // to the provided buffer. Sounds like there's a way to + // get the posix version which returns an error int through some + // #define magic but just gonna handle both flavors for now + char buffer[256]; + buffer[0] = 0; + const char* s = strerror_r(errno, buffer, sizeof(buffer)); + buffer[255] = 0; // Not sure if we need to clamp on overrun but cant hurt. + return s; +#else + char buffer[256]; + buffer[0] = 0; + strerror_r(errno, buffer, sizeof(buffer)); + buffer[255] = 0; // Not sure if we need to clamp on overrun but cant hurt. + return buffer; +#endif +} + +// Return the ballisticacore config dir +// This does not vary across versions. +auto Platform::GetConfigDirectory() -> std::string { + // Make sure args have been handled since we use them. + assert(g_app_globals->args_handled); + + if (!have_config_dir_) { + // If the user provided cfgdir as an arg. + if (!g_app_globals->user_config_dir.empty()) { + config_dir_ = g_app_globals->user_config_dir; + } else { + config_dir_ = GetDefaultConfigDir(); + } + + // Try to make sure the config dir exists. + MakeDir(config_dir_); + + have_config_dir_ = true; + } + return config_dir_; +} + +void Platform::MakeDir(const std::string& dir, bool quiet) { + bool exists = FilePathExists(dir); + if (!exists) { + DoMakeDir(dir, quiet); + + // Non-quiet call should result in directory existing. + // (or an exception should have been raised) + assert(quiet || FilePathExists(dir)); + } +} + +auto Platform::GetExternalStoragePath() -> std::string { + throw Exception("GetExternalStoragePath() unimplemented"); +} + +auto Platform::DoGetUserPythonDirectory() -> std::string { + return GetConfigDirectory() + BA_DIRSLASH + "mods"; +} + +void Platform::DoMakeDir(const std::string& dir, bool quiet) { + // Default case here covers all non-windows platforms. +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + int result = mkdir(dir.c_str(), + // NOLINTNEXTLINE (signed values in bitwise stuff) + S_IRWXU | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH); + if (result != 0 && errno != EEXIST && !quiet) { + throw Exception("Unable to create directory '" + dir + "' (errno " + + std::to_string(errno) + ")"); + } +#endif +} + +auto Platform::GetLocale() -> std::string { + const char* lang = getenv("LANG"); + if (lang) { + return lang; + } else { + if (!g_buildconfig.headless_build()) { + BA_LOG_ONCE("No LANG value available; defaulting to en_US"); + } + return "en_US"; + } +} + +auto Platform::GetDeviceName() -> std::string { + static std::string device_name; + static bool have_device_name = false; + + // In headless-mode we always return our party name if that's available + // (otherwise everything will just be BallisticaCore Game). + // UPDATE: hmm don't think I like this. Device-name is supposed to go into + // user-agent-strings and whatnot. + // Should probably inject public party name at a higher level. + if (g_buildconfig.headless_build()) { + // Hmm this might be called from non-main threads; should + // think about ensuring this is thread-safe perhaps. + if (g_python != nullptr) { + std::string pname = g_game->public_party_name(); + if (!pname.empty()) { + device_name = pname; + have_device_name = true; + } + } + } + + if (!have_device_name) { + device_name = DoGetDeviceName(); + + // Hmm seem to get some funky invalid utf8 out of + // this sometimes (mainly on windows). Should look into that + // more closely or at least log it somewhere. + device_name = Utils::GetValidUTF8(device_name.c_str(), "dn"); + have_device_name = true; + } + return device_name; +} + +auto Platform::DoGetDeviceName() -> std::string { + return "BallisticaCore Game"; +} + +auto Platform::IsRunningOnTV() -> bool { return false; } + +auto Platform::HasTouchScreen() -> bool { + if (!have_has_touchscreen_value_) { + have_touchscreen_ = DoHasTouchScreen(); + have_has_touchscreen_value_ = true; + } + return have_touchscreen_; +} + +auto Platform::IsRunningOnFireTV() -> bool { return false; } + +auto Platform::IsRunningOnDaydream() -> bool { return false; } + +auto Platform::DoHasTouchScreen() -> bool { throw Exception("UNIMPLEMENTED"); } + +auto Platform::IsRunningOnDesktop() -> bool { + // Default case to cover mac, win, etc. + return true; +} + +void Platform::SleepMS(millisecs_t ms) { + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); +} + +// General one-time initialization stuff +static void Init() { + // Sanity check: make sure asserts are stripped out of release builds + // (NDEBUG should do this). +#if !BA_DEBUG_BUILD +#ifndef NDEBUG +#error Expected NDEBUG to be defined for release builds. +#endif // NDEBUG + assert(true); +#endif // !BA_DEBUG_BUILD + + // Are we running in a terminal? + if (g_buildconfig.use_stdin_thread()) { + g_app_globals->is_stdin_a_terminal = g_platform->IsStdinATerminal(); + } else { + g_app_globals->is_stdin_a_terminal = false; + } + + // If we're running in a terminal, print some info. + if (g_app_globals->is_stdin_a_terminal) { + if (g_buildconfig.headless_build()) { + printf("BallisticaCore Headless %s build %d.\n", kAppVersion, + kAppBuildNumber); + fflush(stdout); + } else { + printf("BallisticaCore %s build %d.\n", kAppVersion, kAppBuildNumber); + fflush(stdout); + } + } + + g_app_globals->user_agent_string = g_platform->GetUserAgentString(); + + // Figure out where our data is and chdir there. + g_platform->SetupDataDirectory(); + + // Run these just to make sure these dirs exist. + // (otherwise they might not get made if nothing writes to them). + g_platform->GetConfigDirectory(); + g_platform->GetUserPythonDirectory(); +} + +static void HandleArgs(int argc, char** argv) { + assert(!g_app_globals->args_handled); + g_app_globals->args_handled = true; + + // If there's just one arg and it's "--version", return the version. + if (argc == 2 && !strcmp(argv[1], "--version")) { + printf("Ballistica %s build %d\n", kAppVersion, kAppBuildNumber); + fflush(stdout); + exit(0); + } + for (int i = 1; i < argc; ++i) { + // In our rift build, a '-2d' arg causes us to run in regular 2d mode. + if (g_buildconfig.rift_build() && !strcmp(argv[i], "-2d")) { + g_app_globals->vr_mode = false; + } else if (!strcmp(argv[i], "-exec")) { + if (i + 1 < argc) { + g_app_globals->game_commands = argv[i + 1]; + } else { + printf("%s", "Error: expected arg after -exec\n"); + fflush(stdout); + exit(-1); + } + } else if (!strcmp(argv[i], "-cfgdir")) { + if (i + 1 < argc) { + g_app_globals->user_config_dir = argv[i + 1]; + + // Need to remove this limitation!!! + // Don't remember why it's here; something about not being able to + // create nested directories properly in windows. But perhaps we + // can just error if a dir is provided that's invalid. + if (g_app_globals->user_config_dir != "ba_root") { + printf("%s", "ERROR: -cfgdir currently has to be 'ba_root'\n"); + fflush(stdout); + exit(-1); + } + + // Need to convert this to an abs path since we chdir soon. + std::string buffer = g_platform->GetCWD(); + if (buffer.empty()) { + printf("%s", "ERROR: unable to get cwd for cfgdir setup\n"); + fflush(stdout); + exit(-1); + } + g_app_globals->user_config_dir = + std::string(buffer) + BA_DIRSLASH + g_app_globals->user_config_dir; + } else { + Log("ERROR: expected arg after -cfgdir"); + exit(-1); + } + } + } + + // In Android's case we have to pull our exec arg from the java/kotlin layer. + if (g_buildconfig.ostype_android()) { + g_app_globals->game_commands = g_platform->GetAndroidExecArg(); + } + + // TEMP/HACK: hard code launch args. + if (explicit_bool(false)) { + if (g_buildconfig.ostype_android()) { + g_app_globals->game_commands = + "import ba.internal; ba.internal.run_stress_test()"; + } + } +} + +void Platform::CreateApp() { + assert(g_app_globals); + assert(InMainThread()); + + // Hmm do these belong here?... + HandleArgs(g_app_globals->argc, g_app_globals->argv); + Init(); + +// TEMP - need to init sdl on our legacy mac build even though its not +// technically an SDL app. Kill this once the old mac build is gone. +#if BA_LEGACY_MACOS_BUILD + SDLApp::InitSDL(); +#endif + +#if BA_HEADLESS_BUILD + g_main_thread->AddModule(); +#elif BA_RIFT_BUILD + // Rift build can spin up in either VR or regular mode. + if (g_app_globals->vr_mode) { + g_main_thread->AddModule(); + } else { + g_main_thread->AddModule(); + } +#elif BA_CARDBOARD_BUILD + g_main_thread->AddModule(); +#elif BA_SDL_BUILD + g_main_thread->AddModule(); +#else + g_main_thread->AddModule(); +#endif + + // Let app do any init it needs to after it is fully constructed. + g_app->PostInit(); +} + +auto Platform::CreateGraphics() -> Graphics* { + assert(InGameThread()); +#if BA_VR_BUILD + return new VRGraphics(); +#else + return new Graphics(); +#endif +} + +auto Platform::GetKeyName(int keycode) -> std::string { + // On our actual SDL platforms we're trying to be *pure* sdl so + // call their function for this. Otherwise we call our own version + // of it which is basically the same thing (at least for now). +#if BA_SDL_BUILD && !BA_MINSDL_BUILD + return SDL_GetKeyName(static_cast(keycode)); +#else + return g_input->GetKeyName(keycode); +#endif +} + +void Platform::CreateAuxiliaryModules() { +#if !BA_HEADLESS_BUILD + auto bg_dynamics_thread = new Thread(ThreadIdentifier::kBGDynamics); + g_app_globals->pausable_threads.push_back(bg_dynamics_thread); +#endif +#if !BA_HEADLESS_BUILD + bg_dynamics_thread->AddModule(); +#endif + + if (g_buildconfig.use_stdin_thread()) { + // Start listening for stdin commands (on platforms where that makes sense). + // Note: this thread blocks indefinitely for input so we don't add it to the + // pausable list. + auto std_input_thread = new Thread(ThreadIdentifier::kStdin); + std_input_thread->AddModule(); + g_std_input_module->PushBeginReadCall(); + } +} + +void Platform::WillExitMain(bool errored) {} + +auto Platform::GetInterfaceType() -> UIScale { + // Handles mac/pc/linux cases. + return UIScale::kLarge; +} + +void Platform::HandleLog(const std::string& msg) { + // Do nothing by default. +} + +auto Platform::ReportFatalError(const std::string& message, + bool in_top_level_exception_handler) -> bool { + // Don't override handling by default. + return false; +} + +auto Platform::HandleFatalError(bool exit_cleanly, + bool in_top_level_exception_handler) -> bool { + // Don't override handling by default. + return false; +} + +auto Platform::CanShowBlockingFatalErrorDialog() -> bool { + if (g_buildconfig.sdl2_build()) { + return true; + } else { + return false; + } +} + +auto Platform::BlockingFatalErrorDialog(const std::string& message) -> void { +#if BA_SDL2_BUILD + assert(InMainThread()); + if (!HeadlessMode()) { + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Fatal Error", + message.c_str(), nullptr); + } +#endif +} + +void Platform::SetupDataDirectory() { +// This covers non-windows cases. +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + // Default to './ba_data'. + DIR* d = opendir("ba_data"); + if (d == nullptr) { + throw Exception("ba_data directory not found."); + } + closedir(d); +#endif + + // Apparently Android NDK 22 includes std::filesystem; once that is out + // then we should be able to use this everywhere. + // Oh - and we also need to wait for GCC 8, so when we switch to Ubuntu20... + // Oh; and we should see if switch/etc. supports it before making it a hard + // requirement. + // if (!std::filesystem::is_directory("ba_data")) { + // throw Exception("ba_data directory not found."); + // } +} + +void Platform::SetEnv(const std::string& name, const std::string& value) { +// This covers non-windows cases. +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + auto result = setenv(name.c_str(), value.c_str(), true); + if (result != 0) { + throw Exception("Failed to set environment variable '" + name + + "'; errno=" + std::to_string(errno)); + } +#endif +} + +auto Platform::IsStdinATerminal() -> bool { +// This covers non-windows cases. +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + return static_cast(isatty(fileno(stdin))); +#endif +} + +auto Platform::GetOSVersionString() -> std::string { return ""; } + +auto Platform::GetUserAgentString() -> std::string { + // Fetch our device name here from main thread so it'll be safe + // to from other threads later (it gets cached as a string) + std::string device = GetDeviceName(); + + std::string version = GetOSVersionString(); + if (!version.empty()) { + version = " " + version; + } + + // Include a store identifier in the build. + std::string subplatform; + if (g_buildconfig.headless_build()) { + subplatform = "HdlS"; + } else if (g_buildconfig.cardboard_build()) { + subplatform = "GpCb"; + } else if (g_buildconfig.gearvr_build()) { + subplatform = "OcGVRSt"; + } else if (g_buildconfig.rift_build()) { + subplatform = "OcRftSt"; + } else if (g_buildconfig.amazon_build()) { + subplatform = "AmSt"; + } else if (g_buildconfig.google_build()) { + subplatform = "GpSt"; + } else if (g_buildconfig.use_store_kit() && g_buildconfig.ostype_macos()) { + subplatform = "McApSt"; + } else if (g_buildconfig.use_store_kit() && g_buildconfig.ostype_ios()) { + subplatform = "IosApSt"; + } else if (g_buildconfig.use_store_kit() && g_buildconfig.ostype_tvos()) { + subplatform = "TvsApSt"; + } else if (g_buildconfig.demo_build()) { + subplatform = "DeMo"; + } else if (g_buildconfig.arcade_build()) { + subplatform = "ArCd"; + } else if (g_buildconfig.iircade_build()) { + subplatform = "iiRcd"; + } else { + subplatform = "TstB"; + } + + if (!subplatform.empty()) { + subplatform = " " + subplatform; + } + if (IsRunningOnTV()) { + subplatform += " OnTV"; + } + return std::string("BallisticaCore ") + kAppVersion + subplatform + " (" + + std::to_string(kAppBuildNumber) + ") (" + + g_buildconfig.platform_string() + version + "; " + device + "; " + + GetLocale() + ")"; +} + +auto Platform::GetCWD() -> std::string { +// Covers non-windows cases. +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + char buffer[PATH_MAX]; + return getcwd(buffer, sizeof(buffer)); +#endif +} + +auto Platform::GetAndroidExecArg() -> std::string { return ""; } + +void Platform::GetTextBoundsAndWidth(const std::string& text, Rect* r, + float* width) { + throw Exception(); +} + +void Platform::FreeTextTexture(void* tex) { throw Exception(); } + +auto Platform::CreateTextTexture(int width, int height, + const std::vector& strings, + const std::vector& positions, + const std::vector& widths, float scale) + -> void* { + throw Exception(); +} + +auto Platform::GetTextTextureData(void* tex) -> uint8_t* { throw Exception(); } + +void Platform::OnBootstrapComplete() {} + +auto Platform::ConvertIncomingLeaderboardScore( + const std::string& leaderboard_id, int score) -> int { + return score; +} + +void Platform::GetFriendScores(const std::string& game, + const std::string& game_version, void* data) { + // As a default, just fail gracefully. + Log("FIXME: GetFriendScores unimplemented"); + g_game->PushFriendScoreSetCall(FriendScoreSet(false, data)); +} + +void Platform::SubmitScore(const std::string& game, const std::string& version, + int64_t score) { + Log("FIXME: SubmitScore() unimplemented"); +} + +void Platform::ReportAchievement(const std::string& achievement) {} + +auto Platform::HaveLeaderboard(const std::string& game, + const std::string& config) -> bool { + return false; +} + +void Platform::EditText(const std::string& title, const std::string& value, + int max_chars) { + Log("FIXME: EditText() unimplemented"); +} + +void Platform::ShowOnlineScoreUI(const std::string& show, + const std::string& game, + const std::string& game_version) { + Log("FIXME: ShowOnlineScoreUI() unimplemented"); +} + +void Platform::Purchase(const std::string& item) { + // Just print 'unavailable' by default. + g_python->PushObjCall(Python::ObjID::kUnavailableMessageCall); +} + +void Platform::RestorePurchases() { Log("RestorePurchases() unimplemented"); } + +void Platform::AndroidSetResString(const std::string& res) { + throw Exception(); +} + +void Platform::ApplyConfig() {} + +void Platform::AndroidSynthesizeBackPress() { + Log("AndroidSynthesizeBackPress() unimplemented"); +} + +void Platform::AndroidQuitActivity() { + Log("AndroidQuitActivity() unimplemented"); +} + +auto Platform::GetDeviceAccountID() -> std::string { + if (HeadlessMode()) { + return "S-" + GetUniqueDeviceIdentifier(); + } + + // Everything else is just considered a 'local' account, though we may + // give unique ids for unique builds.. + if (g_buildconfig.iircade_build()) { + return "L-iRc" + GetUniqueDeviceIdentifier(); + } + return "L-" + GetUniqueDeviceIdentifier(); +} + +auto Platform::DemangleCXXSymbol(const std::string& s) -> std::string { + // Do __cxa_demangle on platforms that support it. + // FIXME; I believe there's an equivalent call for windows; should research. +#if !BA_OSTYPE_WINDOWS + int demangle_status; + + // If we pass null for buffers, this mallocs one for us that we have to free. + char* demangled_name = + abi::__cxa_demangle(s.c_str(), nullptr, nullptr, &demangle_status); + if (demangled_name != nullptr) { + if (demangle_status != 0) { + BA_LOG_ONCE("__cxa_demangle got buffer but non-zero status; unexpected"); + } + std::string retval = demangled_name; + free(static_cast(demangled_name)); + return retval; + } else { + return s; + } +#else + return s; +#endif +} + +auto Platform::NewAutoReleasePool() -> void* { throw Exception(); } + +void Platform::DrainAutoReleasePool(void* pool) { throw Exception(); } + +auto Platform::AndroidGPGSNewConnectionToClient(int id) -> ConnectionToClient* { + throw Exception(); +} +auto Platform::AndroidGPGSNewConnectionToHost() -> ConnectionToHost* { + throw Exception(); +} + +auto Platform::AndroidIsGPGSConnectionToClient(ConnectionToClient* c) -> bool { + throw Exception(); +} + +void Platform::OpenURL(const std::string& url) { + // Can't open URLs in VR - just tell the game thread to show the url. + if (IsVRMode()) { + g_game->PushShowURLCall(url); + return; + } + + // Otherwise fall back to our platform-specific handler. + g_platform->DoOpenURL(url); +} + +void Platform::DoOpenURL(const std::string& url) { + Log("DoOpenURL unimplemented on this platform."); +} + +void Platform::ResetAchievements() { Log("ResetAchievements() unimplemented"); } + +void Platform::GameCenterLogin() { throw Exception(); } + +void Platform::PurchaseAck(const std::string& purchase, + const std::string& order_id) { + Log("PurchaseAck() unimplemented"); +} + +void Platform::RunEvents() {} + +auto Platform::GetMemUsageInfo() -> std::string { return "0,0,0"; } + +void Platform::OnAppPause() {} +void Platform::OnAppResume() {} + +void Platform::MusicPlayerPlay(PyObject* target) { + Log("MusicPlayerPlay() unimplemented on this platform"); +} +void Platform::MusicPlayerStop() { + Log("MusicPlayerStop() unimplemented on this platform"); +} +void Platform::MusicPlayerShutdown() { + Log("MusicPlayerShutdown() unimplemented on this platform"); +} + +void Platform::MusicPlayerSetVolume(float volume) { + Log("MusicPlayerSetVolume() unimplemented on this platform"); +} + +auto Platform::IsOSPlayingMusic() -> bool { return false; } + +void Platform::AndroidShowAppInvite(const std::string& title, + const std::string& message, + const std::string& code) { + Log("AndroidShowAppInvite() unimplemented"); +} + +void Platform::IncrementAnalyticsCount(const std::string& name, int increment) { +} + +void Platform::IncrementAnalyticsCountRaw(const std::string& name, + int increment) {} + +void Platform::IncrementAnalyticsCountRaw2(const std::string& name, + int uses_increment, int increment) {} + +void Platform::SetAnalyticsScreen(const std::string& screen) {} + +void Platform::SubmitAnalyticsCounts() {} + +void Platform::SetPlatformMiscReadVals(const std::string& vals) {} + +void Platform::AndroidRefreshFile(const std::string& file) { + Log("AndroidRefreshFile() unimplemented"); +} + +void Platform::ShowAd(const std::string& purpose) { + Log("ShowAd() unimplemented"); +} + +auto Platform::GetHasAds() -> bool { return false; } + +auto Platform::GetHasVideoAds() -> bool { + // By default we assume we have this anywhere we have ads. + return GetHasAds(); +} + +void Platform::AndroidGPGSPartyInvitePlayers() { + Log("AndroidGPGSPartyInvitePlayers() unimplemented"); +} + +void Platform::AndroidGPGSPartyShowInvites() { + Log("AndroidGPGSPartyShowInvites() unimplemented"); +} + +void Platform::AndroidGPGSPartyInviteAccept(const std::string& invite_id) { + Log("AndroidGPGSPartyInviteAccept() unimplemented"); +} + +void Platform::SignIn(const std::string& account_type) { + Log("SignIn() unimplemented"); +} + +void Platform::SignOut() { Log("SignOut() unimplemented"); } + +void Platform::AndroidShowWifiSettings() { + Log("AndroidShowWifiSettings() unimplemented"); +} + +void Platform::SetHardwareCursorVisible(bool visible) { +// FIXME: Forward this to app?.. +#if BA_SDL_BUILD + SDL_ShowCursor(visible ? SDL_ENABLE : SDL_DISABLE); +#endif +} + +void Platform::QuitApp() { exit(g_app_globals->return_value); } + +void Platform::GetScoresToBeat(const std::string& level, + const std::string& config, void* py_callback) { + // By default, return nothing. + g_game->PushScoresToBeatResponseCall(false, std::list(), + py_callback); +} + +void Platform::OpenFileExternally(const std::string& path) { + Log("OpenFileExternally() unimplemented"); +} + +void Platform::OpenDirExternally(const std::string& path) { + Log("OpenDirExternally() unimplemented"); +} + +void Platform::MacMusicAppInit() { Log("MacMusicAppInit() unimplemented"); } + +auto Platform::MacMusicAppGetVolume() -> int { + Log("MacMusicAppGetVolume() unimplemented"); + return 0; +} + +void Platform::MacMusicAppSetVolume(int volume) { + Log("MacMusicAppSetVolume() unimplemented"); +} + +void Platform::MacMusicAppGetLibrarySource() { + Log("MacMusicAppGetLibrarySource() unimplemented"); +} + +void Platform::MacMusicAppStop() { Log("MacMusicAppStop() unimplemented"); } + +auto Platform::MacMusicAppPlayPlaylist(const std::string& playlist) -> bool { + Log("MacMusicAppPlayPlaylist() unimplemented"); + return false; +} +auto Platform::MacMusicAppGetPlaylists() -> std::list { + Log("MacMusicAppGetPlaylists() unimplemented"); + return std::list(); +} + +void Platform::StartListeningForWiiRemotes() { + Log("StartListeningForWiiRemotes() unimplemented"); +} + +void Platform::StopListeningForWiiRemotes() { + Log("StopListeningForWiiRemotes() unimplemented"); +} + +void Platform::SetCurrentThreadName(const std::string& name) { + // Currently we leave the main thread alone, otherwise we show up as + // "BallisticaMainThread" under "top" on linux (should check other platforms). + if (InMainThread()) { + return; + } +#if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS + pthread_setname_np(name.c_str()); +#elif BA_OSTYPE_LINUX || BA_OSTYPE_ANDROID + pthread_setname_np(pthread_self(), name.c_str()); +#endif +} + +void Platform::Unlink(const char* path) { + // This covers all but windows. +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + unlink(path); +#endif +} + +auto Platform::IsEventPushMode() -> bool { return false; } + +auto Platform::GetDisplayResolution(int* x, int* y) -> bool { return false; } + +void Platform::CloseSocket(int socket) { +// This covers all but windows. +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + close(socket); +#endif +} + +auto Platform::SocketPair(int domain, int type, int protocol, int socks[2]) + -> int { + // This covers all but windows. +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + return socketpair(domain, type, protocol, socks); +#endif +} + +auto Platform::GetBroadcastAddrs() -> std::vector { +// This covers all but windows. +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + std::vector addrs; + struct ifaddrs* ifaddr; + if (getifaddrs(&ifaddr) != -1) { + int i = 0; + for (ifaddrs* ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) { + // Turns out this can be null if the interface has no addrs. + if (ifa->ifa_addr == nullptr) { + continue; + } + int family = ifa->ifa_addr->sa_family; + if (family == AF_INET) { + if (ifa->ifa_addr != nullptr) { + uint32_t addr = ntohl( // NOLINT (clang-tidy signed bitwise whining) + (reinterpret_cast(ifa->ifa_addr))->sin_addr.s_addr); + uint32_t sub = ntohl( // NOLINT (clang-tidy signed bitwise whining) + (reinterpret_cast(ifa->ifa_netmask)) + ->sin_addr.s_addr); + uint32_t broadcast = addr | (~sub); + addrs.push_back(broadcast); + i++; + } + } + } + freeifaddrs(ifaddr); + } + return addrs; +#endif +} + +auto Platform::SetSocketNonBlocking(int sd) -> bool { +// This covers all but windows. +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + int result = fcntl(sd, F_SETFL, O_NONBLOCK); + if (result != 0) { + Log("Error setting non-blocking socket: " + + g_platform->GetSocketErrorString()); + return false; + } + return true; +#endif +} + +auto Platform::GetTicks() -> millisecs_t { + return GetCurrentMilliseconds() - starttime_; +} + +auto Platform::GetPlatformName() -> std::string { + throw Exception("UNIMPLEMENTED"); +} + +auto Platform::GetSubplatformName() -> std::string { + // This doesnt always have to be set. + return ""; +} + +auto Platform::ContainsPythonDist() -> bool { return false; } + +#pragma mark Stack Traces + +#if BA_ENABLE_EXECINFO_BACKTRACES + +// Stack traces using the functionality in execinfo.h +class PlatformStackTraceExecInfo : public PlatformStackTrace { + public: + static constexpr int kMaxStackLevels = 64; + + // The stack trace should capture the stack state immediately upon + // construction but should do the bare minimum amount of work to store it. Any + // expensive operations such as symbolification should be deferred until + // GetDescription(). + PlatformStackTraceExecInfo() { nsize_ = backtrace(array_, kMaxStackLevels); } + + auto GetDescription() noexcept -> std::string override { + try { + std::string s; + char** symbols = backtrace_symbols(array_, nsize_); + for (int i = 0; i < nsize_; i++) { + s += std::string(symbols[i]); + if (i < nsize_ - 1) { + s += "\n"; + } + } + free(symbols); + return s; + } catch (const std::exception&) { + return "backtrace construction failed."; + } + } + + auto copy() const noexcept -> PlatformStackTrace* override { + try { + auto s = new PlatformStackTraceExecInfo(*this); + + // Vanilla copy constructor should do the right thing here. + assert(s->nsize_ == nsize_ + && memcmp(s->array_, array_, sizeof(array_)) == 0); + return s; + } catch (const std::exception&) { + // If this is failing we're in big trouble anyway. + return nullptr; + } + } + + private: + void* array_[kMaxStackLevels]{}; + int nsize_{}; +}; +#endif + +auto Platform::GetStackTrace() -> PlatformStackTrace* { +// Our default handler here supports execinfo backtraces where available +// and gives nothing elsewhere. +#if BA_ENABLE_EXECINFO_BACKTRACES + return new PlatformStackTraceExecInfo(); +#else + return nullptr; +#endif +} + +void Platform::RequestPermission(Permission p) { + // No-op. +} + +auto Platform::HavePermission(Permission p) -> bool { + // Its assumed everything is accessible unless we override saying no. + return true; +} + +#if !BA_OSTYPE_WINDOWS +static void HandleSIGINT(int s) { + if (g_game) { + g_game->PushInterruptSignalCall(); + } else { + Log("SigInt handler called before g_game exists."); + } +} +#endif + +void Platform::SetupInterruptHandling() { + // For non-windows platforms, set up a handler for Ctrl-C. +#if !BA_OSTYPE_WINDOWS + struct sigaction handler {}; + handler.sa_handler = HandleSIGINT; + sigemptyset(&handler.sa_mask); + handler.sa_flags = 0; + sigaction(SIGINT, &handler, nullptr); +#endif +} + +void Platform::GetCursorPosition(float* x, float* y) { + assert(x && y); + + // By default, just use our latest event-delivered cursor position; + // this should work everywhere though perhaps might not be most optimal. + if (g_input == nullptr) { + *x = 0.0f; + *y = 0.0f; + return; + } + *x = g_input->cursor_pos_x(); + *y = g_input->cursor_pos_y(); +} +auto Platform::SetDebugKey(const std::string& key, const std::string& value) + -> void {} + +auto Platform::HandleDebugLog(const std::string& msg) -> void {} + +auto Platform::GetCurrentMilliseconds() -> millisecs_t { + return std::chrono::time_point_cast( + std::chrono::steady_clock::now()) + .time_since_epoch() + .count(); +} + +auto Platform::GetCurrentSeconds() -> int64_t { + return std::chrono::time_point_cast( + std::chrono::steady_clock::now()) + .time_since_epoch() + .count(); +} + +} // namespace ballistica diff --git a/src/ballistica/platform/platform.h b/src/ballistica/platform/platform.h new file mode 100644 index 00000000..96897d39 --- /dev/null +++ b/src/ballistica/platform/platform.h @@ -0,0 +1,515 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PLATFORM_PLATFORM_H_ +#define BALLISTICA_PLATFORM_PLATFORM_H_ + +#include + +#include +#include +#include + +#include "ballistica/ballistica.h" + +namespace ballistica { + +/// For capturing and printing stack-traces and related errors. +/// Platforms should subclass this and return instances in GetStackTrace(). +class PlatformStackTrace { + public: + // The stack trace should capture the stack state immediately upon + // construction but should do the bare minimum amount of work to store it. Any + // expensive operations such as symbolification should be deferred until + // GetDescription(). + virtual ~PlatformStackTrace() = default; + + // Return a human readable version of the trace (with symbolification if + // available). + virtual auto GetDescription() noexcept -> std::string = 0; + + // Should return a copy of itself allocated via new() (or nullptr if not + // possible). + virtual auto copy() const noexcept -> PlatformStackTrace* = 0; +}; + +// This class attempts to abstract away most platform-specific functionality. +// Ideally we should need to pull in no platform-specific system headers outside +// of the platform*.cc files and can just go through this. +class Platform { + public: + static auto Create() -> Platform*; + Platform(); + virtual ~Platform(); + +#pragma mark LIFECYCLE/SETTINGS ------------------------------------------------ + + /// Called right after g_platform is created/assigned. Any platform + /// functionality depending on a complete g_platform object existing can + /// be run here. + virtual auto PostInit() -> void; + + /// Create the proper App module and add it to the main_thread. + void CreateApp(); + + /// Create the appropriate Graphics subclass for the app. + Graphics* CreateGraphics(); + + virtual void CreateAuxiliaryModules(); + virtual void WillExitMain(bool errored); + + // Inform the platform that all subsystems are up and running and it can + // start talking to them. + virtual void OnBootstrapComplete(); + + // Get/set values before standard game settings are available + // (for values needed before SDL init/etc). + // FIXME: We should have some sort of 'bootconfig.json' file for these. + // (or simply read the regular config in via c++ immediately) + auto GetLowLevelConfigValue(const char* key, int default_value) -> int; + void SetLowLevelConfigValue(const char* key, int value); + + // Called when the app config is being read/applied. + virtual void ApplyConfig(); + + // Called when the app should set itself up to intercept ctrl-c presses. + virtual void SetupInterruptHandling(); + + void FinalCleanup(); + +#pragma mark FILES ------------------------------------------------------------- + + // remove() support UTF8 strings. + virtual auto Remove(const char* path) -> int; + + // stat() supporting UTF8 strings. + virtual auto Stat(const char* path, struct BA_STAT* buffer) -> int; + + // fopen() supporting UTF8 strings. + virtual auto FOpen(const char* path, const char* mode) -> FILE*; + + // rename() supporting UTF8 strings. + // For cross-platform consistency, this should also remove any file that + // exists at the target location first. + virtual auto Rename(const char* oldname, const char* newname) -> int; + + // Simple cross-platform check for existence of a file. + auto FilePathExists(const std::string& name) -> bool; + + /// Attempt to make a directory; raise an Exception if unable, + /// unless quiet is true. + void MakeDir(const std::string& dir, bool quiet = false); + + // Return the current working directory. + virtual auto GetCWD() -> std::string; + + // Unlink a file. + virtual void Unlink(const char* path); + +#pragma mark PRINTING/LOGGING -------------------------------------------------- + + // Send a message to the default platform handler. + // IMPORTANT: No Object::Refs should be created or destroyed within this call, + // or deadlock can occur. + virtual void HandleLog(const std::string& msg); + +#pragma mark ENVIRONMENT ------------------------------------------------------- + + // Return a simple name for the platform: 'mac', 'windows', 'linux', etc. + virtual auto GetPlatformName() -> std::string; + + // Return a simple name for the subplatform: 'amazon', 'google', etc. + virtual auto GetSubplatformName() -> std::string; + + // Are we running in event-push-mode? + // With this on, we return from Main() and the system handles the event loop. + // With it off, we loop in Main() ourself. + virtual auto IsEventPushMode() -> bool; + + // Return the interface type based on the environment (phone, tablet, etc). + virtual auto GetInterfaceType() -> UIScale; + + // Return a string *reasonably* likely to be unique and consistent for this + // device. Do not assume this is globally unique and *do not* assume that it + // will never ever change (hardware upgrades may affect it, etc). + virtual auto GetUniqueDeviceIdentifier() -> const std::string&; + + // Returns the ID to use for the device account + auto GetDeviceAccountID() -> std::string; + auto GetConfigDirectory() -> std::string; + auto GetConfigFilePath() -> std::string; + auto GetUserPythonDirectory() -> std::string; + auto GetAppPythonDirectory() -> std::string; + auto GetSitePythonDirectory() -> std::string; + auto GetReplaysDir() -> std::string; + + // Return en_US or whatnot. + virtual auto GetLocale() -> std::string; + virtual void SetupDataDirectory(); + virtual auto GetUserAgentString() -> std::string; + virtual auto GetOSVersionString() -> std::string; + + /// Set an environment variable as utf8, overwriting if it already exists. + /// Raises an exception on errors. + virtual void SetEnv(const std::string& name, const std::string& value); + + // Are we being run from a terminal? (should we show prompts, etc?). + virtual auto IsStdinATerminal() -> bool; + + // Return hostname or other id suitable for network searches, etc. + auto GetDeviceName() -> std::string; + + // Are we running on a tv? + virtual auto IsRunningOnTV() -> bool; + + // Are we on a daydream enabled android device? + virtual auto IsRunningOnDaydream() -> bool; + + // Do we have touchscreen hardware? + auto HasTouchScreen() -> bool; + + // Are we running on a desktop setup in general? + virtual auto IsRunningOnDesktop() -> bool; + + // Are we running on fireTV hardware? + virtual auto IsRunningOnFireTV() -> bool; + + // Return the external storage path (currently only relevant on android). + virtual auto GetExternalStoragePath() -> std::string; + + // For enabling some special hardware optimizations for nvidia. + auto is_tegra_k1() const -> bool { return is_tegra_k1_; } + void set_is_tegra_k1(bool val) { is_tegra_k1_ = val; } + + // Return true if this platform includes its own python distribution + // (defaults to false). + virtual auto ContainsPythonDist() -> bool; + +#pragma mark INPUT DEVICES ----------------------------------------------------- + + // Return a name for a ballistica keycode. + virtual auto GetKeyName(int keycode) -> std::string; + +#pragma mark IN APP PURCHASES -------------------------------------------------- + + virtual void Purchase(const std::string& item); + + // Restore purchases (currently only relevant on apple platforms). + virtual void RestorePurchases(); + + // purchase ack'ed by the master-server (so can consume) + virtual void PurchaseAck(const std::string& purchase, + const std::string& order_id); + +#pragma mark ANDROID ----------------------------------------------------------- + + virtual auto GetAndroidExecArg() -> std::string; + virtual void AndroidSetResString(const std::string& res); + virtual auto AndroidIsGPGSConnectionToClient(ConnectionToClient* c) -> bool; + virtual auto AndroidGPGSNewConnectionToClient(int id) -> ConnectionToClient*; + virtual auto AndroidGPGSNewConnectionToHost() -> ConnectionToHost*; + virtual void AndroidSynthesizeBackPress(); + virtual void AndroidQuitActivity(); + virtual void AndroidShowAppInvite(const std::string& title, + const std::string& message, + const std::string& code); + virtual void AndroidRefreshFile(const std::string& file); + virtual void AndroidGPGSPartyInvitePlayers(); + virtual void AndroidGPGSPartyShowInvites(); + virtual void AndroidGPGSPartyInviteAccept(const std::string& invite_id); + virtual void AndroidShowWifiSettings(); + +#pragma mark PERMISSIONS ------------------------------------------------------- + + /// Request the permission asynchronously. + /// If the permission cannot be requested (due to having been denied, etc) + /// then this may also present a message or pop-up instructing the user how + /// to manually grant the permission (up to individual platforms to + /// implement). + virtual void RequestPermission(Permission p); + + /// Returns true if this permission has been granted (or if asking is not + /// required for it). + virtual auto HavePermission(Permission p) -> bool; + +#pragma mark ANALYTICS --------------------------------------------------------- + + virtual void SetAnalyticsScreen(const std::string& screen); + virtual void IncrementAnalyticsCount(const std::string& name, int increment); + virtual void IncrementAnalyticsCountRaw(const std::string& name, + int increment); + virtual void IncrementAnalyticsCountRaw2(const std::string& name, + int uses_increment, int increment); + virtual void SubmitAnalyticsCounts(); + +#pragma mark APPLE ------------------------------------------------------------- + + virtual auto NewAutoReleasePool() -> void*; + virtual void DrainAutoReleasePool(void* pool); + // FIXME: Can we consolidate these with the general music playback calls? + virtual void MacMusicAppInit(); + virtual auto MacMusicAppGetVolume() -> int; + virtual void MacMusicAppSetVolume(int volume); + virtual void MacMusicAppGetLibrarySource(); + virtual void MacMusicAppStop(); + virtual auto MacMusicAppPlayPlaylist(const std::string& playlist) -> bool; + virtual auto MacMusicAppGetPlaylists() -> std::list; + +#pragma mark TEXT RENDERING ---------------------------------------------------- + + // Set bounds/width info for a bit of text. + // (will only be called in BA_ENABLE_OS_FONT_RENDERING is set) + virtual void GetTextBoundsAndWidth(const std::string& text, Rect* r, + float* width); + virtual void FreeTextTexture(void* tex); + virtual auto CreateTextTexture(int width, int height, + const std::vector& strings, + const std::vector& positions, + const std::vector& widths, float scale) + -> void*; + virtual auto GetTextTextureData(void* tex) -> uint8_t*; + +#pragma mark ACCOUNTS ---------------------------------------------------------- + + virtual void SignIn(const std::string& account_type); + virtual void SignOut(); + virtual void GameCenterLogin(); + +#pragma mark MUSIC PLAYBACK ---------------------------------------------------- + + // FIXME: currently these are wired up on android; need to generalize + // to support mac/itunes or other music player types. + virtual void MusicPlayerPlay(PyObject* target); + virtual void MusicPlayerStop(); + virtual void MusicPlayerShutdown(); + virtual void MusicPlayerSetVolume(float volume); + +#pragma mark ADS --------------------------------------------------------------- + + virtual void ShowAd(const std::string& purpose); + + // Return whether we have the ability to show *any* ads. + virtual auto GetHasAds() -> bool; + + // Return whether we have the ability to show longer-form video ads (suitable + // for rewards). + virtual auto GetHasVideoAds() -> bool; + +#pragma mark GAME SERVICES ----------------------------------------------------- + + // Given a raw leaderboard score, convert it to what the game uses. + // For instance, platforms may return times as milliseconds while we require + // hundredths of a second, etc. + virtual auto ConvertIncomingLeaderboardScore( + const std::string& leaderboard_id, int score) -> int; + + virtual void GetFriendScores(const std::string& game, + const std::string& game_version, + void* py_callback); + virtual void SubmitScore(const std::string& game, const std::string& version, + int64_t score); + virtual void ReportAchievement(const std::string& achievement); + virtual auto HaveLeaderboard(const std::string& game, + const std::string& config) -> bool; + + virtual void ShowOnlineScoreUI(const std::string& show, + const std::string& game, + const std::string& game_version); + virtual void ResetAchievements(); + +#pragma mark NETWORKING -------------------------------------------------------- + + virtual void CloseSocket(int socket); + virtual auto SocketPair(int domain, int type, int protocol, int socks[2]) + -> int; + virtual auto GetBroadcastAddrs() -> std::vector; + virtual auto SetSocketNonBlocking(int sd) -> bool; + +#pragma mark ERRORS & DEBUGGING ------------------------------------------------ + + // Should return a subclass of PlatformStackTrace allocated via new. + // Platforms with no meaningful stack trace functionality can return nullptr. + virtual auto GetStackTrace() -> PlatformStackTrace*; + + // Called during stress testing. + virtual auto GetMemUsageInfo() -> std::string; + + // Optionally override fatal error reporting. If true is returned, default + // fatal error reporting will not run. + virtual auto ReportFatalError(const std::string& message, + bool in_top_level_exception_handler) -> bool; + + // Optionally override fatal error handling. If true is returned, default + // fatal error handling will not run. + virtual auto HandleFatalError(bool exit_cleanly, + bool in_top_level_exception_handler) -> bool; + + // If this platform has the ability to show a blocking dialog on the main + // thread for fatal errors, return true here. + virtual auto CanShowBlockingFatalErrorDialog() -> bool; + + // Called on the main thread when a fatal error occurs. + // Will only be called if CanShowBlockingFatalErrorDialog() is true. + virtual auto BlockingFatalErrorDialog(const std::string& message) -> void; + + // Use this instead of looking at errno (translates winsock errors to errno). + virtual auto GetSocketError() -> int; + + // Return a string for the current value of errno. + virtual auto GetErrnoString() -> std::string; + + // Return a description of errno (unix) or WSAGetLastError() (windows). + virtual auto GetSocketErrorString() -> std::string; + + /// Set a key to be included in crash logs or other debug cases. + /// This is expected to be lightweight as it may be called often. + virtual auto SetDebugKey(const std::string& key, const std::string& value) + -> void; + + /// Print a log message to be included in crash logs or other debug + /// mechanisms. Standard log messages (at least with to_server=true) get + /// send here as well. It can be useful to call this directly to report + /// extra details that may help in debugging, as these calls are not + /// considered 'noteworthy' or presented to the user as standard Log() + /// calls are. + virtual auto HandleDebugLog(const std::string& msg) -> void; + + static auto DebugLog(const std::string& msg) -> void { + if (g_platform) { + g_platform->HandleDebugLog(msg); + } + } + + /// Shortcut to set last native Python call we made. + static auto SetLastPyCall(const std::string& name) { + if (g_platform) { + g_platform->py_call_num_++; + g_platform->SetDebugKey( + "LastPyCall" + std::to_string(g_platform->py_call_num_ % 10), + std::to_string(g_platform->py_call_num_) + ":" + name + "@" + + std::to_string(GetCurrentMilliseconds())); + } + } + +#pragma mark MISC -------------------------------------------------------------- + + // Return a monotonic time measurement in milliseconds since launch. + // To get a time value that is guaranteed to not jump around or go backwards, + // use ballistica::GetRealTime() (which is an abstraction around this). + auto GetTicks() -> millisecs_t; + + // A raw milliseconds value (not relative to launch time). + static auto GetCurrentMilliseconds() -> millisecs_t; + static auto GetCurrentSeconds() -> int64_t; + + static void SleepMS(millisecs_t ms); + + // Pop up a text edit dialog. + virtual void EditText(const std::string& title, const std::string& value, + int max_chars); + + // Open the provided URL in a browser or whatnot. + void OpenURL(const std::string& url); + virtual auto DemangleCXXSymbol(const std::string& s) -> std::string; + + // Called each time through the main event loop; for custom pumping/handling. + virtual void RunEvents(); + + // Called when the app module is pausing. + // Note: only app-thread (main thread) stuff should happen here. + // (don't push calls to other threads/etc). + virtual void OnAppPause(); + + // Called when the app module is resuming. + virtual void OnAppResume(); + + // Is the OS currently playing music? (so we can avoid doing so). + virtual auto IsOSPlayingMusic() -> bool; + + // Pass platform-specific misc-read-vals along to the OS (as a json string). + virtual void SetPlatformMiscReadVals(const std::string& vals); + + // Show/hide the hardware cursor. + virtual void SetHardwareCursorVisible(bool visible); + + // Get the most up-to-date cursor position. + virtual void GetCursorPosition(float* x, float* y); + + // Quit the app (can be immediate or via posting some high level event). + virtual void QuitApp(); + + // Do we want to deprecate this?... + virtual void GetScoresToBeat(const std::string& level, + const std::string& config, void* py_callback); + + // Open a file using the system default method (in another app, etc.) + virtual void OpenFileExternally(const std::string& path); + + // Open a directory using the system default method (Finder, etc.) + virtual void OpenDirExternally(const std::string& path); + + // Currently mac-only (could be generalized though). + virtual void StartListeningForWiiRemotes(); + + // Currently mac-only (could be generalized though). + virtual void StopListeningForWiiRemotes(); + + // Set the name of the current thread (for debugging). + virtual void SetCurrentThreadName(const std::string& name); + + // If display-resolution can be directly set on this platform, + // return true and set the native full res here. Otherwise return false; + virtual auto GetDisplayResolution(int* x, int* y) -> bool; + + auto using_custom_app_python_dir() const { + return using_custom_app_python_dir_; + } + + protected: + // Open the provided URL in a browser or whatnot. + virtual void DoOpenURL(const std::string& url); + + // Called once per platform to determine touchscreen presence. + virtual auto DoHasTouchScreen() -> bool; + virtual auto DoGetDeviceName() -> std::string; + + // Attempt to actually create a directory. + // Should not except if it already exists or if quiet is true. + virtual void DoMakeDir(const std::string& dir, bool quiet); + + // Calc the user scripts dir path for this platform. + // This will be called once and the path cached. + virtual auto DoGetUserPythonDirectory() -> std::string; + + // Return the default config directory for this platform. + virtual auto GetDefaultConfigDir() -> std::string; + + // Return the prefix to use for device UUIDs on this platform. + virtual auto GetDeviceUUIDPrefix() -> std::string; + + // Return whether there is an actual unique UUID available for this platform, + // and also return it if so. + virtual auto GetRealDeviceUUID(std::string* uuid) -> bool; + + // Generate a random UUID string. + virtual auto GenerateUUID() -> std::string; + + private: + int py_call_num_{}; + bool using_custom_app_python_dir_{}; + bool have_config_dir_{}; + bool have_has_touchscreen_value_{}; + bool have_touchscreen_{}; + bool is_tegra_k1_{}; + millisecs_t starttime_{}; + std::string device_uuid_; + bool have_device_uuid_{}; + std::string config_dir_; + std::string user_scripts_dir_; + std::string app_python_dir_; + std::string site_python_dir_; + std::string replays_dir_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PLATFORM_PLATFORM_H_ diff --git a/src/ballistica/platform/sdl/sdl_app.cc b/src/ballistica/platform/sdl/sdl_app.cc new file mode 100644 index 00000000..c6537f6c --- /dev/null +++ b/src/ballistica/platform/sdl/sdl_app.cc @@ -0,0 +1,614 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#if BA_SDL_BUILD + +#include "ballistica/platform/sdl/sdl_app.h" + +#include + +#include "ballistica/core/thread.h" +#include "ballistica/dynamics/bg/bg_dynamics.h" +#include "ballistica/game/game.h" +#include "ballistica/graphics/gl/gl_sys.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/input/device/joystick.h" +#include "ballistica/input/input.h" +#include "ballistica/platform/platform.h" +#include "ballistica/python/python.h" + +namespace ballistica { + +// NOTE TO SELF: slowly try to phase everything out from here and into +// non-sdl event/call pushes. +void SDLApp::HandleSDLEvent(const SDL_Event& event) { + assert(InMainThread()); + + switch (event.type) { + case SDL_JOYAXISMOTION: + case SDL_JOYBUTTONDOWN: + case SDL_JOYBUTTONUP: + case SDL_JOYBALLMOTION: + case SDL_JOYHATMOTION: { + // It seems that joystick connection/disconnection callbacks can fire + // while there are still events for that joystick in the queue. + // So take care to ignore events for no-longer-existing joysticks. + assert(event.jaxis.which == event.jbutton.which + && event.jaxis.which == event.jhat.which); + if (static_cast(event.jbutton.which) >= sdl_joysticks_.size() + || sdl_joysticks_[event.jbutton.which] == nullptr) { + return; + } + Joystick* js = GetSDLJoyStickInput(&event); + if (js) { + if (g_input) { + g_input->PushJoystickEvent(event, js); + } + } else { + Log("Error: Unable to get SDL Joystick for event type " + + std::to_string(event.type)); + } + break; + } + + case SDL_MOUSEBUTTONDOWN: { + const SDL_MouseButtonEvent* e = &event.button; + + // Convert sdl's coords to normalized view coords. + float x = static_cast(e->x) / screen_dimensions_.x; + float y = 1.0f - static_cast(e->y) / screen_dimensions_.y; + if (g_input) { + g_input->PushMouseDownEvent(e->button, Vector2f(x, y)); + } + break; + } + case SDL_MOUSEBUTTONUP: { + const SDL_MouseButtonEvent* e = &event.button; + + // Convert sdl's coords to normalized view coords. + float x = static_cast(e->x) / screen_dimensions_.x; + float y = 1.0f - static_cast(e->y) / screen_dimensions_.y; + if (g_input) { + g_input->PushMouseUpEvent(e->button, Vector2f(x, y)); + } + break; + } + case SDL_MOUSEMOTION: { + const SDL_MouseMotionEvent* e = &event.motion; + + // Convert sdl's coords to normalized view coords. + float x = static_cast(e->x) / screen_dimensions_.x; + float y = 1.0f - static_cast(e->y) / screen_dimensions_.y; + if (g_input) { + g_input->PushMouseMotionEvent(Vector2f(x, y)); + } + break; + } + case SDL_KEYDOWN: { + if (g_input) { + g_input->PushKeyPressEvent(event.key.keysym); + } + break; + } + case SDL_KEYUP: { + if (g_input) { + g_input->PushKeyReleaseEvent(event.key.keysym); + } + break; + } + +#if BA_SDL2_BUILD || BA_MINSDL_BUILD + case SDL_MOUSEWHEEL: { + const SDL_MouseWheelEvent* e = &event.wheel; + + // Seems in general scrolling is a lot faster on mac SDL compared to + // windows/linux. (maybe its just for trackpads/etc..).. so lets + // compensate. + int scroll_speed; + if (g_buildconfig.ostype_android()) { + scroll_speed = 1; + } else if (g_buildconfig + .ostype_macos()) { // NOLINT(bugprone-branch-clone) + scroll_speed = 500; + } else { + scroll_speed = 500; + } + if (g_input) { + g_input->PushMouseScrollEvent( + Vector2f(static_cast(e->x * scroll_speed), + static_cast(e->y * scroll_speed))); + } + break; + } +#endif // BA_SDL2_BUILD + +#if BA_OSTYPE_MACOS && BA_XCODE_BUILD + case SDL_SMOOTHSCROLLEVENT: { + const SDL_SmoothScrollEvent* e = &event.scroll; + if (g_input) { + g_input->PushSmoothMouseScrollEvent( + Vector2f(0.2f * e->deltaX, -0.2f * e->deltaY), e->momentum); + } + break; + } +#endif + + // Currently used in our some of our heavily customized builds. + // Should replace this with some sort of PushDrawEvent() thing. +#if BA_XCODE_BUILD + case SDL_RESIZEDRAWEVENT: + case SDL_DRAWEVENT: { + DrawFrame(event.type == SDL_RESIZEDRAWEVENT); + break; + } +#endif // BA_OSTYPE_MACOS || BA_OSTYPE_ANDROID + + // Is there a reason we need to ignore these on ios? + // do they even happen there? + // UPDATE: I think the even types are just not defined on our old iOS SDL. +#if BA_SDL2_BUILD && !BA_OSTYPE_IOS_TVOS && BA_ENABLE_SDL_JOYSTICKS + case SDL_JOYDEVICEREMOVED: + // In this case we're passed the instance-id of the joystick. + SDLJoystickDisconnected(event.jdevice.which); + break; + case SDL_JOYDEVICEADDED: + SDLJoystickConnected(event.jdevice.which); + break; +#endif + + case SDL_QUIT: + g_game->PushShutdownCall(false); + break; + +#if BA_OSTYPE_MACOS && BA_XCODE_BUILD && !BA_HEADLESS_BUILD + case SDL_FULLSCREENSWITCH: + // Our custom hacked-up SDL informs *us* when our window enters or exits + // fullscreen. Let's commit this to our config so that we're in sync.. + g_python->PushObjCall(event.user.code + ? Python::ObjID::kSetConfigFullscreenOnCall + : Python::ObjID::kSetConfigFullscreenOffCall); + g_graphics_server->set_fullscreen_enabled(event.user.code); + break; +#endif + +#if BA_SDL2_BUILD + + case SDL_TEXTINPUT: { + if (g_input) { + g_input->PushTextInputEvent(event.text.text); + } + break; + } + + case SDL_WINDOWEVENT: { + switch (event.window.event) { + case SDL_WINDOWEVENT_MINIMIZED: // NOLINT(bugprone-branch-clone) + + // Hmm do we want to pause the app on desktop when minimized? + // Gonna say no for now. +#if BA_OSTYPE_IOS_TVOS + PauseApp(); +#endif + break; + + case SDL_WINDOWEVENT_RESTORED: +#if BA_OSTYPE_IOS_TVOS + ResumeApp(); +#endif + break; + + case SDL_WINDOWEVENT_RESIZED: + case SDL_WINDOWEVENT_SIZE_CHANGED: { +#if BA_OSTYPE_IOS_TVOS + // Do nothing here currently. +#else // Generic SDL: + int pixels_x, pixels_y; + SDL_GL_GetDrawableSize(g_graphics_server->gl_context()->sdl_window(), + &pixels_x, &pixels_y); + + // Pixel density is number of pixels divided by window dimension. + screen_dimensions_ = Vector2f(event.window.data1, event.window.data2); + SetScreenResolution(static_cast(pixels_x), + static_cast(pixels_y)); +#endif // BA_OSTYPE_IOS_TVOS + + break; + } + default: + break; + } + break; + default: + break; + } +#else // BA_SDL2_BUILD + case SDL_VIDEORESIZE: { + screen_dimensions_ = Vector2f(event.resize.w, event.resize.h); + SetScreenResolution(event.resize.w, event.resize.h); + break; + } +#endif // BA_SDL2_BUILD + } +} + +auto FilterSDLEvent(const SDL_Event* event) -> int { + try { + // If this event is coming from this thread, handle it immediately. + if (std::this_thread::get_id() == g_app_globals->main_thread_id) { + auto app = static_cast_check_type(g_app); + assert(app); + if (app) { + app->HandleSDLEvent(*event); + } + return false; // We handled it; sdl doesn't need to keep it. + } else { + // Otherwise just let SDL post it to the normal queue.. we process this + // every now and then to pick these up. + return true; // sdl should keep this. + } + } catch (const std::exception& e) { + BA_LOG_ONCE(std::string("Exception in inline SDL-Event handling: ") + + e.what()); + throw; + } +} + +#if BA_SDL2_BUILD +inline auto FilterSDL2Event(void* user_data, SDL_Event* event) -> int { + return FilterSDLEvent(event); +} +#endif + +// Note: can move this to SDLApp::SDLApp() once it is no longer needed by +// the legacy mac build. +void SDLApp::InitSDL() { + assert(g_platform != nullptr); + + if (g_buildconfig.ostype_macos()) { + // We don't want sdl to translate command/option clicks to different mouse + // buttons dernit. + g_platform->SetEnv("SDL_HAS3BUTTONMOUSE", "1"); + } + + // Let's turn on extra GL debugging on linux debug builds. + if (g_buildconfig.ostype_linux() && g_buildconfig.debug_build()) { + g_platform->SetEnv("MESA_DEBUG", "true"); + } + + uint32_t sdl_flags{}; + + // We can skip joysticks and video for headless. + if (!g_buildconfig.headless_build()) { + sdl_flags |= SDL_INIT_VIDEO; + if (explicit_bool(true)) { + sdl_flags |= SDL_INIT_JOYSTICK; + + // KILL THIS ONCE MAC SDL1.2 BUILD IS DEAD. + // Register our hotplug callbacks in our funky custom mac build. +#if BA_OSTYPE_MACOS && BA_XCODE_BUILD && !BA_HEADLESS_BUILD + SDL_JoystickSetHotPlugCallbacks(SDLApp::SDLJoystickConnected, + SDLApp::SDLJoystickDisconnected); +#endif + } + } + + // Whatever fancy-pants stuff SDL is trying to do with catching signals/etc, + // we don't want it. + sdl_flags |= SDL_INIT_NOPARACHUTE; + + // We want xinput on windows. + if (g_buildconfig.ostype_windows()) { + if (!g_platform->GetLowLevelConfigValue("enablexinput", 1)) { + SDL_SetHint(SDL_HINT_XINPUT_ENABLED, "0"); + } + } + + int result = SDL_Init(sdl_flags); + if (result < 0) { + throw Exception(std::string("SDL_Init failed: ") + SDL_GetError()); + } + + // KILL THIS ONCE SDL IS NO LONGER USED ON IOS BUILD + if (g_buildconfig.ostype_ios_tvos() || g_buildconfig.ostype_android()) { + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); + } + + // KILL THIS ONCE MAC SDL 1.2 BUILD IS DEAD +#if !BA_SDL2_BUILD + SDL_EnableUNICODE(true); + SDL_EnableKeyRepeat(200, 50); +#endif +} + +SDLApp::SDLApp(Thread* thread) : App(thread) { + InitSDL(); + + // If we're not running our own even loop, we set up a filter to intercept + // SDL events the moment they're generated and we process them immediately. + // This way we don't have to poll for events and can be purely callback-based, + // which fits in nicely with most modern event models. + if (!UsesEventLoop()) { +#if BA_SDL2_BUILD + SDL_SetEventFilter(FilterSDL2Event, nullptr); +#else + SDL_SetEventFilter(FilterSDLEvent); +#endif // BA_SDL2_BUILD + } else { + // Otherwise we do the standard old SDL polling stuff. + + // Set up a timer to chew through events every now and then. Polling isn't + // super elegant, but is necessary in SDL's case. (SDLWaitEvent() itself is + // pretty much a loop with SDL_PollEvents() followed by SDL_Delay(10) until + // something is returned; In spirit, we're pretty much doing that same + // thing, except that we're free to handle other matters concurrently + // instead of being locked in a delay call. + NewThreadTimer(10, true, NewLambdaRunnable([this] { + assert(g_app); + g_app->RunEvents(); + })); + } +} + +void SDLApp::RunEvents() { + App::RunEvents(); + + // Now run all pending SDL events until we run out or we're told to quit. + SDL_Event event; + while (SDL_PollEvent(&event) && (!done())) { + HandleSDLEvent(event); + } +} + +void SDLApp::DidFinishRenderingFrame(FrameDef* frame) { + App::DidFinishRenderingFrame(frame); + SwapBuffers(); +} + +void SDLApp::DoSwap() { + assert(InMainThread()); + + if (g_buildconfig.debug_build()) { + millisecs_t diff = GetRealTime() - swap_start_time_; + if (diff > 5) { + Log("WARNING: Swap handling delay of " + std::to_string(diff)); + } + } + +#if BA_ENABLE_OPENGL +#if BA_SDL2_BUILD + SDL_GL_SwapWindow(g_graphics_server->gl_context()->sdl_window()); +#else + SDL_GL_SwapBuffers(); +#endif // BA_SDL2_BUILD +#endif // BA_ENABLE_OPENGL + + millisecs_t cur_time = GetRealTime(); + + // Do some post-render analysis/updates. + if (last_swap_time_ != 0) { + millisecs_t diff2 = cur_time - last_swap_time_; + if (auto_vsync_) { + UpdateAutoVSync(static_cast(diff2)); + } + + // If we drop to a super-crappy FPS lets take some countermeasures + // such as telling BG-dynamics to kill off some stuff. + if (diff2 >= 1000 / 20) { + too_slow_frame_count_++; + } else { + too_slow_frame_count_ = 0; + } + + // Several slow frames in a row and we take action. + if (too_slow_frame_count_ > 10) { + too_slow_frame_count_ = 0; + + // A common cause of slowness is excessive smoke and bg stuff; + // lets tell the bg dynamics thread to tone it down. + g_bg_dynamics->TooSlow(); + } + } + last_swap_time_ = cur_time; +} + +void SDLApp::SwapBuffers() { + swap_start_time_ = GetRealTime(); + assert(thread()->IsCurrent()); + DoSwap(); + + // FIXME: Move this somewhere reasonable. Not here. + // On mac/ios we wanna delay our game-center login until we've drawn a few + // frames, so lets do that here. + // ...hmm; why is that? I don't remember. Should revisit. + if (g_buildconfig.use_game_center()) { + static int f_count = 0; + f_count++; + if (f_count == 5) { + g_platform->GameCenterLogin(); + } + } +} + +void SDLApp::UpdateAutoVSync(int diff) { + assert(auto_vsync_); + + // If we're currently vsyncing, watch for slow frames. + if (vsync_enabled_) { + // Keep a smoothed average of the FPS we get with VSync on. + { + float this_fps = 1000.0f / static_cast(diff); + float smoothing = 0.95f; + average_vsync_fps_ = + smoothing * average_vsync_fps_ + (1.0f - smoothing) * this_fps; + } + + // If framerate drops significantly below 60, flip vsync off to get a + // better framerate (but *only* if we're pretty sure we can hit 60 with + // it on; otherwise if we're on a 30hz monitor we'll get into a cycle of + // flipping it off and on repeatedly since we slow down a lot with it on + // and then speed up a lot with it off). + if (diff >= 1000 / 40 && average_vsync_fps_ > 55.0f) { + vsync_bad_frame_count_++; + } else { + vsync_bad_frame_count_ = 0; + } + + if (vsync_bad_frame_count_ >= 10) { + vsync_enabled_ = false; +#if BA_ENABLE_OPENGL + g_graphics_server->gl_context()->SetVSync(vsync_enabled_); +#endif + vsync_good_frame_count_ = 0; + } + } else { + // Vsync is currently off.. watch for framerate staying consistently high + // and then turn it on again. + if (diff <= 1000 / 50) { + vsync_good_frame_count_++; + } else { + vsync_good_frame_count_ = 0; + } + if (vsync_good_frame_count_ >= 60) { + vsync_enabled_ = true; +#if BA_ENABLE_OPENGL + g_graphics_server->gl_context()->SetVSync(vsync_enabled_); +#endif + vsync_bad_frame_count_ = 0; + } + } +} + +void SDLApp::SetAutoVSync(bool enable) { + auto_vsync_ = enable; + // If we're doing auto, start with vsync on. + if (enable) { + vsync_enabled_ = true; +#if BA_ENABLE_OPENGL + g_graphics_server->gl_context()->SetVSync(vsync_enabled_); +#endif + } +} + +void SDLApp::OnBootstrapComplete() { + App::OnBootstrapComplete(); + + if (!HeadlessMode() && g_buildconfig.enable_sdl_joysticks()) { + // Add initial sdl joysticks. any added/removed after this will be handled + // via events. (it seems (on mac at least) even the initial ones are handled + // via events, so lets make sure we handle redundant joystick connections + // gracefully. + if (explicit_bool(true)) { + int joystick_count = SDL_NumJoysticks(); + for (int i = 0; i < joystick_count; i++) { + SDLApp::SDLJoystickConnected(i); + } + + // We want events from joysticks. + SDL_JoystickEventState(SDL_ENABLE); + } + } +} + +void SDLApp::SDLJoystickConnected(int device_index) { + assert(InMainThread()); + + // We add all existing inputs when bootstrapping is complete; we should + // never be getting these before that happens. + if (g_input == nullptr || g_app == nullptr || !IsBootstrapped()) { + Log("Unexpected SDLJoystickConnected early in boot sequence."); + return; + } + + // Create the joystick here in the main thread and then pass it over to the + // game thread to be added to the game. + if (g_buildconfig.ostype_ios_tvos()) { + BA_LOG_ONCE("WTF GOT SDL-JOY-CONNECTED ON IOS"); + } else { + auto* j = Object::NewDeferred(device_index); + if (g_buildconfig.sdl2_build() && g_buildconfig.enable_sdl_joysticks()) { + int instance_id = SDL_JoystickInstanceID(j->sdl_joystick()); + get()->AddSDLInputDevice(j, instance_id); + } else { + get()->AddSDLInputDevice(j, device_index); + } + } +} + +void SDLApp::SDLJoystickDisconnected(int index) { + assert(InMainThread()); + assert(index >= 0); + get()->RemoveSDLInputDevice(index); +} + +auto SDLApp::SetInitialScreenDimensions(const Vector2f& dimensions) -> void { + screen_dimensions_ = dimensions; +} + +void SDLApp::AddSDLInputDevice(Joystick* input, int index) { + assert(g_input != nullptr); + assert(input != nullptr); + assert(InMainThread()); + assert(index >= 0); + + // Keep a mapping of SDL input-device indices to Joysticks. + if (static_cast_check_fit(sdl_joysticks_.size()) <= index) { + sdl_joysticks_.resize(static_cast(index) + 1, nullptr); + } + sdl_joysticks_[index] = input; + + g_input->PushAddInputDeviceCall(input, true); +} + +void SDLApp::RemoveSDLInputDevice(int index) { + assert(InMainThread()); + assert(index >= 0); + Joystick* j = GetSDLJoyStickInput(index); + assert(j); + if (static_cast_check_fit(sdl_joysticks_.size()) > index) { + sdl_joysticks_[index] = nullptr; + } else { + Log("Error: Invalid index on RemoveSDLInputDevice: size is " + + std::to_string(sdl_joysticks_.size()) + "; index is " + + std::to_string(index)); + } + g_input->PushRemoveInputDeviceCall(j, true); +} + +auto SDLApp::GetSDLJoyStickInput(const SDL_Event* e) const -> Joystick* { + assert(InMainThread()); + int joy_id; + + // Attempt to pull the joystick id from the event. + switch (e->type) { + case SDL_JOYAXISMOTION: + joy_id = e->jaxis.which; + break; + case SDL_JOYBUTTONDOWN: + case SDL_JOYBUTTONUP: + joy_id = e->jbutton.which; + break; + case SDL_JOYBALLMOTION: + joy_id = e->jball.which; + break; + case SDL_JOYHATMOTION: + joy_id = e->jhat.which; + break; + default: + return nullptr; + } + return GetSDLJoyStickInput(joy_id); +} + +auto SDLApp::GetSDLJoyStickInput(int sdl_joystick_id) const -> Joystick* { + assert(InMainThread()); + for (auto sdl_joystick : sdl_joysticks_) { + if ((sdl_joystick != nullptr) && (*sdl_joystick).sdl_joystick_id() >= 0 + && (*sdl_joystick).sdl_joystick_id() == sdl_joystick_id) + return sdl_joystick; + } + return nullptr; // Epic fail. +} + +} // namespace ballistica + +#endif // BA_SDL_BUILD diff --git a/src/ballistica/platform/sdl/sdl_app.h b/src/ballistica/platform/sdl/sdl_app.h new file mode 100644 index 00000000..4d2fa085 --- /dev/null +++ b/src/ballistica/platform/sdl/sdl_app.h @@ -0,0 +1,65 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PLATFORM_SDL_SDL_APP_H_ +#define BALLISTICA_PLATFORM_SDL_SDL_APP_H_ + +#if BA_SDL_BUILD + +#include + +#include "ballistica/app/app.h" +#include "ballistica/math/vector2f.h" + +namespace ballistica { + +class SDLApp : public App { + public: + static auto InitSDL() -> void; + explicit SDLApp(Thread* thread); + auto HandleSDLEvent(const SDL_Event& event) -> void; + auto RunEvents() -> void override; + auto DidFinishRenderingFrame(FrameDef* frame) -> void override; + auto SetAutoVSync(bool enable) -> void; + static auto SDLJoystickConnected(int index) -> void; + static auto SDLJoystickDisconnected(int index) -> void; + auto OnBootstrapComplete() -> void override; + + /// Return g_app as a SDLApp. (assumes it actually is one). + static SDLApp* get() { + assert(g_app != nullptr); + assert(dynamic_cast(g_app) == static_cast(g_app)); + return static_cast(g_app); + } + auto SetInitialScreenDimensions(const Vector2f& dimensions) -> void; + + private: + // Given an sdl joystick ID, returns our ballistica input for it. + auto GetSDLJoyStickInput(int sdl_joystick_id) const -> Joystick*; + + // The same but using sdl events. + auto GetSDLJoyStickInput(const SDL_Event* e) const -> Joystick*; + + auto DoSwap() -> void; + auto SwapBuffers() -> void; + auto UpdateAutoVSync(int diff) -> void; + auto AddSDLInputDevice(Joystick* input, int index) -> void; + auto RemoveSDLInputDevice(int index) -> void; + millisecs_t last_swap_time_{}; + millisecs_t swap_start_time_{}; + int too_slow_frame_count_{}; + bool auto_vsync_{}; + bool vsync_enabled_{true}; + float average_vsync_fps_{60.0f}; + int vsync_good_frame_count_{}; + int vsync_bad_frame_count_{}; + std::vector sdl_joysticks_; + + /// This is in points; not pixels. + Vector2f screen_dimensions_{1.0f, 1.0f}; +}; + +} // namespace ballistica + +#endif // BA_SDL_BUILD + +#endif // BALLISTICA_PLATFORM_SDL_SDL_APP_H_