From 18d76bee62014057d5f5547d7e87f9508a90bcb9 Mon Sep 17 00:00:00 2001 From: Haroon Qureshi Date: Mon, 28 Nov 2022 22:12:58 +0000 Subject: [PATCH] Integrating latest changes. --- redux/.bazelrc | 16 + redux/WORKSPACE | 256 ++++ redux/external/BUILD.assimp | 106 ++ redux/external/BUILD.bullet | 56 + redux/external/BUILD.eigen | 26 + redux/external/BUILD.filament | 157 +++ redux/external/BUILD.fmtlib | 7 + redux/external/BUILD.freetype | 103 ++ redux/external/BUILD.harfbuzz | 12 + redux/external/BUILD.libfbx | 16 + redux/external/BUILD.libogg | 7 + redux/external/BUILD.libopus | 241 ++++ redux/external/BUILD.libopusfile | 11 + redux/external/BUILD.libpng | 18 + redux/external/BUILD.libunibreak | 20 + redux/external/BUILD.libvorbis | 31 + redux/external/BUILD.libwebp | 54 + redux/external/BUILD.magic_enum | 6 + redux/external/BUILD.pffft | 7 + redux/external/BUILD.rapidjson | 7 + redux/external/BUILD.resonance | 31 + redux/external/BUILD.sdl2 | 110 ++ redux/external/BUILD.stblib | 7 + redux/external/BUILD.utfcpp | 6 + redux/external/BUILD.vectorial | 6 + redux/external/BUILD.zlib | 11 + redux/external/assimp.patch | 1177 +++++++++++++++++ redux/external/filament.patch | 72 + redux/external/libogg.patch | 19 + redux/external/libpng.patch | 354 +++++ redux/external/stblib.patch | 8 + redux/redux/BUILD | 9 + redux/redux/data/asset_defs/BUILD | 53 + .../redux/data/asset_defs/anim_asset_def.fbs | 85 ++ .../redux/data/asset_defs/model_asset_def.fbs | 165 +++ .../data/asset_defs/shader_asset_def.fbs | 52 + .../data/asset_defs/texture_asset_def.fbs | 54 + redux/redux/engines/animation/BUILD | 57 + redux/redux/engines/animation/README.md | 7 + .../redux/engines/animation/animation_clip.cc | 115 ++ .../redux/engines/animation/animation_clip.h | 110 ++ .../engines/animation/animation_engine.cc | 100 ++ .../engines/animation/animation_engine.h | 114 ++ .../engines/animation/animation_playback.h | 54 + redux/redux/engines/animation/common.h | 59 + .../engines/animation/motivator/motivator.cc | 75 ++ .../engines/animation/motivator/motivator.h | 105 ++ .../animation/motivator/rig_motivator.cc | 56 + .../animation/motivator/rig_motivator.h | 71 + .../animation/motivator/spline_motivator.cc | 73 + .../animation/motivator/spline_motivator.h | 74 ++ .../motivator/transform_motivator.cc | 53 + .../animation/motivator/transform_motivator.h | 65 + .../animation/processor/anim_processor.cc | 203 +++ .../animation/processor/anim_processor.h | 220 +++ .../animation/processor/index_allocator.h | 490 +++++++ .../animation/processor/rig_processor.cc | 167 +++ .../animation/processor/rig_processor.h | 101 ++ .../animation/processor/spline_processor.cc | 309 +++++ .../animation/processor/spline_processor.h | 114 ++ .../processor/transform_processor.cc | 386 ++++++ .../animation/processor/transform_processor.h | 126 ++ redux/redux/engines/animation/spline/BUILD | 113 ++ .../animation/spline/bulk_spline_evaluator.cc | 466 +++++++ .../animation/spline/bulk_spline_evaluator.h | 405 ++++++ .../spline/bulk_spline_evaluator_neon.s | 160 +++ .../spline/bulk_spline_evaluator_tests.cc | 227 ++++ .../animation/spline/compact_spline.cc | 403 ++++++ .../engines/animation/spline/compact_spline.h | 608 +++++++++ .../animation/spline/compact_spline_node.h | 176 +++ .../animation/spline/compact_spline_tests.cc | 311 +++++ .../engines/animation/spline/cubic_curve.cc | 96 ++ .../engines/animation/spline/cubic_curve.h | 145 ++ .../animation/spline/cubic_curve_tests.cc | 84 ++ .../engines/animation/spline/dual_cubic.cc | 343 +++++ .../engines/animation/spline/dual_cubic.h | 34 + .../animation/spline/quadratic_curve.cc | 268 ++++ .../animation/spline/quadratic_curve.h | 193 +++ .../animation/spline/quadratic_curve_tests.cc | 188 +++ redux/redux/engines/audio/BUILD | 28 + redux/redux/engines/audio/audio_asset.h | 52 + redux/redux/engines/audio/audio_engine.h | 105 ++ redux/redux/engines/audio/resonance/BUILD | 94 ++ .../audio/resonance/audio_asset_manager.cc | 181 +++ .../audio/resonance/audio_asset_manager.h | 89 ++ .../resonance/audio_asset_manager_tests.cc | 112 ++ .../audio/resonance/audio_asset_stream.cc | 465 +++++++ .../audio/resonance/audio_asset_stream.h | 209 +++ .../resonance/audio_asset_stream_tests.cc | 273 ++++ .../audio/resonance/audio_planar_data.cc | 139 ++ .../audio/resonance/audio_planar_data.h | 70 + .../resonance/audio_planar_data_tests.cc | 101 ++ .../audio/resonance/audio_source_stream.h | 80 ++ .../audio/resonance/audio_stream_manager.cc | 160 +++ .../audio/resonance/audio_stream_manager.h | 76 ++ .../audio/resonance/audio_stream_renderer.cc | 132 ++ .../audio/resonance/audio_stream_renderer.h | 112 ++ .../audio/resonance/resonance_audio_asset.cc | 84 ++ .../audio/resonance/resonance_audio_asset.h | 108 ++ .../audio/resonance/resonance_audio_engine.cc | 478 +++++++ .../audio/resonance/resonance_audio_engine.h | 200 +++ .../audio/resonance/resonance_sound.cc | 107 ++ .../engines/audio/resonance/resonance_sound.h | 70 + .../audio/resonance/resonance_utils.cc | 116 ++ .../engines/audio/resonance/resonance_utils.h | 40 + redux/redux/engines/audio/sound.h | 80 ++ redux/redux/engines/audio/sound_room.h | 66 + redux/redux/engines/physics/BUILD | 53 + redux/redux/engines/physics/bullet/BUILD | 41 + .../physics/bullet/bullet_collision_shape.cc | 123 ++ .../physics/bullet/bullet_collision_shape.h | 53 + .../physics/bullet/bullet_physics_engine.cc | 195 +++ .../physics/bullet/bullet_physics_engine.h | 147 ++ .../physics/bullet/bullet_rigid_body.cc | 197 +++ .../physics/bullet/bullet_rigid_body.h | 138 ++ .../physics/bullet/bullet_trigger_volume.cc | 101 ++ .../physics/bullet/bullet_trigger_volume.h | 65 + .../engines/physics/bullet/bullet_utils.h | 60 + redux/redux/engines/physics/bullet/thunks.cc | 46 + redux/redux/engines/physics/collision_data.cc | 81 ++ redux/redux/engines/physics/collision_data.h | 103 ++ redux/redux/engines/physics/collision_shape.h | 40 + redux/redux/engines/physics/enums.h | 36 + redux/redux/engines/physics/physics_engine.h | 124 ++ redux/redux/engines/physics/physics_enums.fbs | 10 + redux/redux/engines/physics/rigid_body.h | 161 +++ redux/redux/engines/physics/thunks/BUILD | 19 + redux/redux/engines/physics/thunks/README.md | 27 + .../engines/physics/thunks/physics_engine.h | 65 + .../redux/engines/physics/thunks/rigid_body.h | 98 ++ .../engines/physics/thunks/trigger_volume.h | 36 + redux/redux/engines/physics/trigger_volume.h | 80 ++ redux/redux/engines/platform/BUILD | 128 ++ redux/redux/engines/platform/buffered_state.h | 73 + .../redux/engines/platform/device_manager.cc | 101 ++ redux/redux/engines/platform/device_manager.h | 142 ++ .../redux/engines/platform/device_profiles.h | 158 +++ redux/redux/engines/platform/display.cc | 41 + redux/redux/engines/platform/display.h | 57 + redux/redux/engines/platform/keyboard.cc | 68 + redux/redux/engines/platform/keyboard.h | 64 + redux/redux/engines/platform/keycodes.cc | 103 ++ redux/redux/engines/platform/keycodes.h | 185 +++ redux/redux/engines/platform/mainloop.cc | 64 + redux/redux/engines/platform/mainloop.h | 72 + redux/redux/engines/platform/mouse.cc | 93 ++ redux/redux/engines/platform/mouse.h | 68 + redux/redux/engines/platform/sdl2/BUILD | 62 + .../platform/sdl2/get_native_window_osx.mm | 24 + .../engines/platform/sdl2/sdl2_display.cc | 92 ++ .../engines/platform/sdl2/sdl2_display.h | 52 + .../platform/sdl2/sdl2_event_handler.h | 33 + .../engines/platform/sdl2/sdl2_keyboard.cc | 210 +++ .../engines/platform/sdl2/sdl2_keyboard.h | 40 + .../engines/platform/sdl2/sdl2_mainloop.cc | 79 ++ .../engines/platform/sdl2/sdl2_mainloop.h | 48 + .../redux/engines/platform/sdl2/sdl2_mouse.cc | 71 + .../redux/engines/platform/sdl2/sdl2_mouse.h | 39 + .../engines/platform/sdl2/sdl2_speaker.cc | 77 ++ .../engines/platform/sdl2/sdl2_speaker.h | 43 + redux/redux/engines/platform/speaker.cc | 30 + redux/redux/engines/platform/speaker.h | 48 + .../redux/engines/platform/virtual_device.cc | 84 ++ redux/redux/engines/platform/virtual_device.h | 96 ++ redux/redux/engines/render/BUILD | 49 + redux/redux/engines/render/filament/BUILD | 68 + .../filament/filament_indirect_light.cc | 120 ++ .../render/filament/filament_indirect_light.h | 65 + .../engines/render/filament/filament_light.cc | 138 ++ .../engines/render/filament/filament_light.h | 84 ++ .../engines/render/filament/filament_mesh.cc | 275 ++++ .../engines/render/filament/filament_mesh.h | 69 + .../render/filament/filament_render_engine.cc | 262 ++++ .../render/filament/filament_render_engine.h | 118 ++ .../render/filament/filament_render_layer.cc | 118 ++ .../render/filament/filament_render_layer.h | 117 ++ .../render/filament/filament_render_scene.cc | 61 + .../render/filament/filament_render_scene.h | 59 + .../render/filament/filament_render_target.cc | 133 ++ .../render/filament/filament_render_target.h | 63 + .../render/filament/filament_renderable.cc | 439 ++++++ .../render/filament/filament_renderable.h | 147 ++ .../render/filament/filament_shader.cc | 178 +++ .../engines/render/filament/filament_shader.h | 104 ++ .../render/filament/filament_texture.cc | 227 ++++ .../render/filament/filament_texture.h | 77 ++ .../engines/render/filament/filament_utils.h | 91 ++ .../engines/render/filament/mesh_factory.cc | 58 + .../render/filament/render_target_factory.cc | 42 + .../engines/render/filament/shader_factory.cc | 61 + .../render/filament/texture_factory.cc | 171 +++ redux/redux/engines/render/filament/thunks.cc | 95 ++ redux/redux/engines/render/indirect_light.h | 58 + redux/redux/engines/render/light.h | 93 ++ redux/redux/engines/render/mesh.h | 74 ++ redux/redux/engines/render/mesh_factory.h | 66 + redux/redux/engines/render/render_engine.h | 105 ++ redux/redux/engines/render/render_layer.h | 105 ++ redux/redux/engines/render/render_scene.h | 65 + redux/redux/engines/render/render_target.h | 49 + .../engines/render/render_target_factory.h | 105 ++ redux/redux/engines/render/renderable.h | 109 ++ redux/redux/engines/render/shader.h | 44 + redux/redux/engines/render/shader_factory.h | 51 + redux/redux/engines/render/texture.h | 66 + redux/redux/engines/render/texture_factory.h | 95 ++ redux/redux/engines/render/thunks/BUILD | 25 + redux/redux/engines/render/thunks/README.md | 27 + .../engines/render/thunks/indirect_light.h | 34 + redux/redux/engines/render/thunks/light.h | 44 + redux/redux/engines/render/thunks/mesh.h | 37 + .../engines/render/thunks/render_engine.h | 77 ++ .../engines/render/thunks/render_layer.h | 61 + .../engines/render/thunks/render_scene.h | 40 + .../engines/render/thunks/render_target.h | 34 + .../redux/engines/render/thunks/renderable.h | 71 + redux/redux/engines/render/thunks/texture.h | 34 + redux/redux/engines/script/BUILD | 53 + .../engines/script/call_native_function.h | 118 ++ .../script/call_native_function_tests.cc | 78 ++ redux/redux/engines/script/function_binder.cc | 38 + redux/redux/engines/script/function_binder.h | 97 ++ redux/redux/engines/script/redux/BUILD | 216 +++ redux/redux/engines/script/redux/README.md | 151 +++ .../engines/script/redux/functions/array.h | 169 +++ .../script/redux/functions/array_tests.cc | 152 +++ .../engines/script/redux/functions/cond.h | 87 ++ .../script/redux/functions/cond_tests.cc | 38 + .../engines/script/redux/functions/hash.h | 48 + .../engines/script/redux/functions/map.h | 167 +++ .../script/redux/functions/map_tests.cc | 125 ++ .../engines/script/redux/functions/math.h | 148 +++ .../script/redux/functions/math_tests.cc | 196 +++ .../engines/script/redux/functions/message.h | 89 ++ .../script/redux/functions/message_tests.cc | 51 + .../script/redux/functions/operators.h | 416 ++++++ .../script/redux/functions/operators_tests.cc | 179 +++ .../engines/script/redux/functions/typeof.h | 62 + .../script/redux/functions/typeof_tests.cc | 50 + .../script/redux/script_ast_builder.cc | 184 +++ .../engines/script/redux/script_ast_builder.h | 58 + .../engines/script/redux/script_compiler.cc | 263 ++++ .../engines/script/redux/script_compiler.h | 63 + .../script/redux/script_compiler_tests.cc | 204 +++ .../engines/script/redux/script_engine.cc | 167 +++ .../script/redux/script_engine_tests.cc | 92 ++ .../redux/engines/script/redux/script_env.cc | 502 +++++++ redux/redux/engines/script/redux/script_env.h | 148 +++ .../engines/script/redux/script_env_tests.cc | 276 ++++ .../engines/script/redux/script_frame.cc | 48 + .../redux/engines/script/redux/script_frame.h | 79 ++ .../script/redux/script_frame_context.h | 110 ++ .../engines/script/redux/script_parser.cc | 340 +++++ .../engines/script/redux/script_parser.h | 66 + .../script/redux/script_parser_tests.cc | 452 +++++++ .../engines/script/redux/script_stack.cc | 94 ++ .../redux/engines/script/redux/script_stack.h | 103 ++ .../script/redux/script_stack_tests.cc | 106 ++ .../engines/script/redux/script_tests.cc | 71 + .../redux/engines/script/redux/script_types.h | 98 ++ .../engines/script/redux/script_value.cc | 31 + .../redux/engines/script/redux/script_value.h | 112 ++ redux/redux/engines/script/redux/stringify.cc | 156 +++ redux/redux/engines/script/redux/testing.h | 46 + redux/redux/engines/script/script.h | 63 + .../engines/script/script_call_context.h | 64 + redux/redux/engines/script/script_engine.h | 109 ++ redux/redux/engines/text/BUILD | 44 + redux/redux/engines/text/font.cc | 106 ++ redux/redux/engines/text/font.h | 82 ++ redux/redux/engines/text/freetype2/BUILD | 21 + .../engines/text/freetype2/rasterizer.cc | 113 ++ redux/redux/engines/text/harfbuzz/BUILD | 18 + .../redux/engines/text/harfbuzz/sequencer.cc | 173 +++ redux/redux/engines/text/internal/glyph.h | 80 ++ redux/redux/engines/text/internal/locale.cc | 676 ++++++++++ redux/redux/engines/text/internal/locale.h | 41 + .../engines/text/internal/sdf_computer.cc | 443 +++++++ .../engines/text/internal/sdf_computer.h | 46 + .../engines/text/internal/text_layout.cc | 292 ++++ .../redux/engines/text/internal/text_layout.h | 66 + redux/redux/engines/text/stub/BUILD | 17 + redux/redux/engines/text/stub/breaking.cc | 28 + redux/redux/engines/text/text_engine.cc | 72 + redux/redux/engines/text/text_engine.h | 77 ++ redux/redux/engines/text/text_enums.h | 67 + redux/redux/engines/text/unibreak/BUILD | 18 + redux/redux/engines/text/unibreak/breaking.cc | 51 + redux/redux/modules/audio/BUILD | 116 ++ redux/redux/modules/audio/audio_enums.fbs | 39 + redux/redux/modules/audio/audio_reader.h | 75 ++ redux/redux/modules/audio/enums.h | 39 + redux/redux/modules/audio/opus_reader.cc | 169 +++ redux/redux/modules/audio/opus_reader.h | 94 ++ .../redux/modules/audio/opus_reader_tests.cc | 92 ++ .../modules/audio/test_data/ambisonic.wav | Bin 0 -> 1411244 bytes redux/redux/modules/audio/test_data/small.ogg | Bin 0 -> 3761 bytes .../redux/modules/audio/test_data/small.opus | Bin 0 -> 1059 bytes .../redux/modules/audio/test_data/speech.ogg | Bin 0 -> 16306 bytes .../redux/modules/audio/test_data/speech.opus | Bin 0 -> 28832 bytes .../redux/modules/audio/test_data/speech.wav | Bin 0 -> 47500 bytes redux/redux/modules/audio/vorbis_reader.cc | 167 +++ redux/redux/modules/audio/vorbis_reader.h | 96 ++ .../modules/audio/vorbis_reader_tests.cc | 92 ++ redux/redux/modules/audio/wav_reader.cc | 223 ++++ redux/redux/modules/audio/wav_reader.h | 93 ++ redux/redux/modules/audio/wav_reader_tests.cc | 92 ++ redux/redux/modules/base/BUILD | 399 ++++++ redux/redux/modules/base/archiver.h | 162 +++ redux/redux/modules/base/asset_loader.cc | 266 ++++ redux/redux/modules/base/asset_loader.h | 166 +++ .../redux/modules/base/asset_loader_tests.cc | 255 ++++ redux/redux/modules/base/async_processor.h | 236 ++++ .../modules/base/async_processor_tests.cc | 231 ++++ redux/redux/modules/base/bits.h | 128 ++ redux/redux/modules/base/bits_tests.cc | 81 ++ redux/redux/modules/base/choreographer.cc | 77 ++ redux/redux/modules/base/choreographer.h | 209 +++ .../redux/modules/base/choreographer_tests.cc | 105 ++ redux/redux/modules/base/data_buffer.h | 138 ++ redux/redux/modules/base/data_buffer_tests.cc | 197 +++ redux/redux/modules/base/data_builder.h | 110 ++ .../redux/modules/base/data_builder_tests.cc | 68 + redux/redux/modules/base/data_container.h | 130 ++ .../modules/base/data_container_tests.cc | 64 + redux/redux/modules/base/data_reader.cc | 168 +++ redux/redux/modules/base/data_reader.h | 128 ++ redux/redux/modules/base/data_reader_tests.cc | 103 ++ redux/redux/modules/base/data_table.h | 503 +++++++ redux/redux/modules/base/data_table_tests.cc | 316 +++++ redux/redux/modules/base/dependency_graph.h | 159 +++ .../modules/base/dependency_graph_tests.cc | 119 ++ redux/redux/modules/base/filepath.cc | 141 ++ redux/redux/modules/base/filepath.h | 71 + redux/redux/modules/base/filepath_tests.cc | 81 ++ redux/redux/modules/base/function_traits.h | 85 ++ .../modules/base/function_traits_tests.cc | 98 ++ redux/redux/modules/base/hash.cc | 77 ++ redux/redux/modules/base/hash.h | 132 ++ redux/redux/modules/base/hash_tests.cc | 101 ++ redux/redux/modules/base/logging.h | 23 + redux/redux/modules/base/ref_tuple.h | 229 ++++ redux/redux/modules/base/ref_tuple_tests.cc | 152 +++ redux/redux/modules/base/registry.cc | 56 + redux/redux/modules/base/registry.h | 207 +++ redux/redux/modules/base/registry_tests.cc | 148 +++ redux/redux/modules/base/resource_manager.h | 258 ++++ .../modules/base/resource_manager_tests.cc | 266 ++++ redux/redux/modules/base/serialize.h | 112 ++ redux/redux/modules/base/serialize_tests.cc | 73 + redux/redux/modules/base/serialize_traits.h | 80 ++ .../modules/base/serialize_traits_tests.cc | 79 ++ redux/redux/modules/base/static_registry.cc | 37 + redux/redux/modules/base/static_registry.h | 55 + redux/redux/modules/base/thread_safe_deque.h | 112 ++ .../modules/base/thread_safe_deque_tests.cc | 208 +++ redux/redux/modules/base/typed_ptr.h | 89 ++ redux/redux/modules/base/typed_ptr_tests.cc | 71 + redux/redux/modules/base/typeid.h | 101 ++ redux/redux/modules/codecs/BUILD | 69 + redux/redux/modules/codecs/decode_image.cc | 45 + redux/redux/modules/codecs/decode_image.h | 35 + redux/redux/modules/codecs/decode_stb.cc | 71 + redux/redux/modules/codecs/decode_stb.h | 34 + redux/redux/modules/codecs/decode_webp.cc | 81 ++ redux/redux/modules/codecs/decode_webp.h | 34 + redux/redux/modules/codecs/encode_png.cc | 103 ++ redux/redux/modules/codecs/encode_png.h | 30 + redux/redux/modules/codecs/encode_webp.cc | 50 + redux/redux/modules/codecs/encode_webp.h | 30 + redux/redux/modules/datafile/BUILD | 47 + redux/redux/modules/datafile/README.md | 27 + .../redux/modules/datafile/datafile_parser.cc | 400 ++++++ .../redux/modules/datafile/datafile_parser.h | 49 + .../modules/datafile/datafile_parser_tests.cc | 349 +++++ .../redux/modules/datafile/datafile_reader.cc | 87 ++ .../redux/modules/datafile/datafile_reader.h | 205 +++ .../modules/datafile/datafile_reader_tests.cc | 95 ++ redux/redux/modules/dispatcher/BUILD | 71 + redux/redux/modules/dispatcher/dispatcher.cc | 300 +++++ redux/redux/modules/dispatcher/dispatcher.h | 264 ++++ .../modules/dispatcher/dispatcher_tests.cc | 566 ++++++++ redux/redux/modules/dispatcher/message.cc | 92 ++ redux/redux/modules/dispatcher/message.h | 230 ++++ .../redux/modules/dispatcher/message_tests.cc | 148 +++ .../modules/dispatcher/queued_dispatcher.cc | 33 + .../modules/dispatcher/queued_dispatcher.h | 61 + .../dispatcher/queued_dispatcher_tests.cc | 275 ++++ redux/redux/modules/ecs/BUILD | 70 + redux/redux/modules/ecs/blueprint.h | 89 ++ redux/redux/modules/ecs/blueprint_factory.cc | 224 ++++ redux/redux/modules/ecs/blueprint_factory.h | 57 + .../modules/ecs/blueprint_factory_tests.cc | 91 ++ .../redux/modules/ecs/component_serializer.h | 95 ++ redux/redux/modules/ecs/entity.h | 77 ++ redux/redux/modules/ecs/entity_factory.cc | 145 ++ redux/redux/modules/ecs/entity_factory.h | 148 +++ .../redux/modules/ecs/entity_factory_tests.cc | 80 ++ redux/redux/modules/ecs/system.h | 128 ++ redux/redux/modules/flatbuffers/BUILD | 97 ++ redux/redux/modules/flatbuffers/common.fbs | 14 + redux/redux/modules/flatbuffers/common.h | 79 ++ .../redux/modules/flatbuffers/common_tests.cc | 33 + redux/redux/modules/flatbuffers/math.fbs | 77 ++ redux/redux/modules/flatbuffers/math.h | 88 ++ redux/redux/modules/flatbuffers/math_tests.cc | 135 ++ redux/redux/modules/flatbuffers/var.cc | 183 +++ redux/redux/modules/flatbuffers/var.fbs | 89 ++ redux/redux/modules/flatbuffers/var.h | 52 + redux/redux/modules/flatbuffers/var_tests.cc | 303 +++++ redux/redux/modules/graphics/BUILD | 208 +++ redux/redux/modules/graphics/camera_ops.cc | 124 ++ redux/redux/modules/graphics/camera_ops.h | 134 ++ .../modules/graphics/camera_ops_tests.cc | 39 + redux/redux/modules/graphics/color.cc | 112 ++ redux/redux/modules/graphics/color.h | 202 +++ redux/redux/modules/graphics/color_tests.cc | 255 ++++ redux/redux/modules/graphics/enums.h | 113 ++ .../redux/modules/graphics/graphics_enums.fbs | 184 +++ redux/redux/modules/graphics/image_atlaser.cc | 232 ++++ redux/redux/modules/graphics/image_atlaser.h | 129 ++ .../modules/graphics/image_atlaser_tests.cc | 98 ++ redux/redux/modules/graphics/image_data.cc | 67 + redux/redux/modules/graphics/image_data.h | 84 ++ redux/redux/modules/graphics/image_utils.cc | 147 ++ redux/redux/modules/graphics/image_utils.h | 59 + redux/redux/modules/graphics/material_data.h | 46 + redux/redux/modules/graphics/mesh_data.cc | 80 ++ redux/redux/modules/graphics/mesh_data.h | 128 ++ redux/redux/modules/graphics/texture_usage.h | 133 ++ redux/redux/modules/graphics/vertex.h | 180 +++ .../redux/modules/graphics/vertex_attribute.h | 44 + redux/redux/modules/graphics/vertex_format.cc | 106 ++ redux/redux/modules/graphics/vertex_format.h | 88 ++ .../modules/graphics/vertex_format_tests.cc | 152 +++ redux/redux/modules/graphics/vertex_layout.h | 140 ++ redux/redux/modules/graphics/vertex_tests.cc | 95 ++ redux/redux/modules/graphics/vertex_utils.h | 59 + redux/redux/modules/math/BUILD | 215 +++ redux/redux/modules/math/bounds.h | 143 ++ redux/redux/modules/math/bounds_tests.cc | 175 +++ redux/redux/modules/math/constants.h | 46 + .../redux/modules/math/detail/matrix_layout.h | 303 +++++ .../modules/math/detail/quaternion_layout.h | 69 + .../redux/modules/math/detail/vector_layout.h | 159 +++ redux/redux/modules/math/float.cc | 32 + redux/redux/modules/math/float.h | 123 ++ redux/redux/modules/math/float_tests.cc | 164 +++ redux/redux/modules/math/interpolation.cc | 65 + redux/redux/modules/math/interpolation.h | 210 +++ .../redux/modules/math/interpolation_tests.cc | 198 +++ redux/redux/modules/math/math.h | 134 ++ redux/redux/modules/math/math_tests.cc | 191 +++ redux/redux/modules/math/matrix.h | 1158 ++++++++++++++++ redux/redux/modules/math/matrix_tests.cc | 832 ++++++++++++ redux/redux/modules/math/quaternion.h | 732 ++++++++++ redux/redux/modules/math/quaternion_tests.cc | 590 +++++++++ redux/redux/modules/math/ray.h | 67 + redux/redux/modules/math/ray_tests.cc | 50 + redux/redux/modules/math/testing.h | 152 +++ redux/redux/modules/math/transform.h | 112 ++ redux/redux/modules/math/transform_tests.cc | 105 ++ redux/redux/modules/math/vector.h | 980 ++++++++++++++ redux/redux/modules/math/vector_tests.cc | 740 +++++++++++ redux/redux/modules/testing/BUILD | 19 + redux/redux/modules/testing/testing.cc | 39 + redux/redux/modules/testing/testing.h | 31 + redux/redux/modules/var/BUILD | 98 ++ redux/redux/modules/var/README.md | 22 + redux/redux/modules/var/var.cc | 159 +++ redux/redux/modules/var/var.h | 286 ++++ redux/redux/modules/var/var_array.cc | 54 + redux/redux/modules/var/var_array.h | 101 ++ redux/redux/modules/var/var_array_tests.cc | 113 ++ redux/redux/modules/var/var_convert.h | 350 +++++ redux/redux/modules/var/var_convert_tests.cc | 215 +++ redux/redux/modules/var/var_serializer.h | 71 + .../redux/modules/var/var_serializer_tests.cc | 51 + redux/redux/modules/var/var_table.cc | 49 + redux/redux/modules/var/var_table.h | 108 ++ redux/redux/modules/var/var_table_tests.cc | 113 ++ redux/redux/modules/var/var_tests.cc | 161 +++ redux/redux/systems/animation/BUILD | 33 + .../systems/animation/animation_system.cc | 143 ++ .../systems/animation/animation_system.h | 107 ++ .../systems/animation/static_register.cc | 20 + redux/redux/systems/audio/BUILD | 45 + redux/redux/systems/audio/audio_system.cc | 221 ++++ redux/redux/systems/audio/audio_system.h | 129 ++ redux/redux/systems/audio/sound_def.def | 15 + redux/redux/systems/audio/static_register.cc | 20 + redux/redux/systems/camera/BUILD | 48 + redux/redux/systems/camera/camera_def.def | 17 + redux/redux/systems/camera/camera_system.cc | 139 ++ redux/redux/systems/camera/camera_system.h | 96 ++ redux/redux/systems/camera/static_register.cc | 20 + redux/redux/systems/constraint/BUILD | 59 + .../systems/constraint/constraint_system.cc | 329 +++++ .../systems/constraint/constraint_system.h | 126 ++ .../constraint/constraint_system_tests.cc | 260 ++++ redux/redux/systems/constraint/events.h | 70 + .../systems/constraint/static_register.cc | 20 + redux/redux/systems/datastore/BUILD | 51 + .../redux/systems/datastore/datastore_def.def | 7 + .../systems/datastore/datastore_system.cc | 69 + .../systems/datastore/datastore_system.h | 66 + .../datastore/datastore_system_tests.cc | 116 ++ .../systems/datastore/static_register.cc | 20 + redux/redux/systems/dispatcher/BUILD | 40 + .../systems/dispatcher/dispatcher_system.cc | 198 +++ .../systems/dispatcher/dispatcher_system.h | 271 ++++ .../dispatcher/dispatcher_system_tests.cc | 430 ++++++ .../systems/dispatcher/static_register.cc | 20 + redux/redux/systems/light/BUILD | 41 + redux/redux/systems/light/light_def.def | 49 + redux/redux/systems/light/light_system.cc | 283 ++++ redux/redux/systems/light/light_system.h | 125 ++ redux/redux/systems/light/static_register.cc | 18 + redux/redux/systems/model/BUILD | 59 + redux/redux/systems/model/model_asset.cc | 313 +++++ redux/redux/systems/model/model_asset.h | 133 ++ redux/redux/systems/model/model_def.def | 6 + redux/redux/systems/model/model_system.cc | 265 ++++ redux/redux/systems/model/model_system.h | 82 ++ redux/redux/systems/model/static_register.cc | 18 + redux/redux/systems/name/BUILD | 55 + redux/redux/systems/name/name_def.def | 7 + redux/redux/systems/name/name_system.cc | 146 ++ redux/redux/systems/name/name_system.h | 79 ++ redux/redux/systems/name/name_system_tests.cc | 132 ++ redux/redux/systems/name/static_register.cc | 20 + redux/redux/systems/physics/BUILD | 64 + redux/redux/systems/physics/events.h | 55 + redux/redux/systems/physics/physics_system.cc | 244 ++++ redux/redux/systems/physics/physics_system.h | 106 ++ .../redux/systems/physics/rigid_body_def.def | 19 + .../redux/systems/physics/static_register.cc | 20 + redux/redux/systems/physics/trigger_def.def | 13 + redux/redux/systems/render/BUILD | 42 + redux/redux/systems/render/render_def.def | 6 + redux/redux/systems/render/render_system.cc | 298 +++++ redux/redux/systems/render/render_system.h | 242 ++++ redux/redux/systems/render/static_register.cc | 18 + redux/redux/systems/rig/BUILD | 31 + redux/redux/systems/rig/rig_system.cc | 89 ++ redux/redux/systems/rig/rig_system.h | 77 ++ redux/redux/systems/rig/static_register.cc | 20 + redux/redux/systems/script/BUILD | 53 + redux/redux/systems/script/script_def.def | 41 + redux/redux/systems/script/script_system.cc | 168 +++ redux/redux/systems/script/script_system.h | 83 ++ .../systems/script/script_system_tests.cc | 182 +++ redux/redux/systems/script/static_register.cc | 20 + redux/redux/systems/shape/BUILD | 73 + .../redux/systems/shape/box_shape_generator.h | 208 +++ .../shape/box_shape_generator_tests.cc | 128 ++ redux/redux/systems/shape/shape_builder.h | 92 ++ redux/redux/systems/shape/shape_def.def | 21 + redux/redux/systems/shape/shape_system.cc | 103 ++ redux/redux/systems/shape/shape_system.h | 49 + .../systems/shape/sphere_shape_generator.h | 164 +++ .../shape/sphere_shape_generator_tests.cc | 204 +++ redux/redux/systems/shape/static_register.cc | 18 + redux/redux/systems/text/BUILD | 59 + redux/redux/systems/text/static_register.cc | 18 + redux/redux/systems/text/text_def.def | 37 + redux/redux/systems/text/text_system.cc | 208 +++ redux/redux/systems/text/text_system.h | 103 ++ redux/redux/systems/transform/BUILD | 56 + .../systems/transform/static_register.cc | 20 + .../redux/systems/transform/transform_def.def | 16 + .../systems/transform/transform_system.cc | 247 ++++ .../systems/transform/transform_system.h | 148 +++ .../transform/transform_system_tests.cc | 148 +++ redux/redux/systems/tween/BUILD | 44 + redux/redux/systems/tween/static_register.cc | 20 + redux/redux/systems/tween/tween_system.cc | 389 ++++++ redux/redux/systems/tween/tween_system.h | 202 +++ .../redux/systems/tween/tween_system_tests.cc | 487 +++++++ redux/redux/tools/BUILD | 2 + redux/redux/tools/anim_pipeline/BUILD | 110 ++ redux/redux/tools/anim_pipeline/README.md | 9 + redux/redux/tools/anim_pipeline/anim_curve.cc | 280 ++++ redux/redux/tools/anim_pipeline/anim_curve.h | 114 ++ .../tools/anim_pipeline/anim_pipeline.cc | 59 + .../redux/tools/anim_pipeline/anim_pipeline.h | 59 + redux/redux/tools/anim_pipeline/animation.cc | 156 +++ redux/redux/tools/anim_pipeline/animation.h | 111 ++ redux/redux/tools/anim_pipeline/export.cc | 180 +++ redux/redux/tools/anim_pipeline/export.h | 32 + .../redux/tools/anim_pipeline/import_asset.cc | 226 ++++ redux/redux/tools/anim_pipeline/import_fbx.cc | 183 +++ .../tools/anim_pipeline/import_options.h | 59 + redux/redux/tools/anim_pipeline/main.cc | 106 ++ redux/redux/tools/anim_pipeline/tolerances.h | 58 + redux/redux/tools/build_anim.bzl | 64 + redux/redux/tools/build_model.bzl | 64 + redux/redux/tools/build_shader.bzl | 52 + redux/redux/tools/common/BUILD | 171 +++ redux/redux/tools/common/README.md | 4 + redux/redux/tools/common/assimp_utils.cc | 299 +++++ redux/redux/tools/common/assimp_utils.h | 102 ++ redux/redux/tools/common/axis_system.fbs | 33 + redux/redux/tools/common/axis_system.h | 75 ++ redux/redux/tools/common/fbx_utils.cc | 447 +++++++ redux/redux/tools/common/fbx_utils.h | 115 ++ redux/redux/tools/common/file_utils.cc | 132 ++ redux/redux/tools/common/file_utils.h | 58 + redux/redux/tools/common/file_utils_tests.cc | 115 ++ redux/redux/tools/common/flatbuffer_utils.cc | 54 + redux/redux/tools/common/flatbuffer_utils.h | 47 + .../tools/common/flatbuffer_utils_tests.cc | 65 + redux/redux/tools/common/json_utils.h | 114 ++ redux/redux/tools/common/json_utils_tests.cc | 96 ++ redux/redux/tools/common/jsonnet_utils.cc | 79 ++ redux/redux/tools/common/jsonnet_utils.h | 35 + .../redux/tools/common/jsonnet_utils_tests.cc | 49 + redux/redux/tools/common/log_utils.cc | 44 + redux/redux/tools/common/log_utils.h | 184 +++ redux/redux/tools/common/test_data/hello.txt | 1 + redux/redux/tools/common/test_data/schema.fbs | 7 + .../tools/common/test_data/schema_include.fbs | 5 + .../redux/tools/common/test_data/sum.jsonnet | 3 + redux/redux/tools/def_cc_library.bzl | 52 + redux/redux/tools/def_code_generator/BUILD | 56 + .../tools/def_code_generator/code_builder.cc | 72 + .../tools/def_code_generator/code_builder.h | 68 + .../tools/def_code_generator/def_document.h | 36 + .../tools/def_code_generator/generate_code.cc | 221 ++++ .../tools/def_code_generator/generate_code.h | 31 + redux/redux/tools/def_code_generator/main.cc | 58 + .../tools/def_code_generator/metadata_types.h | 60 + .../def_code_generator/parse_def_file.cc | 376 ++++++ .../tools/def_code_generator/parse_def_file.h | 53 + .../parse_def_file_tests.cc | 103 ++ redux/redux/tools/env_repository.bzl | 21 + redux/redux/tools/flatbuffer_cc_library.bzl | 122 ++ redux/redux/tools/model_pipeline/BUILD | 155 +++ redux/redux/tools/model_pipeline/README.md | 8 + redux/redux/tools/model_pipeline/bone.h | 44 + redux/redux/tools/model_pipeline/config.fbs | 137 ++ redux/redux/tools/model_pipeline/drawable.h | 43 + redux/redux/tools/model_pipeline/export.cc | 530 ++++++++ redux/redux/tools/model_pipeline/export.h | 34 + .../tools/model_pipeline/import_asset.cc | 482 +++++++ .../redux/tools/model_pipeline/import_fbx.cc | 638 +++++++++ redux/redux/tools/model_pipeline/main.cc | 154 +++ redux/redux/tools/model_pipeline/material.h | 41 + redux/redux/tools/model_pipeline/model.cc | 364 +++++ redux/redux/tools/model_pipeline/model.h | 105 ++ .../tools/model_pipeline/model_pipeline.cc | 97 ++ .../tools/model_pipeline/model_pipeline.h | 65 + .../redux/tools/model_pipeline/texture_info.h | 53 + .../tools/model_pipeline/texture_locator.cc | 238 ++++ .../tools/model_pipeline/texture_locator.h | 70 + redux/redux/tools/model_pipeline/util.cc | 184 +++ redux/redux/tools/model_pipeline/util.h | 63 + redux/redux/tools/model_pipeline/vertex.cc | 123 ++ redux/redux/tools/model_pipeline/vertex.h | 139 ++ redux/redux/tools/shader_pipeline/BUILD | 44 + redux/redux/tools/shader_pipeline/README.md | 6 + redux/redux/tools/shader_pipeline/main.cc | 64 + .../tools/shader_pipeline/shader_pipeline.cc | 277 ++++ .../tools/shader_pipeline/shader_pipeline.h | 125 ++ redux/redux/tools/texture_pipeline/BUILD | 35 + redux/redux/tools/texture_pipeline/README.md | 9 + .../texture_pipeline/generate_mipmaps.cc | 81 ++ .../tools/texture_pipeline/generate_mipmaps.h | 32 + redux/redux/tools/texture_pipeline/main.cc | 86 ++ redux/third_party/filament/BUILD | 6 + .../third_party/filament/generate_shaders.bzl | 30 + .../filament/licenses/licenses.inc | 29 + redux/third_party/gl/BUILD | 10 + 673 files changed, 82675 insertions(+) create mode 100644 redux/.bazelrc create mode 100644 redux/WORKSPACE create mode 100644 redux/external/BUILD.assimp create mode 100644 redux/external/BUILD.bullet create mode 100644 redux/external/BUILD.eigen create mode 100644 redux/external/BUILD.filament create mode 100644 redux/external/BUILD.fmtlib create mode 100644 redux/external/BUILD.freetype create mode 100644 redux/external/BUILD.harfbuzz create mode 100644 redux/external/BUILD.libfbx create mode 100644 redux/external/BUILD.libogg create mode 100644 redux/external/BUILD.libopus create mode 100644 redux/external/BUILD.libopusfile create mode 100644 redux/external/BUILD.libpng create mode 100644 redux/external/BUILD.libunibreak create mode 100644 redux/external/BUILD.libvorbis create mode 100644 redux/external/BUILD.libwebp create mode 100644 redux/external/BUILD.magic_enum create mode 100644 redux/external/BUILD.pffft create mode 100644 redux/external/BUILD.rapidjson create mode 100644 redux/external/BUILD.resonance create mode 100644 redux/external/BUILD.sdl2 create mode 100644 redux/external/BUILD.stblib create mode 100644 redux/external/BUILD.utfcpp create mode 100644 redux/external/BUILD.vectorial create mode 100644 redux/external/BUILD.zlib create mode 100644 redux/external/assimp.patch create mode 100644 redux/external/filament.patch create mode 100644 redux/external/libogg.patch create mode 100644 redux/external/libpng.patch create mode 100644 redux/external/stblib.patch create mode 100644 redux/redux/BUILD create mode 100644 redux/redux/data/asset_defs/BUILD create mode 100644 redux/redux/data/asset_defs/anim_asset_def.fbs create mode 100644 redux/redux/data/asset_defs/model_asset_def.fbs create mode 100644 redux/redux/data/asset_defs/shader_asset_def.fbs create mode 100644 redux/redux/data/asset_defs/texture_asset_def.fbs create mode 100644 redux/redux/engines/animation/BUILD create mode 100644 redux/redux/engines/animation/README.md create mode 100644 redux/redux/engines/animation/animation_clip.cc create mode 100644 redux/redux/engines/animation/animation_clip.h create mode 100644 redux/redux/engines/animation/animation_engine.cc create mode 100644 redux/redux/engines/animation/animation_engine.h create mode 100644 redux/redux/engines/animation/animation_playback.h create mode 100644 redux/redux/engines/animation/common.h create mode 100644 redux/redux/engines/animation/motivator/motivator.cc create mode 100644 redux/redux/engines/animation/motivator/motivator.h create mode 100644 redux/redux/engines/animation/motivator/rig_motivator.cc create mode 100644 redux/redux/engines/animation/motivator/rig_motivator.h create mode 100644 redux/redux/engines/animation/motivator/spline_motivator.cc create mode 100644 redux/redux/engines/animation/motivator/spline_motivator.h create mode 100644 redux/redux/engines/animation/motivator/transform_motivator.cc create mode 100644 redux/redux/engines/animation/motivator/transform_motivator.h create mode 100644 redux/redux/engines/animation/processor/anim_processor.cc create mode 100644 redux/redux/engines/animation/processor/anim_processor.h create mode 100644 redux/redux/engines/animation/processor/index_allocator.h create mode 100755 redux/redux/engines/animation/processor/rig_processor.cc create mode 100644 redux/redux/engines/animation/processor/rig_processor.h create mode 100644 redux/redux/engines/animation/processor/spline_processor.cc create mode 100644 redux/redux/engines/animation/processor/spline_processor.h create mode 100644 redux/redux/engines/animation/processor/transform_processor.cc create mode 100644 redux/redux/engines/animation/processor/transform_processor.h create mode 100644 redux/redux/engines/animation/spline/BUILD create mode 100644 redux/redux/engines/animation/spline/bulk_spline_evaluator.cc create mode 100644 redux/redux/engines/animation/spline/bulk_spline_evaluator.h create mode 100644 redux/redux/engines/animation/spline/bulk_spline_evaluator_neon.s create mode 100644 redux/redux/engines/animation/spline/bulk_spline_evaluator_tests.cc create mode 100644 redux/redux/engines/animation/spline/compact_spline.cc create mode 100644 redux/redux/engines/animation/spline/compact_spline.h create mode 100644 redux/redux/engines/animation/spline/compact_spline_node.h create mode 100644 redux/redux/engines/animation/spline/compact_spline_tests.cc create mode 100644 redux/redux/engines/animation/spline/cubic_curve.cc create mode 100644 redux/redux/engines/animation/spline/cubic_curve.h create mode 100644 redux/redux/engines/animation/spline/cubic_curve_tests.cc create mode 100644 redux/redux/engines/animation/spline/dual_cubic.cc create mode 100644 redux/redux/engines/animation/spline/dual_cubic.h create mode 100644 redux/redux/engines/animation/spline/quadratic_curve.cc create mode 100644 redux/redux/engines/animation/spline/quadratic_curve.h create mode 100644 redux/redux/engines/animation/spline/quadratic_curve_tests.cc create mode 100644 redux/redux/engines/audio/BUILD create mode 100644 redux/redux/engines/audio/audio_asset.h create mode 100644 redux/redux/engines/audio/audio_engine.h create mode 100644 redux/redux/engines/audio/resonance/BUILD create mode 100644 redux/redux/engines/audio/resonance/audio_asset_manager.cc create mode 100644 redux/redux/engines/audio/resonance/audio_asset_manager.h create mode 100644 redux/redux/engines/audio/resonance/audio_asset_manager_tests.cc create mode 100644 redux/redux/engines/audio/resonance/audio_asset_stream.cc create mode 100644 redux/redux/engines/audio/resonance/audio_asset_stream.h create mode 100644 redux/redux/engines/audio/resonance/audio_asset_stream_tests.cc create mode 100644 redux/redux/engines/audio/resonance/audio_planar_data.cc create mode 100644 redux/redux/engines/audio/resonance/audio_planar_data.h create mode 100644 redux/redux/engines/audio/resonance/audio_planar_data_tests.cc create mode 100644 redux/redux/engines/audio/resonance/audio_source_stream.h create mode 100644 redux/redux/engines/audio/resonance/audio_stream_manager.cc create mode 100644 redux/redux/engines/audio/resonance/audio_stream_manager.h create mode 100644 redux/redux/engines/audio/resonance/audio_stream_renderer.cc create mode 100644 redux/redux/engines/audio/resonance/audio_stream_renderer.h create mode 100644 redux/redux/engines/audio/resonance/resonance_audio_asset.cc create mode 100644 redux/redux/engines/audio/resonance/resonance_audio_asset.h create mode 100644 redux/redux/engines/audio/resonance/resonance_audio_engine.cc create mode 100644 redux/redux/engines/audio/resonance/resonance_audio_engine.h create mode 100644 redux/redux/engines/audio/resonance/resonance_sound.cc create mode 100644 redux/redux/engines/audio/resonance/resonance_sound.h create mode 100644 redux/redux/engines/audio/resonance/resonance_utils.cc create mode 100644 redux/redux/engines/audio/resonance/resonance_utils.h create mode 100644 redux/redux/engines/audio/sound.h create mode 100644 redux/redux/engines/audio/sound_room.h create mode 100644 redux/redux/engines/physics/BUILD create mode 100644 redux/redux/engines/physics/bullet/BUILD create mode 100644 redux/redux/engines/physics/bullet/bullet_collision_shape.cc create mode 100644 redux/redux/engines/physics/bullet/bullet_collision_shape.h create mode 100644 redux/redux/engines/physics/bullet/bullet_physics_engine.cc create mode 100644 redux/redux/engines/physics/bullet/bullet_physics_engine.h create mode 100644 redux/redux/engines/physics/bullet/bullet_rigid_body.cc create mode 100644 redux/redux/engines/physics/bullet/bullet_rigid_body.h create mode 100644 redux/redux/engines/physics/bullet/bullet_trigger_volume.cc create mode 100644 redux/redux/engines/physics/bullet/bullet_trigger_volume.h create mode 100644 redux/redux/engines/physics/bullet/bullet_utils.h create mode 100644 redux/redux/engines/physics/bullet/thunks.cc create mode 100644 redux/redux/engines/physics/collision_data.cc create mode 100644 redux/redux/engines/physics/collision_data.h create mode 100644 redux/redux/engines/physics/collision_shape.h create mode 100644 redux/redux/engines/physics/enums.h create mode 100644 redux/redux/engines/physics/physics_engine.h create mode 100644 redux/redux/engines/physics/physics_enums.fbs create mode 100644 redux/redux/engines/physics/rigid_body.h create mode 100644 redux/redux/engines/physics/thunks/BUILD create mode 100644 redux/redux/engines/physics/thunks/README.md create mode 100644 redux/redux/engines/physics/thunks/physics_engine.h create mode 100644 redux/redux/engines/physics/thunks/rigid_body.h create mode 100644 redux/redux/engines/physics/thunks/trigger_volume.h create mode 100644 redux/redux/engines/physics/trigger_volume.h create mode 100644 redux/redux/engines/platform/BUILD create mode 100644 redux/redux/engines/platform/buffered_state.h create mode 100644 redux/redux/engines/platform/device_manager.cc create mode 100644 redux/redux/engines/platform/device_manager.h create mode 100644 redux/redux/engines/platform/device_profiles.h create mode 100644 redux/redux/engines/platform/display.cc create mode 100644 redux/redux/engines/platform/display.h create mode 100644 redux/redux/engines/platform/keyboard.cc create mode 100644 redux/redux/engines/platform/keyboard.h create mode 100644 redux/redux/engines/platform/keycodes.cc create mode 100644 redux/redux/engines/platform/keycodes.h create mode 100644 redux/redux/engines/platform/mainloop.cc create mode 100644 redux/redux/engines/platform/mainloop.h create mode 100644 redux/redux/engines/platform/mouse.cc create mode 100644 redux/redux/engines/platform/mouse.h create mode 100644 redux/redux/engines/platform/sdl2/BUILD create mode 100644 redux/redux/engines/platform/sdl2/get_native_window_osx.mm create mode 100644 redux/redux/engines/platform/sdl2/sdl2_display.cc create mode 100644 redux/redux/engines/platform/sdl2/sdl2_display.h create mode 100644 redux/redux/engines/platform/sdl2/sdl2_event_handler.h create mode 100644 redux/redux/engines/platform/sdl2/sdl2_keyboard.cc create mode 100644 redux/redux/engines/platform/sdl2/sdl2_keyboard.h create mode 100644 redux/redux/engines/platform/sdl2/sdl2_mainloop.cc create mode 100644 redux/redux/engines/platform/sdl2/sdl2_mainloop.h create mode 100644 redux/redux/engines/platform/sdl2/sdl2_mouse.cc create mode 100644 redux/redux/engines/platform/sdl2/sdl2_mouse.h create mode 100644 redux/redux/engines/platform/sdl2/sdl2_speaker.cc create mode 100644 redux/redux/engines/platform/sdl2/sdl2_speaker.h create mode 100644 redux/redux/engines/platform/speaker.cc create mode 100644 redux/redux/engines/platform/speaker.h create mode 100644 redux/redux/engines/platform/virtual_device.cc create mode 100644 redux/redux/engines/platform/virtual_device.h create mode 100644 redux/redux/engines/render/BUILD create mode 100644 redux/redux/engines/render/filament/BUILD create mode 100644 redux/redux/engines/render/filament/filament_indirect_light.cc create mode 100644 redux/redux/engines/render/filament/filament_indirect_light.h create mode 100644 redux/redux/engines/render/filament/filament_light.cc create mode 100644 redux/redux/engines/render/filament/filament_light.h create mode 100644 redux/redux/engines/render/filament/filament_mesh.cc create mode 100644 redux/redux/engines/render/filament/filament_mesh.h create mode 100644 redux/redux/engines/render/filament/filament_render_engine.cc create mode 100644 redux/redux/engines/render/filament/filament_render_engine.h create mode 100644 redux/redux/engines/render/filament/filament_render_layer.cc create mode 100644 redux/redux/engines/render/filament/filament_render_layer.h create mode 100644 redux/redux/engines/render/filament/filament_render_scene.cc create mode 100644 redux/redux/engines/render/filament/filament_render_scene.h create mode 100644 redux/redux/engines/render/filament/filament_render_target.cc create mode 100644 redux/redux/engines/render/filament/filament_render_target.h create mode 100644 redux/redux/engines/render/filament/filament_renderable.cc create mode 100644 redux/redux/engines/render/filament/filament_renderable.h create mode 100644 redux/redux/engines/render/filament/filament_shader.cc create mode 100644 redux/redux/engines/render/filament/filament_shader.h create mode 100644 redux/redux/engines/render/filament/filament_texture.cc create mode 100644 redux/redux/engines/render/filament/filament_texture.h create mode 100644 redux/redux/engines/render/filament/filament_utils.h create mode 100644 redux/redux/engines/render/filament/mesh_factory.cc create mode 100644 redux/redux/engines/render/filament/render_target_factory.cc create mode 100644 redux/redux/engines/render/filament/shader_factory.cc create mode 100644 redux/redux/engines/render/filament/texture_factory.cc create mode 100644 redux/redux/engines/render/filament/thunks.cc create mode 100644 redux/redux/engines/render/indirect_light.h create mode 100644 redux/redux/engines/render/light.h create mode 100644 redux/redux/engines/render/mesh.h create mode 100644 redux/redux/engines/render/mesh_factory.h create mode 100644 redux/redux/engines/render/render_engine.h create mode 100644 redux/redux/engines/render/render_layer.h create mode 100644 redux/redux/engines/render/render_scene.h create mode 100644 redux/redux/engines/render/render_target.h create mode 100644 redux/redux/engines/render/render_target_factory.h create mode 100644 redux/redux/engines/render/renderable.h create mode 100644 redux/redux/engines/render/shader.h create mode 100644 redux/redux/engines/render/shader_factory.h create mode 100644 redux/redux/engines/render/texture.h create mode 100644 redux/redux/engines/render/texture_factory.h create mode 100644 redux/redux/engines/render/thunks/BUILD create mode 100644 redux/redux/engines/render/thunks/README.md create mode 100644 redux/redux/engines/render/thunks/indirect_light.h create mode 100644 redux/redux/engines/render/thunks/light.h create mode 100644 redux/redux/engines/render/thunks/mesh.h create mode 100644 redux/redux/engines/render/thunks/render_engine.h create mode 100644 redux/redux/engines/render/thunks/render_layer.h create mode 100644 redux/redux/engines/render/thunks/render_scene.h create mode 100644 redux/redux/engines/render/thunks/render_target.h create mode 100644 redux/redux/engines/render/thunks/renderable.h create mode 100644 redux/redux/engines/render/thunks/texture.h create mode 100644 redux/redux/engines/script/BUILD create mode 100644 redux/redux/engines/script/call_native_function.h create mode 100644 redux/redux/engines/script/call_native_function_tests.cc create mode 100644 redux/redux/engines/script/function_binder.cc create mode 100644 redux/redux/engines/script/function_binder.h create mode 100644 redux/redux/engines/script/redux/BUILD create mode 100644 redux/redux/engines/script/redux/README.md create mode 100644 redux/redux/engines/script/redux/functions/array.h create mode 100644 redux/redux/engines/script/redux/functions/array_tests.cc create mode 100644 redux/redux/engines/script/redux/functions/cond.h create mode 100644 redux/redux/engines/script/redux/functions/cond_tests.cc create mode 100644 redux/redux/engines/script/redux/functions/hash.h create mode 100644 redux/redux/engines/script/redux/functions/map.h create mode 100644 redux/redux/engines/script/redux/functions/map_tests.cc create mode 100644 redux/redux/engines/script/redux/functions/math.h create mode 100644 redux/redux/engines/script/redux/functions/math_tests.cc create mode 100644 redux/redux/engines/script/redux/functions/message.h create mode 100644 redux/redux/engines/script/redux/functions/message_tests.cc create mode 100644 redux/redux/engines/script/redux/functions/operators.h create mode 100644 redux/redux/engines/script/redux/functions/operators_tests.cc create mode 100644 redux/redux/engines/script/redux/functions/typeof.h create mode 100644 redux/redux/engines/script/redux/functions/typeof_tests.cc create mode 100644 redux/redux/engines/script/redux/script_ast_builder.cc create mode 100644 redux/redux/engines/script/redux/script_ast_builder.h create mode 100644 redux/redux/engines/script/redux/script_compiler.cc create mode 100644 redux/redux/engines/script/redux/script_compiler.h create mode 100644 redux/redux/engines/script/redux/script_compiler_tests.cc create mode 100644 redux/redux/engines/script/redux/script_engine.cc create mode 100644 redux/redux/engines/script/redux/script_engine_tests.cc create mode 100644 redux/redux/engines/script/redux/script_env.cc create mode 100644 redux/redux/engines/script/redux/script_env.h create mode 100644 redux/redux/engines/script/redux/script_env_tests.cc create mode 100644 redux/redux/engines/script/redux/script_frame.cc create mode 100644 redux/redux/engines/script/redux/script_frame.h create mode 100644 redux/redux/engines/script/redux/script_frame_context.h create mode 100644 redux/redux/engines/script/redux/script_parser.cc create mode 100644 redux/redux/engines/script/redux/script_parser.h create mode 100644 redux/redux/engines/script/redux/script_parser_tests.cc create mode 100644 redux/redux/engines/script/redux/script_stack.cc create mode 100644 redux/redux/engines/script/redux/script_stack.h create mode 100644 redux/redux/engines/script/redux/script_stack_tests.cc create mode 100644 redux/redux/engines/script/redux/script_tests.cc create mode 100644 redux/redux/engines/script/redux/script_types.h create mode 100644 redux/redux/engines/script/redux/script_value.cc create mode 100644 redux/redux/engines/script/redux/script_value.h create mode 100644 redux/redux/engines/script/redux/stringify.cc create mode 100644 redux/redux/engines/script/redux/testing.h create mode 100644 redux/redux/engines/script/script.h create mode 100644 redux/redux/engines/script/script_call_context.h create mode 100644 redux/redux/engines/script/script_engine.h create mode 100644 redux/redux/engines/text/BUILD create mode 100644 redux/redux/engines/text/font.cc create mode 100644 redux/redux/engines/text/font.h create mode 100644 redux/redux/engines/text/freetype2/BUILD create mode 100644 redux/redux/engines/text/freetype2/rasterizer.cc create mode 100644 redux/redux/engines/text/harfbuzz/BUILD create mode 100644 redux/redux/engines/text/harfbuzz/sequencer.cc create mode 100644 redux/redux/engines/text/internal/glyph.h create mode 100644 redux/redux/engines/text/internal/locale.cc create mode 100644 redux/redux/engines/text/internal/locale.h create mode 100644 redux/redux/engines/text/internal/sdf_computer.cc create mode 100644 redux/redux/engines/text/internal/sdf_computer.h create mode 100644 redux/redux/engines/text/internal/text_layout.cc create mode 100644 redux/redux/engines/text/internal/text_layout.h create mode 100644 redux/redux/engines/text/stub/BUILD create mode 100644 redux/redux/engines/text/stub/breaking.cc create mode 100644 redux/redux/engines/text/text_engine.cc create mode 100644 redux/redux/engines/text/text_engine.h create mode 100644 redux/redux/engines/text/text_enums.h create mode 100644 redux/redux/engines/text/unibreak/BUILD create mode 100644 redux/redux/engines/text/unibreak/breaking.cc create mode 100644 redux/redux/modules/audio/BUILD create mode 100644 redux/redux/modules/audio/audio_enums.fbs create mode 100644 redux/redux/modules/audio/audio_reader.h create mode 100644 redux/redux/modules/audio/enums.h create mode 100644 redux/redux/modules/audio/opus_reader.cc create mode 100644 redux/redux/modules/audio/opus_reader.h create mode 100644 redux/redux/modules/audio/opus_reader_tests.cc create mode 100644 redux/redux/modules/audio/test_data/ambisonic.wav create mode 100644 redux/redux/modules/audio/test_data/small.ogg create mode 100644 redux/redux/modules/audio/test_data/small.opus create mode 100644 redux/redux/modules/audio/test_data/speech.ogg create mode 100644 redux/redux/modules/audio/test_data/speech.opus create mode 100644 redux/redux/modules/audio/test_data/speech.wav create mode 100644 redux/redux/modules/audio/vorbis_reader.cc create mode 100644 redux/redux/modules/audio/vorbis_reader.h create mode 100644 redux/redux/modules/audio/vorbis_reader_tests.cc create mode 100644 redux/redux/modules/audio/wav_reader.cc create mode 100644 redux/redux/modules/audio/wav_reader.h create mode 100644 redux/redux/modules/audio/wav_reader_tests.cc create mode 100644 redux/redux/modules/base/BUILD create mode 100644 redux/redux/modules/base/archiver.h create mode 100644 redux/redux/modules/base/asset_loader.cc create mode 100644 redux/redux/modules/base/asset_loader.h create mode 100644 redux/redux/modules/base/asset_loader_tests.cc create mode 100644 redux/redux/modules/base/async_processor.h create mode 100644 redux/redux/modules/base/async_processor_tests.cc create mode 100644 redux/redux/modules/base/bits.h create mode 100644 redux/redux/modules/base/bits_tests.cc create mode 100644 redux/redux/modules/base/choreographer.cc create mode 100644 redux/redux/modules/base/choreographer.h create mode 100644 redux/redux/modules/base/choreographer_tests.cc create mode 100644 redux/redux/modules/base/data_buffer.h create mode 100644 redux/redux/modules/base/data_buffer_tests.cc create mode 100644 redux/redux/modules/base/data_builder.h create mode 100644 redux/redux/modules/base/data_builder_tests.cc create mode 100644 redux/redux/modules/base/data_container.h create mode 100644 redux/redux/modules/base/data_container_tests.cc create mode 100644 redux/redux/modules/base/data_reader.cc create mode 100644 redux/redux/modules/base/data_reader.h create mode 100644 redux/redux/modules/base/data_reader_tests.cc create mode 100644 redux/redux/modules/base/data_table.h create mode 100644 redux/redux/modules/base/data_table_tests.cc create mode 100644 redux/redux/modules/base/dependency_graph.h create mode 100644 redux/redux/modules/base/dependency_graph_tests.cc create mode 100644 redux/redux/modules/base/filepath.cc create mode 100644 redux/redux/modules/base/filepath.h create mode 100644 redux/redux/modules/base/filepath_tests.cc create mode 100644 redux/redux/modules/base/function_traits.h create mode 100644 redux/redux/modules/base/function_traits_tests.cc create mode 100644 redux/redux/modules/base/hash.cc create mode 100644 redux/redux/modules/base/hash.h create mode 100644 redux/redux/modules/base/hash_tests.cc create mode 100644 redux/redux/modules/base/logging.h create mode 100644 redux/redux/modules/base/ref_tuple.h create mode 100644 redux/redux/modules/base/ref_tuple_tests.cc create mode 100644 redux/redux/modules/base/registry.cc create mode 100644 redux/redux/modules/base/registry.h create mode 100644 redux/redux/modules/base/registry_tests.cc create mode 100644 redux/redux/modules/base/resource_manager.h create mode 100644 redux/redux/modules/base/resource_manager_tests.cc create mode 100644 redux/redux/modules/base/serialize.h create mode 100644 redux/redux/modules/base/serialize_tests.cc create mode 100644 redux/redux/modules/base/serialize_traits.h create mode 100644 redux/redux/modules/base/serialize_traits_tests.cc create mode 100644 redux/redux/modules/base/static_registry.cc create mode 100644 redux/redux/modules/base/static_registry.h create mode 100644 redux/redux/modules/base/thread_safe_deque.h create mode 100644 redux/redux/modules/base/thread_safe_deque_tests.cc create mode 100644 redux/redux/modules/base/typed_ptr.h create mode 100644 redux/redux/modules/base/typed_ptr_tests.cc create mode 100644 redux/redux/modules/base/typeid.h create mode 100644 redux/redux/modules/codecs/BUILD create mode 100644 redux/redux/modules/codecs/decode_image.cc create mode 100644 redux/redux/modules/codecs/decode_image.h create mode 100644 redux/redux/modules/codecs/decode_stb.cc create mode 100644 redux/redux/modules/codecs/decode_stb.h create mode 100644 redux/redux/modules/codecs/decode_webp.cc create mode 100644 redux/redux/modules/codecs/decode_webp.h create mode 100644 redux/redux/modules/codecs/encode_png.cc create mode 100644 redux/redux/modules/codecs/encode_png.h create mode 100644 redux/redux/modules/codecs/encode_webp.cc create mode 100644 redux/redux/modules/codecs/encode_webp.h create mode 100644 redux/redux/modules/datafile/BUILD create mode 100644 redux/redux/modules/datafile/README.md create mode 100644 redux/redux/modules/datafile/datafile_parser.cc create mode 100644 redux/redux/modules/datafile/datafile_parser.h create mode 100644 redux/redux/modules/datafile/datafile_parser_tests.cc create mode 100644 redux/redux/modules/datafile/datafile_reader.cc create mode 100644 redux/redux/modules/datafile/datafile_reader.h create mode 100644 redux/redux/modules/datafile/datafile_reader_tests.cc create mode 100644 redux/redux/modules/dispatcher/BUILD create mode 100644 redux/redux/modules/dispatcher/dispatcher.cc create mode 100644 redux/redux/modules/dispatcher/dispatcher.h create mode 100644 redux/redux/modules/dispatcher/dispatcher_tests.cc create mode 100644 redux/redux/modules/dispatcher/message.cc create mode 100644 redux/redux/modules/dispatcher/message.h create mode 100644 redux/redux/modules/dispatcher/message_tests.cc create mode 100644 redux/redux/modules/dispatcher/queued_dispatcher.cc create mode 100644 redux/redux/modules/dispatcher/queued_dispatcher.h create mode 100644 redux/redux/modules/dispatcher/queued_dispatcher_tests.cc create mode 100644 redux/redux/modules/ecs/BUILD create mode 100644 redux/redux/modules/ecs/blueprint.h create mode 100644 redux/redux/modules/ecs/blueprint_factory.cc create mode 100644 redux/redux/modules/ecs/blueprint_factory.h create mode 100644 redux/redux/modules/ecs/blueprint_factory_tests.cc create mode 100644 redux/redux/modules/ecs/component_serializer.h create mode 100644 redux/redux/modules/ecs/entity.h create mode 100644 redux/redux/modules/ecs/entity_factory.cc create mode 100644 redux/redux/modules/ecs/entity_factory.h create mode 100644 redux/redux/modules/ecs/entity_factory_tests.cc create mode 100644 redux/redux/modules/ecs/system.h create mode 100644 redux/redux/modules/flatbuffers/BUILD create mode 100644 redux/redux/modules/flatbuffers/common.fbs create mode 100644 redux/redux/modules/flatbuffers/common.h create mode 100644 redux/redux/modules/flatbuffers/common_tests.cc create mode 100644 redux/redux/modules/flatbuffers/math.fbs create mode 100644 redux/redux/modules/flatbuffers/math.h create mode 100644 redux/redux/modules/flatbuffers/math_tests.cc create mode 100644 redux/redux/modules/flatbuffers/var.cc create mode 100644 redux/redux/modules/flatbuffers/var.fbs create mode 100644 redux/redux/modules/flatbuffers/var.h create mode 100644 redux/redux/modules/flatbuffers/var_tests.cc create mode 100644 redux/redux/modules/graphics/BUILD create mode 100644 redux/redux/modules/graphics/camera_ops.cc create mode 100644 redux/redux/modules/graphics/camera_ops.h create mode 100644 redux/redux/modules/graphics/camera_ops_tests.cc create mode 100644 redux/redux/modules/graphics/color.cc create mode 100644 redux/redux/modules/graphics/color.h create mode 100644 redux/redux/modules/graphics/color_tests.cc create mode 100644 redux/redux/modules/graphics/enums.h create mode 100644 redux/redux/modules/graphics/graphics_enums.fbs create mode 100644 redux/redux/modules/graphics/image_atlaser.cc create mode 100644 redux/redux/modules/graphics/image_atlaser.h create mode 100644 redux/redux/modules/graphics/image_atlaser_tests.cc create mode 100644 redux/redux/modules/graphics/image_data.cc create mode 100644 redux/redux/modules/graphics/image_data.h create mode 100644 redux/redux/modules/graphics/image_utils.cc create mode 100644 redux/redux/modules/graphics/image_utils.h create mode 100644 redux/redux/modules/graphics/material_data.h create mode 100644 redux/redux/modules/graphics/mesh_data.cc create mode 100644 redux/redux/modules/graphics/mesh_data.h create mode 100644 redux/redux/modules/graphics/texture_usage.h create mode 100644 redux/redux/modules/graphics/vertex.h create mode 100644 redux/redux/modules/graphics/vertex_attribute.h create mode 100644 redux/redux/modules/graphics/vertex_format.cc create mode 100644 redux/redux/modules/graphics/vertex_format.h create mode 100644 redux/redux/modules/graphics/vertex_format_tests.cc create mode 100644 redux/redux/modules/graphics/vertex_layout.h create mode 100644 redux/redux/modules/graphics/vertex_tests.cc create mode 100644 redux/redux/modules/graphics/vertex_utils.h create mode 100644 redux/redux/modules/math/BUILD create mode 100644 redux/redux/modules/math/bounds.h create mode 100644 redux/redux/modules/math/bounds_tests.cc create mode 100644 redux/redux/modules/math/constants.h create mode 100644 redux/redux/modules/math/detail/matrix_layout.h create mode 100644 redux/redux/modules/math/detail/quaternion_layout.h create mode 100644 redux/redux/modules/math/detail/vector_layout.h create mode 100644 redux/redux/modules/math/float.cc create mode 100644 redux/redux/modules/math/float.h create mode 100644 redux/redux/modules/math/float_tests.cc create mode 100644 redux/redux/modules/math/interpolation.cc create mode 100644 redux/redux/modules/math/interpolation.h create mode 100644 redux/redux/modules/math/interpolation_tests.cc create mode 100644 redux/redux/modules/math/math.h create mode 100644 redux/redux/modules/math/math_tests.cc create mode 100644 redux/redux/modules/math/matrix.h create mode 100644 redux/redux/modules/math/matrix_tests.cc create mode 100644 redux/redux/modules/math/quaternion.h create mode 100644 redux/redux/modules/math/quaternion_tests.cc create mode 100644 redux/redux/modules/math/ray.h create mode 100644 redux/redux/modules/math/ray_tests.cc create mode 100644 redux/redux/modules/math/testing.h create mode 100644 redux/redux/modules/math/transform.h create mode 100644 redux/redux/modules/math/transform_tests.cc create mode 100644 redux/redux/modules/math/vector.h create mode 100644 redux/redux/modules/math/vector_tests.cc create mode 100644 redux/redux/modules/testing/BUILD create mode 100644 redux/redux/modules/testing/testing.cc create mode 100644 redux/redux/modules/testing/testing.h create mode 100644 redux/redux/modules/var/BUILD create mode 100644 redux/redux/modules/var/README.md create mode 100644 redux/redux/modules/var/var.cc create mode 100644 redux/redux/modules/var/var.h create mode 100644 redux/redux/modules/var/var_array.cc create mode 100644 redux/redux/modules/var/var_array.h create mode 100644 redux/redux/modules/var/var_array_tests.cc create mode 100644 redux/redux/modules/var/var_convert.h create mode 100644 redux/redux/modules/var/var_convert_tests.cc create mode 100644 redux/redux/modules/var/var_serializer.h create mode 100644 redux/redux/modules/var/var_serializer_tests.cc create mode 100644 redux/redux/modules/var/var_table.cc create mode 100644 redux/redux/modules/var/var_table.h create mode 100644 redux/redux/modules/var/var_table_tests.cc create mode 100644 redux/redux/modules/var/var_tests.cc create mode 100644 redux/redux/systems/animation/BUILD create mode 100644 redux/redux/systems/animation/animation_system.cc create mode 100644 redux/redux/systems/animation/animation_system.h create mode 100644 redux/redux/systems/animation/static_register.cc create mode 100644 redux/redux/systems/audio/BUILD create mode 100644 redux/redux/systems/audio/audio_system.cc create mode 100644 redux/redux/systems/audio/audio_system.h create mode 100644 redux/redux/systems/audio/sound_def.def create mode 100644 redux/redux/systems/audio/static_register.cc create mode 100644 redux/redux/systems/camera/BUILD create mode 100644 redux/redux/systems/camera/camera_def.def create mode 100644 redux/redux/systems/camera/camera_system.cc create mode 100644 redux/redux/systems/camera/camera_system.h create mode 100644 redux/redux/systems/camera/static_register.cc create mode 100644 redux/redux/systems/constraint/BUILD create mode 100644 redux/redux/systems/constraint/constraint_system.cc create mode 100644 redux/redux/systems/constraint/constraint_system.h create mode 100644 redux/redux/systems/constraint/constraint_system_tests.cc create mode 100644 redux/redux/systems/constraint/events.h create mode 100644 redux/redux/systems/constraint/static_register.cc create mode 100644 redux/redux/systems/datastore/BUILD create mode 100644 redux/redux/systems/datastore/datastore_def.def create mode 100644 redux/redux/systems/datastore/datastore_system.cc create mode 100644 redux/redux/systems/datastore/datastore_system.h create mode 100644 redux/redux/systems/datastore/datastore_system_tests.cc create mode 100644 redux/redux/systems/datastore/static_register.cc create mode 100644 redux/redux/systems/dispatcher/BUILD create mode 100644 redux/redux/systems/dispatcher/dispatcher_system.cc create mode 100644 redux/redux/systems/dispatcher/dispatcher_system.h create mode 100644 redux/redux/systems/dispatcher/dispatcher_system_tests.cc create mode 100644 redux/redux/systems/dispatcher/static_register.cc create mode 100644 redux/redux/systems/light/BUILD create mode 100644 redux/redux/systems/light/light_def.def create mode 100644 redux/redux/systems/light/light_system.cc create mode 100644 redux/redux/systems/light/light_system.h create mode 100644 redux/redux/systems/light/static_register.cc create mode 100644 redux/redux/systems/model/BUILD create mode 100644 redux/redux/systems/model/model_asset.cc create mode 100644 redux/redux/systems/model/model_asset.h create mode 100644 redux/redux/systems/model/model_def.def create mode 100644 redux/redux/systems/model/model_system.cc create mode 100644 redux/redux/systems/model/model_system.h create mode 100644 redux/redux/systems/model/static_register.cc create mode 100644 redux/redux/systems/name/BUILD create mode 100644 redux/redux/systems/name/name_def.def create mode 100644 redux/redux/systems/name/name_system.cc create mode 100644 redux/redux/systems/name/name_system.h create mode 100644 redux/redux/systems/name/name_system_tests.cc create mode 100644 redux/redux/systems/name/static_register.cc create mode 100644 redux/redux/systems/physics/BUILD create mode 100644 redux/redux/systems/physics/events.h create mode 100644 redux/redux/systems/physics/physics_system.cc create mode 100644 redux/redux/systems/physics/physics_system.h create mode 100644 redux/redux/systems/physics/rigid_body_def.def create mode 100644 redux/redux/systems/physics/static_register.cc create mode 100644 redux/redux/systems/physics/trigger_def.def create mode 100644 redux/redux/systems/render/BUILD create mode 100644 redux/redux/systems/render/render_def.def create mode 100644 redux/redux/systems/render/render_system.cc create mode 100644 redux/redux/systems/render/render_system.h create mode 100644 redux/redux/systems/render/static_register.cc create mode 100644 redux/redux/systems/rig/BUILD create mode 100644 redux/redux/systems/rig/rig_system.cc create mode 100644 redux/redux/systems/rig/rig_system.h create mode 100644 redux/redux/systems/rig/static_register.cc create mode 100644 redux/redux/systems/script/BUILD create mode 100644 redux/redux/systems/script/script_def.def create mode 100644 redux/redux/systems/script/script_system.cc create mode 100644 redux/redux/systems/script/script_system.h create mode 100644 redux/redux/systems/script/script_system_tests.cc create mode 100644 redux/redux/systems/script/static_register.cc create mode 100644 redux/redux/systems/shape/BUILD create mode 100644 redux/redux/systems/shape/box_shape_generator.h create mode 100644 redux/redux/systems/shape/box_shape_generator_tests.cc create mode 100644 redux/redux/systems/shape/shape_builder.h create mode 100644 redux/redux/systems/shape/shape_def.def create mode 100644 redux/redux/systems/shape/shape_system.cc create mode 100644 redux/redux/systems/shape/shape_system.h create mode 100644 redux/redux/systems/shape/sphere_shape_generator.h create mode 100644 redux/redux/systems/shape/sphere_shape_generator_tests.cc create mode 100644 redux/redux/systems/shape/static_register.cc create mode 100644 redux/redux/systems/text/BUILD create mode 100644 redux/redux/systems/text/static_register.cc create mode 100644 redux/redux/systems/text/text_def.def create mode 100644 redux/redux/systems/text/text_system.cc create mode 100644 redux/redux/systems/text/text_system.h create mode 100644 redux/redux/systems/transform/BUILD create mode 100644 redux/redux/systems/transform/static_register.cc create mode 100644 redux/redux/systems/transform/transform_def.def create mode 100644 redux/redux/systems/transform/transform_system.cc create mode 100644 redux/redux/systems/transform/transform_system.h create mode 100644 redux/redux/systems/transform/transform_system_tests.cc create mode 100644 redux/redux/systems/tween/BUILD create mode 100644 redux/redux/systems/tween/static_register.cc create mode 100644 redux/redux/systems/tween/tween_system.cc create mode 100644 redux/redux/systems/tween/tween_system.h create mode 100644 redux/redux/systems/tween/tween_system_tests.cc create mode 100644 redux/redux/tools/BUILD create mode 100644 redux/redux/tools/anim_pipeline/BUILD create mode 100644 redux/redux/tools/anim_pipeline/README.md create mode 100644 redux/redux/tools/anim_pipeline/anim_curve.cc create mode 100644 redux/redux/tools/anim_pipeline/anim_curve.h create mode 100644 redux/redux/tools/anim_pipeline/anim_pipeline.cc create mode 100644 redux/redux/tools/anim_pipeline/anim_pipeline.h create mode 100644 redux/redux/tools/anim_pipeline/animation.cc create mode 100644 redux/redux/tools/anim_pipeline/animation.h create mode 100644 redux/redux/tools/anim_pipeline/export.cc create mode 100644 redux/redux/tools/anim_pipeline/export.h create mode 100644 redux/redux/tools/anim_pipeline/import_asset.cc create mode 100644 redux/redux/tools/anim_pipeline/import_fbx.cc create mode 100644 redux/redux/tools/anim_pipeline/import_options.h create mode 100644 redux/redux/tools/anim_pipeline/main.cc create mode 100644 redux/redux/tools/anim_pipeline/tolerances.h create mode 100644 redux/redux/tools/build_anim.bzl create mode 100644 redux/redux/tools/build_model.bzl create mode 100644 redux/redux/tools/build_shader.bzl create mode 100644 redux/redux/tools/common/BUILD create mode 100644 redux/redux/tools/common/README.md create mode 100644 redux/redux/tools/common/assimp_utils.cc create mode 100644 redux/redux/tools/common/assimp_utils.h create mode 100644 redux/redux/tools/common/axis_system.fbs create mode 100644 redux/redux/tools/common/axis_system.h create mode 100644 redux/redux/tools/common/fbx_utils.cc create mode 100644 redux/redux/tools/common/fbx_utils.h create mode 100644 redux/redux/tools/common/file_utils.cc create mode 100644 redux/redux/tools/common/file_utils.h create mode 100644 redux/redux/tools/common/file_utils_tests.cc create mode 100644 redux/redux/tools/common/flatbuffer_utils.cc create mode 100644 redux/redux/tools/common/flatbuffer_utils.h create mode 100644 redux/redux/tools/common/flatbuffer_utils_tests.cc create mode 100644 redux/redux/tools/common/json_utils.h create mode 100644 redux/redux/tools/common/json_utils_tests.cc create mode 100644 redux/redux/tools/common/jsonnet_utils.cc create mode 100644 redux/redux/tools/common/jsonnet_utils.h create mode 100644 redux/redux/tools/common/jsonnet_utils_tests.cc create mode 100644 redux/redux/tools/common/log_utils.cc create mode 100644 redux/redux/tools/common/log_utils.h create mode 100644 redux/redux/tools/common/test_data/hello.txt create mode 100644 redux/redux/tools/common/test_data/schema.fbs create mode 100644 redux/redux/tools/common/test_data/schema_include.fbs create mode 100644 redux/redux/tools/common/test_data/sum.jsonnet create mode 100644 redux/redux/tools/def_cc_library.bzl create mode 100644 redux/redux/tools/def_code_generator/BUILD create mode 100644 redux/redux/tools/def_code_generator/code_builder.cc create mode 100644 redux/redux/tools/def_code_generator/code_builder.h create mode 100644 redux/redux/tools/def_code_generator/def_document.h create mode 100644 redux/redux/tools/def_code_generator/generate_code.cc create mode 100644 redux/redux/tools/def_code_generator/generate_code.h create mode 100644 redux/redux/tools/def_code_generator/main.cc create mode 100644 redux/redux/tools/def_code_generator/metadata_types.h create mode 100644 redux/redux/tools/def_code_generator/parse_def_file.cc create mode 100644 redux/redux/tools/def_code_generator/parse_def_file.h create mode 100644 redux/redux/tools/def_code_generator/parse_def_file_tests.cc create mode 100644 redux/redux/tools/env_repository.bzl create mode 100644 redux/redux/tools/flatbuffer_cc_library.bzl create mode 100644 redux/redux/tools/model_pipeline/BUILD create mode 100644 redux/redux/tools/model_pipeline/README.md create mode 100644 redux/redux/tools/model_pipeline/bone.h create mode 100644 redux/redux/tools/model_pipeline/config.fbs create mode 100644 redux/redux/tools/model_pipeline/drawable.h create mode 100644 redux/redux/tools/model_pipeline/export.cc create mode 100644 redux/redux/tools/model_pipeline/export.h create mode 100644 redux/redux/tools/model_pipeline/import_asset.cc create mode 100644 redux/redux/tools/model_pipeline/import_fbx.cc create mode 100644 redux/redux/tools/model_pipeline/main.cc create mode 100644 redux/redux/tools/model_pipeline/material.h create mode 100644 redux/redux/tools/model_pipeline/model.cc create mode 100644 redux/redux/tools/model_pipeline/model.h create mode 100644 redux/redux/tools/model_pipeline/model_pipeline.cc create mode 100644 redux/redux/tools/model_pipeline/model_pipeline.h create mode 100644 redux/redux/tools/model_pipeline/texture_info.h create mode 100644 redux/redux/tools/model_pipeline/texture_locator.cc create mode 100644 redux/redux/tools/model_pipeline/texture_locator.h create mode 100644 redux/redux/tools/model_pipeline/util.cc create mode 100644 redux/redux/tools/model_pipeline/util.h create mode 100644 redux/redux/tools/model_pipeline/vertex.cc create mode 100644 redux/redux/tools/model_pipeline/vertex.h create mode 100644 redux/redux/tools/shader_pipeline/BUILD create mode 100644 redux/redux/tools/shader_pipeline/README.md create mode 100644 redux/redux/tools/shader_pipeline/main.cc create mode 100644 redux/redux/tools/shader_pipeline/shader_pipeline.cc create mode 100644 redux/redux/tools/shader_pipeline/shader_pipeline.h create mode 100644 redux/redux/tools/texture_pipeline/BUILD create mode 100644 redux/redux/tools/texture_pipeline/README.md create mode 100644 redux/redux/tools/texture_pipeline/generate_mipmaps.cc create mode 100644 redux/redux/tools/texture_pipeline/generate_mipmaps.h create mode 100644 redux/redux/tools/texture_pipeline/main.cc create mode 100644 redux/third_party/filament/BUILD create mode 100644 redux/third_party/filament/generate_shaders.bzl create mode 100644 redux/third_party/filament/licenses/licenses.inc create mode 100644 redux/third_party/gl/BUILD diff --git a/redux/.bazelrc b/redux/.bazelrc new file mode 100644 index 0000000..ef8b0d6 --- /dev/null +++ b/redux/.bazelrc @@ -0,0 +1,16 @@ +common --enable_platform_specific_config + +# linux +build:linux --cxxopt='-std=c++17' +build:linux --host_cxxopt='-std=c++17' +build:linux --cxxopt='-fpermissive' + +# macos +build:macos --cxxopt='-std=c++17' +build:macos --host_cxxopt='-std=c++17' + +# windows +build:windows --cxxopt=/std:c++20 +build:windows --host_cxxopt=/std:c++20 +build:windows --copt=/D_USE_MATH_DEFINES +build:windows --host_copt=/D_USE_MATH_DEFINES diff --git a/redux/WORKSPACE b/redux/WORKSPACE new file mode 100644 index 0000000..f490466 --- /dev/null +++ b/redux/WORKSPACE @@ -0,0 +1,256 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("//redux/tools:env_repository.bzl", "env_repository") + +# Setup the bazel skylib workspace. Needed by absl. +http_archive( + name = "bazel_skylib", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.3.0/bazel-skylib-1.3.0.tar.gz", + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.3.0/bazel-skylib-1.3.0.tar.gz", + ], + sha256 = "74d544d96f4a5bb630d465ca8bbcfe231e3594e5aae57e1edbf17a6eb3ca2506", +) +load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace") +bazel_skylib_workspace() + +http_archive( + name = "absl", + urls = ["https://github.com/abseil/abseil-cpp/archive/2052c2e37c7554c52db3c117b3ad468924e10cd2.tar.gz"], + strip_prefix = "abseil-cpp-2052c2e37c7554c52db3c117b3ad468924e10cd2", + sha256 = "8721f4824f274a0a062848188f70f65ecb348872be2241896e318aec974e91a1", +) + +http_archive( + name = "assimp", + build_file = "BUILD.assimp", + urls = ["https://github.com/assimp/assimp/archive/fd6c534efc78c6a27bc2ef35ef4b0e20977a31d8.tar.gz"], + strip_prefix = "assimp-fd6c534efc78c6a27bc2ef35ef4b0e20977a31d8", + sha256 = "c7f3ed606df3af1f4c496973c68443412128d7d1645ba515fbbb55c0bbab4166", + patches = ["assimp.patch"], + patch_args = ["-p1"], +) + +http_archive( + name = "bullet", + build_file = "BUILD.bullet", + urls = ["https://github.com/bulletphysics/bullet3/archive/dad061fc132cda4bd4f834d9384994af0f1fd9c4.tar.gz"], + strip_prefix = "bullet3-dad061fc132cda4bd4f834d9384994af0f1fd9c4", + sha256 = "d2d7ceba9a441bd941aab76b56dadc6355d41fe8b4e7789872be7e0abfaa89e6", +) + +http_archive( + name = "eigen", + build_file = "BUILD.eigen", + urls = ["https://gitlab.com/libeigen/eigen/-/archive/8f8e36458f4b590adf54936aefd1c0d3fb667c8f/eigen-8f8e36458f4b590adf54936aefd1c0d3fb667c8f.tar.gz"], + strip_prefix = "eigen-8f8e36458f4b590adf54936aefd1c0d3fb667c8f", + sha256 = "388ad37ae993bdd910d0c0e442efcc1c61412b51797db42383c880e529bedab7", +) + +http_archive( + name = "filament", + build_file = "BUILD.filament", + urls = ["https://github.com/google/filament/archive/0c3d59aba3833c36e984b0c08e7e789b56d40b7b.tar.gz"], + strip_prefix = "filament-0c3d59aba3833c36e984b0c08e7e789b56d40b7b", + sha256 = "07bf6d0c5872e1c57b592c22775fa2645a12a32dcaf4241219eb825195d81c7c", + patches = ["filament.patch"], + patch_args = ["-p1"], +) + +http_archive( + name = "flatbuffers", + urls = ["https://github.com/google/flatbuffers/archive/f124e41ae62ae548e045dadb26b6ad172bf8020b.tar.gz"], + strip_prefix = "flatbuffers-f124e41ae62ae548e045dadb26b6ad172bf8020b", + sha256 = "d2ccc4bb90c972fea3136a310ccdda617fdd289d789ffb24d014e8769e36513d", +) + +http_archive( + name = "fmtlib", + build_file = "BUILD.fmtlib", + urls = ["https://github.com/fmtlib/fmt/archive/29c6000137dd94151b61e34fc9f658304878d71e.tar.gz"], + strip_prefix = "fmt-29c6000137dd94151b61e34fc9f658304878d71e", + sha256 = "e1adb6fda8d346697c080afea12ce5a99f281e14193c781d84b35deef099e9a2", +) + +http_archive( + name = "freetype", + build_file = "BUILD.freetype", + url = "https://downloads.sourceforge.net/project/freetype/freetype2/2.12.1/freetype-2.12.1.tar.gz", + strip_prefix = "freetype-2.12.1", + sha256 = "efe71fd4b8246f1b0b1b9bfca13cfff1c9ad85930340c27df469733bbb620938", +) + +http_archive( + name = "gtest", + urls = ["https://github.com/google/googletest/archive/e68764c147ea0dac1e8811925c531d937396878e.tar.gz"], + strip_prefix = "googletest-e68764c147ea0dac1e8811925c531d937396878e", + sha256 = "8ab279eb344f443f0d3a39d6b540245ea8988c7232687cee91a0815f161c4e3f", +) + +http_archive( + name = "harfbuzz", + build_file = "BUILD.harfbuzz", + urls = ["https://github.com/harfbuzz/harfbuzz/archive/e3548c206990f23caba4fa31fed1aaf3fceeb04f.tar.gz"], + strip_prefix = "harfbuzz-e3548c206990f23caba4fa31fed1aaf3fceeb04f", + sha256 = "b0a3e3a07ad8a9653fa8f0cdb0bb585d7fc150b8460c486a9ec884dc4ffcbabd", +) + +http_archive( + name = "jsonnet", + urls = ["https://github.com/google/jsonnet/archive/c1fde47edc65bd43b1a2342eae28759d039276a8.tar.gz"], + strip_prefix = "jsonnet-c1fde47edc65bd43b1a2342eae28759d039276a8", + sha256 = "83262b0575d2dcc58898b95fcc178ed8c16417396c4a82ec2eb1b6bc135e3226", +) + +env_repository( + name = "libfbx", + env = "FBX_SDK_ROOT", + build_file = "BUILD.libfbx", +) + +http_archive( + name = "libpng", + build_file = "BUILD.libpng", + urls = ["https://github.com/glennrp/libpng/archive/a37d4836519517bdce6cb9d956092321eca3e73b.tar.gz"], + strip_prefix = "libpng-a37d4836519517bdce6cb9d956092321eca3e73b", + sha256 = "7491fb7056fbac282460290115210ac979d758d66406228bcd551107dca49c3d", + patches = ["libpng.patch"], + patch_args = ["-p1"], +) + +http_archive( + name = "libogg", + build_file = "BUILD.libogg", + urls = ["https://github.com/xiph/ogg/archive/db5c7a49ce7ebda47b15b78471e78fb7f2483e22.tar.gz"], + strip_prefix = "ogg-db5c7a49ce7ebda47b15b78471e78fb7f2483e22", + sha256 = "d34fdd3b0d06d7ab3222299f517472c29e0367e220e182c69906e05e53f7e4ea", + patches = ["libogg.patch"], + patch_args = ["-p1"], +) + +http_archive( + name = "libopus", + build_file = "BUILD.libopus", + urls = ["https://github.com/xiph/opus/archive/bce1f392353d72d77d543bb2069a044ae1045e9d.tar.gz"], + strip_prefix = "opus-bce1f392353d72d77d543bb2069a044ae1045e9d", + sha256 = "f28e625d4627cb4f194e10add5dece6cf8c46f12ded00d16dc80482a669cccb8", +) + +http_archive( + name = "libopusfile", + build_file = "BUILD.libopusfile", + urls = ["https://github.com/xiph/opusfile/archive/0a4cd796df5b030cb866f3f4a5e41a4b92caddf5.tar.gz"], + strip_prefix = "opusfile-0a4cd796df5b030cb866f3f4a5e41a4b92caddf5", + sha256 = "4bda0933377dfd1ea5a9ad0b76c4acf2425017f306ac086b30669cbbee633197", +) + +http_archive( + name = "libunibreak", + build_file = "BUILD.libunibreak", + urls = ["https://github.com/adah1972/libunibreak/archive/c8fbb31cab50a3f0a728b5b40e89bb6083bb2648.tar.gz"], + strip_prefix = "libunibreak-c8fbb31cab50a3f0a728b5b40e89bb6083bb2648", + sha256 = "1a12b2be0ec4e3d675e8307c73a044e524fb0b3a6369367c075047f3ca51f24c", +) + +http_archive( + name = "libvorbis", + build_file = "BUILD.libvorbis", + urls = ["https://github.com/xiph/vorbis/archive/84c023699cdf023a32fa4ded32019f194afcdad0.tar.gz"], + strip_prefix = "vorbis-84c023699cdf023a32fa4ded32019f194afcdad0", + sha256 = "97cfed8b9acb13bf7ec7410b6f303b69c312fc17a009298e9b8a7356cfe8ce7f", +) + +http_archive( + name = "libwebp", + build_file = "BUILD.libwebp", + urls = ["https://github.com/webmproject/libwebp/archive/e8f83de2867456a75b13aa1e876b22e2d1e1c077.tar.gz"], + strip_prefix = "libwebp-e8f83de2867456a75b13aa1e876b22e2d1e1c077", + sha256 = "be8735d6811920ee4f1b13489d1afa00bc2dc9837760ca097adf4125093f9858", +) + +http_archive( + name = "magic_enum", + build_file = "BUILD.magic_enum", + urls = ["https://github.com/Neargye/magic_enum/archive/1b1194bcd5e0f62047a43161689bae593b12e607.tar.gz"], + strip_prefix = "magic_enum-1b1194bcd5e0f62047a43161689bae593b12e607", + sha256 = "b31e74e3d2fb07f9e2ef92b62c0c50fd42ccb89b9b974582e660d3b97911709e", +) + +http_archive( + name = "pffft", + build_file = "BUILD.pffft", + urls = ["https://bitbucket.org/jpommier/pffft/get/7c3b5a7dc510.zip"], + strip_prefix = "jpommier-pffft-7c3b5a7dc510", + sha256 = "20f48fbbd5737d9a7db419f216c8d01ec8fdead48442c04774425a4c6b8b3949", +) + +http_archive( + name = "resonance", + build_file = "BUILD.resonance", + urls = ["https://github.com/resonance-audio/resonance-audio/archive/4f5a95f3d677fecedd3108821e53fec00e94e9e7.tar.gz"], + strip_prefix = "resonance-audio-4f5a95f3d677fecedd3108821e53fec00e94e9e7", + sha256 = "5a23da4c21548a192d7d2ea88091ff484b072bc77053815b0666e5faf5f71b21", +) + +http_archive( + name = "rapidjson", + build_file = "BUILD.rapidjson", + urls = ["https://github.com/Tencent/rapidjson/archive/06d58b9e848c650114556a23294d0b6440078c61.tar.gz"], + strip_prefix = "rapidjson-06d58b9e848c650114556a23294d0b6440078c61", + sha256 = "30d28bbe0bfff9d8dc5d3cf62799b6ee550499cc1520e44bdece81e002480d19", +) + +http_archive( + name = "sdl2", + build_file = "BUILD.sdl2", + urls = ["https://github.com/libsdl-org/SDL/archive/03485db0a30f0a42b08f398efa58e7ef51e75d0a.tar.gz"], + strip_prefix = "SDL-03485db0a30f0a42b08f398efa58e7ef51e75d0a", + sha256 = "74c1b0a887e279f53ce1dbfa62d749ce184290b7fd5397c3410d74e664fc92a1", +) + +http_archive( + name = "stblib", + build_file = "BUILD.stblib", + urls = ["https://github.com/nothings/stb/archive/af1a5bc352164740c1cc1354942b1c6b72eacb8a.tar.gz"], + strip_prefix = "stb-af1a5bc352164740c1cc1354942b1c6b72eacb8a", + sha256 = "936b4e506b5f55db178207e528ecdf5a411f67431447767d06c9b7061765cd7e", + patches = ["stblib.patch"], + patch_args = ["-p1"], +) + +http_archive( + name = "spirv_headers", + urls = ["https://github.com/KhronosGroup/SPIRV-Headers/archive/b2a156e1c0434bc8c99aaebba1c7be98be7ac580.tar.gz"], + strip_prefix = "SPIRV-Headers-b2a156e1c0434bc8c99aaebba1c7be98be7ac580", + sha256 = "b200990e1c07894906e298368e7e56d5ab9d728d851f9292587ec740c2b4d409", +) + +http_archive( + name = "spirv_tools", + urls = ["https://github.com/KhronosGroup/SPIRV-Tools/archive/82d91083cb56c89d2cb8e9d56d4d69f07ac34fed.tar.gz"], + strip_prefix = "SPIRV-Tools-82d91083cb56c89d2cb8e9d56d4d69f07ac34fed", + sha256 = "8ecdfc602c6ed82b94655cb4b269efd1c6ae62db4769f83c9822091313064b4e", +) + +http_archive( + name = "utfcpp", + build_file = "BUILD.utfcpp", + urls = ["https://github.com/nemtrif/utfcpp/archive/d8b92208fddde08d628004563a1a3614a32cf8c8.tar.gz"], + strip_prefix = "utfcpp-d8b92208fddde08d628004563a1a3614a32cf8c8", + sha256 = "792a380660d016f511d9a18ce208fc21226fffcdad7ab29022f3ac1314029c2c", +) + +http_archive( + name = "vectorial", + build_file = "BUILD.vectorial", + urls = ["https://github.com/scoopr/vectorial/archive/3a00e8c00d017cb49b12eeffd7464246d172ea97.tar.gz"], + strip_prefix = "vectorial-3a00e8c00d017cb49b12eeffd7464246d172ea97", + sha256 = "06eaac3df75fc2aebb36cf164dae9375f6669d8ef64d6e617b04f2f070f058d1", +) + +http_archive( + name = "zlib", + build_file = "BUILD.zlib", + urls = ["https://github.com/madler/zlib/archive/21767c654d31d2dccdde4330529775c6c5fd5389.tar.gz"], + strip_prefix = "zlib-21767c654d31d2dccdde4330529775c6c5fd5389", + sha256 = "ef47b0fbe646d69a2fc5ba012cb278de8e8946a8e9649f83a807cc05559f0eff", +) diff --git a/redux/external/BUILD.assimp b/redux/external/BUILD.assimp new file mode 100644 index 0000000..c7bb749 --- /dev/null +++ b/redux/external/BUILD.assimp @@ -0,0 +1,106 @@ +cc_library( + name = "assimp", + srcs = glob([ + "code/Common/**/*.cpp", + "code/Material/**/*.cpp", + "code/PostProcessing/**/*.cpp", + # Asset libraries + "code/AssetLib/Obj/**/*.cpp", + "code/AssetLib/glTF/**/*.cpp", + "code/AssetLib/glTF2/**/*.cpp", + # Private headers. + "code/**/*.h", + "code/**/*.hpp", + "code/**/*.inl", + ]), + hdrs = glob([ + "include/assimp/**/*.h", + "include/assimp/**/*.hpp", + "include/assimp/**/*.inl", + ]), + includes = ["include", "include/assimp", "code"], + deps = [ + ":contrib", + ], + defines = [ + "ASSIMP_BUILD_NO_3D_IMPORTER", + "ASSIMP_BUILD_NO_3DS_IMPORTER", + "ASSIMP_BUILD_NO_3MF_IMPORTER", + "ASSIMP_BUILD_NO_AC_IMPORTER", + "ASSIMP_BUILD_NO_AMF_IMPORTER", + "ASSIMP_BUILD_NO_ASE_IMPORTER", + "ASSIMP_BUILD_NO_ASSBIN_IMPORTER", + "ASSIMP_BUILD_NO_B3D_IMPORTER", + "ASSIMP_BUILD_NO_BLEND_IMPORTER", + "ASSIMP_BUILD_NO_BVH_IMPORTER", + "ASSIMP_BUILD_NO_C4D_IMPORTER", + "ASSIMP_BUILD_NO_COB_IMPORTER", + "ASSIMP_BUILD_NO_COLLADA_IMPORTER", + "ASSIMP_BUILD_NO_CSM_IMPORTER", + "ASSIMP_BUILD_NO_DXF_IMPORTER", + "ASSIMP_BUILD_NO_FBX_IMPORTER", + "ASSIMP_BUILD_NO_HMP_IMPORTER", + "ASSIMP_BUILD_NO_IFC_IMPORTER", + "ASSIMP_BUILD_NO_IQM_IMPORTER", + "ASSIMP_BUILD_NO_IRR_IMPORTER", + "ASSIMP_BUILD_NO_IRRMESH_IMPORTER", + "ASSIMP_BUILD_NO_LWO_IMPORTER", + "ASSIMP_BUILD_NO_LWS_IMPORTER", + "ASSIMP_BUILD_NO_M3D_IMPORTER", + "ASSIMP_BUILD_NO_MD2_IMPORTER", + "ASSIMP_BUILD_NO_MD3_IMPORTER", + "ASSIMP_BUILD_NO_MD5_IMPORTER", + "ASSIMP_BUILD_NO_MDC_IMPORTER", + "ASSIMP_BUILD_NO_MDL_IMPORTER", + "ASSIMP_BUILD_NO_MMD_IMPORTER", + "ASSIMP_BUILD_NO_MS3D_IMPORTER", + "ASSIMP_BUILD_NO_NDO_IMPORTER", + "ASSIMP_BUILD_NO_NFF_IMPORTER", + "ASSIMP_BUILD_NO_OFF_IMPORTER", + "ASSIMP_BUILD_NO_OGRE_IMPORTER", + "ASSIMP_BUILD_NO_OPENGEX_IMPORTER", + "ASSIMP_BUILD_NO_PLY_IMPORTER", + "ASSIMP_BUILD_NO_Q3BSP_IMPORTER", + "ASSIMP_BUILD_NO_Q3D_IMPORTER", + "ASSIMP_BUILD_NO_RAW_IMPORTER", + "ASSIMP_BUILD_NO_SIB_IMPORTER", + "ASSIMP_BUILD_NO_SMD_IMPORTER", + "ASSIMP_BUILD_NO_STL_IMPORTER", + "ASSIMP_BUILD_NO_TERRAGEN_IMPORTER", + "ASSIMP_BUILD_NO_X3D_IMPORTER", + "ASSIMP_BUILD_NO_XGL_IMPORTER", + "ASSIMP_BUILD_NO_X_IMPORTER", + "AI_MATKEY_GLTF_PBRSPECULARGLOSSINESS=AI_MATKEY_GLOSSINESS_FACTOR", + ], + copts = select({ + "@platforms//os:windows": ["/wd4244"], + "//conditions:default": [], + }), + visibility = ["//visibility:public"], +) + +cc_library( + name = "contrib", + srcs = glob([ + "contrib/zip/*.c", + "contrib/unzip/*.c", + ]), + hdrs = glob([ + "contrib/unzip/**/*.h", + "contrib/zip/**/*.h", + # assimp includes this as "../contrib/zlib/zlib.h" + "contrib/zlib/zlib.h", + # assimp includes this as "../contrib/utf8cpp/source/utf8.h" + "contrib/utf8cpp/source/utf8.h", + ]), + includes = [ + "contrib/unzip", + "contrib/zip", + ], + deps = [ + "@rapidjson//:rapidjson", + "@stblib//:stblib", + "@utfcpp//:utfcpp", + "@zlib//:zlib", + ], +) diff --git a/redux/external/BUILD.bullet b/redux/external/BUILD.bullet new file mode 100644 index 0000000..6b0e303 --- /dev/null +++ b/redux/external/BUILD.bullet @@ -0,0 +1,56 @@ +package( + default_visibility = ["//visibility:public"], +) + +cc_library( + name = "Bullet3Common", + srcs = glob(["src/Bullet3Common/**/*.cpp"]), + hdrs = glob(["src/Bullet3Common/**/*.h"]), + includes = ["src"], +) + +cc_library( + name = "LinearMath", + srcs = glob(["src/LinearMath/**/*.cpp"]), + hdrs = glob(["src/LinearMath/**/*.h"]), + includes = ["src"], +) + +cc_library( + name = "BulletCollision", + srcs = glob(["src/BulletCollision/**/*.cpp"]), + hdrs = glob(["src/BulletCollision/**/*.h"]) + ["src/btBulletCollisionCommon.h"], + includes = ["src"], + deps = [":LinearMath"], +) + +cc_library( + name = "BulletDynamics", + srcs = glob(["src/BulletDynamics/**/*.cpp"]), + hdrs = glob(["src/BulletDynamics/**/*.h"]) + ["src/btBulletDynamicsCommon.h"], + includes = ["src"], + deps = [":BulletCollision"], +) + +cc_library( + name = "BulletSoftBody", + srcs = glob(["src/BulletSoftBody/**/*.cpp"]), + hdrs = glob(["src/BulletSoftBody/**/*.h"]), + includes = ["src"], + deps = [ + ":BulletCollision", + ":BulletDynamics", + ":LinearMath", + ], +) + +cc_library( + name = "BulletInverseDynamics", + srcs = glob(["src/BulletInverseDynamics/**/*.cpp"]), + hdrs = glob(["src/BulletInverseDynamics/**/*.hpp"]), + includes = ["src"], + deps = [ + ":BulletDynamics", + ":LinearMath", + ], +) diff --git a/redux/external/BUILD.eigen b/redux/external/BUILD.eigen new file mode 100644 index 0000000..7fbaaa2 --- /dev/null +++ b/redux/external/BUILD.eigen @@ -0,0 +1,26 @@ +EIGEN_HDRS = glob( + [ + "Eigen/*", + ], + exclude = [ + "**/src/**", + ] + glob(["**/*.*"]) +) + +EIGEN_SRCS = glob( + [ + "Eigen/**/src/**/*.h", + "Eigen/**/src/**/*.inc", + ], + exclude = [ + "Eigen/src/Core/util/NonMPL2.h", + ], +) + +cc_library( + name = "eigen", + srcs = EIGEN_SRCS, + hdrs = EIGEN_HDRS, + includes = ["."], + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.filament b/redux/external/BUILD.filament new file mode 100644 index 0000000..1545737 --- /dev/null +++ b/redux/external/BUILD.filament @@ -0,0 +1,157 @@ +load("@//third_party/filament:generate_shaders.bzl", "generate_shaders") + +package( + default_visibility = ["//visibility:public"], +) + +cc_library( + name = "backend_hdrs", + hdrs = glob(["filament/backend/include/**/*.h"]), + includes = ["filament/backend/include"], + deps = [":math_hdrs"], +) + +cc_library( + name = "filament_hdrs", + hdrs = glob(["filament/include/**/*.h"]), + includes = ["filament/include"], + deps = [ + ":filabridge", + ":backend_hdrs", + ":utils_hdrs", + ], +) + +cc_library( + name = "math_hdrs", + hdrs = glob(["libs/math/include/**/*.h"]), + includes = ["libs/math/include"], +) + +cc_library( + name = "utils_hdrs", + hdrs = glob(["libs/utils/include/**/*.h"]), + includes = ["libs/utils/include"], +) + +cc_library( + name = "utils", + srcs = glob([ + "libs/utils/src/*.cpp", + "libs/utils/src/*.h", + ]), + deps = [ + ":utils_hdrs", + ":third_party_robin_map", + ] + select({ + "@platforms//os:osx": [":utils_darwin"], + "@platforms//os:windows": [":utils_windows"], + "//conditions:default": [":utils_default"], + }), +) + +cc_library( + name = "utils_windows", + srcs = ["libs/utils/src/win32/Path.cpp"], + deps = [":utils_hdrs"], + defines = ["WIN32"], +) + +cc_library( + name = "utils_default", + srcs = ["libs/utils/src/linux/Path.cpp"], + deps = [":utils_hdrs"], +) + +objc_library( + name = "utils_darwin", + srcs = ["libs/utils/src/darwin/Path.mm"], + deps = [":utils_hdrs"], +) + +cc_library( + name = "filabridge", + srcs = glob(["libs/filabridge/src/**/*.cpp"]), + hdrs = glob(["libs/filabridge/include/**/*.h"]), + includes = ["libs/filabridge/include"], + deps = [ + ":utils_hdrs", + ":backend_hdrs", + ], +) + +cc_library( + name = "filamat", + srcs = glob(["libs/filamat/src/**/*.cpp"]), + hdrs = glob([ + "libs/filamat/include/**/*.h", + "libs/filamat/src/**/*.h", + "third_party/glslang/glslang/SPIRV/**/*.h", + ]), + includes = [ + "libs/filamat/include", + "libs/filamat/src", + "libs/filamat/src/eiff", + "third_party/glslang", + "third_party/glslang/glslang/Include", + "third_party/glslang/glslang/MachineIndependent", + "third_party/glslang/glslang/Public", + ], + deps = [ + ":generated_shaders", + ":filabridge", + ":third_party_robin_map", + ":third_party_smolv", + ":third_party_spirv_cross", + "@filament//third_party/glslang", + "@filament//third_party/glslang:SPIRV", + "@spirv_tools//:spirv_tools_opt", + ], +) + +cc_binary( + name = "resgen", + srcs = glob([ + "tools/resgen/src/*.cpp", + "tools/resgen/src/*.h", + ]), + includes = ["tools/resgen/src"], + deps = [ + ":utils", + ":utils_hdrs", + ":third_party_getopt", + "@//third_party/filament:resgen_licenses", + ], +) + +generate_shaders( + name = "generated_shaders", +) + +cc_library( + name = "third_party_getopt", + srcs = glob(["third_party/getopt/src/*.c"]), + hdrs = glob(["third_party/getopt/include/**/*.h"]), + includes = ["third_party/getopt/include"], +) + +cc_library( + name = "third_party_smolv", + srcs = glob(["third_party/smol-v/source/*.cpp"]), + hdrs = glob(["third_party/smol-v/source/*.h"]), + includes = ["third_party/smol-v/source"], +) + +cc_library( + name = "third_party_robin_map", + hdrs = glob(["third_party/robin-map/tsl/*.h"]), + includes = ["third_party/robin-map"], +) + +cc_library( + name = "third_party_spirv_cross", + srcs = glob(["third_party/spirv-cross/*.cpp"]), + hdrs = glob(["third_party/spirv-cross/*.h", "third_party/spirv-cross/*.hpp"]), + includes = ["third_party/spirv-cross"], + deps = ["@spirv_headers//:spirv_c_headers"], +) diff --git a/redux/external/BUILD.fmtlib b/redux/external/BUILD.fmtlib new file mode 100644 index 0000000..f563764 --- /dev/null +++ b/redux/external/BUILD.fmtlib @@ -0,0 +1,7 @@ +cc_library( + name = "fmtlib", + srcs = ["src/format.cc", "src/os.cc"], + hdrs = glob(["include/fmt/*.h"]), + includes = ["include"], + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.freetype b/redux/external/BUILD.freetype new file mode 100644 index 0000000..b2807ec --- /dev/null +++ b/redux/external/BUILD.freetype @@ -0,0 +1,103 @@ +# The base sources, extracted from CMakeLists.txt +_BASE_SRCS = glob([ + "src/autofit/autofit.c", + "src/base/ftbase.c", + "src/base/ftbbox.c", + "src/base/ftbdf.c", + "src/base/ftbitmap.c", + "src/base/ftcid.c", + "src/base/ftfstype.c", + "src/base/ftgasp.c", + "src/base/ftglyph.c", + "src/base/ftgxval.c", + "src/base/ftinit.c", + "src/base/ftmm.c", + "src/base/ftotval.c", + "src/base/ftpatent.c", + "src/base/ftpfr.c", + "src/base/ftstroke.c", + "src/base/ftsynth.c", + "src/base/fttype1.c", + "src/base/ftwinfnt.c", + "src/bdf/bdf.c", + "src/bzip2/ftbzip2.c", + "src/cache/ftcache.c", + "src/cff/cff.c", + "src/cid/type1cid.c", + "src/gzip/ftgzip.c", + "src/lzw/ftlzw.c", + "src/pcf/pcf.c", + "src/pfr/pfr.c", + "src/psaux/psaux.c", + "src/pshinter/pshinter.c", + "src/psnames/psnames.c", + "src/raster/raster.c", + "src/sdf/sdf.c", + "src/sfnt/sfnt.c", + "src/smooth/smooth.c", + "src/svg/svg.c", + "src/truetype/truetype.c", + "src/type1/type1.c", + "src/type42/type42.c", + "src/winfonts/winfnt.c", +]) + +_LINUX_SRCS = [ + "builds/unix/ftsystem.c", + "src/base/ftdebug.c", +] + +_WINDOWS_SRCS = [ + "builds/windows/ftsystem.c", + "builds/windows/ftdebug.c", +] + +_OSX_SRCS = [ + "src/base/ftsystem.c", + "src/base/ftdebug.c", +] + +_PUBLIC_HEADERS = glob([ + "include/ft2build.h", + "include/freetype/*.h", +]) + +_PRIVATE_HEADERS = glob([ + "src/**/*.h", + "include/freetype/config/**/*.h", + "include/freetype/internal/**/*.h", +]) + +# Many of the .c files above include other .c files, so we include them +# all as textual_hdrs. +_TEXTUAL_HEADERS = glob([ + "src/**/*.c", +]) + +cc_library( + name = "freetype", + srcs = _BASE_SRCS + _PRIVATE_HEADERS + select({ + "@platforms//os:linux": _LINUX_SRCS, + "@platforms//os:osx": _OSX_SRCS, + "@platforms//os:windows": _WINDOWS_SRCS, + }), + hdrs = _PUBLIC_HEADERS, + textual_hdrs = _TEXTUAL_HEADERS, + includes = [ + "include", + "include/freetype/config", + ], + defines = [ + "FT_CONFIG_OPTION_SYSTEM_ZLIB", + "FT_CONFIG_CONFIG_H=", + "FT_CONFIG_OPTION_USE_PNG", + "FT2_BUILD_LIBRARY", + "HAVE_FCNTL_H", + "FT_CONFIG_MODULES_H=", + ], + deps = [ + "@libpng//:libpng", + "@zlib//:zlib", + ], + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.harfbuzz b/redux/external/BUILD.harfbuzz new file mode 100644 index 0000000..843b2f0 --- /dev/null +++ b/redux/external/BUILD.harfbuzz @@ -0,0 +1,12 @@ +cc_library( + name = "harfbuzz", + srcs = ["src/harfbuzz.cc"], + hdrs = glob([ + "src/**/*.h", + "src/**/*.hh", + ]), + textual_hdrs = glob(["src/**/*.cc"]), + includes = ["src"], + deps = ["@freetype//:freetype"], + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.libfbx b/redux/external/BUILD.libfbx new file mode 100644 index 0000000..3cf92b7 --- /dev/null +++ b/redux/external/BUILD.libfbx @@ -0,0 +1,16 @@ +cc_library( + name = "libfbx", + srcs = select({ + "@platforms//os:osx": ["FBX_SDK_ROOT/lib/osx/release/libfbxsdk.a"], + "@platforms//os:linux": ["FBX_SDK_ROOT/lib/linux/release/libfbxsdk.a"], + "@platforms//os:windows": ["FBX_SDK_ROOT/lib/windows/release/libfbxsdk.lib"], + "//conditions:default": [], + }), + hdrs = glob(["FBX_SDK_ROOT/include/**/*.h"]), + includes = ["FBX_SDK_ROOT/include"], + visibility = ["//visibility:public"], + linkopts = [ + "-ldl", + "-pthread", + ] +) diff --git a/redux/external/BUILD.libogg b/redux/external/BUILD.libogg new file mode 100644 index 0000000..9e45c28 --- /dev/null +++ b/redux/external/BUILD.libogg @@ -0,0 +1,7 @@ +cc_library( + name = "libogg", + srcs = glob(["src/**/*.c", "src/**/*.h"]), + hdrs = glob(["include/**/*.h"]), + includes = ["include"], + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.libopus b/redux/external/BUILD.libopus new file mode 100644 index 0000000..d5eaa45 --- /dev/null +++ b/redux/external/BUILD.libopus @@ -0,0 +1,241 @@ + +INTERNAL_HDRS = glob([ + "src/**/*.h", + "celt/**/*.h", + "silk/**/*.h", +]) + +OPUS_COMMON_SRCS = [ + "src/analysis.c", + "src/mapping_matrix.c", + "src/mlp.c", + "src/mlp_data.c", + "src/opus.c", + "src/opus_decoder.c", + "src/opus_encoder.c", + "src/opus_multistream.c", + "src/opus_multistream_decoder.c", + "src/opus_multistream_encoder.c", + "src/opus_projection_decoder.c", + "src/opus_projection_encoder.c", + "src/repacketizer.c", +] + +CELT_COMMON_SRCS = [ + "celt/bands.c", + "celt/celt.c", + "celt/celt_decoder.c", + "celt/celt_encoder.c", + "celt/celt_lpc.c", + "celt/cwrs.c", + "celt/entcode.c", + "celt/entdec.c", + "celt/entenc.c", + "celt/kiss_fft.c", + "celt/laplace.c", + "celt/mathops.c", + "celt/mdct.c", + "celt/modes.c", + "celt/pitch.c", + "celt/quant_bands.c", + "celt/rate.c", + "celt/vq.c", +] + +CELT_ARM_SRCS = [ + "celt/arm/armcpu.c", + "celt/arm/arm_celt_map.c", + "celt/arm/celt_pitch_xcorr_arm_gnu.s", +] + +CELT_NEON_SRCS = [ + "celt/arm/celt_neon_intr.c", + "celt/arm/pitch_neon_intr.c", +] + +CELT_SSE_SRCS = [ + "celt/x86/x86cpu.c", + "celt/x86/x86_celt_map.c", + "celt/x86/pitch_sse.c", +] + +CELT_SSE2_SRCS = [ + "celt/x86/pitch_sse2.c", + "celt/x86/vq_sse2.c", +] + +CELT_SSE4_SRCS = [ + "celt/x86/celt_lpc_sse4_1.c", + "celt/x86/pitch_sse4_1.c", +] + +SILK_COMMON_SRCS = [ + "silk/A2NLSF.c", + "silk/CNG.c", + "silk/HP_variable_cutoff.c", + "silk/LPC_analysis_filter.c", + "silk/LPC_fit.c", + "silk/LPC_inv_pred_gain.c", + "silk/LP_variable_cutoff.c", + "silk/NLSF2A.c", + "silk/NLSF_VQ.c", + "silk/NLSF_VQ_weights_laroia.c", + "silk/NLSF_decode.c", + "silk/NLSF_del_dec_quant.c", + "silk/NLSF_encode.c", + "silk/NLSF_stabilize.c", + "silk/NLSF_unpack.c", + "silk/NSQ.c", + "silk/NSQ_del_dec.c", + "silk/PLC.c", + "silk/VAD.c", + "silk/VQ_WMat_EC.c", + "silk/ana_filt_bank_1.c", + "silk/biquad_alt.c", + "silk/bwexpander.c", + "silk/bwexpander_32.c", + "silk/check_control_input.c", + "silk/code_signs.c", + "silk/control_SNR.c", + "silk/control_audio_bandwidth.c", + "silk/control_codec.c", + "silk/debug.c", + "silk/dec_API.c", + "silk/decode_core.c", + "silk/decode_frame.c", + "silk/decode_indices.c", + "silk/decode_parameters.c", + "silk/decode_pitch.c", + "silk/decode_pulses.c", + "silk/decoder_set_fs.c", + "silk/enc_API.c", + "silk/encode_indices.c", + "silk/encode_pulses.c", + "silk/gain_quant.c", + "silk/init_decoder.c", + "silk/init_encoder.c", + "silk/inner_prod_aligned.c", + "silk/interpolate.c", + "silk/lin2log.c", + "silk/log2lin.c", + "silk/pitch_est_tables.c", + "silk/process_NLSFs.c", + "silk/quant_LTP_gains.c", + "silk/resampler.c", + "silk/resampler_down2.c", + "silk/resampler_down2_3.c", + "silk/resampler_private_AR2.c", + "silk/resampler_private_IIR_FIR.c", + "silk/resampler_private_down_FIR.c", + "silk/resampler_private_up2_HQ.c", + "silk/resampler_rom.c", + "silk/shell_coder.c", + "silk/sigm_Q15.c", + "silk/sort.c", + "silk/stereo_LR_to_MS.c", + "silk/stereo_MS_to_LR.c", + "silk/stereo_decode_pred.c", + "silk/stereo_encode_pred.c", + "silk/stereo_find_predictor.c", + "silk/stereo_quant_pred.c", + "silk/sum_sqr_shift.c", + "silk/table_LSF_cos.c", + "silk/tables_LTP.c", + "silk/tables_NLSF_CB_NB_MB.c", + "silk/tables_NLSF_CB_WB.c", + "silk/tables_gain.c", + "silk/tables_other.c", + "silk/tables_pitch_lag.c", + "silk/tables_pulses_per_block.c", +] + +SILK_FIXED_SRCS = [ + "silk/fixed/apply_sine_window_FIX.c", + "silk/fixed/autocorr_FIX.c", + "silk/fixed/burg_modified_FIX.c", + "silk/fixed/corrMatrix_FIX.c", + "silk/fixed/encode_frame_FIX.c", + "silk/fixed/find_LPC_FIX.c", + "silk/fixed/find_LTP_FIX.c", + "silk/fixed/find_pitch_lags_FIX.c", + "silk/fixed/find_pred_coefs_FIX.c", + "silk/fixed/k2a_FIX.c", + "silk/fixed/k2a_Q16_FIX.c", + "silk/fixed/LTP_analysis_filter_FIX.c", + "silk/fixed/LTP_scale_ctrl_FIX.c", + "silk/fixed/noise_shape_analysis_FIX.c", + "silk/fixed/pitch_analysis_core_FIX.c", + "silk/fixed/process_gains_FIX.c", + "silk/fixed/regularize_correlations_FIX.c", + "silk/fixed/residual_energy16_FIX.c", + "silk/fixed/residual_energy_FIX.c", + "silk/fixed/schur64_FIX.c", + "silk/fixed/schur_FIX.c", + "silk/fixed/vector_ops_FIX.c", + "silk/fixed/warped_autocorrelation_FIX.c", +] + +SILK_FLOAT_SRCS = [ + "silk/float/LPC_analysis_filter_FLP.c", + "silk/float/LPC_inv_pred_gain_FLP.c", + "silk/float/LTP_analysis_filter_FLP.c", + "silk/float/LTP_scale_ctrl_FLP.c", + "silk/float/apply_sine_window_FLP.c", + "silk/float/autocorrelation_FLP.c", + "silk/float/burg_modified_FLP.c", + "silk/float/bwexpander_FLP.c", + "silk/float/corrMatrix_FLP.c", + "silk/float/encode_frame_FLP.c", + "silk/float/energy_FLP.c", + "silk/float/find_LPC_FLP.c", + "silk/float/find_LTP_FLP.c", + "silk/float/find_pitch_lags_FLP.c", + "silk/float/find_pred_coefs_FLP.c", + "silk/float/inner_product_FLP.c", + "silk/float/k2a_FLP.c", + "silk/float/noise_shape_analysis_FLP.c", + "silk/float/pitch_analysis_core_FLP.c", + "silk/float/process_gains_FLP.c", + "silk/float/regularize_correlations_FLP.c", + "silk/float/residual_energy_FLP.c", + "silk/float/scale_copy_vector_FLP.c", + "silk/float/scale_vector_FLP.c", + "silk/float/schur_FLP.c", + "silk/float/sort_FLP.c", + "silk/float/warped_autocorrelation_FLP.c", + "silk/float/wrappers_FLP.c", +] + +SILK_NEON_SRCS = [ + "silk/arm/arm_silk_map.c", + "silk/arm/biquad_alt_neon_intr.c", + "silk/arm/LPC_inv_pred_gain_neon_intr.c", + "silk/arm/NSQ_del_dec_neon_intr.c", + "silk/arm/NSQ_neon.c", + "silk/fixed/arm/warped_autocorrelation_FIX_neon_intr.c", +] + +SILK_SSE4_SRCS = [ + "silk/x86/NSQ_sse4_1.c", + "silk/x86/NSQ_del_dec_sse4_1.c", + "silk/x86/x86_silk_map.c", + "silk/x86/VAD_sse4_1.c", + "silk/x86/VQ_WMat_EC_sse4_1.c", + "silk/fixed/x86/vector_ops_FIX_sse4_1.c", + "silk/fixed/x86/burg_modified_FIX_sse4_1.c", +] + +cc_library( + name = "libopus", + srcs = INTERNAL_HDRS + OPUS_COMMON_SRCS + CELT_COMMON_SRCS + SILK_COMMON_SRCS + SILK_FIXED_SRCS, + hdrs = glob(["include/*.h"]), + includes = ["include", "src", "celt", "silk", "silk/fixed"], + defines = [ + "OPUS_BUILD", + "FIXED_POINT", + ] + select({ + "@platforms//os:windows": ["USE_ALLOCA"], + "//conditions:default": ["VAR_ARRAYS"], + }), + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.libopusfile b/redux/external/BUILD.libopusfile new file mode 100644 index 0000000..dc7349d --- /dev/null +++ b/redux/external/BUILD.libopusfile @@ -0,0 +1,11 @@ +cc_library( + name = "libopusfile", + srcs = glob(["src/**/*.c", "src/**/*.h"]), + hdrs = glob(["include/*.h"]), + includes = ["include"], + deps = [ + "@libogg//:libogg", + "@libopus//:libopus", + ], + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.libpng b/redux/external/BUILD.libpng new file mode 100644 index 0000000..067e3fe --- /dev/null +++ b/redux/external/BUILD.libpng @@ -0,0 +1,18 @@ +cc_library( + name = "libpng", + srcs = glob([ + "*.c" + ], exclude = [ + "pngtest.c", + ]), + hdrs = glob(["*.h"]), + copts = [ + "-DHAVE_CONFIG_H", + ], + includes = ["."], + linkopts = ["-lm"], + deps = [ + "@zlib//:zlib", + ], + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.libunibreak b/redux/external/BUILD.libunibreak new file mode 100644 index 0000000..4d57ef9 --- /dev/null +++ b/redux/external/BUILD.libunibreak @@ -0,0 +1,20 @@ +cc_library( + name = "libunibreak", + srcs = glob([ + "src/*.c" + ], exclude = [ + "src/emojidata.c", + "src/graphemebreakdata.c", + "src/wordbreakdata.c", + ]), + hdrs = glob([ + "src/*.h", + ]), + textual_hdrs = glob([ + "src/emojidata.c", + "src/graphemebreakdata.c", + "src/wordbreakdata.c", + ]), + includes = ["src"], + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.libvorbis b/redux/external/BUILD.libvorbis new file mode 100644 index 0000000..7c9e167 --- /dev/null +++ b/redux/external/BUILD.libvorbis @@ -0,0 +1,31 @@ +cc_library( + name = "libvorbis", + srcs = [ + "lib/analysis.c", + "lib/bitrate.c", + "lib/block.c", + "lib/codebook.c", + "lib/envelope.c", + "lib/floor0.c", + "lib/floor1.c", + "lib/info.c", + "lib/lookup.c", + "lib/lpc.c", + "lib/lsp.c", + "lib/mapping0.c", + "lib/mdct.c", + "lib/psy.c", + "lib/registry.c", + "lib/res0.c", + "lib/sharedbook.c", + "lib/smallft.c", + "lib/synthesis.c", + "lib/window.c", + "lib/vorbisenc.c", + "lib/vorbisfile.c", + ] + glob(["lib/**/*.h"]), + hdrs = glob(["include/**/*.h"]), + includes = ["include", "lib"], + deps = ["@libogg//:libogg"], + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.libwebp b/redux/external/BUILD.libwebp new file mode 100644 index 0000000..bcbb23d --- /dev/null +++ b/redux/external/BUILD.libwebp @@ -0,0 +1,54 @@ +package( + default_visibility = ["//visibility:public"], +) + +cc_library( + name = "webp_encode", + srcs = glob(["src/enc/*.c"]), + hdrs = glob(["src/**/*.h"]), + includes = [".", "src"], + deps = [ + ":webp_dsp", + ":webp_utils", + ":webp_sharpyuv", + ], +) + +cc_library( + name = "webp_decode", + srcs = glob(["src/dec/*.c"]), + hdrs = glob(["src/**/*.h"]), + includes = [".", "src"], + deps = [ + ":webp_dsp", + ":webp_utils", + ], +) + +cc_library( + name = "webp_demux", + srcs = glob(["src/demux/*.c"]), + hdrs = glob(["src/**/*.h"]), + includes = [".", "src"], +) + +cc_library( + name = "webp_dsp", + srcs = glob(["src/dsp/*.c"]), + hdrs = glob(["src/**/*.h"]), + includes = [".", "src"], +) + +cc_library( + name = "webp_utils", + srcs = glob(["src/utils/*.c"]), + hdrs = glob(["src/**/*.h"]), + includes = [".", "src"], +) + +cc_library( + name = "webp_sharpyuv", + srcs = glob(["sharpyuv/*.c"]), + hdrs = glob(["sharpyuv/*.h", "src/**/*.h"]), + includes = ["."], +) diff --git a/redux/external/BUILD.magic_enum b/redux/external/BUILD.magic_enum new file mode 100644 index 0000000..2fe3abe --- /dev/null +++ b/redux/external/BUILD.magic_enum @@ -0,0 +1,6 @@ +cc_library( + name = "magic_enum", + hdrs = glob(["include/*.hpp"]), + includes = ["include"], + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.pffft b/redux/external/BUILD.pffft new file mode 100644 index 0000000..c8d631b --- /dev/null +++ b/redux/external/BUILD.pffft @@ -0,0 +1,7 @@ +cc_library( + name = "pffft", + srcs = ["pffft.c"], + hdrs = ["pffft.h"], + includes = ["."], + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.rapidjson b/redux/external/BUILD.rapidjson new file mode 100644 index 0000000..36b486d --- /dev/null +++ b/redux/external/BUILD.rapidjson @@ -0,0 +1,7 @@ +cc_library( + name = "rapidjson", + hdrs = glob(["include/rapidjson/**/*.h"]), + includes = ["include"], + visibility = ["//visibility:public"], + defines = ["RAPIDJSON_HAS_STDSTRING"], +) diff --git a/redux/external/BUILD.resonance b/redux/external/BUILD.resonance new file mode 100644 index 0000000..0f91f75 --- /dev/null +++ b/redux/external/BUILD.resonance @@ -0,0 +1,31 @@ +RESONANCE_SRCS = glob( + ["resonance_audio/**/*.cc"], + exclude = [ + "**/*_test.cc", + "**/geometrical_acoustics/**", + "**/test_util.cc", + ]) + +RESONANCE_HDRS = glob(["resonance_audio/**/*.h"]) + +PLATFORM_SRCS = glob(["platforms/common/**/*.cc"], exclude=["**/*_test.cc"]) + +PLATFORM_HDRS = glob(["platforms/common/**/*.h"]) + +SADIE_SRCS = ["third_party/SADIE_hrtf_database/generated/hrtf_assets.cc"] + +SADIE_HDRS = ["third_party/SADIE_hrtf_database/generated/hrtf_assets.h"] + +cc_library( + name = "resonance", + srcs = RESONANCE_SRCS + PLATFORM_SRCS + SADIE_SRCS, + hdrs = RESONANCE_HDRS + PLATFORM_HDRS + SADIE_HDRS, + deps = [ + "@eigen//:eigen", + "@libogg//:libogg", + "@libvorbis//:libvorbis", + "@pffft//:pffft", + ], + includes = [".", "resonance_audio"], + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.sdl2 b/redux/external/BUILD.sdl2 new file mode 100644 index 0000000..0d4a01c --- /dev/null +++ b/redux/external/BUILD.sdl2 @@ -0,0 +1,110 @@ +cc_library( + name = "sdl2", + srcs = glob([ + "src/*.c", + "src/**/*.h", + "src/atomic/*.c", + "src/audio/*.c", + "src/audio/dummy/*.c", + "src/core/*.c", + "src/core/dummy/*.c", + "src/cpuinfo/*.c", + "src/dynapi/*.c", + "src/events/*.c", + "src/file/*.c", + "src/filesystem/*.c", + "src/filesystem/dummy/*.c", + "src/haptic/*.c", + "src/haptic/dummy/*.c", + # "src/hidapi/*.c", + "src/joystick/*.c", + "src/joystick/dummy/*.c", + "src/loadso/*.c", + "src/loadso/dummy/*.c", + "src/locale/*.c", + "src/locale/dummy/*.c", + "src/misc/*.c", + "src/misc/dummy/*.c", + "src/render/*.c", + "src/sensor/*.c", + "src/sensor/dummy/*.c", + "src/stdlib/*.c", + "src/thread/*.c", + "src/timer/*.c", + "src/timer/dummy/*.c", + "src/video/*.c", + "src/video/dummy/*.c", + "src/video/offscreen/*.c", + "src/video/yuv2rgb/*.c", + ], exclude = [ + "src/hidapi/mac/hid.c", + ]) + select({ + "@platforms//os:osx": glob([ + "src/audio/coreaudio/*.c", + "src/audio/disk/*.c", + "src/core/unix/*.c", + "src/filesystem/unix/*.c", + "src/haptic/darwin/*.c", + # "src/hidapi/mac/*.c", + "src/joystick/darwin/*.c", + "src/loadso/dlopen/*.c", + "src/locale/macosx/*.c", + "src/misc/macosx/*.c", + "src/power/macosx/*.c", + "src/render/opengl/*.c", + "src/render/metal/*.c", + "src/render/software/*.c", + "src/sensor/coremotion/*.c", + "src/thread/pthread/*.c", + "src/timer/unix/*.c", + "src/video/dummy/cocoa/*.c", + "src/video/dummy/cocoa/*.c", + ]), + "@platforms//os:linux": glob([ + "src/audio/linux/*.c", + "src/core/unix/*.c", + "src/filesystem/unix/*.c", + "src/haptic/linux/*.c", + # "src/hidapi/linux/*.c", + "src/joystick/linux/*.c", + "src/loadso/dlopen/*.c", + "src/locale/unix/*.c", + "src/misc/unix/*.c", + "src/power/linux/*.c", + "src/render/opengl/*.c", + "src/render/software/*.c", + "src/thread/pthread/*.c", + "src/timer/unix/*.c", + "src/video/dummy/x11/*.c", + ]), + "@platforms//os:windows": glob([ + "src/core/windows/*.c", + "src/filesystem/windows/*.c", + "src/haptic/windows/*.c", + # "src/hidapi/windows/*.c", + "src/joystick/windows/*.c", + "src/loadso/windows/*.c", + "src/locale/windows/*.c", + "src/misc/windows/*.c", + "src/power/windows/*.c", + "src/render/opengl/*.c", + "src/render/software/*.c", + "src/sensor/windows/*.c", + "src/thread/windows/*.c", + "src/timer/windows/*.c", + "src/video/dummy/windows/*.c", + ]), + "//conditions:default": glob([ + "src/render/opengl/*.c", + "src/render/software/*.c", + ]), + }), + hdrs = glob(["include/**/*.h"]), + defines = select({ + "@platforms//os:linux": ["SDL_VIDEO_DRIVER_X11"], + "//conditions:default": [], + }), + textual_hdrs = ["src/thread/generic/SDL_syssem.c", "src/hidapi/mac/hid.c"], + includes = ["include", "src", "src/hidapi", "src/video/khronos"], + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.stblib b/redux/external/BUILD.stblib new file mode 100644 index 0000000..6bd16de --- /dev/null +++ b/redux/external/BUILD.stblib @@ -0,0 +1,7 @@ +cc_library( + name = "stblib", + srcs = glob(["*.c"]), + hdrs = glob(["*.h"]), + includes = ["."], + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.utfcpp b/redux/external/BUILD.utfcpp new file mode 100644 index 0000000..1f706a7 --- /dev/null +++ b/redux/external/BUILD.utfcpp @@ -0,0 +1,6 @@ +cc_library( + name = "utfcpp", + includes = ["source"], + hdrs = glob(["source/**/*.h"]), + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.vectorial b/redux/external/BUILD.vectorial new file mode 100644 index 0000000..14eccf4 --- /dev/null +++ b/redux/external/BUILD.vectorial @@ -0,0 +1,6 @@ +cc_library( + name = "vectorial", + includes = ["include"], + hdrs = glob(["include/vectorial/*.h"]), + visibility = ["//visibility:public"], +) diff --git a/redux/external/BUILD.zlib b/redux/external/BUILD.zlib new file mode 100644 index 0000000..fd2b011 --- /dev/null +++ b/redux/external/BUILD.zlib @@ -0,0 +1,11 @@ +cc_library( + name = "zlib", + srcs = glob(["*.c"]), + hdrs = glob(["*.h"]), + includes = ["."], + copts = select({ + "@platforms//os:macos": ["-Wno-implicit-function-declaration"], + "//conditions:default": [], + }), + visibility = ["//visibility:public"], +) diff --git a/redux/external/assimp.patch b/redux/external/assimp.patch new file mode 100644 index 0000000..9e32939 --- /dev/null +++ b/redux/external/assimp.patch @@ -0,0 +1,1177 @@ +diff --git a/code/Common/StbCommon.h b/code/Common/StbCommon.h +index 1265d25da..8ef283e69 100644 +--- a/code/Common/StbCommon.h ++++ b/code/Common/StbCommon.h +@@ -48,7 +48,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + #pragma GCC diagnostic ignored "-Wunused-function" + #endif + +-#include "stb/stb_image.h" ++#include "stb_image.h" + + #if _MSC_VER + #pragma warning(pop) +diff --git a/include/assimp/config.h b/include/assimp/config.h +new file mode 100644 +index 000000000..ca009e877 +--- /dev/null ++++ b/include/assimp/config.h +@@ -0,0 +1,1139 @@ ++/* ++--------------------------------------------------------------------------- ++Open Asset Import Library (assimp) ++--------------------------------------------------------------------------- ++ ++Copyright (c) 2006-2020, assimp team ++ ++ ++All rights reserved. ++ ++Redistribution and use of this software in source and binary forms, ++with or without modification, are permitted provided that the following ++conditions are met: ++ ++* Redistributions of source code must retain the above ++ copyright notice, this list of conditions and the ++ following disclaimer. ++ ++* Redistributions in binary form must reproduce the above ++ copyright notice, this list of conditions and the ++ following disclaimer in the documentation and/or other ++ materials provided with the distribution. ++ ++* Neither the name of the assimp team, nor the names of its ++ contributors may be used to endorse or promote products ++ derived from this software without specific prior ++ written permission of the assimp team. ++ ++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ++A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ++OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ++SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ++LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ++DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ++THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ++(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ++OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ++--------------------------------------------------------------------------- ++*/ ++ ++/** @file config.h ++ * @brief Defines constants for configurable properties for the library ++ * ++ * Typically these properties are set via ++ * #Assimp::Importer::SetPropertyFloat, ++ * #Assimp::Importer::SetPropertyInteger or ++ * #Assimp::Importer::SetPropertyString, ++ * depending on the data type of a property. All properties have a ++ * default value. See the doc for the mentioned methods for more details. ++ * ++ *

++ * The corresponding functions for use with the plain-c API are: ++ * #aiSetImportPropertyInteger, ++ * #aiSetImportPropertyFloat, ++ * #aiSetImportPropertyString ++ */ ++#pragma once ++#ifndef AI_CONFIG_H_INC ++#define AI_CONFIG_H_INC ++ ++ ++// ########################################################################### ++// LIBRARY SETTINGS ++// General, global settings ++// ########################################################################### ++ ++// --------------------------------------------------------------------------- ++/** @brief Enables time measurements. ++ * ++ * If enabled, measures the time needed for each part of the loading ++ * process (i.e. IO time, importing, postprocessing, ..) and dumps ++ * these timings to the DefaultLogger. See the @link perf Performance ++ * Page@endlink for more information on this topic. ++ * ++ * Property type: bool. Default value: false. ++ */ ++#define AI_CONFIG_GLOB_MEASURE_TIME \ ++ "GLOB_MEASURE_TIME" ++ ++ ++// --------------------------------------------------------------------------- ++/** @brief Global setting to disable generation of skeleton dummy meshes ++ * ++ * Skeleton dummy meshes are generated as a visualization aid in cases which ++ * the input data contains no geometry, but only animation data. ++ * Property data type: bool. Default value: false ++ */ ++// --------------------------------------------------------------------------- ++#define AI_CONFIG_IMPORT_NO_SKELETON_MESHES \ ++ "IMPORT_NO_SKELETON_MESHES" ++ ++ ++ ++# if 0 // not implemented yet ++// --------------------------------------------------------------------------- ++/** @brief Set Assimp's multithreading policy. ++ * ++ * This setting is ignored if Assimp was built without boost.thread ++ * support (ASSIMP_BUILD_NO_THREADING, which is implied by ASSIMP_BUILD_BOOST_WORKAROUND). ++ * Possible values are: -1 to let Assimp decide what to do, 0 to disable ++ * multithreading entirely and any number larger than 0 to force a specific ++ * number of threads. Assimp is always free to ignore this settings, which is ++ * merely a hint. Usually, the default value (-1) will be fine. However, if ++ * Assimp is used concurrently from multiple user threads, it might be useful ++ * to limit each Importer instance to a specific number of cores. ++ * ++ * For more information, see the @link threading Threading page@endlink. ++ * Property type: int, default value: -1. ++ */ ++#define AI_CONFIG_GLOB_MULTITHREADING \ ++ "GLOB_MULTITHREADING" ++#endif ++ ++// ########################################################################### ++// POST PROCESSING SETTINGS ++// Various stuff to fine-tune the behavior of a specific post processing step. ++// ########################################################################### ++ ++ ++// --------------------------------------------------------------------------- ++/** @brief Maximum bone count per mesh for the SplitbyBoneCount step. ++ * ++ * Meshes are split until the maximum number of bones is reached. The default ++ * value is AI_SBBC_DEFAULT_MAX_BONES, which may be altered at ++ * compile-time. ++ * Property data type: integer. ++ */ ++// --------------------------------------------------------------------------- ++#define AI_CONFIG_PP_SBBC_MAX_BONES \ ++ "PP_SBBC_MAX_BONES" ++ ++ ++// default limit for bone count ++#if (!defined AI_SBBC_DEFAULT_MAX_BONES) ++# define AI_SBBC_DEFAULT_MAX_BONES 60 ++#endif ++ ++ ++// --------------------------------------------------------------------------- ++/** @brief Specifies the maximum angle that may be between two vertex tangents ++ * that their tangents and bi-tangents are smoothed. ++ * ++ * This applies to the CalcTangentSpace-Step. The angle is specified ++ * in degrees. The maximum value is 175. ++ * Property type: float. Default value: 45 degrees ++ */ ++#define AI_CONFIG_PP_CT_MAX_SMOOTHING_ANGLE \ ++ "PP_CT_MAX_SMOOTHING_ANGLE" ++ ++// --------------------------------------------------------------------------- ++/** @brief Source UV channel for tangent space computation. ++ * ++ * The specified channel must exist or an error will be raised. ++ * Property type: integer. Default value: 0 ++ */ ++// --------------------------------------------------------------------------- ++#define AI_CONFIG_PP_CT_TEXTURE_CHANNEL_INDEX \ ++ "PP_CT_TEXTURE_CHANNEL_INDEX" ++ ++// --------------------------------------------------------------------------- ++/** @brief Specifies the maximum angle that may be between two face normals ++ * at the same vertex position that their are smoothed together. ++ * ++ * Sometimes referred to as 'crease angle'. ++ * This applies to the GenSmoothNormals-Step. The angle is specified ++ * in degrees, so 180 is PI. The default value is 175 degrees (all vertex ++ * normals are smoothed). The maximum value is 175, too. Property type: float. ++ * Warning: setting this option may cause a severe loss of performance. The ++ * performance is unaffected if the #AI_CONFIG_FAVOUR_SPEED flag is set but ++ * the output quality may be reduced. ++ */ ++#define AI_CONFIG_PP_GSN_MAX_SMOOTHING_ANGLE \ ++ "PP_GSN_MAX_SMOOTHING_ANGLE" ++ ++ ++// --------------------------------------------------------------------------- ++/** @brief Sets the colormap (= palette) to be used to decode embedded ++ * textures in MDL (Quake or 3DGS) files. ++ * ++ * This must be a valid path to a file. The file is 768 (256*3) bytes ++ * large and contains RGB triplets for each of the 256 palette entries. ++ * The default value is colormap.lmp. If the file is not found, ++ * a default palette (from Quake 1) is used. ++ * Property type: string. ++ */ ++#define AI_CONFIG_IMPORT_MDL_COLORMAP \ ++ "IMPORT_MDL_COLORMAP" ++ ++// --------------------------------------------------------------------------- ++/** @brief Configures the #aiProcess_RemoveRedundantMaterials step to ++ * keep materials matching a name in a given list. ++ * ++ * This is a list of 1 to n strings, ' ' serves as delimiter character. ++ * Identifiers containing whitespaces must be enclosed in *single* ++ * quotation marks. For example: ++ * "keep-me and_me_to anotherMaterialToBeKept \'name with whitespace\'". ++ * If a material matches on of these names, it will not be modified or ++ * removed by the postprocessing step nor will other materials be replaced ++ * by a reference to it.
++ * This option might be useful if you are using some magic material names ++ * to pass additional semantics through the content pipeline. This ensures ++ * they won't be optimized away, but a general optimization is still ++ * performed for materials not contained in the list. ++ * Property type: String. Default value: n/a ++ * @note Linefeeds, tabs or carriage returns are treated as whitespace. ++ * Material names are case sensitive. ++ */ ++#define AI_CONFIG_PP_RRM_EXCLUDE_LIST \ ++ "PP_RRM_EXCLUDE_LIST" ++ ++// --------------------------------------------------------------------------- ++/** @brief Configures the #aiProcess_PreTransformVertices step to ++ * keep the scene hierarchy. Meshes are moved to worldspace, but ++ * no optimization is performed (read: meshes with equal materials are not ++ * joined. The total number of meshes won't change). ++ * ++ * This option could be of use for you if the scene hierarchy contains ++ * important additional information which you intend to parse. ++ * For rendering, you can still render all meshes in the scene without ++ * any transformations. ++ * Property type: bool. Default value: false. ++ */ ++#define AI_CONFIG_PP_PTV_KEEP_HIERARCHY \ ++ "PP_PTV_KEEP_HIERARCHY" ++ ++// --------------------------------------------------------------------------- ++/** @brief Configures the #aiProcess_PreTransformVertices step to normalize ++ * all vertex components into the [-1,1] range. That is, a bounding box ++ * for the whole scene is computed, the maximum component is taken and all ++ * meshes are scaled appropriately (uniformly of course!). ++ * This might be useful if you don't know the spatial dimension of the input ++ * data*/ ++#define AI_CONFIG_PP_PTV_NORMALIZE \ ++ "PP_PTV_NORMALIZE" ++ ++// --------------------------------------------------------------------------- ++/** @brief Configures the #aiProcess_PreTransformVertices step to use ++ * a users defined matrix as the scene root node transformation before ++ * transforming vertices. ++ * Property type: bool. Default value: false. ++ */ ++#define AI_CONFIG_PP_PTV_ADD_ROOT_TRANSFORMATION \ ++ "PP_PTV_ADD_ROOT_TRANSFORMATION" ++ ++// --------------------------------------------------------------------------- ++/** @brief Configures the #aiProcess_PreTransformVertices step to use ++ * a users defined matrix as the scene root node transformation before ++ * transforming vertices. This property correspond to the 'a1' component ++ * of the transformation matrix. ++ * Property type: aiMatrix4x4. ++ */ ++#define AI_CONFIG_PP_PTV_ROOT_TRANSFORMATION \ ++ "PP_PTV_ROOT_TRANSFORMATION" ++ ++// --------------------------------------------------------------------------- ++/** @brief Configures the #aiProcess_FindDegenerates step to ++ * remove degenerated primitives from the import - immediately. ++ * ++ * The default behaviour converts degenerated triangles to lines and ++ * degenerated lines to points. See the documentation to the ++ * #aiProcess_FindDegenerates step for a detailed example of the various ways ++ * to get rid of these lines and points if you don't want them. ++ * Property type: bool. Default value: false. ++ */ ++#define AI_CONFIG_PP_FD_REMOVE \ ++ "PP_FD_REMOVE" ++ ++// --------------------------------------------------------------------------- ++/** ++ * @brief Configures the #aiProcess_FindDegenerates to check the area of a ++ * trinagle to be greates than e-6. If this is not the case the triangle will ++ * be removed if #AI_CONFIG_PP_FD_REMOVE is set to true. ++ */ ++#define AI_CONFIG_PP_FD_CHECKAREA \ ++ "PP_FD_CHECKAREA" ++ ++// --------------------------------------------------------------------------- ++/** @brief Configures the #aiProcess_OptimizeGraph step to preserve nodes ++ * matching a name in a given list. ++ * ++ * This is a list of 1 to n strings, ' ' serves as delimiter character. ++ * Identifiers containing whitespaces must be enclosed in *single* ++ * quotation marks. For example: ++ * "keep-me and_me_to anotherNodeToBeKept \'name with whitespace\'". ++ * If a node matches on of these names, it will not be modified or ++ * removed by the postprocessing step.
++ * This option might be useful if you are using some magic node names ++ * to pass additional semantics through the content pipeline. This ensures ++ * they won't be optimized away, but a general optimization is still ++ * performed for nodes not contained in the list. ++ * Property type: String. Default value: n/a ++ * @note Linefeeds, tabs or carriage returns are treated as whitespace. ++ * Node names are case sensitive. ++ */ ++#define AI_CONFIG_PP_OG_EXCLUDE_LIST \ ++ "PP_OG_EXCLUDE_LIST" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set the maximum number of triangles in a mesh. ++ * ++ * This is used by the "SplitLargeMeshes" PostProcess-Step to determine ++ * whether a mesh must be split or not. ++ * @note The default value is AI_SLM_DEFAULT_MAX_TRIANGLES ++ * Property type: integer. ++ */ ++#define AI_CONFIG_PP_SLM_TRIANGLE_LIMIT \ ++ "PP_SLM_TRIANGLE_LIMIT" ++ ++// default value for AI_CONFIG_PP_SLM_TRIANGLE_LIMIT ++#if (!defined AI_SLM_DEFAULT_MAX_TRIANGLES) ++# define AI_SLM_DEFAULT_MAX_TRIANGLES 1000000 ++#endif ++ ++// --------------------------------------------------------------------------- ++/** @brief Set the maximum number of vertices in a mesh. ++ * ++ * This is used by the "SplitLargeMeshes" PostProcess-Step to determine ++ * whether a mesh must be split or not. ++ * @note The default value is AI_SLM_DEFAULT_MAX_VERTICES ++ * Property type: integer. ++ */ ++#define AI_CONFIG_PP_SLM_VERTEX_LIMIT \ ++ "PP_SLM_VERTEX_LIMIT" ++ ++// default value for AI_CONFIG_PP_SLM_VERTEX_LIMIT ++#if (!defined AI_SLM_DEFAULT_MAX_VERTICES) ++# define AI_SLM_DEFAULT_MAX_VERTICES 1000000 ++#endif ++ ++// --------------------------------------------------------------------------- ++/** @brief Set the maximum number of bones affecting a single vertex ++ * ++ * This is used by the #aiProcess_LimitBoneWeights PostProcess-Step. ++ * @note The default value is AI_LMW_MAX_WEIGHTS ++ * Property type: integer.*/ ++#define AI_CONFIG_PP_LBW_MAX_WEIGHTS \ ++ "PP_LBW_MAX_WEIGHTS" ++ ++// default value for AI_CONFIG_PP_LBW_MAX_WEIGHTS ++#if (!defined AI_LMW_MAX_WEIGHTS) ++# define AI_LMW_MAX_WEIGHTS 0x4 ++#endif // !! AI_LMW_MAX_WEIGHTS ++ ++// --------------------------------------------------------------------------- ++/** @brief Lower the deboning threshold in order to remove more bones. ++ * ++ * This is used by the #aiProcess_Debone PostProcess-Step. ++ * @note The default value is AI_DEBONE_THRESHOLD ++ * Property type: float.*/ ++#define AI_CONFIG_PP_DB_THRESHOLD \ ++ "PP_DB_THRESHOLD" ++ ++// default value for AI_CONFIG_PP_LBW_MAX_WEIGHTS ++#if (!defined AI_DEBONE_THRESHOLD) ++# define AI_DEBONE_THRESHOLD 1.0f ++#endif // !! AI_DEBONE_THRESHOLD ++ ++// --------------------------------------------------------------------------- ++/** @brief Require all bones qualify for deboning before removing any ++ * ++ * This is used by the #aiProcess_Debone PostProcess-Step. ++ * @note The default value is 0 ++ * Property type: bool.*/ ++#define AI_CONFIG_PP_DB_ALL_OR_NONE \ ++ "PP_DB_ALL_OR_NONE" ++ ++/** @brief Default value for the #AI_CONFIG_PP_ICL_PTCACHE_SIZE property ++ */ ++#ifndef PP_ICL_PTCACHE_SIZE ++# define PP_ICL_PTCACHE_SIZE 12 ++#endif ++ ++// --------------------------------------------------------------------------- ++/** @brief Set the size of the post-transform vertex cache to optimize the ++ * vertices for. This configures the #aiProcess_ImproveCacheLocality step. ++ * ++ * The size is given in vertices. Of course you can't know how the vertex ++ * format will exactly look like after the import returns, but you can still ++ * guess what your meshes will probably have. ++ * @note The default value is #PP_ICL_PTCACHE_SIZE. That results in slight ++ * performance improvements for most nVidia/AMD cards since 2002. ++ * Property type: integer. ++ */ ++#define AI_CONFIG_PP_ICL_PTCACHE_SIZE "PP_ICL_PTCACHE_SIZE" ++ ++// --------------------------------------------------------------------------- ++/** @brief Enumerates components of the aiScene and aiMesh data structures ++ * that can be excluded from the import using the #aiProcess_RemoveComponent step. ++ * ++ * See the documentation to #aiProcess_RemoveComponent for more details. ++ */ ++enum aiComponent ++{ ++ /** Normal vectors */ ++#ifdef SWIG ++ aiComponent_NORMALS = 0x2, ++#else ++ aiComponent_NORMALS = 0x2u, ++#endif ++ ++ /** Tangents and bitangents go always together ... */ ++#ifdef SWIG ++ aiComponent_TANGENTS_AND_BITANGENTS = 0x4, ++#else ++ aiComponent_TANGENTS_AND_BITANGENTS = 0x4u, ++#endif ++ ++ /** ALL color sets ++ * Use aiComponent_COLORn(N) to specify the N'th set */ ++ aiComponent_COLORS = 0x8, ++ ++ /** ALL texture UV sets ++ * aiComponent_TEXCOORDn(N) to specify the N'th set */ ++ aiComponent_TEXCOORDS = 0x10, ++ ++ /** Removes all bone weights from all meshes. ++ * The scenegraph nodes corresponding to the bones are NOT removed. ++ * use the #aiProcess_OptimizeGraph step to do this */ ++ aiComponent_BONEWEIGHTS = 0x20, ++ ++ /** Removes all node animations (aiScene::mAnimations). ++ * The corresponding scenegraph nodes are NOT removed. ++ * use the #aiProcess_OptimizeGraph step to do this */ ++ aiComponent_ANIMATIONS = 0x40, ++ ++ /** Removes all embedded textures (aiScene::mTextures) */ ++ aiComponent_TEXTURES = 0x80, ++ ++ /** Removes all light sources (aiScene::mLights). ++ * The corresponding scenegraph nodes are NOT removed. ++ * use the #aiProcess_OptimizeGraph step to do this */ ++ aiComponent_LIGHTS = 0x100, ++ ++ /** Removes all cameras (aiScene::mCameras). ++ * The corresponding scenegraph nodes are NOT removed. ++ * use the #aiProcess_OptimizeGraph step to do this */ ++ aiComponent_CAMERAS = 0x200, ++ ++ /** Removes all meshes (aiScene::mMeshes). */ ++ aiComponent_MESHES = 0x400, ++ ++ /** Removes all materials. One default material will ++ * be generated, so aiScene::mNumMaterials will be 1. */ ++ aiComponent_MATERIALS = 0x800, ++ ++ ++ /** This value is not used. It is just there to force the ++ * compiler to map this enum to a 32 Bit integer. */ ++#ifndef SWIG ++ _aiComponent_Force32Bit = 0x9fffffff ++#endif ++}; ++ ++// Remove a specific color channel 'n' ++#define aiComponent_COLORSn(n) (1u << (n+20u)) ++ ++// Remove a specific UV channel 'n' ++#define aiComponent_TEXCOORDSn(n) (1u << (n+25u)) ++ ++// --------------------------------------------------------------------------- ++/** @brief Input parameter to the #aiProcess_RemoveComponent step: ++ * Specifies the parts of the data structure to be removed. ++ * ++ * See the documentation to this step for further details. The property ++ * is expected to be an integer, a bitwise combination of the ++ * #aiComponent flags defined above in this header. The default ++ * value is 0. Important: if no valid mesh is remaining after the ++ * step has been executed (e.g you thought it was funny to specify ALL ++ * of the flags defined above) the import FAILS. Mainly because there is ++ * no data to work on anymore ... ++ */ ++#define AI_CONFIG_PP_RVC_FLAGS \ ++ "PP_RVC_FLAGS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Input parameter to the #aiProcess_SortByPType step: ++ * Specifies which primitive types are removed by the step. ++ * ++ * This is a bitwise combination of the aiPrimitiveType flags. ++ * Specifying all of them is illegal, of course. A typical use would ++ * be to exclude all line and point meshes from the import. This ++ * is an integer property, its default value is 0. ++ */ ++#define AI_CONFIG_PP_SBP_REMOVE \ ++ "PP_SBP_REMOVE" ++ ++// --------------------------------------------------------------------------- ++/** @brief Input parameter to the #aiProcess_FindInvalidData step: ++ * Specifies the floating-point accuracy for animation values. The step ++ * checks for animation tracks where all frame values are absolutely equal ++ * and removes them. This tweakable controls the epsilon for floating-point ++ * comparisons - two keys are considered equal if the invariant ++ * abs(n0-n1)>epsilon holds true for all vector respectively quaternion ++ * components. The default value is 0.f - comparisons are exact then. ++ */ ++#define AI_CONFIG_PP_FID_ANIM_ACCURACY \ ++ "PP_FID_ANIM_ACCURACY" ++ ++// --------------------------------------------------------------------------- ++/** @brief Input parameter to the #aiProcess_FindInvalidData step: ++ * Set to true to ignore texture coordinates. This may be useful if you have ++ * to assign different kind of textures like one for the summer or one for the winter. ++ */ ++#define AI_CONFIG_PP_FID_IGNORE_TEXTURECOORDS \ ++ "PP_FID_IGNORE_TEXTURECOORDS" ++ ++// TransformUVCoords evaluates UV scalings ++#define AI_UVTRAFO_SCALING 0x1 ++ ++// TransformUVCoords evaluates UV rotations ++#define AI_UVTRAFO_ROTATION 0x2 ++ ++// TransformUVCoords evaluates UV translation ++#define AI_UVTRAFO_TRANSLATION 0x4 ++ ++// Everything baked together -> default value ++#define AI_UVTRAFO_ALL (AI_UVTRAFO_SCALING | AI_UVTRAFO_ROTATION | AI_UVTRAFO_TRANSLATION) ++ ++// --------------------------------------------------------------------------- ++/** @brief Input parameter to the #aiProcess_TransformUVCoords step: ++ * Specifies which UV transformations are evaluated. ++ * ++ * This is a bitwise combination of the AI_UVTRAFO_XXX flags (integer ++ * property, of course). By default all transformations are enabled ++ * (AI_UVTRAFO_ALL). ++ */ ++#define AI_CONFIG_PP_TUV_EVALUATE \ ++ "PP_TUV_EVALUATE" ++ ++// --------------------------------------------------------------------------- ++/** @brief A hint to assimp to favour speed against import quality. ++ * ++ * Enabling this option may result in faster loading, but it needn't. ++ * It represents just a hint to loaders and post-processing steps to use ++ * faster code paths, if possible. ++ * This property is expected to be an integer, != 0 stands for true. ++ * The default value is 0. ++ */ ++#define AI_CONFIG_FAVOUR_SPEED \ ++ "FAVOUR_SPEED" ++ ++ ++// ########################################################################### ++// IMPORTER SETTINGS ++// Various stuff to fine-tune the behaviour of specific importer plugins. ++// ########################################################################### ++ ++// --------------------------------------------------------------------------- ++/** @brief Importers which parse JSON may use this to obtain a pointer to a ++ * rapidjson::IRemoteSchemaDocumentProvider. ++ * ++ * The default value is nullptr ++ * Property type: void* ++ */ ++#define AI_CONFIG_IMPORT_SCHEMA_DOCUMENT_PROVIDER \ ++ "IMPORT_SCHEMA_DOCUMENT_PROVIDER" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the fbx importer will merge all geometry layers present ++ * in the source file or take only the first. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_FBX_READ_ALL_GEOMETRY_LAYERS \ ++ "IMPORT_FBX_READ_ALL_GEOMETRY_LAYERS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the fbx importer will read all materials present in the ++ * source file or take only the referenced materials. ++ * ++ * This is void unless IMPORT_FBX_READ_MATERIALS=1. ++ * ++ * The default value is false (0) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_FBX_READ_ALL_MATERIALS \ ++ "IMPORT_FBX_READ_ALL_MATERIALS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the fbx importer will read materials. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_FBX_READ_MATERIALS \ ++ "IMPORT_FBX_READ_MATERIALS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the fbx importer will read embedded textures. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_FBX_READ_TEXTURES \ ++ "IMPORT_FBX_READ_TEXTURES" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the fbx importer will read cameras. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_FBX_READ_CAMERAS \ ++ "IMPORT_FBX_READ_CAMERAS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the fbx importer will read light sources. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_FBX_READ_LIGHTS \ ++ "IMPORT_FBX_READ_LIGHTS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the fbx importer will read animations. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS \ ++ "IMPORT_FBX_READ_ANIMATIONS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the fbx importer will read weights. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_FBX_READ_WEIGHTS \ ++ "IMPORT_FBX_READ_WEIGHTS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the fbx importer will act in strict mode in which only ++ * FBX 2013 is supported and any other sub formats are rejected. FBX 2013 ++ * is the primary target for the importer, so this format is best ++ * supported and well-tested. ++ * ++ * The default value is false (0) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_FBX_STRICT_MODE \ ++ "IMPORT_FBX_STRICT_MODE" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the fbx importer will preserve pivot points for ++ * transformations (as extra nodes). If set to false, pivots and offsets ++ * will be evaluated whenever possible. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS \ ++ "IMPORT_FBX_PRESERVE_PIVOTS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Specifies whether the importer will drop empty animation curves or ++ * animation curves which match the bind pose transformation over their ++ * entire defined range. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES \ ++ "IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the fbx importer will use the legacy embedded texture naming. ++ * ++ * The default value is false (0) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_FBX_EMBEDDED_TEXTURES_LEGACY_NAMING \ ++ "AI_CONFIG_IMPORT_FBX_EMBEDDED_TEXTURES_LEGACY_NAMING" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set wether the importer shall not remove empty bones. ++ * ++ * Empty bone are often used to define connections for other models. ++ */ ++#define AI_CONFIG_IMPORT_REMOVE_EMPTY_BONES \ ++ "AI_CONFIG_IMPORT_REMOVE_EMPTY_BONES" ++ ++ ++// --------------------------------------------------------------------------- ++/** @brief Set wether the FBX importer shall convert the unit from cm to m. ++ */ ++#define AI_CONFIG_FBX_CONVERT_TO_M \ ++ "AI_CONFIG_FBX_CONVERT_TO_M" ++ ++// --------------------------------------------------------------------------- ++/** @brief Will enable the skeleton structo to store bone data. ++ * ++ * This will decouple the bone coupling to the mesh. This feature is ++ * experimental. ++ */ ++#define AI_CONFIG_FBX_USE_SKELETON_BONE_CONTAINER \ ++ "AI_CONFIG_FBX_USE_SKELETON_BONE_CONTAINER" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set the vertex animation keyframe to be imported ++ * ++ * ASSIMP does not support vertex keyframes (only bone animation is supported). ++ * The library reads only one frame of models with vertex animations. ++ * By default this is the first frame. ++ * \note The default value is 0. This option applies to all importers. ++ * However, it is also possible to override the global setting ++ * for a specific loader. You can use the AI_CONFIG_IMPORT_XXX_KEYFRAME ++ * options (where XXX is a placeholder for the file format for which you ++ * want to override the global setting). ++ * Property type: integer. ++ */ ++#define AI_CONFIG_IMPORT_GLOBAL_KEYFRAME "IMPORT_GLOBAL_KEYFRAME" ++ ++#define AI_CONFIG_IMPORT_MD3_KEYFRAME "IMPORT_MD3_KEYFRAME" ++#define AI_CONFIG_IMPORT_MD2_KEYFRAME "IMPORT_MD2_KEYFRAME" ++#define AI_CONFIG_IMPORT_MDL_KEYFRAME "IMPORT_MDL_KEYFRAME" ++#define AI_CONFIG_IMPORT_MDC_KEYFRAME "IMPORT_MDC_KEYFRAME" ++#define AI_CONFIG_IMPORT_SMD_KEYFRAME "IMPORT_SMD_KEYFRAME" ++#define AI_CONFIG_IMPORT_UNREAL_KEYFRAME "IMPORT_UNREAL_KEYFRAME" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the MDL (HL1) importer will read animations. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_MDL_HL1_READ_ANIMATIONS "IMPORT_MDL_HL1_READ_ANIMATIONS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the MDL (HL1) importer will read animation events. ++ * \note This property requires AI_CONFIG_IMPORT_MDL_HL1_READ_ANIMATIONS to be set to true. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_MDL_HL1_READ_ANIMATION_EVENTS "IMPORT_MDL_HL1_READ_ANIMATION_EVENTS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the MDL (HL1) importer will read blend controllers. ++ * \note This property requires AI_CONFIG_IMPORT_MDL_HL1_READ_ANIMATIONS to be set to true. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_MDL_HL1_READ_BLEND_CONTROLLERS "IMPORT_MDL_HL1_READ_BLEND_CONTROLLERS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the MDL (HL1) importer will read sequence transition graph. ++ * \note This property requires AI_CONFIG_IMPORT_MDL_HL1_READ_ANIMATIONS to be set to true. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_MDL_HL1_READ_SEQUENCE_TRANSITIONS "IMPORT_MDL_HL1_READ_SEQUENCE_TRANSITIONS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the MDL (HL1) importer will read attachments info. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_MDL_HL1_READ_ATTACHMENTS "IMPORT_MDL_HL1_READ_ATTACHMENTS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the MDL (HL1) importer will read bone controllers info. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_MDL_HL1_READ_BONE_CONTROLLERS "IMPORT_MDL_HL1_READ_BONE_CONTROLLERS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the MDL (HL1) importer will read hitboxes info. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_MDL_HL1_READ_HITBOXES "IMPORT_MDL_HL1_READ_HITBOXES" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set whether the MDL (HL1) importer will read miscellaneous global model info. ++ * ++ * The default value is true (1) ++ * Property type: bool ++ */ ++#define AI_CONFIG_IMPORT_MDL_HL1_READ_MISC_GLOBAL_INFO "IMPORT_MDL_HL1_READ_MISC_GLOBAL_INFO" ++ ++// --------------------------------------------------------------------------- ++/** Smd load multiple animations ++ * ++ * Property type: bool. Default value: true. ++ */ ++#define AI_CONFIG_IMPORT_SMD_LOAD_ANIMATION_LIST "IMPORT_SMD_LOAD_ANIMATION_LIST" ++ ++// --------------------------------------------------------------------------- ++/** @brief Configures the AC loader to collect all surfaces which have the ++ * "Backface cull" flag set in separate meshes. ++ * ++ * Property type: bool. Default value: true. ++ */ ++#define AI_CONFIG_IMPORT_AC_SEPARATE_BFCULL \ ++ "IMPORT_AC_SEPARATE_BFCULL" ++ ++// --------------------------------------------------------------------------- ++/** @brief Configures whether the AC loader evaluates subdivision surfaces ( ++ * indicated by the presence of the 'subdiv' attribute in the file). By ++ * default, Assimp performs the subdivision using the standard ++ * Catmull-Clark algorithm ++ * ++ * * Property type: bool. Default value: true. ++ */ ++#define AI_CONFIG_IMPORT_AC_EVAL_SUBDIVISION \ ++ "IMPORT_AC_EVAL_SUBDIVISION" ++ ++// --------------------------------------------------------------------------- ++/** @brief Configures the UNREAL 3D loader to separate faces with different ++ * surface flags (e.g. two-sided vs. single-sided). ++ * ++ * * Property type: bool. Default value: true. ++ */ ++#define AI_CONFIG_IMPORT_UNREAL_HANDLE_FLAGS \ ++ "UNREAL_HANDLE_FLAGS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Configures the terragen import plugin to compute uv's for ++ * terrains, if not given. Furthermore a default texture is assigned. ++ * ++ * UV coordinates for terrains are so simple to compute that you'll usually ++ * want to compute them on your own, if you need them. This option is intended ++ * for model viewers which want to offer an easy way to apply textures to ++ * terrains. ++ * * Property type: bool. Default value: false. ++ */ ++#define AI_CONFIG_IMPORT_TER_MAKE_UVS \ ++ "IMPORT_TER_MAKE_UVS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Configures the ASE loader to always reconstruct normal vectors ++ * basing on the smoothing groups loaded from the file. ++ * ++ * Some ASE files have carry invalid normals, other don't. ++ * * Property type: bool. Default value: true. ++ */ ++#define AI_CONFIG_IMPORT_ASE_RECONSTRUCT_NORMALS \ ++ "IMPORT_ASE_RECONSTRUCT_NORMALS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Configures the M3D loader to detect and process multi-part ++ * Quake player models. ++ * ++ * These models usually consist of 3 files, lower.md3, upper.md3 and ++ * head.md3. If this property is set to true, Assimp will try to load and ++ * combine all three files if one of them is loaded. ++ * Property type: bool. Default value: true. ++ */ ++#define AI_CONFIG_IMPORT_MD3_HANDLE_MULTIPART \ ++ "IMPORT_MD3_HANDLE_MULTIPART" ++ ++// --------------------------------------------------------------------------- ++/** @brief Tells the MD3 loader which skin files to load. ++ * ++ * When loading MD3 files, Assimp checks whether a file ++ * [md3_file_name]_[skin_name].skin is existing. These files are used by ++ * Quake III to be able to assign different skins (e.g. red and blue team) ++ * to models. 'default', 'red', 'blue' are typical skin names. ++ * Property type: String. Default value: "default". ++ */ ++#define AI_CONFIG_IMPORT_MD3_SKIN_NAME \ ++ "IMPORT_MD3_SKIN_NAME" ++ ++// --------------------------------------------------------------------------- ++/** @brief Specify if to try load Quake 3 shader files. This also controls ++ * original surface name handling: when disabled it will be used unchanged. ++ * ++ * Property type: bool. Default value: true. ++ */ ++#define AI_CONFIG_IMPORT_MD3_LOAD_SHADERS \ ++ "IMPORT_MD3_LOAD_SHADERS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Specify the Quake 3 shader file to be used for a particular ++ * MD3 file. This can also be a search path. ++ * ++ * By default Assimp's behaviour is as follows: If a MD3 file ++ * any_path/models/any_q3_subdir/model_name/file_name.md3 is ++ * loaded, the library tries to locate the corresponding shader file in ++ * any_path/scripts/model_name.shader. This property overrides this ++ * behaviour. It can either specify a full path to the shader to be loaded ++ * or alternatively the path (relative or absolute) to the directory where ++ * the shaders for all MD3s to be loaded reside. Assimp attempts to open ++ * IMPORT_MD3_SHADER_SRC/model_name.shader first, IMPORT_MD3_SHADER_SRC/file_name.shader ++ * is the fallback file. Note that IMPORT_MD3_SHADER_SRC should have a terminal (back)slash. ++ * Property type: String. Default value: n/a. ++ */ ++#define AI_CONFIG_IMPORT_MD3_SHADER_SRC \ ++ "IMPORT_MD3_SHADER_SRC" ++ ++// --------------------------------------------------------------------------- ++/** @brief Configures the LWO loader to load just one layer from the model. ++ * ++ * LWO files consist of layers and in some cases it could be useful to load ++ * only one of them. This property can be either a string - which specifies ++ * the name of the layer - or an integer - the index of the layer. If the ++ * property is not set the whole LWO model is loaded. Loading fails if the ++ * requested layer is not available. The layer index is zero-based and the ++ * layer name may not be empty.
++ * Property type: Integer. Default value: all layers are loaded. ++ */ ++#define AI_CONFIG_IMPORT_LWO_ONE_LAYER_ONLY \ ++ "IMPORT_LWO_ONE_LAYER_ONLY" ++ ++// --------------------------------------------------------------------------- ++/** @brief Configures the MD5 loader to not load the MD5ANIM file for ++ * a MD5MESH file automatically. ++ * ++ * The default strategy is to look for a file with the same name but the ++ * MD5ANIM extension in the same directory. If it is found, it is loaded ++ * and combined with the MD5MESH file. This configuration option can be ++ * used to disable this behaviour. ++ * ++ * * Property type: bool. Default value: false. ++ */ ++#define AI_CONFIG_IMPORT_MD5_NO_ANIM_AUTOLOAD \ ++ "IMPORT_MD5_NO_ANIM_AUTOLOAD" ++ ++// --------------------------------------------------------------------------- ++/** @brief Defines the begin of the time range for which the LWS loader ++ * evaluates animations and computes aiNodeAnim's. ++ * ++ * Assimp provides full conversion of LightWave's envelope system, including ++ * pre and post conditions. The loader computes linearly subsampled animation ++ * chanels with the frame rate given in the LWS file. This property defines ++ * the start time. Note: animation channels are only generated if a node ++ * has at least one envelope with more tan one key assigned. This property. ++ * is given in frames, '0' is the first frame. By default, if this property ++ * is not set, the importer takes the animation start from the input LWS ++ * file ('FirstFrame' line)
++ * Property type: Integer. Default value: taken from file. ++ * ++ * @see AI_CONFIG_IMPORT_LWS_ANIM_END - end of the imported time range ++ */ ++#define AI_CONFIG_IMPORT_LWS_ANIM_START \ ++ "IMPORT_LWS_ANIM_START" ++#define AI_CONFIG_IMPORT_LWS_ANIM_END \ ++ "IMPORT_LWS_ANIM_END" ++ ++// --------------------------------------------------------------------------- ++/** @brief Defines the output frame rate of the IRR loader. ++ * ++ * IRR animations are difficult to convert for Assimp and there will ++ * always be a loss of quality. This setting defines how many keys per second ++ * are returned by the converter.
++ * Property type: integer. Default value: 100 ++ */ ++#define AI_CONFIG_IMPORT_IRR_ANIM_FPS \ ++ "IMPORT_IRR_ANIM_FPS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Ogre Importer will try to find referenced materials from this file. ++ * ++ * Ogre meshes reference with material names, this does not tell Assimp the file ++ * where it is located in. Assimp will try to find the source file in the following ++ * order: .material, .material and ++ * lastly the material name defined by this config property. ++ *
++ * Property type: String. Default value: Scene.material. ++ */ ++#define AI_CONFIG_IMPORT_OGRE_MATERIAL_FILE \ ++ "IMPORT_OGRE_MATERIAL_FILE" ++ ++// --------------------------------------------------------------------------- ++/** @brief Ogre Importer detect the texture usage from its filename. ++ * ++ * Ogre material texture units do not define texture type, the textures usage ++ * depends on the used shader or Ogre's fixed pipeline. If this config property ++ * is true Assimp will try to detect the type from the textures filename postfix: ++ * _n, _nrm, _nrml, _normal, _normals and _normalmap for normal map, _s, _spec, ++ * _specular and _specularmap for specular map, _l, _light, _lightmap, _occ ++ * and _occlusion for light map, _disp and _displacement for displacement map. ++ * The matching is case insensitive. Post fix is taken between the last ++ * underscore and the last period. ++ * Default behavior is to detect type from lower cased texture unit name by ++ * matching against: normalmap, specularmap, lightmap and displacementmap. ++ * For both cases if no match is found aiTextureType_DIFFUSE is used. ++ *
++ * Property type: Bool. Default value: false. ++ */ ++#define AI_CONFIG_IMPORT_OGRE_TEXTURETYPE_FROM_FILENAME \ ++ "IMPORT_OGRE_TEXTURETYPE_FROM_FILENAME" ++ ++ /** @brief Specifies whether the Android JNI asset extraction is supported. ++ * ++ * Turn on this option if you want to manage assets in native ++ * Android application without having to keep the internal directory and asset ++ * manager pointer. ++ */ ++ #define AI_CONFIG_ANDROID_JNI_ASSIMP_MANAGER_SUPPORT "AI_CONFIG_ANDROID_JNI_ASSIMP_MANAGER_SUPPORT" ++ ++// --------------------------------------------------------------------------- ++/** @brief Specifies whether the IFC loader skips over IfcSpace elements. ++ * ++ * IfcSpace elements (and their geometric representations) are used to ++ * represent, well, free space in a building storey.
++ * Property type: Bool. Default value: true. ++ */ ++#define AI_CONFIG_IMPORT_IFC_SKIP_SPACE_REPRESENTATIONS "IMPORT_IFC_SKIP_SPACE_REPRESENTATIONS" ++ ++// --------------------------------------------------------------------------- ++/** @brief Specifies whether the IFC loader will use its own, custom triangulation ++ * algorithm to triangulate wall and floor meshes. ++ * ++ * If this property is set to false, walls will be either triangulated by ++ * #aiProcess_Triangulate or will be passed through as huge polygons with ++ * faked holes (i.e. holes that are connected with the outer boundary using ++ * a dummy edge). It is highly recommended to set this property to true ++ * if you want triangulated data because #aiProcess_Triangulate is known to ++ * have problems with the kind of polygons that the IFC loader spits out for ++ * complicated meshes. ++ * Property type: Bool. Default value: true. ++ */ ++#define AI_CONFIG_IMPORT_IFC_CUSTOM_TRIANGULATION "IMPORT_IFC_CUSTOM_TRIANGULATION" ++ ++// --------------------------------------------------------------------------- ++/** @brief Set the tessellation conic angle for IFC smoothing curves. ++ * ++ * This is used by the IFC importer to determine the tessellation parameter ++ * for smoothing curves. ++ * @note The default value is AI_IMPORT_IFC_DEFAULT_SMOOTHING_ANGLE and the ++ * accepted values are in range [5.0, 120.0]. ++ * Property type: Float. ++ */ ++#define AI_CONFIG_IMPORT_IFC_SMOOTHING_ANGLE "IMPORT_IFC_SMOOTHING_ANGLE" ++ ++// default value for AI_CONFIG_IMPORT_IFC_SMOOTHING_ANGLE ++#if (!defined AI_IMPORT_IFC_DEFAULT_SMOOTHING_ANGLE) ++# define AI_IMPORT_IFC_DEFAULT_SMOOTHING_ANGLE 10.0f ++#endif ++ ++// --------------------------------------------------------------------------- ++/** @brief Set the tessellation for IFC cylindrical shapes. ++ * ++ * This is used by the IFC importer to determine the tessellation parameter ++ * for cylindrical shapes, i.e. the number of segments used to approximate a circle. ++ * @note The default value is AI_IMPORT_IFC_DEFAULT_CYLINDRICAL_TESSELLATION and the ++ * accepted values are in range [3, 180]. ++ * Property type: Integer. ++ */ ++#define AI_CONFIG_IMPORT_IFC_CYLINDRICAL_TESSELLATION "IMPORT_IFC_CYLINDRICAL_TESSELLATION" ++ ++// default value for AI_CONFIG_IMPORT_IFC_CYLINDRICAL_TESSELLATION ++#if (!defined AI_IMPORT_IFC_DEFAULT_CYLINDRICAL_TESSELLATION) ++# define AI_IMPORT_IFC_DEFAULT_CYLINDRICAL_TESSELLATION 32 ++#endif ++ ++// --------------------------------------------------------------------------- ++/** @brief Specifies whether the Collada loader will ignore the provided up direction. ++ * ++ * If this property is set to true, the up direction provided in the file header will ++ * be ignored and the file will be loaded as is. ++ * Property type: Bool. Default value: false. ++ */ ++#define AI_CONFIG_IMPORT_COLLADA_IGNORE_UP_DIRECTION "IMPORT_COLLADA_IGNORE_UP_DIRECTION" ++ ++// --------------------------------------------------------------------------- ++/** @brief Specifies whether the Collada loader should use Collada names. ++ * ++ * If this property is set to true, the Collada names will be used as the node and ++ * mesh names. The default is to use the id tag (resp. sid tag, if no id tag is present) ++ * instead. ++ * Property type: Bool. Default value: false. ++ */ ++#define AI_CONFIG_IMPORT_COLLADA_USE_COLLADA_NAMES "IMPORT_COLLADA_USE_COLLADA_NAMES" ++ ++// ---------- All the Export defines ------------ ++ ++/** @brief Specifies the xfile use double for real values of float ++ * ++ * Property type: Bool. Default value: false. ++ */ ++ ++#define AI_CONFIG_EXPORT_XFILE_64BIT "EXPORT_XFILE_64BIT" ++ ++/** @brief Specifies whether the assimp export shall be able to export point clouds ++ * ++ * When this flag is not defined the render data has to contain valid faces. ++ * Point clouds are only a collection of vertices which have nor spatial organization ++ * by a face and the validation process will remove them. Enabling this feature will ++ * switch off the flag and enable the functionality to export pure point clouds. ++ */ ++#define AI_CONFIG_EXPORT_POINT_CLOUDS "EXPORT_POINT_CLOUDS" ++ ++/** ++ * @brief Specifies the blob name, assimp uses for exporting. ++ * ++ * Some formats require auxiliary files to be written, that need to be linked back into ++ * the original file. For example, OBJ files export materials to a separate MTL file and ++ * use the `mtllib` keyword to reference this file. ++ * ++ * When exporting blobs using #ExportToBlob, assimp does not know the name of the blob ++ * file and thus outputs `mtllib $blobfile.mtl`, which might not be desired, since the ++ * MTL file might be called differently. ++ * ++ * This property can be used to give the exporter a hint on how to use the magic ++ * `$blobfile` keyword. If the exporter detects the keyword and is provided with a name ++ * for the blob, it instead uses this name. ++ */ ++#define AI_CONFIG_EXPORT_BLOB_NAME "EXPORT_BLOB_NAME" ++ ++/** ++ * @brief Specifies a gobal key factor for scale, float value ++ */ ++#define AI_CONFIG_GLOBAL_SCALE_FACTOR_KEY "GLOBAL_SCALE_FACTOR" ++ ++#if (!defined AI_CONFIG_GLOBAL_SCALE_FACTOR_DEFAULT) ++# define AI_CONFIG_GLOBAL_SCALE_FACTOR_DEFAULT 1.0f ++#endif // !! AI_DEBONE_THRESHOLD ++ ++#define AI_CONFIG_APP_SCALE_KEY "APP_SCALE_FACTOR" ++ ++#if (!defined AI_CONFIG_APP_SCALE_KEY) ++# define AI_CONFIG_APP_SCALE_KEY 1.0 ++#endif // AI_CONFIG_APP_SCALE_KEY ++ ++ ++// ---------- All the Build/Compile-time defines ------------ ++ ++/** @brief Specifies if double precision is supported inside assimp ++ * ++ * Property type: Bool. Default value: undefined. ++ */ ++ ++#define ASSIMP_DOUBLE_PRECISION 1 ++ ++#endif // !! AI_CONFIG_H_INC ++ +diff --git a/include/assimp/revision.h b/include/assimp/revision.h +new file mode 100644 +index 000000000..a55112235 +--- /dev/null ++++ b/include/assimp/revision.h +@@ -0,0 +1,13 @@ ++#ifndef ASSIMP_REVISION_H_INC ++#define ASSIMP_REVISION_H_INC ++ ++#define GitVersion 0xc8dafe0d ++#define GitBranch "5.2.4" ++ ++#define VER_MAJOR 5 ++#define VER_MINOR 2 ++#define VER_PATCH 4 ++#define VER_BUILD 0 ++ ++ ++#endif // ASSIMP_REVISION_H_INC diff --git a/redux/external/filament.patch b/redux/external/filament.patch new file mode 100644 index 0000000..b383016 --- /dev/null +++ b/redux/external/filament.patch @@ -0,0 +1,72 @@ +diff --git a/libs/filamat/include/filamat/MaterialBuilder.h b/libs/filamat/include/filamat/MaterialBuilder.h +index 9bd6182e3..6fc3b8649 100644 +--- a/libs/filamat/include/filamat/MaterialBuilder.h ++++ b/libs/filamat/include/filamat/MaterialBuilder.h +@@ -35,6 +35,7 @@ + #include + + #include ++#include + #include + #include + #include +diff --git a/libs/utils/include/utils/algorithm.h b/libs/utils/include/utils/algorithm.h +index d92555cc9..8524cb0b5 100644 +--- a/libs/utils/include/utils/algorithm.h ++++ b/libs/utils/include/utils/algorithm.h +@@ -22,6 +22,7 @@ + #include // for std::enable_if + + #include ++#include + #include + + namespace utils { +diff --git a/libs/utils/include/utils/bitset.h b/libs/utils/include/utils/bitset.h +index baad307b5..281e5dfcf 100644 +--- a/libs/utils/include/utils/bitset.h ++++ b/libs/utils/include/utils/bitset.h +@@ -26,6 +26,7 @@ + #include + + #include // for std::fill ++#include + #include + + #if defined(__ARM_NEON) +diff --git a/libs/utils/src/NameComponentManager.cpp b/libs/utils/src/NameComponentManager.cpp +index 98bb3839a..2c67126c8 100644 +--- a/libs/utils/src/NameComponentManager.cpp ++++ b/libs/utils/src/NameComponentManager.cpp +@@ -14,6 +14,7 @@ + * limitations under the License. + */ + ++#include + #include + #include + +diff --git a/libs/utils/src/Profiler.cpp b/libs/utils/src/Profiler.cpp +index 75caf8778..61513b8da 100644 +--- a/libs/utils/src/Profiler.cpp ++++ b/libs/utils/src/Profiler.cpp +@@ -27,6 +27,7 @@ + #endif + + #include ++#include + #include + + #if defined(__linux__) +diff --git a/third_party/glslang/BUILD.bazel b/third_party/glslang/BUILD.bazel +index 12168fae1..5ad7bfffb 100644 +--- a/third_party/glslang/BUILD.bazel ++++ b/third_party/glslang/BUILD.bazel +@@ -92,6 +92,7 @@ COMMON_COPTS = select({ + + cc_library( + name = "glslang", ++ includes = ["."], + srcs = glob( + [ + "glslang/GenericCodeGen/*.cpp", diff --git a/redux/external/libogg.patch b/redux/external/libogg.patch new file mode 100644 index 0000000..c111710 --- /dev/null +++ b/redux/external/libogg.patch @@ -0,0 +1,19 @@ +diff --git a/include/ogg/config_types.h b/include/ogg/config_types.h +new file mode 100644 +index 0000000..8920d04 +--- /dev/null ++++ b/include/ogg/config_types.h +@@ -0,0 +1,13 @@ ++#ifndef __CONFIG_TYPES_H__ ++#define __CONFIG_TYPES_H__ ++ ++#include ++ ++typedef int16_t ogg_int16_t; ++typedef uint16_t ogg_uint16_t; ++typedef int32_t ogg_int32_t; ++typedef uint32_t ogg_uint32_t; ++typedef int64_t ogg_int64_t; ++typedef uint64_t ogg_uint64_t; ++ ++#endif diff --git a/redux/external/libpng.patch b/redux/external/libpng.patch new file mode 100644 index 0000000..2760f11 --- /dev/null +++ b/redux/external/libpng.patch @@ -0,0 +1,354 @@ +diff --git a/config.h b/config.h +new file mode 100644 +index 000000000..3309c9fd5 +--- /dev/null ++++ b/config.h +@@ -0,0 +1,123 @@ ++/* config.h.in. Generated from configure.ac by autoheader. */ ++ ++/* Define to 1 if you have the header file. */ ++#undef HAVE_DLFCN_H ++ ++/* Define to 1 if you have the `feenableexcept' function. */ ++#undef HAVE_FEENABLEEXCEPT ++ ++/* Define to 1 if you have the header file. */ ++#undef HAVE_INTTYPES_H ++ ++/* Define to 1 if you have the `m' library (-lm). */ ++#undef HAVE_LIBM ++ ++/* Define to 1 if you have the `z' library (-lz). */ ++#undef HAVE_LIBZ ++ ++/* Define to 1 if you have the `pow' function. */ ++#undef HAVE_POW ++ ++/* Define to 1 if you have the header file. */ ++#undef HAVE_STDINT_H ++ ++/* Define to 1 if you have the header file. */ ++#undef HAVE_STDIO_H ++ ++/* Define to 1 if you have the header file. */ ++#undef HAVE_STDLIB_H ++ ++/* Define to 1 if you have the header file. */ ++#undef HAVE_STRINGS_H ++ ++/* Define to 1 if you have the header file. */ ++#undef HAVE_STRING_H ++ ++/* Define to 1 if you have the header file. */ ++#undef HAVE_SYS_STAT_H ++ ++/* Define to 1 if you have the header file. */ ++#undef HAVE_SYS_TYPES_H ++ ++/* Define to 1 if you have the header file. */ ++#undef HAVE_UNISTD_H ++ ++/* Define to the sub-directory where libtool stores uninstalled libraries. */ ++#undef LT_OBJDIR ++ ++/* Name of package */ ++#undef PACKAGE ++ ++/* Define to the address where bug reports for this package should be sent. */ ++#undef PACKAGE_BUGREPORT ++ ++/* Define to the full name of this package. */ ++#undef PACKAGE_NAME ++ ++/* Define to the full name and version of this package. */ ++#undef PACKAGE_STRING ++ ++/* Define to the one symbol short name of this package. */ ++#undef PACKAGE_TARNAME ++ ++/* Define to the home page for this package. */ ++#undef PACKAGE_URL ++ ++/* Define to the version of this package. */ ++#undef PACKAGE_VERSION ++ ++/* Turn on ARM Neon optimizations at run-time */ ++#undef PNG_ARM_NEON_API_SUPPORTED ++ ++/* Check for ARM Neon support at run-time */ ++#undef PNG_ARM_NEON_CHECK_SUPPORTED ++ ++/* Enable ARM Neon optimizations */ ++#undef PNG_ARM_NEON_OPT ++ ++/* Enable Intel SSE optimizations */ ++#undef PNG_INTEL_SSE_OPT ++ ++/* Turn on MIPS MSA optimizations at run-time */ ++#undef PNG_MIPS_MSA_API_SUPPORTED ++ ++/* Check for MIPS MSA support at run-time */ ++#undef PNG_MIPS_MSA_CHECK_SUPPORTED ++ ++/* Enable MIPS MSA optimizations */ ++#undef PNG_MIPS_MSA_OPT ++ ++/* Turn on POWERPC VSX optimizations at run-time */ ++#undef PNG_POWERPC_VSX_API_SUPPORTED ++ ++/* Check for POWERPC VSX support at run-time */ ++#undef PNG_POWERPC_VSX_CHECK_SUPPORTED ++ ++/* Enable POWERPC VSX optimizations */ ++#undef PNG_POWERPC_VSX_OPT ++ ++/* Define to 1 if all of the C90 standard headers exist (not just the ones ++ required in a freestanding environment). This macro is provided for ++ backward compatibility; new code need not use it. */ ++#undef STDC_HEADERS ++ ++/* Define to 1 if your declares `struct tm'. */ ++#undef TM_IN_SYS_TIME ++ ++/* Version number of package */ ++#undef VERSION ++ ++/* Define to the equivalent of the C99 'restrict' keyword, or to ++ nothing if this is not supported. Do not define if restrict is ++ supported only directly. */ ++#undef restrict ++/* Work around a bug in older versions of Sun C++, which did not ++ #define __restrict__ or support _Restrict or __restrict__ ++ even though the corresponding Sun C compiler ended up with ++ "#define restrict _Restrict" or "#define restrict __restrict__" ++ in the previous line. This workaround can be removed once ++ we assume Oracle Developer Studio 12.5 (2016) or later. */ ++#if defined __SUNPRO_CC && !defined __RESTRICT && !defined __restrict__ ++# define _Restrict ++# define __restrict__ ++#endif +diff --git a/pnglibconf.h b/pnglibconf.h +new file mode 100644 +index 000000000..e5948c8ce +--- /dev/null ++++ b/pnglibconf.h +@@ -0,0 +1,219 @@ ++/* pnglibconf.h - library build configuration */ ++ ++/* libpng version 1.6.39 */ ++ ++/* Copyright (c) 2018-2022 Cosmin Truta */ ++/* Copyright (c) 1998-2002,2004,2006-2018 Glenn Randers-Pehrson */ ++ ++/* This code is released under the libpng license. */ ++/* For conditions of distribution and use, see the disclaimer */ ++/* and license in png.h */ ++ ++/* pnglibconf.h */ ++/* Machine generated file: DO NOT EDIT */ ++/* Derived from: scripts/pnglibconf.dfa */ ++#ifndef PNGLCONF_H ++#define PNGLCONF_H ++/* options */ ++#define PNG_16BIT_SUPPORTED ++#define PNG_ALIGNED_MEMORY_SUPPORTED ++/*#undef PNG_ARM_NEON_API_SUPPORTED*/ ++/*#undef PNG_ARM_NEON_CHECK_SUPPORTED*/ ++#define PNG_BENIGN_ERRORS_SUPPORTED ++#define PNG_BENIGN_READ_ERRORS_SUPPORTED ++/*#undef PNG_BENIGN_WRITE_ERRORS_SUPPORTED*/ ++#define PNG_BUILD_GRAYSCALE_PALETTE_SUPPORTED ++#define PNG_CHECK_FOR_INVALID_INDEX_SUPPORTED ++#define PNG_COLORSPACE_SUPPORTED ++#define PNG_CONSOLE_IO_SUPPORTED ++#define PNG_CONVERT_tIME_SUPPORTED ++#define PNG_EASY_ACCESS_SUPPORTED ++/*#undef PNG_ERROR_NUMBERS_SUPPORTED*/ ++#define PNG_ERROR_TEXT_SUPPORTED ++#define PNG_FIXED_POINT_SUPPORTED ++#define PNG_FLOATING_ARITHMETIC_SUPPORTED ++#define PNG_FLOATING_POINT_SUPPORTED ++#define PNG_FORMAT_AFIRST_SUPPORTED ++#define PNG_FORMAT_BGR_SUPPORTED ++#define PNG_GAMMA_SUPPORTED ++#define PNG_GET_PALETTE_MAX_SUPPORTED ++#define PNG_HANDLE_AS_UNKNOWN_SUPPORTED ++#define PNG_INCH_CONVERSIONS_SUPPORTED ++#define PNG_INFO_IMAGE_SUPPORTED ++#define PNG_IO_STATE_SUPPORTED ++#define PNG_MNG_FEATURES_SUPPORTED ++#define PNG_POINTER_INDEXING_SUPPORTED ++/*#undef PNG_POWERPC_VSX_API_SUPPORTED*/ ++/*#undef PNG_POWERPC_VSX_CHECK_SUPPORTED*/ ++#define PNG_PROGRESSIVE_READ_SUPPORTED ++#define PNG_READ_16BIT_SUPPORTED ++#define PNG_READ_ALPHA_MODE_SUPPORTED ++#define PNG_READ_ANCILLARY_CHUNKS_SUPPORTED ++#define PNG_READ_BACKGROUND_SUPPORTED ++#define PNG_READ_BGR_SUPPORTED ++#define PNG_READ_CHECK_FOR_INVALID_INDEX_SUPPORTED ++#define PNG_READ_COMPOSITE_NODIV_SUPPORTED ++#define PNG_READ_COMPRESSED_TEXT_SUPPORTED ++#define PNG_READ_EXPAND_16_SUPPORTED ++#define PNG_READ_EXPAND_SUPPORTED ++#define PNG_READ_FILLER_SUPPORTED ++#define PNG_READ_GAMMA_SUPPORTED ++#define PNG_READ_GET_PALETTE_MAX_SUPPORTED ++#define PNG_READ_GRAY_TO_RGB_SUPPORTED ++#define PNG_READ_INTERLACING_SUPPORTED ++#define PNG_READ_INT_FUNCTIONS_SUPPORTED ++#define PNG_READ_INVERT_ALPHA_SUPPORTED ++#define PNG_READ_INVERT_SUPPORTED ++#define PNG_READ_OPT_PLTE_SUPPORTED ++#define PNG_READ_PACKSWAP_SUPPORTED ++#define PNG_READ_PACK_SUPPORTED ++#define PNG_READ_QUANTIZE_SUPPORTED ++#define PNG_READ_RGB_TO_GRAY_SUPPORTED ++#define PNG_READ_SCALE_16_TO_8_SUPPORTED ++#define PNG_READ_SHIFT_SUPPORTED ++#define PNG_READ_STRIP_16_TO_8_SUPPORTED ++#define PNG_READ_STRIP_ALPHA_SUPPORTED ++#define PNG_READ_SUPPORTED ++#define PNG_READ_SWAP_ALPHA_SUPPORTED ++#define PNG_READ_SWAP_SUPPORTED ++#define PNG_READ_TEXT_SUPPORTED ++#define PNG_READ_TRANSFORMS_SUPPORTED ++#define PNG_READ_UNKNOWN_CHUNKS_SUPPORTED ++#define PNG_READ_USER_CHUNKS_SUPPORTED ++#define PNG_READ_USER_TRANSFORM_SUPPORTED ++#define PNG_READ_bKGD_SUPPORTED ++#define PNG_READ_cHRM_SUPPORTED ++#define PNG_READ_eXIf_SUPPORTED ++#define PNG_READ_gAMA_SUPPORTED ++#define PNG_READ_hIST_SUPPORTED ++#define PNG_READ_iCCP_SUPPORTED ++#define PNG_READ_iTXt_SUPPORTED ++#define PNG_READ_oFFs_SUPPORTED ++#define PNG_READ_pCAL_SUPPORTED ++#define PNG_READ_pHYs_SUPPORTED ++#define PNG_READ_sBIT_SUPPORTED ++#define PNG_READ_sCAL_SUPPORTED ++#define PNG_READ_sPLT_SUPPORTED ++#define PNG_READ_sRGB_SUPPORTED ++#define PNG_READ_tEXt_SUPPORTED ++#define PNG_READ_tIME_SUPPORTED ++#define PNG_READ_tRNS_SUPPORTED ++#define PNG_READ_zTXt_SUPPORTED ++#define PNG_SAVE_INT_32_SUPPORTED ++#define PNG_SAVE_UNKNOWN_CHUNKS_SUPPORTED ++#define PNG_SEQUENTIAL_READ_SUPPORTED ++#define PNG_SETJMP_SUPPORTED ++#define PNG_SET_OPTION_SUPPORTED ++#define PNG_SET_UNKNOWN_CHUNKS_SUPPORTED ++#define PNG_SET_USER_LIMITS_SUPPORTED ++#define PNG_SIMPLIFIED_READ_AFIRST_SUPPORTED ++#define PNG_SIMPLIFIED_READ_BGR_SUPPORTED ++#define PNG_SIMPLIFIED_READ_SUPPORTED ++#define PNG_SIMPLIFIED_WRITE_AFIRST_SUPPORTED ++#define PNG_SIMPLIFIED_WRITE_BGR_SUPPORTED ++#define PNG_SIMPLIFIED_WRITE_STDIO_SUPPORTED ++#define PNG_SIMPLIFIED_WRITE_SUPPORTED ++#define PNG_STDIO_SUPPORTED ++#define PNG_STORE_UNKNOWN_CHUNKS_SUPPORTED ++#define PNG_TEXT_SUPPORTED ++#define PNG_TIME_RFC1123_SUPPORTED ++#define PNG_UNKNOWN_CHUNKS_SUPPORTED ++#define PNG_USER_CHUNKS_SUPPORTED ++#define PNG_USER_LIMITS_SUPPORTED ++#define PNG_USER_MEM_SUPPORTED ++#define PNG_USER_TRANSFORM_INFO_SUPPORTED ++#define PNG_USER_TRANSFORM_PTR_SUPPORTED ++#define PNG_WARNINGS_SUPPORTED ++#define PNG_WRITE_16BIT_SUPPORTED ++#define PNG_WRITE_ANCILLARY_CHUNKS_SUPPORTED ++#define PNG_WRITE_BGR_SUPPORTED ++#define PNG_WRITE_CHECK_FOR_INVALID_INDEX_SUPPORTED ++#define PNG_WRITE_COMPRESSED_TEXT_SUPPORTED ++#define PNG_WRITE_CUSTOMIZE_COMPRESSION_SUPPORTED ++#define PNG_WRITE_CUSTOMIZE_ZTXT_COMPRESSION_SUPPORTED ++#define PNG_WRITE_FILLER_SUPPORTED ++#define PNG_WRITE_FILTER_SUPPORTED ++#define PNG_WRITE_FLUSH_SUPPORTED ++#define PNG_WRITE_GET_PALETTE_MAX_SUPPORTED ++#define PNG_WRITE_INTERLACING_SUPPORTED ++#define PNG_WRITE_INT_FUNCTIONS_SUPPORTED ++#define PNG_WRITE_INVERT_ALPHA_SUPPORTED ++#define PNG_WRITE_INVERT_SUPPORTED ++#define PNG_WRITE_OPTIMIZE_CMF_SUPPORTED ++#define PNG_WRITE_PACKSWAP_SUPPORTED ++#define PNG_WRITE_PACK_SUPPORTED ++#define PNG_WRITE_SHIFT_SUPPORTED ++#define PNG_WRITE_SUPPORTED ++#define PNG_WRITE_SWAP_ALPHA_SUPPORTED ++#define PNG_WRITE_SWAP_SUPPORTED ++#define PNG_WRITE_TEXT_SUPPORTED ++#define PNG_WRITE_TRANSFORMS_SUPPORTED ++#define PNG_WRITE_UNKNOWN_CHUNKS_SUPPORTED ++#define PNG_WRITE_USER_TRANSFORM_SUPPORTED ++#define PNG_WRITE_WEIGHTED_FILTER_SUPPORTED ++#define PNG_WRITE_bKGD_SUPPORTED ++#define PNG_WRITE_cHRM_SUPPORTED ++#define PNG_WRITE_eXIf_SUPPORTED ++#define PNG_WRITE_gAMA_SUPPORTED ++#define PNG_WRITE_hIST_SUPPORTED ++#define PNG_WRITE_iCCP_SUPPORTED ++#define PNG_WRITE_iTXt_SUPPORTED ++#define PNG_WRITE_oFFs_SUPPORTED ++#define PNG_WRITE_pCAL_SUPPORTED ++#define PNG_WRITE_pHYs_SUPPORTED ++#define PNG_WRITE_sBIT_SUPPORTED ++#define PNG_WRITE_sCAL_SUPPORTED ++#define PNG_WRITE_sPLT_SUPPORTED ++#define PNG_WRITE_sRGB_SUPPORTED ++#define PNG_WRITE_tEXt_SUPPORTED ++#define PNG_WRITE_tIME_SUPPORTED ++#define PNG_WRITE_tRNS_SUPPORTED ++#define PNG_WRITE_zTXt_SUPPORTED ++#define PNG_bKGD_SUPPORTED ++#define PNG_cHRM_SUPPORTED ++#define PNG_eXIf_SUPPORTED ++#define PNG_gAMA_SUPPORTED ++#define PNG_hIST_SUPPORTED ++#define PNG_iCCP_SUPPORTED ++#define PNG_iTXt_SUPPORTED ++#define PNG_oFFs_SUPPORTED ++#define PNG_pCAL_SUPPORTED ++#define PNG_pHYs_SUPPORTED ++#define PNG_sBIT_SUPPORTED ++#define PNG_sCAL_SUPPORTED ++#define PNG_sPLT_SUPPORTED ++#define PNG_sRGB_SUPPORTED ++#define PNG_tEXt_SUPPORTED ++#define PNG_tIME_SUPPORTED ++#define PNG_tRNS_SUPPORTED ++#define PNG_zTXt_SUPPORTED ++/* end of options */ ++/* settings */ ++#define PNG_API_RULE 0 ++#define PNG_DEFAULT_READ_MACROS 1 ++#define PNG_GAMMA_THRESHOLD_FIXED 5000 ++#define PNG_IDAT_READ_SIZE PNG_ZBUF_SIZE ++#define PNG_INFLATE_BUF_SIZE 1024 ++#define PNG_LINKAGE_API extern ++#define PNG_LINKAGE_CALLBACK extern ++#define PNG_LINKAGE_DATA extern ++#define PNG_LINKAGE_FUNCTION extern ++#define PNG_MAX_GAMMA_8 11 ++#define PNG_QUANTIZE_BLUE_BITS 5 ++#define PNG_QUANTIZE_GREEN_BITS 5 ++#define PNG_QUANTIZE_RED_BITS 5 ++#define PNG_TEXT_Z_DEFAULT_COMPRESSION (-1) ++#define PNG_TEXT_Z_DEFAULT_STRATEGY 0 ++#define PNG_USER_CHUNK_CACHE_MAX 1000 ++#define PNG_USER_CHUNK_MALLOC_MAX 8000000 ++#define PNG_USER_HEIGHT_MAX 1000000 ++#define PNG_USER_WIDTH_MAX 1000000 ++#define PNG_ZBUF_SIZE 8192 ++#define PNG_ZLIB_VERNUM 0 /* unknown */ ++#define PNG_Z_DEFAULT_COMPRESSION (-1) ++#define PNG_Z_DEFAULT_NOFILTER_STRATEGY 0 ++#define PNG_Z_DEFAULT_STRATEGY 1 ++#define PNG_sCAL_PRECISION 5 ++#define PNG_sRGB_PROFILE_CHECKS 2 ++/* end of settings */ ++#endif /* PNGLCONF_H */ diff --git a/redux/external/stblib.patch b/redux/external/stblib.patch new file mode 100644 index 0000000..182c83a --- /dev/null +++ b/redux/external/stblib.patch @@ -0,0 +1,8 @@ +diff --git a/stb_image.c b/stb_image.c +new file mode 100644 +index 0000000..8ddfd1f +--- /dev/null ++++ b/stb_image.c +@@ -0,0 +1,2 @@ ++#define STB_IMAGE_IMPLEMENTATION ++#include "stb_image.h" diff --git a/redux/redux/BUILD b/redux/redux/BUILD new file mode 100644 index 0000000..8753988 --- /dev/null +++ b/redux/redux/BUILD @@ -0,0 +1,9 @@ +licenses(["notice"]) + +# Visibility group that specifies which projects can use the redux libraries. +package_group( + name = "visibility", + packages = [ + "//redux/...", + ], +) diff --git a/redux/redux/data/asset_defs/BUILD b/redux/redux/data/asset_defs/BUILD new file mode 100644 index 0000000..f5ad3b3 --- /dev/null +++ b/redux/redux/data/asset_defs/BUILD @@ -0,0 +1,53 @@ +# Flatbuffer schemas for redux asset formats (eg. models, animations, etc.) + +load( + "//redux/tools:flatbuffer_cc_library.bzl", + "flatbuffer_cc_library", +) + +licenses(["notice"]) + +package( + default_visibility = ["//redux:visibility"], +) + +flatbuffer_cc_library( + name = "anim_asset_def_fbs", + srcs = ["anim_asset_def.fbs"], + deps = [ + "//redux/modules/flatbuffers:common_fbs", + "//redux/modules/flatbuffers:math_fbs", + "//redux/modules/flatbuffers:var_fbs", + ], +) + +flatbuffer_cc_library( + name = "model_asset_def_fbs", + srcs = ["model_asset_def.fbs"], + deps = [ + ":texture_asset_def_fbs", + "//redux/modules/flatbuffers:common_fbs", + "//redux/modules/flatbuffers:math_fbs", + "//redux/modules/flatbuffers:var_fbs", + "//redux/modules/graphics:graphics_enums_fbs", + ], +) + +flatbuffer_cc_library( + name = "shader_asset_def_fbs", + srcs = ["shader_asset_def.fbs"], + deps = [ + "//redux/modules/flatbuffers:common_fbs", + "//redux/modules/graphics:graphics_enums_fbs", + ], +) + +flatbuffer_cc_library( + name = "texture_asset_def_fbs", + srcs = ["texture_asset_def.fbs"], + deps = [ + "//redux/modules/flatbuffers:common_fbs", + "//redux/modules/flatbuffers:math_fbs", + "//redux/modules/graphics:graphics_enums_fbs", + ], +) diff --git a/redux/redux/data/asset_defs/anim_asset_def.fbs b/redux/redux/data/asset_defs/anim_asset_def.fbs new file mode 100644 index 0000000..cf074e3 --- /dev/null +++ b/redux/redux/data/asset_defs/anim_asset_def.fbs @@ -0,0 +1,85 @@ +include "redux/modules/flatbuffers/common.fbs"; +include "redux/modules/flatbuffers/math.fbs"; +include "redux/modules/flatbuffers/var.fbs"; + +// The rxanim (redux animation) file format. + +namespace redux; + +// Each channel of data is used to animate one of the following properties of +// a bone. +enum AnimChannelType : byte { + Invalid, + TranslateX, + TranslateY, + TranslateZ, + ScaleX, + ScaleY, + ScaleZ, + QuaternionX, + QuaternionY, + QuaternionZ, + QuaternionW, +} + +// The root of the rxanim data format. +table AnimAssetDef { + // Version number to help decide how to interpret the data. + version: int = 1; + + // The names of the bones in the animation. + bone_names: [fbs.HashString]; + + // Each bone in the animation has its own animation. + bone_anims: [BoneAnimAssetDef]; + + // Specifies bone hierarchy. Bone `i` is a child of `bone_parents[i]`. + // If a root bone (i.e. no parent), set to 255. + bone_parents: [ushort]; + + // The total length of the animation in seconds. + length_in_seconds: float; + + // True if all the data streams end where they started. + repeat: bool; +} + +// Animation for a single bone. +table BoneAnimAssetDef { + ops: [AnimChannelAssetDef]; +} + +// An animation channel is either a constant value or a spline. +union AnimChannelDataAssetDef { + AnimChannelConstValueAssetDef, + AnimChannelSplineAssetDef, +} + +// A single channel of animation (for a given bone). +table AnimChannelAssetDef { + type: AnimChannelType; + data: AnimChannelDataAssetDef; +} + +// A single const value animation channel. +table AnimChannelConstValueAssetDef { + value: float; +} + +// A node in an animation spline. This is designed to match CompactSplineNode. +struct AnimSplineNodeAssetDef { + x: ushort; + y: ushort; + angle: short; +} + +// An animation spline. This is designed to match CompactSpline. +table AnimChannelSplineAssetDef { + x_granularity: float; + y_range_start: float; + y_range_end: float; + nodes: [AnimSplineNodeAssetDef]; +} + +root_type AnimAssetDef; +file_extension "rxanim"; diff --git a/redux/redux/data/asset_defs/model_asset_def.fbs b/redux/redux/data/asset_defs/model_asset_def.fbs new file mode 100644 index 0000000..a671eab --- /dev/null +++ b/redux/redux/data/asset_defs/model_asset_def.fbs @@ -0,0 +1,165 @@ +include "redux/data/asset_defs/texture_asset_def.fbs"; +include "redux/modules/flatbuffers/common.fbs"; +include "redux/modules/flatbuffers/math.fbs"; +include "redux/modules/flatbuffers/var.fbs"; +include "redux/modules/graphics/graphics_enums.fbs"; + +// The rxmodel (redux model) file format. + +namespace redux; + +// The root of the rxmodel data format. +table ModelAssetDef { + // Version number to help decide how to interpret the data. + version: int = 1; + + // Model data for different rendering LODs. + lods: [ModelInstanceAssetDef]; + + // Information about the textures associated with this model. + textures: [ModelTextureAssetDef]; + + // The skeletal bone information to support skinned animations. + skeleton: ModelSkeletonAssetDef; + + // The minimum and maximum bounds contained in the vertex data. + bounding_box: fbs.Boxf; +} + +table ModelInstancePartAssetDef { + name: fbs.HashString; + + // The material describing the "look" of the part. + material: MaterialAssetDef; + + // The range of indices for the part. + range: ModelIndexRangeAssetDef; + + // The bounding box for the part. + bounding_box: fbs.Boxf; +} + +// A single instance of model data used to render an object at a given LOD. +table ModelInstanceAssetDef { + vertices: ModelVertexBufferAssetDef; + indices: ModelIndexBufferAssetDef; + + // The "submeshes" that can be individually targetted in the model. + parts: [ModelInstancePartAssetDef]; + + // A collection of blend shapes. The geometry defined in these blend shapes + // will match the main geometry (ie. + blend_shapes: [ModelBlendShapeAssetDef]; + + // The shape to use for collisions. + collision_shape: ModelCollisionShapeAssetDef; + + // Maps the skeleton bone index to the shader bone index. The shader bones + // are only the bones that have at least one vertex weighted to them and, as + // such, are a subset of all the bones in the skeleton. + shader_to_mesh_bones: [ushort]; +} + +table ModelBlendShapeAssetDef { + // The name of this blend shape. + name: fbs.HashString; + + vertices: ModelVertexBufferAssetDef; +} + +table ModelCollisionShapeAssetDef { + vertices: ModelVertexBufferAssetDef; + indices: ModelIndexBufferAssetDef; +} + +// Description of a single vertex attribute. +struct ModelVertexAttributeAssetDef { + usage: VertexUsage; + type: VertexType; +} + +// The range of indices associated with a single drawable. +struct ModelIndexRangeAssetDef { + start: uint32; + end: uint32; +} + +// The texture information related to the model. +table ModelTextureAssetDef { + // The name of the texture. This should match the name defined in the + // material. + name: fbs.HashString; + + // Information about how the texture will be used for rendering. This is a + // fully embedded rxtexture object. + texture: TextureAssetDef; + + // The URI to the texture to be loaded if the binary data is not contained in + // the 'texture' object above. If this is an rxtexture object, then the + // parameters stored in the 'texture' field above will be ignored. If this is + // an image file (eg. png, etc.), then the parameters in the 'texture' field + // above will be used. + uri: string; +} + +// The vertices of a model. +table ModelVertexBufferAssetDef { + // Binary data storing the vertex buffer. + data: [uint8]; + + // The format of the data in the vertex buffer. + vertex_format: [ModelVertexAttributeAssetDef]; + + // The total number of vertices stored in the vertex data. + num_vertices: uint32; + + // Whether or not the attributes in the vertex data are interleaved. + interleaved: bool = true; +} + +table ModelIndexBufferAssetDef { + // The indices used for submitting draw calls. Both indices16 and indices32 + // should not be set at the same time. + data16: [uint16]; + data32: [uint32]; +} + +// Describes the "look" applied to a single surface of a mesh. +table MaterialAssetDef { + // The name of the material. + name: fbs.HashString; + + // A dictionary of all material properties extracted from the source file. + // These properties are interpretted by the RenderSystem to create the + // appropriate Material. + properties: fbs.VarTableDef; + + // The list of textures associated with the Material. + textures: [MaterialTextureAssetDef]; +} + +// Information about a texture associated with a material. +table MaterialTextureAssetDef { + // Reference to the ModelTextureAssetDef stored in the ModelAssetDef. + name: fbs.HashString; + + // Information about how the material uses the texture data. + usage: [MaterialTextureType]; +} + +// Describes the skeleton used for skinned animations. +table ModelSkeletonAssetDef { + // The names of each bone in the skeleton. Each bone in the skeleton can + // be uniquely identified by an index into this array. + bone_names: [string]; + + // Effectively a map of a bone to its parent bone. + bone_parents: [ushort]; + + // The "inverse bind matrices" for each bone. Transforms from mesh space to + // bone space so that skinning may be applied. + bone_transforms: [fbs.Mat3x4f]; +} + +root_type ModelAssetDef; +file_extension "rxmodel"; diff --git a/redux/redux/data/asset_defs/shader_asset_def.fbs b/redux/redux/data/asset_defs/shader_asset_def.fbs new file mode 100644 index 0000000..3cfa7d5 --- /dev/null +++ b/redux/redux/data/asset_defs/shader_asset_def.fbs @@ -0,0 +1,52 @@ +include "redux/modules/flatbuffers/common.fbs"; +include "redux/modules/graphics/graphics_enums.fbs"; + +// The rxshader (redux shader) file format. + +namespace redux; + +// The metadata for a single shader/material property. +table ShaderPropertyAssetDef { + // The name of this property. + name: fbs.HashString; + + // The data type of the property. + type: MaterialPropertyType; + + // If the property is a sample, identifies how the texture is intended to be + // used. + texture_usage: [MaterialTextureType]; + + // The default value for the property. + default_ints: [int]; + default_floats: [float]; +} + +// A shader material variant that supports a specific configuration of +// conditions and features as identified by the runtime. +table ShaderVariantAssetDef { + // The name of this variant, used for debugging. + name: string; + + // The preconditions (ie. hardware capabilities) that are necessary for this + // shader variant to be used. + conditions: [uint]; + + // The list of features that this shader variant fullfils. + features: [uint]; + + // The list of properties that can be set in this shader variant. + properties: [ShaderPropertyAssetDef]; + + // The binary filament material which has been built using filament matc. + filament_material: [ubyte]; +} + +// The root shader data. +table ShaderAssetDef { + // The shading model name that is used to identify this shader. + shading_model: string; + + // The list of variants that are + variants: [ShaderVariantAssetDef]; +} diff --git a/redux/redux/data/asset_defs/texture_asset_def.fbs b/redux/redux/data/asset_defs/texture_asset_def.fbs new file mode 100644 index 0000000..65d6c96 --- /dev/null +++ b/redux/redux/data/asset_defs/texture_asset_def.fbs @@ -0,0 +1,54 @@ +include "redux/modules/flatbuffers/math.fbs"; +include "redux/modules/graphics/graphics_enums.fbs"; + +namespace redux; + +// The binary data and metadata containing the actual image used by the texture. +table ImageAssetDef { + data: [uint8]; + size: fbs.Vec2i; + format: ImageFormat; +} + +// Definition of a texture resource. +table TextureAssetDef { + // Binary data containing the image. + image: ImageAssetDef; + + // The texture minifying function is used whenever the pixel being textured + // maps to an area greater than one texture element. There are six defined + // minifying functions. Two of them use the nearest one or nearest four + // texture elements to compute the texture value. The other four use mipmaps. + // + // The allowed TextureFiltering options are: [Nearest, Linear, + // NearestMipmapNearest, LinearMipmapNearest, NearestMipmapLinear, + // LinearMipmapLinear]. + min_filter: TextureFilter = Linear; + + // The texture magnification function is used when the pixel being textured + // maps to an area less than or equal to one texture element. + // + // The allowed TextureFiltering options are: [Nearest, Linear] + mag_filter: TextureFilter = Linear; + + // Sets the wrap parameter for texture coordinate s. + wrap_s : TextureWrap = Repeat; + // Sets the wrap parameter for texture coordinate t. + wrap_t : TextureWrap = Repeat; + // Sets the wrap parameter for texture coordinate r. + wrap_r : TextureWrap = Repeat; + + // The type of texture (eg. 2D, CubeMap). + target_type: TextureTarget = Normal2D; + + // Flag that specifies whether the texture mip chain should be automatically + // generated (note: this does not apply to GPU compressed texture formats). + generate_mipmaps: bool = false; + + // Whether or not the texture should have alpha premultiplied. + premultiply_alpha: bool = false; + + // Indicates that the texture is storing HDR image data where the multiplier + // to be applied to the RGB data is encoded into the alpha channel. + is_rgbm: bool = false; +} diff --git a/redux/redux/engines/animation/BUILD b/redux/redux/engines/animation/BUILD new file mode 100644 index 0000000..d54ea4a --- /dev/null +++ b/redux/redux/engines/animation/BUILD @@ -0,0 +1,57 @@ +# Animation engine. + +licenses(["notice"]) + +package( + default_visibility = ["//redux:visibility"], +) + +cc_library( + name = "animation", + srcs = [ + "animation_clip.cc", + "animation_engine.cc", + "motivator/motivator.cc", + "motivator/rig_motivator.cc", + "motivator/spline_motivator.cc", + "motivator/transform_motivator.cc", + "processor/anim_processor.cc", + "processor/rig_processor.cc", + "processor/spline_processor.cc", + "processor/transform_processor.cc", + ], + hdrs = [ + "animation_clip.h", + "animation_engine.h", + "animation_playback.h", + "common.h", + "motivator/motivator.h", + "motivator/rig_motivator.h", + "motivator/spline_motivator.h", + "motivator/transform_motivator.h", + "processor/anim_processor.h", + "processor/index_allocator.h", + "processor/rig_processor.h", + "processor/spline_processor.h", + "processor/transform_processor.h", + ], + deps = [ + "@absl//absl/algorithm:container", + "@absl//absl/container:flat_hash_map", + "@absl//absl/time", + "//redux/data/asset_defs:anim_asset_def_fbs", + "//redux/engines/animation/spline:compact_spline", + "//redux/modules/base:asset_loader", + "//redux/modules/base:choreographer", + "//redux/modules/base:data_container", + "//redux/modules/base:logging", + "//redux/modules/base:registry", + "//redux/modules/base:resource_manager", + "//redux/modules/base:static_registry", + "//redux/modules/base:typeid", + "//redux/modules/math:constants", + "//redux/modules/math:matrix", + "//redux/modules/math:transform", + "//redux/modules/math:vector", + ], +) diff --git a/redux/redux/engines/animation/README.md b/redux/redux/engines/animation/README.md new file mode 100644 index 0000000..a4b36c4 --- /dev/null +++ b/redux/redux/engines/animation/README.md @@ -0,0 +1,7 @@ +At its most basic, the AnimationEngine is responsible for updating floating +point values over time using splines. The AnimationEngine builds upon this core +functionality to implement skeletal animations using rxanim files (which can be +created using the anim_pipeline). + +This engine is a heavily modified version of the Motive animation engine +(www.github.com/google/motive). diff --git a/redux/redux/engines/animation/animation_clip.cc b/redux/redux/engines/animation/animation_clip.cc new file mode 100644 index 0000000..173586d --- /dev/null +++ b/redux/redux/engines/animation/animation_clip.cc @@ -0,0 +1,115 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/animation/animation_clip.h" + +#include "redux/modules/base/logging.h" + +namespace redux { + +static AnimationChannel ReadChannelAssetDef(const AnimChannelAssetDef* def) { + const AnimChannelType type = def->type(); + + if (def->data_type() == + AnimChannelDataAssetDef::AnimChannelConstValueAssetDef) { + const float value = def->data_as_AnimChannelConstValueAssetDef()->value(); + return AnimationChannel(type, value); + } else if (def->data_type() == + AnimChannelDataAssetDef::AnimChannelSplineAssetDef) { + const AnimChannelSplineAssetDef* spline_def = + def->data_as_AnimChannelSplineAssetDef(); + CHECK(spline_def); + + const int num_nodes = spline_def->nodes()->size(); + CompactSplinePtr spline = CompactSpline::Create(num_nodes); + const Interval y_range(spline_def->y_range_start(), + spline_def->y_range_end()); + spline->Init(y_range, spline_def->x_granularity()); + for (int i = 0; i < num_nodes; ++i) { + const AnimSplineNodeAssetDef* node = spline_def->nodes()->Get(i); + spline->AddNodeVerbatim(node->x(), node->y(), node->angle()); + } + return AnimationChannel(type, std::move(spline)); + } else { + return AnimationChannel(type); + } +} + +AnimationClip::AnimationClip(DataContainer data) { + Initialize(std::move(data)); +} + +void AnimationClip::Initialize(DataContainer data) { + data_ = std::move(data); + if (data_.GetNumBytes() == 0) { + return; + } + + def_ = flatbuffers::GetRoot(data_.GetBytes()); + if (def_->bone_anims()) { + anims_.resize(def_->bone_anims()->size()); + for (std::size_t i = 0; i < def_->bone_anims()->size(); ++i) { + const BoneAnimAssetDef* bone_anim_def = def_->bone_anims()->Get(i); + if (bone_anim_def->ops()) { + for (std::size_t j = 0; j < bone_anim_def->ops()->size(); ++j) { + const AnimChannelAssetDef* def = bone_anim_def->ops()->Get(j); + AnimationChannel channel = ReadChannelAssetDef(def); + anims_[i].push_back(std::move(channel)); + } + } + } + } + repeat_ = def_->repeat(); + duration_ = absl::Seconds(def_->length_in_seconds()); +} + +void AnimationClip::Finalize() { + if (data_.GetNumBytes() > 0) { + ready_ = true; + for (auto& cb : on_ready_callbacks_) { + cb(); + } + on_ready_callbacks_.clear(); + } +} + +const AnimationClip::BoneAnimation& AnimationClip::GetBoneAnimation( + BoneIndex idx) const { + CHECK(idx < anims_.size()); + return anims_[idx]; +} + +const char* AnimationClip::BoneName(BoneIndex idx) const { + CHECK(ready_); + CHECK(def_->bone_names()); + CHECK(def_->bone_names()->size() > idx); + return def_->bone_names()->GetAsString(idx)->c_str(); +} + +absl::Span AnimationClip::BoneParents() const { + CHECK(ready_); + CHECK(def_->bone_parents()); + return {def_->bone_parents()->data(), NumBones()}; +} + +void AnimationClip::OnReady(const std::function& callback) { + if (ready_) { + callback(); + } else { + on_ready_callbacks_.push_back(callback); + } +} +} // namespace redux diff --git a/redux/redux/engines/animation/animation_clip.h b/redux/redux/engines/animation/animation_clip.h new file mode 100644 index 0000000..3ebfc25 --- /dev/null +++ b/redux/redux/engines/animation/animation_clip.h @@ -0,0 +1,110 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_ANIMATION_CLIP_H_ +#define REDUX_ENGINES_ANIMATION_ANIMATION_CLIP_H_ + +#include +#include +#include +#include + +#include "redux/engines/animation/common.h" +#include "redux/engines/animation/spline/compact_spline.h" +#include "redux/modules/base/data_container.h" + +namespace redux { + +// Contains the type and data for a single channel of an animation. +struct AnimationChannel { + // An animation channel which has no data (so use the default value for this + // channel). + explicit AnimationChannel(AnimChannelType type) : type(type) {} + + // An animation channel that only contains a single value. + AnimationChannel(AnimChannelType type, float const_value) + : type(type), const_value(const_value) {} + + // An animation channel that will be animated along a spline. + AnimationChannel(AnimChannelType type, CompactSplinePtr spline) + : type(type), spline(std::move(spline)) {} + + AnimChannelType type = AnimChannelType::Invalid; + std::optional const_value; + CompactSplinePtr spline; +}; + +// Drives a fully rigged model. +class AnimationClip { + public: + AnimationClip() = default; + + // An animation for a single bone is basically just a collection of data + // streams. + using BoneAnimation = std::vector; + + // Reads the animation clip from an rx anim data blob. + explicit AnimationClip(DataContainer data); + void Initialize(DataContainer data); + void Finalize(); + + // Number of bones. Bones are arranged in an hierarchy. Each bone animates + // a matrix. The matrix describes the transform of the bone from its parent. + BoneIndex NumBones() const { return static_cast(anims_.size()); } + + // Returns the animations channels for a single bone. + const BoneAnimation& GetBoneAnimation(BoneIndex idx) const; + + // Amount of time required by this animation. Time units are set by the + // caller. + absl::Duration Duration() const { return duration_; } + + // Animation is repeatable. That is, when the end of the animation is + // reached, it can be started at the beginning again without glitching. + // Generally, an animation is repeatable if it's curves have the same values + // and derivatives at the start and end. + bool Repeats() const { return repeat_; } + + bool IsReady() const { return ready_; } + + // For debugging. Very useful when an animation is applied to a mesh + // that doesn't match: with the bone names you can determine whether the + // mesh or the animation is out of date. + const char* BoneName(BoneIndex idx) const; + + // Returns an array of length NumBones() representing the bone heirarchy. + // `bone_parents()[i]` is the bone index of the ith bone's parent. + // `bone_parents()[i]` < `bone_parents()[j]` for all i < j. + // For bones at the root (i.e. no parent) value is kInvalidBoneIdx. + absl::Span BoneParents() const; + + void OnReady(const std::function& callback); + + private: + std::vector anims_; + std::vector> on_ready_callbacks_; + DataContainer data_; + const AnimAssetDef* def_ = nullptr; + absl::Duration duration_ = absl::ZeroDuration(); + bool repeat_ = false; + bool ready_ = false; +}; + +using AnimationClipPtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_ANIMATION_ANIMATION_CLIP_H_ diff --git a/redux/redux/engines/animation/animation_engine.cc b/redux/redux/engines/animation/animation_engine.cc new file mode 100644 index 0000000..2ae1188 --- /dev/null +++ b/redux/redux/engines/animation/animation_engine.cc @@ -0,0 +1,100 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/animation/animation_engine.h" + +#include + +#include "absl/algorithm/container.h" +#include "absl/time/time.h" +#include "redux/engines/animation/motivator/rig_motivator.h" +#include "redux/engines/animation/motivator/spline_motivator.h" +#include "redux/engines/animation/motivator/transform_motivator.h" +#include "redux/engines/animation/processor/rig_processor.h" +#include "redux/engines/animation/processor/spline_processor.h" +#include "redux/engines/animation/processor/transform_processor.h" +#include "redux/modules/base/asset_loader.h" +#include "redux/modules/base/choreographer.h" +#include "redux/modules/base/static_registry.h" + +namespace redux { + +void AnimationEngine::Create(Registry* registry) { + auto ptr = new AnimationEngine(registry); + registry->Register(std::unique_ptr(ptr)); + + ptr->RegisterMotivator(); + ptr->RegisterMotivator(); + ptr->RegisterMotivator(); +} + +void AnimationEngine::OnRegistryInitialize() { + auto* choreographer = registry_->Get(); + if (choreographer) { + choreographer->Add<&AnimationEngine::AdvanceFrame>( + Choreographer::Stage::kAnimation); + } +} + +void AnimationEngine::AdvanceFrame(absl::Duration delta_time) { + // Advance the simulation in each processor. + // TODO: At some point, we'll want to do several passes. An item in + // processor A might depend on the output of an item in processor B, + // which might in turn depend on the output of a *different* item in + // processor A. In this case, we have to do two passes. For now, just + // assume that one pass is sufficient. + if (sorted_processors_.empty()) { + sorted_processors_.reserve(processors_.size()); + for (auto& iter : processors_) { + sorted_processors_.emplace_back(iter.second.get()); + } + absl::c_sort(sorted_processors_, [](AnimProcessor* a, AnimProcessor* b) { + return a->Priority() < b->Priority(); + }); + } + for (AnimProcessor* processor : sorted_processors_) { + processor->AdvanceFrame(delta_time); + } +} + +AnimationClipPtr AnimationEngine::LoadAnimationClip(std::string_view uri) { + const HashValue key = Hash(uri); + AnimationClipPtr clip = animation_clips_.Find(key); + if (clip == nullptr) { + clip = std::make_shared(); + auto on_load = [=](AssetLoader::StatusOrData& asset) mutable { + if (asset.ok()) { + clip->Initialize(std::move(*asset)); + } + }; + auto on_finalize = [=](AssetLoader::StatusOrData& asset) mutable { + clip->Finalize(); + }; + + auto asset_loader = registry_->Get(); + asset_loader->LoadAsync(uri, on_load, on_finalize); + animation_clips_.Register(key, clip); + } + return clip; +} + +AnimationClipPtr AnimationEngine::GetAnimationClip(HashValue key) { + return animation_clips_.Find(key); +} + +static StaticRegistry Static_Register(AnimationEngine::Create); + +} // namespace redux diff --git a/redux/redux/engines/animation/animation_engine.h b/redux/redux/engines/animation/animation_engine.h new file mode 100644 index 0000000..b34b2b7 --- /dev/null +++ b/redux/redux/engines/animation/animation_engine.h @@ -0,0 +1,114 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_ANIMATION_ENGINE_H_ +#define REDUX_ENGINES_ANIMATION_ANIMATION_ENGINE_H_ + +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "redux/engines/animation/animation_clip.h" +#include "redux/engines/animation/common.h" +#include "redux/engines/animation/processor/anim_processor.h" +#include "redux/modules/base/registry.h" +#include "redux/modules/base/resource_manager.h" +#include "redux/modules/base/typeid.h" + +namespace redux { + +// Holds and updates all animation data. +// +// The engine holds all of the AnimProcessors, and updates them all when +// AdvanceFrame() is called. The processing is kept central, in this manner, +// for scalability. The engine is not a singleton, but you should try to +// minimize the number of engines. As more Motivators are added to the +// processors, you start to get economies of scale. +class AnimationEngine { + public: + AnimationEngine(const AnimationEngine&) = delete; + AnimationEngine& operator=(const AnimationEngine&) = delete; + + // Creates an instance of the AnimationEngine and adds it to the registry. + static void Create(Registry* registry); + + void OnRegistryInitialize(); + + // Update all the AnimProcessors by 'delta_time'. This advances all Motivators + // created with this AnimationEngine. + void AdvanceFrame(absl::Duration delta_time); + + // Registers a Motivator and Processor with the Engine. Motivators can then be + // created using AcquireMotivator which will then use the specified Processor + // to animate the underlying values. + template + void RegisterMotivator() { + const TypeId processor_type_id = GetTypeId(); + const TypeId motivator_type_id = GetTypeId(); + + auto ptr = new ProcessorT(this); + processors_[processor_type_id].reset(ptr); + allocators_[motivator_type_id] = [ptr](Motivator* motivator, + int dimensions) { + MotivatorT m = ptr->AllocateMotivator(dimensions); + CHECK(sizeof(MotivatorT) == sizeof(MotivatorT)); + *motivator = std::move(m); + }; + + sorted_processors_.clear(); + } + + // Acquires a Motivator that can be used to control and access a value that + // is being animated by the AnimationEngine. + template + MotivatorT AcquireMotivator(int dimensions = 1) { + MotivatorT motivator; + auto iter = allocators_.find(GetTypeId()); + if (iter != allocators_.end()) { + iter->second(&motivator, dimensions); + } + CHECK(motivator.Valid()) << "Did you register this motivator?"; + return motivator; + } + + // Loads and returns the animation data file at the given uri. + AnimationClipPtr LoadAnimationClip(std::string_view uri); + + // Returns an animation data file that has been previously loaded. The `key` + // will be the Hash of the `uri`. Returns nullptr if the clip has been + // unloaded which happens when all references to this clip are released. + AnimationClipPtr GetAnimationClip(HashValue key); + + private: + explicit AnimationEngine(Registry* registry) : registry_(registry) {} + + using ProcessorPtr = std::unique_ptr; + using AllocateMotivatorFn = std::function; + + Registry* registry_ = nullptr; + absl::flat_hash_map processors_; + absl::flat_hash_map allocators_; + std::vector sorted_processors_; + ResourceManager animation_clips_; +}; + +} // namespace redux + +REDUX_SETUP_TYPEID(redux::AnimationEngine); + +#endif // REDUX_ENGINES_ANIMATION_ANIMATION_ENGINE_H_ diff --git a/redux/redux/engines/animation/animation_playback.h b/redux/redux/engines/animation/animation_playback.h new file mode 100644 index 0000000..21c36bf --- /dev/null +++ b/redux/redux/engines/animation/animation_playback.h @@ -0,0 +1,54 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_ANIMATION_PLAYBACK_H_ +#define REDUX_ENGINES_ANIMATION_ANIMATION_PLAYBACK_H_ + +#include "absl/time/time.h" + +namespace redux { + +// Parameters to specify how an animation will be "played". +struct AnimationPlayback { + // Scales `delta_time` of the update to allow for slower/faster playback. + // 0.0 ==> paused + // 0.5 ==> half speed (slow motion) + // 1.0 ==> authored speed + // 2.0 ==> double speed (fast forward) + float playback_rate = 1.f; + + // If played "on top" of a previously playing animation, this specifies how + // much longer in the future the animation will be 100% playing the new + // animation. We create a smooth transition from the current state to the + // animation that lasts for this duration. + absl::Duration blend_time = absl::ZeroDuration(); + + // The starting point from which to play an animation. Useful to start an + // animation at an arbitrary start point. + absl::Duration start_time = absl::ZeroDuration(); + + // If true, start back at the beginning after we reach the end. + bool repeat = false; + + // Shifts the values returned by the animation by the given amounts. The value + // is first scaled by 'value_scale' and then shifted by 'value_offset'. This + // will shift each dimension of the animation by the same amount. + float value_offset = 0.f; + float value_scale = 1.f; +}; +} // namespace redux + +#endif // REDUX_ENGINES_ANIMATION_ANIMATION_PLAYBACK_H_ diff --git a/redux/redux/engines/animation/common.h b/redux/redux/engines/animation/common.h new file mode 100644 index 0000000..8dbf582 --- /dev/null +++ b/redux/redux/engines/animation/common.h @@ -0,0 +1,59 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_COMMON_H_ +#define REDUX_ENGINES_ANIMATION_COMMON_H_ + +#include +#include + +#include "redux/data/asset_defs/anim_asset_def_generated.h" + +namespace redux { + +// Identify bone for skeletal animation. Each non-root bone has a parent whose +// BoneIndex is less than its own. Each bone has a transformation matrix. By +// traversing up the tree to a root bone, multiplying the transformation +// matrices as you go, you can get the global transform for the bone. +using BoneIndex = std::uint16_t; +static const BoneIndex kMaxNumBones = std::numeric_limits::max() - 1; +static const BoneIndex kInvalidBoneIdx = std::numeric_limits::max(); + +inline bool IsTranslationChannel(AnimChannelType type) { + return type == AnimChannelType::TranslateX || + type == AnimChannelType::TranslateY || + type == AnimChannelType::TranslateZ; +} + +inline bool IsQuaternionChannel(AnimChannelType type) { + return type == AnimChannelType::QuaternionX || + type == AnimChannelType::QuaternionY || + type == AnimChannelType::QuaternionZ || + type == AnimChannelType::QuaternionW; +} + +inline bool IsScaleChannel(AnimChannelType type) { + return type == AnimChannelType::ScaleX || type == AnimChannelType::ScaleY || + type == AnimChannelType::ScaleZ; +} + +inline float ChannelDefaultValue(AnimChannelType type) { + return IsScaleChannel(type) || (type == AnimChannelType::QuaternionW) ? 1.f + : 0.f; +} +} // namespace redux + +#endif // REDUX_ENGINES_ANIMATION_COMMON_H_ diff --git a/redux/redux/engines/animation/motivator/motivator.cc b/redux/redux/engines/animation/motivator/motivator.cc new file mode 100644 index 0000000..3e8a352 --- /dev/null +++ b/redux/redux/engines/animation/motivator/motivator.cc @@ -0,0 +1,75 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/animation/motivator/motivator.h" + +#include "redux/engines/animation/common.h" +#include "redux/engines/animation/processor/anim_processor.h" + +namespace redux { + +Motivator::Motivator(Motivator&& rhs) noexcept { + if (rhs.Valid()) { + rhs.processor_->TransferMotivator(rhs.index_, this); + } +} + +Motivator::~Motivator() { Invalidate(); } + +Motivator& Motivator::operator=(Motivator&& rhs) noexcept { + Invalidate(); + if (rhs.Valid()) { + rhs.processor_->TransferMotivator(rhs.index_, this); + } + return *this; +} + +void Motivator::Init(AnimProcessor* processor, Index index) { + // Do not call Invalidate() here. + processor_ = processor; + index_ = index; +} + +void Motivator::Reset() { + processor_ = nullptr; + index_ = kInvalidIndex; +} + +void Motivator::Invalidate() { + if (Valid()) { + processor_->RemoveMotivator(index_); + } +} + +bool Motivator::Valid() const { + return processor_ != nullptr && index_ != kInvalidIndex; +} + +bool Motivator::Sane() const { + return (processor_ == nullptr && index_ == kInvalidIndex) || + (processor_ != nullptr && processor_->ValidMotivator(index_, this)); +} + +int Motivator::Dimensions() const { return processor_->Dimensions(index_); } + +void Motivator::CloneFrom(const Motivator* other) { + Invalidate(); + if (other && other->Valid()) { + other->processor_->CloneMotivator(this, other->index_); + } +} + +} // namespace redux diff --git a/redux/redux/engines/animation/motivator/motivator.h b/redux/redux/engines/animation/motivator/motivator.h new file mode 100644 index 0000000..d200ded --- /dev/null +++ b/redux/redux/engines/animation/motivator/motivator.h @@ -0,0 +1,105 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_MOTIVATOR_MOTIVATOR_H_ +#define REDUX_ENGINES_ANIMATION_MOTIVATOR_MOTIVATOR_H_ + +#include "redux/engines/animation/common.h" + +namespace redux { + +class AnimProcessor; + +// Drives a value towards a target value or along a path. +// +// The value can be one-dimensional (e.g. a float), or multi-dimensional +// (e.g. a transform). This is defined by the sub-class of this class. +// +// The algorithm that drives a Motivator's value moves towards its target is +// determined by the AnimProcessor to which the Motivator belongs. This is +// specified in Motivator::Init(). +// +// A Motivator does not store any data itself. It is an index into its owning +// AnimProcessor. Sub-classes should define an API for the Motivator that allows +// users to query the data. +// +// Only one Motivator can reference a specific index in an AnimProcessor. +class Motivator { + public: + using Index = int; + static constexpr Index kInvalidIndex = static_cast(-1); + + ~Motivator(); + + // Disallow copying of Motivators since only one Motivator can reference a + // specific index in a AnimProcessor. + Motivator(const Motivator& original) = delete; + Motivator& operator=(const Motivator& original) = delete; + + // Transfers ownership of `rhs` motivator to `this` motivator. + // `rhs` motivator is reset and must be initialized again before being + // read. We want to allow moves primarily so that we can have vectors of + // Motivators. + Motivator(Motivator&& rhs) noexcept; + Motivator& operator=(Motivator&& rhs) noexcept; + + // Detatches this Motivator from its AnimProcessor. Functions other than + // Init() and Valid() should no longer be called. + void Invalidate(); + + // Returns true if this Motivator is currently being driven by an + // AnimProcessor. + bool Valid() const; + + // Returns the number of values that this Motivator is driving. For example, a + // 3D position would return 3, since it drives three floats. A single 4x4 + // matrix would return 1, since it's driving one transform. This value is + // determined by the AnimProcessor backing this motivator. + int Dimensions() const; + + // Initializes this Motivator to the current state of another Motivator. + // + // This function is explicitly not a copy constructor because it has a + // different index that references different data. + void CloneFrom(const Motivator* other); + + // Checks the consistency of internal state. Useful for debugging. + // If this function ever returns false, there has been some sort of memory + // corruption or similar bug. + bool Sane() const; + + protected: + // The AnimProcessor uses the functions below. It does not modify data + // directly. + friend class AnimProcessor; + + // These should only be called by AnimProcessor! + Motivator() = default; + void Init(AnimProcessor* processor, Index index); + void Reset(); + + // All calls to an Motivator are proxied to an AnimProcessor. Motivator data + // and processing is centralized to allow for scalable optimizations (e.g. + // SIMD or parallelization). + AnimProcessor* processor_ = nullptr; + + // This index here uniquely identifies this Motivator to the AnimProcessor. + Index index_ = kInvalidIndex; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_ANIMATION_MOTIVATOR_MOTIVATOR_H_ diff --git a/redux/redux/engines/animation/motivator/rig_motivator.cc b/redux/redux/engines/animation/motivator/rig_motivator.cc new file mode 100644 index 0000000..a6dc7f5 --- /dev/null +++ b/redux/redux/engines/animation/motivator/rig_motivator.cc @@ -0,0 +1,56 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/animation/motivator/rig_motivator.h" + +#include "redux/engines/animation/processor/rig_processor.h" + +namespace redux { + +void RigMotivator::BlendToAnim(const AnimationClipPtr& animation, + const AnimationPlayback& playback) { + Processor().BlendToAnim(index_, animation, playback); +} + +void RigMotivator::SetPlaybackRate(float playback_rate) { + Processor().SetPlaybackRate(index_, playback_rate); +} + +void RigMotivator::SetRepeating(bool repeat) { + Processor().SetRepeating(index_, repeat); +} + +absl::Span RigMotivator::GlobalTransforms() const { + return Processor().GlobalTransforms(index_); +} + +const AnimationClipPtr& RigMotivator::CurrentAnimationClip() const { + return Processor().CurrentAnimationClip(index_); +} + +absl::Duration RigMotivator::TimeRemaining() const { + return Processor().TimeRemaining(index_); +} + +RigProcessor& RigMotivator::Processor() { + return *static_cast(processor_); +} + +const RigProcessor& RigMotivator::Processor() const { + return *static_cast(processor_); +} + +} // namespace redux diff --git a/redux/redux/engines/animation/motivator/rig_motivator.h b/redux/redux/engines/animation/motivator/rig_motivator.h new file mode 100644 index 0000000..51fb989 --- /dev/null +++ b/redux/redux/engines/animation/motivator/rig_motivator.h @@ -0,0 +1,71 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_MOTIVATOR_RIG_MOTIVATOR_H_ +#define REDUX_ENGINES_ANIMATION_MOTIVATOR_RIG_MOTIVATOR_H_ + +#include "redux/engines/animation/animation_clip.h" +#include "redux/engines/animation/animation_playback.h" +#include "redux/engines/animation/motivator/motivator.h" +#include "redux/modules/base/typeid.h" +#include "redux/modules/math/matrix.h" + +namespace redux { + +class RigProcessor; + +// Drives a "rig" (which is a hierarchical set of transforms) using data stored +// in an AnimationClip. +class RigMotivator : public Motivator { + public: + using ProcessorT = RigProcessor; + + RigMotivator() = default; + + // Blends from the currently playing animation to the given animation using + // the provided set of playback parameters. If there is no currently playing + // animation, blend_time will be treated as 0 which results in "snapping" to + // the given animation. + void BlendToAnim(const AnimationClipPtr& animation, + const AnimationPlayback& playback); + + // Instantly changes the playback speed of this animation. + void SetPlaybackRate(float playback_rate); + + // Instantly changes the repeat state of this animation. If the current + // animation is done playing, then this call has no effect. + void SetRepeating(bool repeat); + + // Returns an array of matricies: one for each bone in the rig. The matrices + // are all in the space of the root bone. That is, the bone hierarchy has been + // flattened. + absl::Span GlobalTransforms() const; + + // Returns the time remaining in the current animation. + absl::Duration TimeRemaining() const; + + // Returns the currently playing animation clip driving this Motivator. + const AnimationClipPtr& CurrentAnimationClip() const; + + private: + RigProcessor& Processor(); + const RigProcessor& Processor() const; +}; +} // namespace redux + +REDUX_SETUP_TYPEID(redux::RigMotivator); + +#endif // REDUX_ENGINES_ANIMATION_MOTIVATOR_RIG_MOTIVATOR_H_ diff --git a/redux/redux/engines/animation/motivator/spline_motivator.cc b/redux/redux/engines/animation/motivator/spline_motivator.cc new file mode 100644 index 0000000..82bb7c2 --- /dev/null +++ b/redux/redux/engines/animation/motivator/spline_motivator.cc @@ -0,0 +1,73 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/animation/motivator/spline_motivator.h" + +#include "redux/engines/animation/common.h" +#include "redux/engines/animation/processor/spline_processor.h" + +namespace redux { + +float SplineMotivator::Value() const { return Processor().Values(index_)[0]; } + +float SplineMotivator::Velocity() const { + float out = 0.f; + Processor().Velocities(index_, 1, &out); + return out; +} + +absl::Duration SplineMotivator::TimeRemaining() const { + return Processor().TimeRemaining(index_, 1); +} + +void SplineMotivator::SetSpline(const CompactSpline& spline, + const AnimationPlayback& playback) { + Processor().SetSplines(index_, 1, &spline, playback); +} + +void SplineMotivator::SetTarget(float value, absl::Duration time) { + SetTarget(value, 0.f, time); +} + +void SplineMotivator::SetTarget(float value, float velocity, + absl::Duration time) { + Processor().SetTargets(index_, 1, &value, &velocity, time); +} + +void SplineMotivator::SetRepeating(bool repeat) { + const int dimensions = Processor().Dimensions(index_); + Processor().SetSplineRepeating(index_, dimensions, repeat); +} + +void SplineMotivator::SetPlaybackRate(float playback_rate) { + const int dimensions = Processor().Dimensions(index_); + Processor().SetSplinePlaybackRate(index_, dimensions, playback_rate); +} + +bool SplineMotivator::Settled(float max_difference, float max_velocity) const { + const int dimensions = Processor().Dimensions(index_); + return Processor().Settled(index_, dimensions, max_difference, max_velocity); +} + +SplineProcessor& SplineMotivator::Processor() { + return *static_cast(processor_); +} + +const SplineProcessor& SplineMotivator::Processor() const { + return *static_cast(processor_); +} + +} // namespace redux diff --git a/redux/redux/engines/animation/motivator/spline_motivator.h b/redux/redux/engines/animation/motivator/spline_motivator.h new file mode 100644 index 0000000..080c338 --- /dev/null +++ b/redux/redux/engines/animation/motivator/spline_motivator.h @@ -0,0 +1,74 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_MOTIVATOR_SPLINE_MOTIVATOR_H_ +#define REDUX_ENGINES_ANIMATION_MOTIVATOR_SPLINE_MOTIVATOR_H_ + +#include "redux/engines/animation/animation_playback.h" +#include "redux/engines/animation/motivator/motivator.h" +#include "redux/engines/animation/spline/compact_spline.h" +#include "redux/modules/base/typeid.h" +#include "redux/modules/math/matrix.h" + +namespace redux { + +class SplineProcessor; + +// Drives a scalar value using CompactSplines. +class SplineMotivator : public Motivator { + public: + using ProcessorT = SplineProcessor; + + SplineMotivator() = default; + + // Drives the motivator values using the provides spline. + void SetSpline(const CompactSpline& spline, + const AnimationPlayback& playback); + + // Drives the motivator to the specified value and optional target velocity + // over the given timeframe. + void SetTarget(float value, absl::Duration time); + void SetTarget(float value, float velocity, absl::Duration time); + + // Instantly changes whether or not the spline should be repeated when the + // end is reached. + void SetRepeating(bool repeat); + + // Instantly changes the speed at which the spline is followed. + void SetPlaybackRate(float playback_rate); + + // Returns the value of the spline at the current time of the Motivator. + float Value() const; + + // Returns the derivate of the spline at the current time of the Motivator. + float Velocity() const; + + // Returns the amount of time left in the animation. + absl::Duration TimeRemaining() const; + + // Returns true if the spline has reached its end state (within the given + // tolerances). + bool Settled(float max_difference = 0.f, float max_velocity = 0.f) const; + + private: + SplineProcessor& Processor(); + const SplineProcessor& Processor() const; +}; +} // namespace redux + +REDUX_SETUP_TYPEID(redux::SplineMotivator); + +#endif // REDUX_ENGINES_ANIMATION_MOTIVATOR_SPLINE_MOTIVATOR_H_ diff --git a/redux/redux/engines/animation/motivator/transform_motivator.cc b/redux/redux/engines/animation/motivator/transform_motivator.cc new file mode 100644 index 0000000..3bd6248 --- /dev/null +++ b/redux/redux/engines/animation/motivator/transform_motivator.cc @@ -0,0 +1,53 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/animation/motivator/transform_motivator.h" + +#include "redux/engines/animation/common.h" +#include "redux/engines/animation/processor/transform_processor.h" + +namespace redux { + +const Transform& TransformMotivator::Value() const { + return Processor().Value(index_); +} + +absl::Duration TransformMotivator::TimeRemaining() const { + return Processor().TimeRemaining(index_); +} + +void TransformMotivator::BlendTo(const std::vector& animation, + const AnimationPlayback& playback) { + Processor().BlendTo(index_, animation, playback); +} + +void TransformMotivator::SetPlaybackRate(float playback_rate) { + Processor().SetPlaybackRate(index_, playback_rate); +} + +void TransformMotivator::SetRepeating(bool repeat) { + Processor().SetRepeating(index_, repeat); +} + +TransformProcessor& TransformMotivator::Processor() { + return *static_cast(processor_); +} + +const TransformProcessor& TransformMotivator::Processor() const { + return *static_cast(processor_); +} + +} // namespace redux diff --git a/redux/redux/engines/animation/motivator/transform_motivator.h b/redux/redux/engines/animation/motivator/transform_motivator.h new file mode 100644 index 0000000..69a8d27 --- /dev/null +++ b/redux/redux/engines/animation/motivator/transform_motivator.h @@ -0,0 +1,65 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_MOTIVATOR_TRANSFORM_MOTIVATOR_H_ +#define REDUX_ENGINES_ANIMATION_MOTIVATOR_TRANSFORM_MOTIVATOR_H_ + +#include "redux/engines/animation/animation_clip.h" +#include "redux/engines/animation/animation_playback.h" +#include "redux/engines/animation/motivator/motivator.h" +#include "redux/modules/base/typeid.h" +#include "redux/modules/math/transform.h" + +namespace redux { + +class TransformProcessor; + +// Drives a Transform (ie. translation, rotation, and scale) using data from a +// series of animating channels each of which drives a single scalar value. +class TransformMotivator : public Motivator { + public: + using ProcessorT = TransformProcessor; + + TransformMotivator() = default; + + // Returns the current Transform value of the Motivator. + const Transform& Value() const; + + // Returns the time remaining in the current animation. + absl::Duration TimeRemaining() const; + + // Smoothly transitions to the given `animation` as defined by a collection + // of individual channels of animations for scalar components. Information + // about the transition and other options are provided in the playback params. + void BlendTo(const std::vector& animation, + const AnimationPlayback& playback); + + // Instantly changes the playback speed of this animation. + void SetPlaybackRate(float playback_rate); + + // Instantly changes the repeat state of this animation. If the current + // animation is done playing, then this call has no effect. + void SetRepeating(bool repeat); + + private: + TransformProcessor& Processor(); + const TransformProcessor& Processor() const; +}; +} // namespace redux + +REDUX_SETUP_TYPEID(redux::TransformMotivator); + +#endif // REDUX_ENGINES_ANIMATION_MOTIVATOR_TRANSFORM_MOTIVATOR_H_ diff --git a/redux/redux/engines/animation/processor/anim_processor.cc b/redux/redux/engines/animation/processor/anim_processor.cc new file mode 100644 index 0000000..9bde31f --- /dev/null +++ b/redux/redux/engines/animation/processor/anim_processor.cc @@ -0,0 +1,203 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/animation/processor/anim_processor.h" + +#include "redux/engines/animation/motivator/motivator.h" + +namespace redux { + +AnimProcessor::~AnimProcessor() { + // Reset all of the Motivators that we're currently driving. + // We don't want any of them to reference us after we've been destroyed. + for (Motivator::Index index = 0; index < index_allocator_.num_indices(); + index += Dimensions(index)) { + if (motivators_[index] != nullptr) { + RemoveMotivatorWithoutNotifying(index); + } + } + + // Sanity-check: Ensure that we have no more active Motivators. + assert(index_allocator_.Empty()); +} + +void AnimProcessor::VerifyInternalState() const { +#if REDUX_VERIFY_INTERNAL_PROCESSOR_STATE + // Check the validity of the IndexAllocator. + index_allocator_.VerifyInternalState(); + + // Check the validity of each Motivator. + Motivator::Index len = static_cast(motivators_.size()); + for (Motivator::Index i = 0; i < len; i += Dimensions(i)) { + // If a Motivator is nullptr, its index should not be allocated. + assert((motivators_[i] == nullptr && !index_allocator_.ValidIndex(i)) || + motivators_[i]->Valid()); + + if (motivators_[i] == nullptr) continue; + + // All back pointers for a motivator should be the same. + const int dimensions = Dimensions(i); + for (Motivator::Index j = i + 1; j < i + dimensions; ++j) { + assert(motivators_[i] == motivators_[j]); + } + + // A Motivator should be referenced once. + for (Motivator::Index j = i + dimensions; j < len; j += Dimensions(j)) { + assert(motivators_[i] != motivators_[j]); + } + } +#endif // REDUX_VERIFY_INTERNAL_PROCESSOR_STATE +} + +void AnimProcessor::CloneMotivator(Motivator* dst, Motivator::Index src) { + // Early out if the Processor doesn't support duplication to avoid allocating + // and destroying new indices. + if (!SupportsCloning()) { + return; + } + + // Assign an 'index' to reference the new Motivator. All interactions between + // the Motivator and AnimProcessor use this 'index' to identify the data. + const int dimensions = Dimensions(src); + const Motivator::Index dst_index = AllocateMotivatorIndices(dst, dimensions); + + // Call the AnimProcessor-specific cloning routine. + CloneIndices(dst_index, src, dimensions, Engine()); +} + +// Don't notify derived classes. Useful in the destructor, since derived classes +// have already been destroyed. +void AnimProcessor::RemoveMotivatorWithoutNotifying(Motivator::Index index) { + // Ensure the Motivator no longer references us. + motivators_[index]->Reset(); + + // Ensure we no longer reference the Motivator. + const int dimensions = Dimensions(index); + for (int i = 0; i < dimensions; ++i) { + motivators_[index + i] = nullptr; + } + + // Recycle 'index'. It will be used in the next allocation, or back-filled in + // the next call to Defragment(). + index_allocator_.Free(index); +} + +void AnimProcessor::RemoveMotivator(Motivator::Index index) { + assert(ValidMotivatorIndex(index)); + + // Call the AnimProcessor-specific remove routine. + ResetIndices(index, Dimensions(index)); + + // Need this version since the destructor can't call the pure virtual + // RemoveIndex() above. + RemoveMotivatorWithoutNotifying(index); + + VerifyInternalState(); +} + +void AnimProcessor::TransferMotivator(Motivator::Index index, + Motivator* new_motivator) { + assert(ValidMotivatorIndex(index)); + + // Ensure old Motivator does not reference us anymore. Only one Motivator is + // allowed to reference 'index'. + Motivator* old_motivator = motivators_[index]; + old_motivator->Reset(); + + // Set up new_motivator to reference 'index'. + new_motivator->Init(this, index); + + // Update our reference to the unique Motivator that references 'index'. + const int dimensions = Dimensions(index); + for (int i = 0; i < dimensions; ++i) { + motivators_[index + i] = new_motivator; + } + + VerifyInternalState(); +} + +bool AnimProcessor::IsMotivatorIndex(Motivator::Index index) const { + return motivators_[index] != nullptr && + (index == 0 || motivators_[index - 1] != motivators_[index]); +} + +bool AnimProcessor::ValidIndex(Motivator::Index index) const { + return index < index_allocator_.num_indices() && + motivators_[index] != nullptr && + motivators_[index]->processor_ == this; +} + +bool AnimProcessor::ValidMotivatorIndex(Motivator::Index index) const { + return ValidIndex(index) && IsMotivatorIndex(index); +} + +Motivator::Index AnimProcessor::AllocateMotivatorIndices(Motivator* motivator, + int dimensions) { + // Assign an 'index' to reference the new Motivator. All interactions between + // the Motivator and AnimProcessor use this 'index' to identify the data. + const Motivator::Index index = index_allocator_.Alloc(dimensions); + + // Keep a pointer to the Motivator around. We may Defragment() the indices and + // move the data around. We also need to remove the Motivator when we're + // destroyed. + for (int i = 0; i < dimensions; ++i) { + motivators_[index + i] = motivator; + } + + // Initialize the motivator to point at our AnimProcessor. + motivator->Init(this, index); + VerifyInternalState(); + return index; +} + +void AnimProcessor::SetNumIndicesBase(Motivator::Index num_indices) { + // When the size decreases, we don't bother reallocating the size of the + // 'motivators_' vector. We want to avoid reallocating as much as possible, + // so we let it grow to its high-water mark. + // + // TODO: Ideally, we should reserve approximately the right amount of storage + // for motivators_. That would require adding a user-defined initialization + // parameter. + motivators_.resize(num_indices); + + // Call derived class. + SetNumIndices(num_indices); +} + +void AnimProcessor::MoveIndexRangeBase(const IndexRange& source, + Motivator::Index target) { + // Reinitialize the motivators to point to the new index. + const Motivator::Index index_diff = target - source.start(); + for (Motivator::Index i = source.start(); i < source.end(); + i += Dimensions(i)) { + motivators_[i]->Init(this, i + index_diff); + } + + // Tell derivated class about the move. + MoveIndices(source.start(), target, source.Length()); + + // Reinitialize the motivator pointers. + for (Motivator::Index i = source.start(); i < source.end(); ++i) { + // Assert we're moving something valid onto something invalid. + assert(motivators_[i] != nullptr); + assert(motivators_[i + index_diff] == nullptr); + + // Move our internal data too. + motivators_[i + index_diff] = motivators_[i]; + motivators_[i] = nullptr; + } +} +} // namespace redux diff --git a/redux/redux/engines/animation/processor/anim_processor.h b/redux/redux/engines/animation/processor/anim_processor.h new file mode 100644 index 0000000..0c9ba38 --- /dev/null +++ b/redux/redux/engines/animation/processor/anim_processor.h @@ -0,0 +1,220 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_PROCESSOR_ANIM_PROCESSOR_H_ +#define REDUX_ENGINES_ANIMATION_PROCESSOR_ANIM_PROCESSOR_H_ + +#include + +#include "redux/engines/animation/common.h" +#include "redux/engines/animation/motivator/motivator.h" +#include "redux/engines/animation/processor/index_allocator.h" +#include "redux/modules/base/typeid.h" + +namespace redux { + +class AnimationEngine; + +// An AnimProcessor processes *all* instances of one type of Motivator. +// +// Each derivation of AnimProcessor is one animation algorithm. It holds +// all the data for all Motivators that are currently using that animation +// algorithm. +// +// We pool the processing for potential optimization opportunities. We may have +// hundreds of smoothly-interpolating one-dimensional Motivators, for example. +// It's nice to be able to update those 4 or 8 or 16 at a time using SIMD. +// And it's nice to have the data gathered in one spot if we want to use +// multiple threads. +// +// AnimProcessors exists in the internal API. For the external API, please +// see Motivator. +class AnimProcessor { + public: + virtual ~AnimProcessor(); + + // Initializes `dst` to be a clone of the Motivator referenced by `src`. + void CloneMotivator(Motivator* dst, Motivator::Index src); + + // Removes a motivator and return its index to the pile of allocatable + // indices. + // + // This function should only be called by Motivator::Invalidate(). + void RemoveMotivator(Motivator::Index index); + + // Transfers ownership of the motivator at `index` to 'new_motivator'. + // Resets the Motivator that currently owns `index` and initializes + // 'new_motivator'. + // + // This function should only be called by Motivator's move operations. + void TransferMotivator(Motivator::Index index, Motivator* new_motivator); + + // Returns true if `index` is currently driving a motivator. Does not do any + // validity checking, however, like ValidMotivatorIndex() does. + bool IsMotivatorIndex(Motivator::Index index) const; + + // Returns true if `index` is currently in a block of indices driven by + // a motivator. + bool ValidIndex(Motivator::Index index) const; + + // Returns true if a Motivator is referencing this index. That is, if this + // index is part of a block of indices (for example a block of 3 indices + // referenced by a Motivator3f), then this index is the *first* index in that + // block. + bool ValidMotivatorIndex(Motivator::Index index) const; + + // Returns true if `index` is currently driving `motivator`. + bool ValidMotivator(Motivator::Index index, + const Motivator* motivator) const { + return ValidIndex(index) && motivators_[index] == motivator; + } + + // Advance the simulation by `delta_time`. + // + // This function should only be called by AnimationEngine::AdvanceFrame. + virtual void AdvanceFrame(absl::Duration delta_time) = 0; + + // The number of slots occupied in the AnimProcessor. For example, + // a position in 3D space would return 3. A single 4x4 matrix would return 1. + int Dimensions(Motivator::Index index) const { + return index_allocator_.CountForIndex(index); + } + + // The lower the number, the sooner the AnimProcessor gets updated. + // Should never change. We want a static ordering of processors. + // Some AnimProcessors use the output of other AnimProcessors, so + // we impose a strict ordering here. + virtual int Priority() const { return 0; } + + // Ensure that the internal state is consistent. Call periodically when + // debugging problems where the internal state is corrupt. + void VerifyInternalState() const; + + protected: + AnimProcessor() = delete; + explicit AnimProcessor(AnimationEngine* engine) + : index_allocator_(allocator_callbacks_), engine_(engine) { + allocator_callbacks_.set_processor(this); + } + + // Allocates an index for `motivator` and initialize it to that index. Returns + // the newly allocated index. + Motivator::Index AllocateMotivatorIndices(Motivator* motivator, + int dimensions); + + // Indicates whether or not this Processor supports cloning. + // If overridden to return true, the Processor should also override the + // CloneIndices() signature below to implement the actual duplication. + virtual bool SupportsCloning() { return false; } + + // Initializes data at [dst, dst + dimensions) to a clone of the data at + // [src, src + dimensions). Processors that support initializing new + // Motivators from existing Motivators should override this function. + virtual void CloneIndices(Motivator::Index dst, Motivator::Index src, + int dimensions, AnimationEngine* engine) { + // Hitting this assertion means SupportsCloning() returned true but the + // Processor didn't override this function. + assert(false); + } + + // Resets data at [index, index + dimensions). + // See comment above InitializeIndex for meaning of `index`. + // If your AnimProcessor stores data in a plain array, you probably have + // nothing to do. But if you use dynamic memory per index, + // (which you really shouldn't - too slow!), you should deallocate it here. + // For debugging, it might be nice to invalidate the data. + virtual void ResetIndices(Motivator::Index index, int dimensions) = 0; + + // Moves the data chunk of length `dimensions` from `old_index` into + // `new_index`. Used by Defragment(). + // Note that the index range starting at `new_index` is guaranteed to be + // inactive. + virtual void MoveIndices(Motivator::Index old_index, + Motivator::Index new_index, int dimensions) = 0; + + // Increases or decreases the total number of indices. + // If decreased, existing indices >= num_indices should be uninitialized. + // If increased, internal arrays should be extended to new_indices, and + // new items in the arrays should be initialized as reset. + virtual void SetNumIndices(Motivator::Index num_indices) = 0; + + // When an index is moved, the Motivator that references that index is + // updated. Can be called at the discretion of your AnimProcessor, + // but normally called at the beginning of your + // AnimProcessor::AdvanceFrame. + void Defragment() { index_allocator_.Defragment(); } + + // Returns a handle to the AnimationEngine instance that owns this processor. + AnimationEngine* Engine() { return engine_; } + const AnimationEngine* Engine() const { return engine_; } + + private: + using MotivatorIndexAllocator = IndexAllocator; + using IndexRange = MotivatorIndexAllocator::IndexRange; + + // Don't notify derived class. + void RemoveMotivatorWithoutNotifying(Motivator::Index index); + + // Handle callbacks from IndexAllocator. + void MoveIndexRangeBase(const IndexRange& source, Motivator::Index target); + void SetNumIndicesBase(Motivator::Index num_indices); + + // Proxy callbacks from IndexAllocator into AnimProcessor. + class AllocatorCallbacks : public MotivatorIndexAllocator::CallbackInterface { + public: + AllocatorCallbacks() : processor_(nullptr) {} + void set_processor(AnimProcessor* processor) { processor_ = processor; } + void SetNumIndices(Motivator::Index num_indices) override { + processor_->SetNumIndicesBase(num_indices); + } + void MoveIndexRange(const IndexRange& source, + Motivator::Index target) override { + processor_->MoveIndexRangeBase(source, target); + } + + private: + AnimProcessor* processor_; + }; + + // Back-pointer to the Motivators for each index. The Motivators reference + // this AnimProcessor and a specific index into the AnimProcessor, + // so when the index is moved, or when the AnimProcessor itself is + // destroyed, we need to update the Motivator. + // Note that we only keep a reference to a single Motivator per index. + // When a Motivator is moved, the old Motivator is Reset and the reference + // here is updated. + std::vector motivators_; + + // Proxy calbacks into AnimProcessor. The other option is to derive + // AnimProcessor from IndexAllocator::CallbackInterface, but that would + // create a messier API, and not be great OOP. + // This member should be initialized before index_allocator_ is initialized. + AllocatorCallbacks allocator_callbacks_; + + // When an index is freed, we keep track of it here. When an index is + // allocated, we use one off this array, if one exists. + // When Defragment() is called, we empty this array by filling all the + // unused indices with the highest allocated indices. This reduces the total + // size of the data arrays. + MotivatorIndexAllocator index_allocator_; + + // A handle to the owning AnimationEngine. This is required when new + // Motivators are created outside of typical initialization times. + AnimationEngine* engine_; +}; +} // namespace redux + +#endif // REDUX_ENGINES_ANIMATION_PROCESSOR_ANIM_PROCESSOR_H_ diff --git a/redux/redux/engines/animation/processor/index_allocator.h b/redux/redux/engines/animation/processor/index_allocator.h new file mode 100644 index 0000000..04b2c8e --- /dev/null +++ b/redux/redux/engines/animation/processor/index_allocator.h @@ -0,0 +1,490 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_PROCESSOR_INDEX_ALLOCATOR_H_ +#define REDUX_ENGINES_ANIMATION_PROCESSOR_INDEX_ALLOCATOR_H_ + +#include +#include +#include + +#include "redux/modules/base/logging.h" + +namespace redux { + +// Allocates and frees indices into an array. Tries to keep the array as small +// as possible by recycling indices that have been freed. +// +// Let's say we have an array of items that we would like to process with SIMD +// instructions. Items can be added and deleted from the array though. We don't +// want many unused indices in the array, since these holes still have to be +// processed with SIMD (which processes indices in groups of 4 or 8 or 16). +// +// The IndexAllocator is great for this situation since you can call +// Defragment() before running the SIMD algorithm. The Defragment() call will +// backfill unused indices and ensure the data is contiguous. +// +// Details +// ======= +// Periodically, you can call Defragment() to backfill indices that have been +// freed with the largest indices. This minimizes the length of the array, and +// more importantly makes the array data contiguous. +// +// During Defragment() when an index is moved, a callback +// CallbackInterface::MoveIndex() is called so that the user can move the +// corresponding data. +// +// Whenever the array size is increased (durring Alloc()) or decreased (during +// Defragment()), a callback CallbackInterface::SetNumIndices() is called so +// that the user can grow or shrink the corresponding data. +template +class IndexAllocator { + public: + // The number of indices to allocate. Same base type as Index, since the + // `counts_` array can be as long as the largest index. + typedef Index Count; + + class IndexRange { + public: + IndexRange() : start_(1), end_(0) {} + IndexRange(Index start, Index end) : start_(start), end_(end) {} + bool Valid() const { return start_ <= end_; } + Index Length() const { return end_ - start_; } + Index start() const { return start_; } + Index end() const { return end_; } + + private: + Index start_; + Index end_; + }; + + class CallbackInterface { + public: + virtual ~CallbackInterface() {} + virtual void SetNumIndices(Index num_indices) = 0; + virtual void MoveIndexRange(const IndexRange& source, Index target) = 0; + }; + + // Create an empty IndexAllocator that uses the specified callback interface. + explicit IndexAllocator(CallbackInterface& callbacks) + : callbacks_(&callbacks) { + static_assert(std::is_signed::value, + "Count (and therefore Index) must be a signed type"); + } + + // If a previously-freed index can be recycled, allocates that index. + // Otherwise, increases the total number of indices by `count`, and return + // the first new index. When the number of indices is increased, + // the SetNumIndices() callback is called. + // @param count The number of indices in this allocation. Each block of + // allocated indices is kept contiguous during Defragment() + // calls. The index returned is the first index in the block. + Index Alloc(Count count) { + // Recycle an unused index, if one exists and has the correct count. + typename std::vector::iterator least_excess_it = + unused_indices_.end(); + Count least_excess = std::numeric_limits::max(); + for (auto it = unused_indices_.begin(); it != unused_indices_.end(); ++it) { + const Index unused_index = *it; + const Count excess = CountForIndex(unused_index) - count; + + // Not big enough. + if (excess < 0) continue; + + // Perfect size. Remove from `unused_indices_` pool. + if (excess == 0) { + unused_indices_.erase(it); + return unused_index; + } + + // Too big. We'll return the one with the least excess size. + if (excess < least_excess) { + least_excess = excess; + least_excess_it = it; + } + } + + // The unused index has a count that's too high. + if (least_excess_it != unused_indices_.end()) { + // Return the first `count` indices. + const Index excess_index = *least_excess_it; + InitializeIndex(excess_index, count); + + // Put the remainder in the `unused_indices_` pool. + const Index remainder_index = excess_index + count; + InitializeIndex(remainder_index, least_excess); + *least_excess_it = remainder_index; + + return excess_index; + } + + // Allocate a new index. + const Index new_index = num_indices(); + SetNumIndices(new_index + count); + InitializeIndex(new_index, count); + return new_index; + } + + // Recycle 'index'. It will be used in the next allocation, or backfilled in + // the next call to Defragment(). + // @param index Index to be freed. Must be in the range + // [0, num_indices_ - 1]. + void Free(Index index) { + DCHECK(ValidIndex(index)); + unused_indices_.push_back(index); + } + + // Only one block of unused indices left, and they're at the end of the + // array. + bool UnusedAtEnd() const { + return unused_indices_.size() == 1 && + NextIndex(unused_indices_[0]) == num_indices(); + } + + // Backfill all unused index blocks. That is, move index blocks around + // until all the unused index blocks have the *highest* indices. Then, + // shrink the number of indices to remove all unused index blocks. + // + // Every time we move an index block, we call callbacks_->MoveIndexRange(). + // In MoveIndexRange(), the callee can correspondingly move its + // internal data around to match the index shuffle. At the end of + // Degragment(), the callee's internal data will be contiguous. + // Contiguous data is essential in data-oriented design, since it + // minimizes cache misses. + // + // Note that we could eliminate Defragment() function by calling MoveIndex() + // from Free(). The code would be simpler. We move the indices lazily, + // however, for performance: Defragment() is something that can happen on a + // background thread. + // + // This function has worst case runtime of O(n) index moves, where n is + // the total number of indices. To see why, notice that indices are only + // moved forward, and are always moved into the forward-most hole. + // + // Note that there is some inefficiency with setting the `count_` array + // excessively. The worst-case number of operations on the `count_` array + // is greater than O(n). However, the assumption is that since `count_` is + // just an array of integers, operations on it are insignificant compared + // to the actual data movement that happens in callbacks_->MoveIndexRange(). + // However, there is an optimization opportunity here, most likely. + // + // In practice, this function will normally perform much better than O(n) + // moves. We endeavour to fill holes with index blocks near the end of the + // array. That is, we try to leapfrog the hole to the end of the array + // when possible. + // + // Because of this, when all allocations are the same size, the worst case + // runtime improves significantly to O(k) index moves, where k is the + // total number of *unused* indices. + // + // If moving an index is cheaper than processing data for an index, + // then you should call Defragment() right before you process data, + // for optimal performance. + // + // Note that the number of indices shrinks or stays the same in this + // function, so the final call to SetNumIndices() will never result in a + // reallocation of the underlying array (which would be slow). + // + void Defragment() { + // Quick check. An optimization. + if (unused_indices_.size() == 0) return; + + for (;;) { + // We check if unused index is the last index, so must be in sorted order. + ConsolidateUnusedIndices(); + + // If all the holes have been pushed to the end, we are done and can + // trim the number of indices. + const bool unused_at_end = unused_indices_.size() == 1 && + NextIndex(unused_indices_[0]) == num_indices(); + if (unused_at_end) break; + + // Find range of indices that will fit into the first block of + // unused indices and move them into it. + BackfillFirstUnused(); + } + + // Remove hole at end. + SetNumIndices(unused_indices_[0]); + unused_indices_.clear(); + } + + // Returns true if there are no indices allocated. + bool Empty() const { return num_indices() == NumUnusedIndices(); } + + // Returns true if the index is current allocated. + bool ValidIndex(Index index) const { + if (index < 0 || index >= num_indices()) return false; + + if (counts_[index] == 0) return false; + + for (ConstIndexIterator it = unused_indices_.begin(); + it != unused_indices_.end(); ++it) { + if (index == *it) return false; + } + + return true; + } + + // Returns the number of wasted indices. These holes will be plugged when + // Degragment() is called. + Index NumUnusedIndices() const { + Count count = 0; + for (std::size_t i = 0; i < unused_indices_.size(); ++i) { + count += CountForIndex(unused_indices_[i]); + } + return count; + } + + // Returns the `count` value specified in Alloc. That is, the number of + // consecutive indices associated with `index`. + Count CountForIndex(Index index) const { + DCHECK_GT(counts_[index], 0); + return counts_[index]; + } + + // Assert if the internal state is invalid in any way. + // Sanity check on this data structure. + void VerifyInternalState() const { + for (Index i = 0; i < num_indices();) { + // Each block of indices must start with the positive size of the block. + const Count count = counts_[i]; + CHECK_GT(count, 0); + + // Succeeding elements in a block give the offset back to the start. + for (Index j = 1; j < count; ++j) { + CHECK(counts_[i + j] == -j); + } + + // Jump to the next block. + i += count; + } + } + + // Returns the size of the array that number of contiguous indices. + // This includes all the indices that have been free. + Index num_indices() const { return static_cast(counts_.size()); } + + private: + typedef typename std::vector::const_iterator ConstIndexIterator; + static const Index kInvalidIndex = static_cast(-1); + + // Returns the next allocated index. Skips over all indices associated + // with `index`. + Index NextIndex(Index index) const { + DCHECK(0 <= index && index < num_indices() && counts_[index] > 0); + return index + counts_[index]; + } + + // Returns the previous allocated index. Skips over all indices associated + // with `index` - 1. + Index PrevIndex(Index index) const { + DCHECK(0 < index && index <= num_indices() && + (index == num_indices() || counts_[index - 1] == 1 || + counts_[index - 1] < 0)); + const Count prev_count = counts_[index - 1]; + return prev_count > 0 ? index - 1 : index - 1 + prev_count; + } + + // Set up the `counts_` array to hold the size of `index`. Only the value + // at `counts_[index]` really matters. The others are initialized for + // debugging, and to make traversal of the `counts_` array easier. + void InitializeIndex(Index index, Count count) { + // Initialize the count for this index. + counts_[index] = count; + for (Count i = 1; i < count; ++i) { + counts_[index + i] = -i; + } + } + + // Adjust internal state to match the new index size, and notify the + // callback that size has changed. + void SetNumIndices(Index new_num_indices) { + // Increase (or decrease) the count logger. + counts_.resize(new_num_indices, 0); + + // Report size change. + callbacks_->SetNumIndices(new_num_indices); + } + + // Combine adjacent blocks of unused incides in `unused_indices_`. + void ConsolidateUnusedIndices() { + // First put the indices in order so that we can process them efficiently. + std::sort(unused_indices_.begin(), unused_indices_.end()); + + // Consolidate adjacent blocks of unused indices. + std::size_t new_num_unused = 0; + for (std::size_t i = 0; i < unused_indices_.size();) { + const Index unused = unused_indices_[i]; + + // Find first non-consecutive index in unused_indices_. + std::size_t j = i + 1; + while (j < unused_indices_.size() && + unused_indices_[j] == NextIndex(unused_indices_[j - 1])) { + ++j; + } + + // Consolidate consecutive unused indices. + const std::size_t num_consecutive = j - i; + if (num_consecutive > 1) { + const Count consolidated_count = + NextIndex(unused_indices_[j - 1]) - unused; + InitializeIndex(unused, consolidated_count); + } + + // Write to the output array. + unused_indices_[new_num_unused++] = unused; + + // Increment the read-counter, skipping over any we've consolidated. + i += num_consecutive; + } + + // Shrink number of unused indices. Size can only get smaller. + DCHECK(new_num_unused <= unused_indices_.size()); + unused_indices_.resize(new_num_unused); + } + + // Move later blocks of indices into the first hole in `unused_indices_`. + // That is, move the first hole farther back in the index array. + void BackfillFirstUnused() { + DCHECK(unused_indices_.size() > 0); + const IndexRange unused_range( + unused_indices_[0], + unused_indices_[0] + CountForIndex(unused_indices_[0])); + + // Find a fill_range after unused_range that we can move into unused_range. + // + // Case 1. Fill + // indices in fill_range are moved into the first part of unused_range. + // fill_range.Length() <= unused_range.Length() + // + // before: unused_range fill_range + // |..................| |abcdefghij| + // + // after: |abcdefghij|.......| |..........| + // unused_hole fill_hole + // + // + // Case 2. Shift + // indices in fill_range are moved to the left, into unused_range. + // this opens up a hole where fill_range used to end + // + // before: unused_range fill_range + // |............|abcdefghijklmnop| + // + // after: |abcdefghijklmnop|............| + // shift_hole + // + IndexRange fill_range = LastIndexRangeSmallerThanHole(unused_range.start()); + const bool is_fill = fill_range.Valid(); + if (!is_fill) { + // If there's no index range that will fit into the hole, shift over + // all the indices between this hole and the next. + const Index next_hole_index = + unused_indices_.size() > 1 ? unused_indices_[1] : num_indices(); + fill_range = IndexRange(NextIndex(unused_range.start()), next_hole_index); + } + + // Allow the callback to move data associated with the indices. + callbacks_->MoveIndexRange(fill_range, unused_range.start()); + + // Move `counts_` to fill_range's new location + memmove(&counts_[unused_range.start()], &counts_[fill_range.start()], + fill_range.Length() * sizeof(counts_[0])); + + // Re-initialize `counts_` for new holes. + if (is_fill) { + // See Case 1 above: Add a hole for the range we just moved. + InitializeIndex(fill_range.start(), fill_range.Length()); + unused_indices_[0] = fill_range.start(); + + // If we didn't completely fill unused_range, add a hole for the rest. + const IndexRange unused_hole(unused_range.start() + fill_range.Length(), + unused_range.end()); + if (unused_hole.Length() > 0) { + InitializeIndex(unused_hole.start(), unused_hole.Length()); + unused_indices_.push_back(unused_hole.start()); + } + } else { + // See Case 2 above: Add a hole at the end of the range we shifted over. + const IndexRange shift_hole(unused_range.start() + fill_range.Length(), + fill_range.end()); + InitializeIndex(shift_hole.start(), shift_hole.Length()); + unused_indices_[0] = shift_hole.start(); + } + } + + IndexRange LastIndexRangeSmallerThanHole(Index index) const { + // We want the last consecutive range of indices of length <= count. + const Count count = CountForIndex(index); + + // Loop from the back. `end` is the end of the range. + DCHECK(unused_indices_.size() > 0); + std::size_t unused_i = unused_indices_.size() - 1; + for (Index end = num_indices(); end > index; end = PrevIndex(end)) { + // Skip over unused indices. + const Index unused_start = unused_indices_[unused_i]; + const Index unused_end = NextIndex(unused_start); + DCHECK(unused_end <= end); + if (end == unused_end) { + unused_i--; + continue; + } + + // Loop towards the front while the size still fits into `count`. + Index start = end; + for (Index j = PrevIndex(end); j > index; j = PrevIndex(j)) { + if (end - j > count) break; + if (j == unused_start) break; + start = j; + } + + // If at least some indices are in range, use those. + if (start < end) return IndexRange(start, end); + } + + // No index range fits, so return an invalid range. + return IndexRange(); + } + + // When indices are moved or the number of inidices changes, we notify the + // caller via these callbacks. + CallbackInterface* callbacks_; + + // For every valid index, the number of indices + // associated with that index. For intermediate indices, negative number + // representing the offset to the actual index. + // + // valid indices + // | | | | | + // v v v v v + // For example: 1 | 2 -1 | 4 -1 -2 -3 | 1 | 1 + // ^ ^ ^ ^ + // | | | | + // offset to the actual index + std::vector counts_; + + // When an index is freed, we keep track of it here. When an index is + // allocated, we use one off this array, if one exists. + // When Defragment() is called, we empty this array by filling all the + // unused indices with the highest allocated indices. This reduces the total + // size of the data arrays. + std::vector unused_indices_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_ANIMATION_PROCESSOR_INDEX_ALLOCATOR_H_ diff --git a/redux/redux/engines/animation/processor/rig_processor.cc b/redux/redux/engines/animation/processor/rig_processor.cc new file mode 100755 index 0000000..b5231f0 --- /dev/null +++ b/redux/redux/engines/animation/processor/rig_processor.cc @@ -0,0 +1,167 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/animation/processor/rig_processor.h" + +#include "absl/time/time.h" +#include "redux/engines/animation/animation_clip.h" +#include "redux/engines/animation/animation_engine.h" +#include "redux/modules/base/logging.h" + +namespace redux { + +RigMotivator RigProcessor::AllocateMotivator(int dimensions) { + RigMotivator motivator; + const Motivator::Index index = + AllocateMotivatorIndices(&motivator, dimensions); + data_[index].end_time = time_; + return motivator; +} + +void RigProcessor::AdvanceFrame(absl::Duration delta_time) { + Defragment(); + for (auto& data : data_) { + data.UpdateGlobalTransforms(); + } + // Update our global time. It shouldn't matter if this wraps around, since we + // only calculate times relative to it. + time_ += delta_time; +} + +void RigProcessor::RigData::Reset() { + animation = nullptr; + end_time = absl::ZeroDuration(); + motivators.clear(); + global_transforms.clear(); +} + +// Traverse hierarchy, converting local transforms from `motivators_` into +// global transforms. The `parents` are layed out such that the parent +// always come before the child. +// TODO OPT: optimize `parents` layout so that we can parallelize this call. +void RigProcessor::RigData::UpdateGlobalTransforms() { + const absl::Span parents = animation->BoneParents(); + const int num_bones = animation->NumBones(); + for (int i = 0; i < num_bones; ++i) { + const int parent_idx = parents[i]; + const TransformMotivator& motivator = motivators[i]; + const Transform local_transform = + motivator.Valid() ? motivator.Value() : Transform(); + + const mat4 local_matrix = TransformMatrix(local_transform); + if (parent_idx == kInvalidBoneIdx) { + global_transforms[i] = local_matrix; + } else { + CHECK_GT(i, parent_idx); + global_transforms[i] = global_transforms[parent_idx] * local_matrix; + } + } + + // TODO: We should let go of the animation once we've reached the end and no + // longer need to hold on to the splines. +} + +void RigProcessor::BlendToAnim(Motivator::Index index, + const AnimationClipPtr& anim, + const AnimationPlayback& playback) { + RigData& data = Data(index); + data.end_time = time_ + anim->Duration(); + + // When animation has only one bone, or mesh has only one bone, + // we simply animate the root node only. + data.animation = anim; + const int num_bones = anim->NumBones(); + + data.motivators.resize(num_bones); + data.global_transforms.resize(num_bones); + + // Update the motivators to blend to our new values. + for (BoneIndex i = 0; i < num_bones; ++i) { + TransformMotivator& motivator = data.motivators[i]; + if (!motivator.Valid()) { + motivator = Engine()->AcquireMotivator(); + } + motivator.BlendTo(anim->GetBoneAnimation(i), playback); + } +} + +void RigProcessor::SetPlaybackRate(Motivator::Index index, + float playback_rate) { + RigData& data = Data(index); + for (size_t i = 0; i < data.motivators.size(); ++i) { + data.motivators[i].SetPlaybackRate(playback_rate); + } +} + +void RigProcessor::SetRepeating(Motivator::Index index, bool repeat) { + RigData& data = Data(index); + for (size_t i = 0; i < data.motivators.size(); ++i) { + data.motivators[i].SetRepeating(repeat); + } +} + +const AnimationClipPtr& RigProcessor::CurrentAnimationClip( + Motivator::Index index) const { + return Data(index).animation; +} + +absl::Span RigProcessor::GlobalTransforms( + Motivator::Index index) const { + return Data(index).global_transforms; +} + +absl::Duration RigProcessor::TimeRemaining(Motivator::Index index) const { + const RigData& data = Data(index); + if (data.end_time == absl::InfiniteDuration()) { + return absl::InfiniteDuration(); + } + + absl::Duration time = absl::ZeroDuration(); + for (size_t i = 0; i < data.motivators.size(); ++i) { + time = std::max(time, data.motivators[i].TimeRemaining()); + } + return time; +} + +void RigProcessor::SetNumIndices(Motivator::Index num_indices) { + data_.resize(num_indices); +} + +void RigProcessor::ResetIndices(Motivator::Index index, int dimensions) { + for (Motivator::Index i = index; i < index + dimensions; ++i) { + data_[i].Reset(); + } +} + +void RigProcessor::MoveIndices(Motivator::Index old_index, + Motivator::Index new_index, int dimensions) { + using std::swap; + for (int i = 0; i < dimensions; ++i) { + swap(data_[new_index + i], data_[old_index + i]); + data_[old_index + i].Reset(); + } +} + +RigProcessor::RigData& RigProcessor::Data(Motivator::Index index) { + CHECK(ValidIndex(index)); + return data_[index]; +} + +const RigProcessor::RigData& RigProcessor::Data(Motivator::Index index) const { + CHECK(ValidIndex(index)); + return data_[index]; +} +} // namespace redux diff --git a/redux/redux/engines/animation/processor/rig_processor.h b/redux/redux/engines/animation/processor/rig_processor.h new file mode 100644 index 0000000..0411f35 --- /dev/null +++ b/redux/redux/engines/animation/processor/rig_processor.h @@ -0,0 +1,101 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_PROCESSOR_RIG_PROCESSOR_H_ +#define REDUX_ENGINES_ANIMATION_PROCESSOR_RIG_PROCESSOR_H_ + +#include +#include + +#include "redux/engines/animation/animation_clip.h" +#include "redux/engines/animation/common.h" +#include "redux/engines/animation/motivator/rig_motivator.h" +#include "redux/engines/animation/motivator/transform_motivator.h" +#include "redux/engines/animation/processor/anim_processor.h" +#include "redux/modules/base/typeid.h" +#include "redux/modules/math/matrix.h" + +namespace redux { + +class RigProcessor : public AnimProcessor { + public: + explicit RigProcessor(AnimationEngine* engine) : AnimProcessor(engine) {} + + RigMotivator AllocateMotivator(int dimensions = 1); + + int Priority() const override { return 3; } + + void AdvanceFrame(absl::Duration delta_time) override; + + // Return the animation that is currently playing. + const AnimationClipPtr& CurrentAnimationClip(Motivator::Index index) const; + + // Returns an array of length `DefiningAnim.NumBones()`. + // The i'th element of the array represents the transform from the root + // bone to the bone-space on the i'th bone. + absl::Span GlobalTransforms(Motivator::Index index) const; + + // Return the time remaining in the current rig animation. + absl::Duration TimeRemaining(Motivator::Index index) const; + + // Smoothly transition to the animation in `anim`. + void BlendToAnim(Motivator::Index index, const AnimationClipPtr& anim, + const AnimationPlayback& playback); + + // Instantly change the playback speed of this animation. If multiple + // animations are running, set the playback rate for all of them. + void SetPlaybackRate(Motivator::Index index, float playback_rate); + + // Instantly change the repeat state of this animation. If multiple + // animations are running, set the repeat state for all of them. If no + // animations are running, has no effect. + void SetRepeating(Motivator::Index index, bool repeat); + + private: + struct RigData { + RigData() = default; + RigData(RigData&&) noexcept = default; + RigData& operator=(RigData&&) noexcept = default; + + void Reset(); + void UpdateGlobalTransforms(); + + AnimationClipPtr animation = nullptr; + + // Time that the animation is expected to complete. + absl::Duration end_time = absl::ZeroDuration(); + + std::vector motivators; + std::vector global_transforms; + }; + + void SetNumIndices(Motivator::Index num_indices) override; + void MoveIndices(Motivator::Index old_index, Motivator::Index new_index, + int dimensions) override; + void ResetIndices(Motivator::Index index, int dimensions) override; + + RigData& Data(Motivator::Index index); + const RigData& Data(Motivator::Index index) const; + + std::vector data_; + absl::Duration time_ = absl::ZeroDuration(); +}; + +} // namespace redux + +REDUX_SETUP_TYPEID(redux::RigProcessor); + +#endif // REDUX_ENGINES_ANIMATION_PROCESSOR_RIG_PROCESSOR_H_ diff --git a/redux/redux/engines/animation/processor/spline_processor.cc b/redux/redux/engines/animation/processor/spline_processor.cc new file mode 100644 index 0000000..522ed31 --- /dev/null +++ b/redux/redux/engines/animation/processor/spline_processor.cc @@ -0,0 +1,309 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/animation/processor/spline_processor.h" + +#include "redux/engines/animation/common.h" + +namespace redux { + +inline float DurationToSplineTime(absl::Duration duration) { + return static_cast(absl::ToDoubleMilliseconds(duration)); +} + +inline absl::Duration SplineTimeToDuration(float time) { + return absl::Milliseconds(time); +} + +inline SplinePlayback AsSplinePlayback(const AnimationPlayback& anim) { + SplinePlayback spline; + spline.playback_rate = anim.playback_rate; + spline.blend_x = DurationToSplineTime(anim.blend_time); + spline.start_x = DurationToSplineTime(anim.start_time); + spline.y_offset = anim.value_offset; + spline.y_scale = anim.value_scale; + spline.repeat = anim.repeat; + return spline; +} + +SplineMotivator SplineProcessor::AllocateMotivator(int dimensions) { + SplineMotivator motivator; + AllocateMotivatorIndices(&motivator, dimensions); + return motivator; +} + +void SplineProcessor::SetSplines(Motivator::Index index, int dimensions, + const CompactSpline* splines, + const AnimationPlayback& playback) { + // Return the local splines to the spline pool. We use external splines now. + for (int i = index; i < index + dimensions; ++i) { + FreeSplineForIndex(i); + } + + // Initialize spline to follow way points. + // Snaps the current value and velocity to the way point's start value + // and velocity. + interpolator_.SetSplines(index, dimensions, splines, + AsSplinePlayback(playback)); +} + +void SplineProcessor::SetTargets(Motivator::Index index, int dimensions, + const float* values, const float* velocities, + absl::Duration time) { + for (int i = 0; i < dimensions; ++i) { + CreateSplineToTarget(index + i, values[i], velocities[i], time); + } +} + +void SplineProcessor::AdvanceFrame(absl::Duration delta_time) { + Defragment(); + auto spline_time = DurationToSplineTime(delta_time); + interpolator_.AdvanceFrame(spline_time); +} + +// Accessors to allow the user to get and set simluation values. +const float* SplineProcessor::Values(Motivator::Index index) const { + return interpolator_.Ys(index); +} + +void SplineProcessor::Velocities(Motivator::Index index, int dimensions, + float* out) const { + interpolator_.Derivatives(index, dimensions, out); +} + +void SplineProcessor::Directions(Motivator::Index index, int dimensions, + float* out) const { + interpolator_.DerivativesWithoutPlayback(index, dimensions, out); +} + +void SplineProcessor::TargetValues(Motivator::Index index, int dimensions, + float* out) const { + interpolator_.EndYs(index, dimensions, out); +} + +void SplineProcessor::TargetVelocities(Motivator::Index index, int dimensions, + float* out) const { + interpolator_.EndDerivatives(index, dimensions, out); +} + +void SplineProcessor::Differences(Motivator::Index index, int dimensions, + float* out) const { + interpolator_.YDifferencesToEnd(index, dimensions, out); +} + +absl::Duration SplineProcessor::TimeRemaining(Motivator::Index index, + int dimensions) const { + float greatest = std::numeric_limits::min(); + for (int i = 0; i < dimensions; ++i) { + const float spline_time = + interpolator_.EndX(index + i) - interpolator_.X(index + i); + greatest = std::max(greatest, spline_time); + } + return SplineTimeToDuration(greatest); +} + +absl::Duration SplineProcessor::SplineTime(Motivator::Index index) const { + return SplineTimeToDuration(interpolator_.X(index)); +} + +void SplineProcessor::Splines(Motivator::Index index, int dimensions, + const CompactSpline** splines) const { + interpolator_.Splines(index, dimensions, splines); +} + +void SplineProcessor::SetSplineTime(Motivator::Index index, int dimensions, + absl::Duration time) { + interpolator_.SetXs(index, dimensions, DurationToSplineTime(time)); +} + +void SplineProcessor::SetSplinePlaybackRate(Motivator::Index index, + int dimensions, + float playback_rate) { + interpolator_.SetPlaybackRates(index, dimensions, playback_rate); +} + +void SplineProcessor::SetSplineRepeating(Motivator::Index index, int dimensions, + bool repeat) { + interpolator_.SetRepeating(index, dimensions, repeat); +} + +bool SplineProcessor::Settled(Motivator::Index index, int dimensions, + float max_difference, float max_velocity) const { + for (int i = 0; i < dimensions; ++i) { + float difference = 0.f; + interpolator_.YDifferencesToEnd(index + i, 1, &difference); + if (std::fabs(difference) > max_difference) { + return false; + } + + float velocity = 0.f; + interpolator_.Derivatives(index + i, 1, &velocity); + if (std::fabs(velocity) > max_velocity) { + return false; + } + } + return true; +} + +void SplineProcessor::CreateSplineToTarget(Motivator::Index index, float value, + float velocity, + absl::Duration time) { + // If the first node specifies time=0 or there is no valid data in the + // interpolator, we want to override the current values with the values + // specified in the first node. + const bool override_current = + time == absl::ZeroDuration() || !interpolator_.Valid(index); + + // TODO(b/65298927): It seems that the animation pipeline can produce data + // that is out of range. Instead of just using |value| directly, if + // the interpolator is doing modular arithmetic, normalize the y value to + // the modulator's range. + const Interval& modular_range = interpolator_.ModularRange(index); + + const float node_y = + modular_range.Size() > -0.f + ? NormalizeWildValueWithinInterval(modular_range, value) + : value; + const float start_y = + override_current ? node_y : interpolator_.NormalizedY(index); + + float velocity_at_index = 0.f; + if (!override_current) { + Velocities(index, 1, &velocity_at_index); + } + + const float start_derivative = + override_current ? velocity : velocity_at_index; + + CompactSpline* spline = data_[index].get(); + if (spline == nullptr) { + // The default number of nodes is enough. + data_[index] = AllocateSpline(CompactSpline::kDefaultMaxNodes); + spline = data_[index].get(); + } + + Interval y_range; + if (interpolator_.ModularArithmetic(index)) { + // For modular splines, we need to expand the spline's y-range to match + // the number of nodes in the spline. It's possible for the spline to jump + // up the entire range every node, so the range has to be broad enough + // to hold it all. + // + // Note that we only normalize the first value of the spline, and + // subsequent values are allowed to curve out of the normalized range. + y_range = interpolator_.ModularRange(index).Scaled(1.f); + } else { + // Add some buffer to the y-range to allow for intermediate nodes + // that go above or below the supplied nodes. + static constexpr float kYRangeBufferPercent = 1.2f; + + // Calculate the union of the y ranges in the target, then expand it a + // little to allow for intermediate nodes that jump slightly beyond the + // union's range. + y_range = Interval(std::min(value, start_y), std::max(value, start_y)); + y_range = y_range.Scaled(kYRangeBufferPercent); + } + + const float spline_time = DurationToSplineTime(time); + const float x_granularity = CompactSpline::RecommendXGranularity(spline_time); + spline->Init(y_range, x_granularity); + spline->AddNode(0.0f, start_y, start_derivative); + + float y = value; + if (!override_current) { + // Use modular arithmetic for ranged values. + if (modular_range.Size() > 0.f) { + const float target_y = NormalizeWildValueWithinInterval(modular_range, y); + const float x = target_y - start_y; + const float length = modular_range.Size(); + const float adjustment = x <= modular_range.min ? length + : x > modular_range.max ? -length + : 0.f; + y = start_y + x + adjustment; + } + spline->AddNode(spline_time, y, velocity, kAddWithoutModification); + } + + // Point the interpolator at the spline we just created. Always start our + // spline at time 0. + interpolator_.SetSplines(index, 1, spline, SplinePlayback()); +} + +bool SplineProcessor::SupportsCloning() { return true; } + +void SplineProcessor::CloneIndices(Motivator::Index dst, Motivator::Index src, + int dimensions, AnimationEngine* engine) { + interpolator_.CopyIndices( + dst, src, dimensions, + [this](Motivator::Index index, const CompactSpline* src_spline) { + CompactSplinePtr dest_spline = AllocateSpline(src_spline->max_nodes()); + *dest_spline = *src_spline; + data_[index] = std::move(dest_spline); + return data_[index].get(); + }); +} + +void SplineProcessor::ResetIndices(Motivator::Index index, int dimensions) { + // Clear reference to this spline. + interpolator_.ClearSplines(index, dimensions); + + // Return splines to the pool of splines. + for (Motivator::Index i = index; i < index + dimensions; ++i) { + FreeSplineForIndex(i); + } +} + +void SplineProcessor::MoveIndices(Motivator::Index old_index, + Motivator::Index new_index, int dimensions) { + for (int i = 0; i < dimensions; ++i) { + using std::swap; + swap(data_[new_index + i], data_[old_index + i]); + data_[old_index + i].reset(); + } + interpolator_.MoveIndices(old_index, new_index, dimensions); +} + +void SplineProcessor::SetNumIndices(Motivator::Index num_indices) { + data_.resize(num_indices); + interpolator_.SetNumIndices(num_indices); +} + +CompactSplinePtr SplineProcessor::AllocateSpline(CompactSplineIndex max_nodes) { + // Return a spline from the pool. Eventually we'll reach a high water mark + // and we will stop allocating new splines. The returned spline must have + // enough nodes. + for (size_t i = 0; i < spline_pool_.size(); ++i) { + if (spline_pool_[i]->max_nodes() >= max_nodes) { + using std::swap; + swap(spline_pool_[i], spline_pool_.back()); + CompactSplinePtr spline = std::move(spline_pool_.back()); + spline_pool_.pop_back(); + return spline; + } + } + + // Create a spline with enough nodes otherwise. + return CompactSpline::Create(max_nodes); +} + +void SplineProcessor::FreeSplineForIndex(Motivator::Index index) { + if (data_[index]) { + using std::swap; + spline_pool_.emplace_back(); + swap(data_[index], spline_pool_.back()); + } +} +} // namespace redux diff --git a/redux/redux/engines/animation/processor/spline_processor.h b/redux/redux/engines/animation/processor/spline_processor.h new file mode 100644 index 0000000..8f7a5a2 --- /dev/null +++ b/redux/redux/engines/animation/processor/spline_processor.h @@ -0,0 +1,114 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_PROCESSOR_SPLINE_PROCESSOR_H_ +#define REDUX_ENGINES_ANIMATION_PROCESSOR_SPLINE_PROCESSOR_H_ + +#include + +#include "redux/engines/animation/common.h" +#include "redux/engines/animation/motivator/spline_motivator.h" +#include "redux/engines/animation/processor/anim_processor.h" +#include "redux/engines/animation/spline/bulk_spline_evaluator.h" +#include "redux/engines/animation/spline/compact_spline.h" +#include "redux/modules/base/typeid.h" + +namespace redux { + +class SplineProcessor : public AnimProcessor { + public: + explicit SplineProcessor(AnimationEngine* engine) : AnimProcessor(engine) {} + + SplineMotivator AllocateMotivator(int dimensions = 1); + + void SetSplines(Motivator::Index index, int dimensions, + const CompactSpline* splines, + const AnimationPlayback& playback); + + void SetTargets(Motivator::Index index, int dimensions, const float* values, + const float* velocities, absl::Duration time); + + void AdvanceFrame(absl::Duration delta_time) override; + + // Accessors to allow the user to get and set simluation values. + const float* Values(Motivator::Index index) const; + + void Velocities(Motivator::Index index, int dimensions, float* out) const; + + void Directions(Motivator::Index index, int dimensions, float* out) const; + + void TargetValues(Motivator::Index index, int dimensions, float* out) const; + + void TargetVelocities(Motivator::Index index, int dimensions, + float* out) const; + + void Differences(Motivator::Index index, int dimensions, float* out) const; + + absl::Duration TimeRemaining(Motivator::Index index, int dimensions) const; + + absl::Duration SplineTime(Motivator::Index index) const; + + void Splines(Motivator::Index index, int dimensions, + const CompactSpline** splines) const; + + void SetSplineTime(Motivator::Index index, int dimensions, + absl::Duration time); + + void SetSplinePlaybackRate(Motivator::Index index, int dimensions, + float playback_rate); + + void SetSplineRepeating(Motivator::Index index, int dimensions, bool repeat); + + bool Settled(Motivator::Index index, int dimensions, float max_difference, + float max_velocity) const; + + protected: + void CreateSplineToTarget(Motivator::Index index, float value, float velocity, + absl::Duration time); + + bool SupportsCloning() override; + + void CloneIndices(Motivator::Index dst, Motivator::Index src, int dimensions, + AnimationEngine* engine) override; + + void ResetIndices(Motivator::Index index, int dimensions) override; + + void MoveIndices(Motivator::Index old_index, Motivator::Index new_index, + int dimensions) override; + + void SetNumIndices(Motivator::Index num_indices) override; + + CompactSplinePtr AllocateSpline(CompactSplineIndex max_nodes); + void FreeSplineForIndex(Motivator::Index index); + + // Hold index-specific data, for example a pointer to the spline allocated + // from 'spline_pool_'. + std::vector data_; + + // Holds unused splines. When we need another local spline (because we're + // supplied with target values but not the actual curve to get there), + // try to recycle an old one from this pool first. + std::vector spline_pool_; + + // Perform the spline evaluation, over time. Indices in 'interpolator_' + // are the same as the Motivator::Index values in this class. + BulkSplineEvaluator interpolator_; +}; +} // namespace redux + +REDUX_SETUP_TYPEID(redux::SplineProcessor); + +#endif // REDUX_ENGINES_ANIMATION_PROCESSOR_SPLINE_PROCESSOR_H_ diff --git a/redux/redux/engines/animation/processor/transform_processor.cc b/redux/redux/engines/animation/processor/transform_processor.cc new file mode 100644 index 0000000..c3aaf4f --- /dev/null +++ b/redux/redux/engines/animation/processor/transform_processor.cc @@ -0,0 +1,386 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/animation/processor/transform_processor.h" + +#include "absl/time/time.h" +#include "redux/engines/animation/animation_clip.h" +#include "redux/engines/animation/animation_engine.h" +#include "redux/engines/animation/common.h" +#include "redux/engines/animation/processor/spline_processor.h" +#include "redux/modules/base/logging.h" +#include "redux/modules/math/constants.h" + +namespace redux { + +static inline float StartValue(const AnimationChannel& channel) { + if (channel.spline) { + return channel.spline->StartY(); + } else if (channel.const_value) { + return *channel.const_value; + } else { + return ChannelDefaultValue(channel.type); + } +} + +static void ApplyOp(AnimChannelType op, float value, Transform* transform) { + switch (op) { + case AnimChannelType::TranslateX: + transform->translation.x = value; + break; + case AnimChannelType::TranslateY: + transform->translation.y = value; + break; + case AnimChannelType::TranslateZ: + transform->translation.z = value; + break; + case AnimChannelType::ScaleX: + transform->scale.x = value; + break; + case AnimChannelType::ScaleY: + transform->scale.y = value; + break; + case AnimChannelType::ScaleZ: + transform->scale.z = value; + break; + case AnimChannelType::QuaternionX: + transform->rotation.x = value; + break; + case AnimChannelType::QuaternionY: + transform->rotation.y = value; + break; + case AnimChannelType::QuaternionZ: + transform->rotation.z = value; + break; + case AnimChannelType::QuaternionW: + transform->rotation.w = value; + break; + default: + // All other operations are not supported. + CHECK(false); + } +} + +TransformMotivator TransformProcessor::AllocateMotivator(int dimensions) { + TransformMotivator motivator; + AllocateMotivatorIndices(&motivator, dimensions); + return motivator; +} + +void TransformProcessor::AdvanceFrame(absl::Duration delta_time) { + Defragment(); + for (TransformData& data : data_) { + data.transform = Transform(); + for (const auto& op : data.ops) { + ApplyOp(op.Type(), op.Value(), &data.transform); + } + // Values may be interpolated so normalize the rotation just in case. + data.transform.rotation.SetNormalized(); + } + // TODO: Once we've reached the end of a non-loooping animation and the + // values are constant, we should consider switching out the splines to + // constants to improve performance. +} + +const Transform& TransformProcessor::Value(Motivator::Index index) const { + return Data(index).transform; +} + +void TransformProcessor::BlendTo(Motivator::Index index, + const std::vector& anim, + const AnimationPlayback& playback) { + TransformData& data = Data(index); + + // Since q and -q represent the same orientation, the current quaternion + // values may need to be negated to ensure the blend doesn't wildly change + // individual component values. + AlignQuaternionOps(data, anim); + + // Ops are always stored in order of ascending IDs. Scan through the old + // and new ops trying to match IDs. + size_t old_idx = 0; + size_t new_idx = 0; + const size_t num_new_ops = anim.size(); + + // Initialize the transform to the start value of the new animation. + if (data.ops.empty()) { + for (const auto& op : anim) { + ApplyOp(op.type, StartValue(op), &data.transform); + } + data.transform.rotation.SetNormalized(); + } + data.ops.reserve(num_new_ops); + + while (old_idx < data.ops.size() && new_idx < num_new_ops) { + auto& old_op = data.ops[old_idx]; + const auto& new_op = anim[new_idx]; + + // Ops are blendable if they have identical IDs. If not, handle whichever + // has the lower ID since it cannot possibly have a Blendable op in the + // other list. + if (old_op.Type() == new_op.type) { + if (new_op.spline) { + old_op.BlendToSpline(new_op.spline.get(), playback, Engine()); + } else { + old_op.BlendToValue(StartValue(new_op), playback, Engine()); + } + ++old_idx; + ++new_idx; + } else if (old_op.Type() < new_op.type) { + // There is no target op, so blend to the default value. If we're already + // at the default value, then we can remove the op entirely. + const float default_value = ChannelDefaultValue(new_op.type); + if (old_op.IsSettled(default_value)) { + data.ops.erase(data.ops.begin() + old_idx); + } else { + old_op.BlendToValue(default_value, playback, Engine()); + ++old_idx; + } + } else { + // New ops are inserted in order. `old_idx` points to the old op with + // the next highest ID compared to `new_op`, meaning it also provides + // the correct insertion point. + auto iter = data.ops.begin() + old_idx; + iter = data.ops.emplace(iter, new_op.type); + if (new_op.spline) { + iter->BlendToSpline(new_op.spline.get(), playback, Engine()); + } else { + iter->BlendToValue(StartValue(new_op), playback, Engine()); + } + ++new_idx; + // Advance `old_idx` so it still points to the same `old_op` now that + // one has been inserted before it. + ++old_idx; + } + } + + // Fill out remaining old ops by blending them to default. + int num_ops = data.ops.size(); + while (old_idx < num_ops) { + TransformOp& op = data.ops[old_idx++]; + const float default_value = ChannelDefaultValue(op.Type()); + op.BlendToValue(default_value, playback, Engine()); + } + + // Fill out remaining new ops by inserting them. + while (new_idx < num_new_ops) { + const auto& new_op = anim[new_idx++]; + TransformOp& op = data.ops.emplace_back(new_op.type); + if (new_op.spline) { + op.BlendToSpline(new_op.spline.get(), playback, Engine()); + } else { + op.BlendToValue(StartValue(new_op), playback, Engine()); + } + } +} + +void TransformProcessor::AlignQuaternionOps( + TransformData& data, const std::vector& anim) { + // Extract the first quaternion from the target animation. + quat target = quat::Identity(); + for (const auto& op : anim) { + if (op.type == AnimChannelType::QuaternionW) { + target.w = StartValue(op); + } else if (op.type == AnimChannelType::QuaternionX) { + target.x = StartValue(op); + } else if (op.type == AnimChannelType::QuaternionY) { + target.y = StartValue(op); + } else if (op.type == AnimChannelType::QuaternionZ) { + target.z = StartValue(op); + } + } + target.SetNormalized(); + + // Since q and -q represent the same orientation, we can negate the current + // quaternion operations if it will make them closer to `next`. + if (data.transform.rotation.Dot(target) < 0.f) { + for (auto& op : data.ops) { + op.NegateIfQuaternionOp(); + } + } +} + +void TransformProcessor::SetPlaybackRate(Motivator::Index index, + float playback_rate) { + TransformData& data = Data(index); + for (auto& op : data.ops) { + op.SetPlaybackRate(playback_rate); + } +} + +void TransformProcessor::SetRepeating(Motivator::Index index, bool repeat) { + TransformData& data = Data(index); + for (auto& op : data.ops) { + op.SetRepeating(repeat); + } +} + +absl::Duration TransformProcessor::TimeRemaining(Motivator::Index index) const { + absl::Duration time = absl::ZeroDuration(); + const TransformData& data = Data(index); + for (const auto& op : data.ops) { + time = std::max(time, op.TimeRemaining()); + } + return time; +} + +Motivator::Index TransformProcessor::NumIndices() const { + return static_cast(data_.size()); +} + +void TransformProcessor::CloneIndices(Motivator::Index dest, + Motivator::Index src, int dimensions, + AnimationEngine* engine) { + for (Motivator::Index i = 0; i < dimensions; ++i) { + const TransformData& data_src = Data(src + i); + TransformData& data_dst = Data(dest + i); + data_dst.transform = data_src.transform; + data_dst.ops.clear(); + data_dst.ops.reserve(data_src.ops.size()); + for (const auto& op : data_src.ops) { + data_dst.ops.emplace_back(op.Type()).CloneFrom(op); + } + } +} + +void TransformProcessor::ResetIndices(Motivator::Index index, int dimensions) { + // Callers depend on indices staying consistent between calls to this + // function, so just reset the TransformData states to empty instead + // of erasing them. + for (Motivator::Index i = index; i < index + dimensions; ++i) { + TransformData& data = data_[i]; + data.transform = Transform(); + data.ops.clear(); + } +} + +void TransformProcessor::MoveIndices(Motivator::Index old_index, + Motivator::Index new_index, + int dimensions) { + for (int i = 0; i < dimensions; ++i) { + using std::swap; + swap(data_[new_index + i], data_[old_index + i]); + data_[old_index + i].transform = Transform(); + data_[old_index + i].ops.clear(); + } +} + +void TransformProcessor::SetNumIndices(Motivator::Index num_indices) { + data_.resize(num_indices); +} + +TransformProcessor::TransformData& TransformProcessor::Data( + Motivator::Index index) { + CHECK(ValidIndex(index)); + return data_[index]; +} + +const TransformProcessor::TransformData& TransformProcessor::Data( + Motivator::Index index) const { + CHECK(ValidIndex(index)); + return data_[index]; +} + +TransformProcessor::TransformOp::TransformOp(AnimChannelType type) + : type_(type) { + const_value_ = ChannelDefaultValue(type); +} + +void TransformProcessor::TransformOp::SetRepeating(bool repeat) { + if (motivator_.Valid()) { + motivator_.SetRepeating(repeat); + } +} + +void TransformProcessor::TransformOp::SetPlaybackRate(float playback_rate) { + if (motivator_.Valid()) { + motivator_.SetPlaybackRate(playback_rate); + } +} + +float TransformProcessor::TransformOp::Value() const { + return motivator_.Valid() ? motivator_.Value() : const_value_; +} + +float TransformProcessor::TransformOp::Velocity() const { + return motivator_.Valid() ? motivator_.Velocity() : 0.f; +} + +absl::Duration TransformProcessor::TransformOp::TimeRemaining() const { + return motivator_.Valid() ? motivator_.TimeRemaining() : absl::ZeroDuration(); +} + +void TransformProcessor::TransformOp::CloneFrom(const TransformOp& rhs) { + type_ = rhs.type_; + const_value_ = rhs.const_value_; + motivator_.CloneFrom(&rhs.motivator_); +} + +bool TransformProcessor::TransformOp::IsSettled(float value) const { + if (motivator_.Valid()) { + const float diff = std::fabs(motivator_.Value() - value); + const float velocity = std::fabs(motivator_.Velocity()); + return diff < kDefaultEpsilon && velocity < kDefaultEpsilon; + } else { + return const_value_ == value; + } +} + +void TransformProcessor::TransformOp::BlendToValue( + float value, const AnimationPlayback& playback, AnimationEngine* engine) { + if (!motivator_.Valid()) { + // This channel did not exist previously, so snap it to the new constant + // Blending nothing to constant happens immediately. + const float default_value = ChannelDefaultValue(type_); + motivator_ = engine->AcquireMotivator(); + motivator_.SetTarget(default_value, 0.f, absl::ZeroDuration()); + motivator_.SetTarget(value, 0.f, playback.blend_time); + } else if (AreNearlyEqual(motivator_.Value(), value) && + AreNearlyEqual(motivator_.Velocity(), 0.f)) { + // Blending a spline already at the desired constant and with 0 velocity + // transforms this op into a constant immediately. + motivator_.Invalidate(); + const_value_ = value; + } else { + motivator_.SetTarget(value, 0.f, playback.blend_time); + } +} + +void TransformProcessor::TransformOp::BlendToSpline( + const CompactSpline* spline, const AnimationPlayback& playback, + AnimationEngine* engine) { + if (!motivator_.Valid()) { + motivator_ = engine->AcquireMotivator(); + motivator_.SetTarget(const_value_, absl::ZeroDuration()); + } + DCHECK(spline); + motivator_.SetSpline(*spline, playback); +} + +void TransformProcessor::TransformOp::NegateIfQuaternionOp() { + if (type_ < AnimChannelType::QuaternionX) { + return; + } else if (type_ > AnimChannelType::QuaternionW) { + return; + } + if (motivator_.Valid()) { + motivator_.SetTarget(-motivator_.Value(), -motivator_.Velocity(), + absl::ZeroDuration()); + } else { + const_value_ = -const_value_; + } +} +} // namespace redux diff --git a/redux/redux/engines/animation/processor/transform_processor.h b/redux/redux/engines/animation/processor/transform_processor.h new file mode 100644 index 0000000..7fc8c68 --- /dev/null +++ b/redux/redux/engines/animation/processor/transform_processor.h @@ -0,0 +1,126 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_PROCESSOR_TRANSFORM_PROCESSOR_H_ +#define REDUX_ENGINES_ANIMATION_PROCESSOR_TRANSFORM_PROCESSOR_H_ + +#include "redux/engines/animation/animation_clip.h" +#include "redux/engines/animation/common.h" +#include "redux/engines/animation/motivator/spline_motivator.h" +#include "redux/engines/animation/motivator/transform_motivator.h" +#include "redux/engines/animation/processor/anim_processor.h" +#include "redux/modules/base/typeid.h" +#include "redux/modules/math/transform.h" + +namespace redux { + +class TransformProcessor : public AnimProcessor { + public: + explicit TransformProcessor(AnimationEngine* engine) + : AnimProcessor(engine) {} + + TransformMotivator AllocateMotivator(int dimensions = 1); + + int Priority() const override { return 2; } + + void AdvanceFrame(absl::Duration delta_time) override; + + const Transform& Value(Motivator::Index index) const; + + void BlendTo(Motivator::Index index, + const std::vector& anim, + const AnimationPlayback& playback); + + void SetPlaybackRate(Motivator::Index index, float playback_rate); + + void SetRepeating(Motivator::Index index, bool repeat); + + absl::Duration TimeRemaining(Motivator::Index index) const; + + private: + class TransformOp { + public: + explicit TransformOp(AnimChannelType type); + + TransformOp(const TransformOp&) = delete; + TransformOp& operator=(const TransformOp&) = delete; + + TransformOp(TransformOp&&) = default; + TransformOp& operator=(TransformOp&&) = default; + + void CloneFrom(const TransformOp& rhs); + + void BlendToValue(float value, const AnimationPlayback& playback, + AnimationEngine* engine); + void BlendToSpline(const CompactSpline* spline, + const AnimationPlayback& playback, + AnimationEngine* engine); + + void NegateIfQuaternionOp(); // useful for blending quaternions + + void SetRepeating(bool repeat); + void SetPlaybackRate(float playback_rate); + + bool IsSettled(float value) const; + AnimChannelType Type() const { return type_; } + + float Value() const; + float Velocity() const; + absl::Duration TimeRemaining() const; + + private: + AnimChannelType type_ = AnimChannelType::Invalid; + float const_value_ = 0.f; + SplineMotivator motivator_; + }; + + struct TransformData { + TransformData() = default; + TransformData(const TransformData&) = delete; + TransformData& operator=(const TransformData&) = delete; + TransformData(TransformData&&) = default; + TransformData& operator=(TransformData&&) = default; + + Transform transform; + std::vector ops; + }; + + // Ensures that the current quaternion values are close to the initial values + // in `ops`. This function should be called prior to blending to `ops` to + // ensure quaternion blends work. + void AlignQuaternionOps(TransformData& data, + const std::vector& anim); + + bool SupportsCloning() override { return true; } + + Motivator::Index NumIndices() const; + void SetNumIndices(Motivator::Index num_indices) override; + void CloneIndices(Motivator::Index dest, Motivator::Index src, int dimensions, + AnimationEngine* engine) override; + void ResetIndices(Motivator::Index index, int dimensions) override; + void MoveIndices(Motivator::Index old_index, Motivator::Index new_index, + int dimensions) override; + + TransformData& Data(Motivator::Index index); + const TransformData& Data(Motivator::Index index) const; + + std::vector data_; +}; +} // namespace redux + +REDUX_SETUP_TYPEID(redux::TransformProcessor); + +#endif // REDUX_ENGINES_ANIMATION_PROCESSOR_TRANSFORM_PROCESSOR_H_ diff --git a/redux/redux/engines/animation/spline/BUILD b/redux/redux/engines/animation/spline/BUILD new file mode 100644 index 0000000..7744259 --- /dev/null +++ b/redux/redux/engines/animation/spline/BUILD @@ -0,0 +1,113 @@ +# Spline library for animation engine. + +licenses(["notice"]) + +package( + default_visibility = ["//redux:visibility"], +) + +cc_test( + name = "bulk_spline_evaluator_tests", + srcs = ["bulk_spline_evaluator_tests.cc"], + deps = [ + ":compact_spline", + "@gtest//:gtest_main", + ], +) + +cc_library( + name = "compact_spline", + srcs = [ + "bulk_spline_evaluator.cc", + "compact_spline.cc", + ], + hdrs = [ + "bulk_spline_evaluator.h", + "compact_spline.h", + "compact_spline_node.h", + ], + deps = [ + ":cubic_curve", + ":dual_cubic", + "@absl//absl/base:core_headers", + "@absl//absl/time", + "//redux/modules/math", + "//redux/modules/math:bounds", + "//redux/modules/math:interpolation", + "//redux/modules/math:vector", + ], +) + +cc_test( + name = "compact_spline_tests", + srcs = ["compact_spline_tests.cc"], + deps = [ + ":compact_spline", + "@gtest//:gtest_main", + ], +) + +cc_library( + name = "cubic_curve", + srcs = [ + "cubic_curve.cc", + ], + hdrs = [ + "cubic_curve.h", + ], + deps = [ + "//redux/modules/math:bounds", + "//redux/modules/math:float", + "//redux/modules/math:vector", + ], +) + +cc_test( + name = "cubic_curve_tests", + srcs = ["cubic_curve_tests.cc"], + deps = [ + ":cubic_curve", + "@gtest//:gtest_main", + ], +) + +cc_library( + name = "dual_cubic", + srcs = [ + "dual_cubic.cc", + ], + hdrs = [ + "dual_cubic.h", + ], + deps = [ + ":cubic_curve", + ":quadratic_curve", + "//redux/modules/math:bounds", + "//redux/modules/math:interpolation", + ], +) + +cc_library( + name = "quadratic_curve", + srcs = [ + "quadratic_curve.cc", + ], + hdrs = [ + "quadratic_curve.h", + ], + deps = [ + "//redux/modules/base:logging", + "//redux/modules/math:bounds", + "//redux/modules/math:float", + "//redux/modules/math:vector", + ], +) + +cc_test( + name = "quadratic_curve_tests", + srcs = ["quadratic_curve_tests.cc"], + deps = [ + ":quadratic_curve", + "@gtest//:gtest_main", + ], +) diff --git a/redux/redux/engines/animation/spline/bulk_spline_evaluator.cc b/redux/redux/engines/animation/spline/bulk_spline_evaluator.cc new file mode 100644 index 0000000..c4050d2 --- /dev/null +++ b/redux/redux/engines/animation/spline/bulk_spline_evaluator.cc @@ -0,0 +1,466 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/animation/spline/bulk_spline_evaluator.h" + +namespace redux { + +// These functions are implemented in assembly language. +extern "C" void UpdateCubicXsAndGetMask_Neon(const float& delta_x, + const float* x_ends, int num_xs, + float* xs, uint8_t* masks); + +// y_range pointer is of type BulkSplineEvaluator::YRange (not used here because +// it's private, and extern "C" functions cannot be friends). +extern "C" void EvaluateCubics_Neon(const CubicCurve* curves, const float* xs, + const void* y_ranges, int num_curves, + float* ys); + +inline float NormalizeInterval(const Interval& range, float x) { + const float length = range.Size(); + const float adjustment = x <= range.min ? length + : x > range.max ? -length + : 0.f; + return x + adjustment; +} + +void BulkSplineEvaluator::SetNumIndices(const Index num_indices) { + sources_.resize(num_indices); + y_ranges_.resize(num_indices); + cubic_xs_.resize(num_indices, 0.0f); + cubic_x_ends_.resize(num_indices, 0.0f); + cubics_.resize(num_indices); + ys_.resize(num_indices, 0.0f); + scratch_.resize(num_indices, 0); +} + +void BulkSplineEvaluator::MoveIndices(const Index old_index, + const Index new_index, + const Index count) { + for (Index i = 0; i < count; ++i) { + const Index old_i = old_index + i; + const Index new_i = new_index + i; + sources_[new_i] = sources_[old_i]; + y_ranges_[new_i] = y_ranges_[old_i]; + cubic_xs_[new_i] = cubic_xs_[old_i]; + cubic_x_ends_[new_i] = cubic_x_ends_[old_i]; + cubics_[new_i] = cubics_[old_i]; + ys_[new_i] = ys_[old_i]; + } +} + +void BulkSplineEvaluator::SetYRanges(const Index index, const Index count, + const Interval& modular_range) { + for (int i = index; i < index + count; ++i) { + YRange& r = y_ranges_[i]; + r.modular_range = modular_range; + } +} + +CubicInit BulkSplineEvaluator::CalculateBlendInit( + const Index index, const CompactSpline& spline, + const SplinePlayback& playback) const { + // Calculate spline segment where the blend will end. + const float blend_width = playback.blend_x * playback.playback_rate; + float blend_end_x = 0.0f; + const CompactSplineIndex blend_end_index = spline.IndexForXAllowingRepeat( + playback.start_x + blend_width, kInvalidSplineIndex, playback.repeat, + &blend_end_x); + + // Gather the spline values. Only create the cubic if we have to. + float end_y = 0.0f; + float end_derivative = 0.0f; + if (OutsideSpline(blend_end_index)) { + // Get the start or end y-values of the spline. + end_y = spline.NodeY(blend_end_index); + } else { + // Create the cubic for the end segment. + const float curve_x = blend_end_x - spline.NodeX(blend_end_index); + const CubicInit curve_init = spline.CreateCubicInit(blend_end_index); + const CubicCurve curve(curve_init); + end_y = curve.Evaluate(curve_x); + end_derivative = curve.Derivative(curve_x); + } + + // Scale and shift the end value by the playback parameters. + end_y *= playback.y_scale; + end_y += playback.y_offset; + + // Use the current values for the curve start. + float start_y = ys_[index]; + const float start_derivative = Derivative(index); + + // Account for modular arithmentic. Always start in the normalized range. + const YRange& r = y_ranges_[index]; + if (r.modular_range.Size() > 0.f) { + // We take the shortest modular path to the new curve. + // So if we're blending from angle 170 to angle -170 (=+190), + // we will blend from 170-->190 instead of 170-->-170. + start_y = NormalizeCloseValueWithinInterval(r.modular_range, start_y); + const float end_y_normalized = + NormalizeCloseValueWithinInterval(r.modular_range, end_y); + const float diff_y = + NormalizeInterval(r.modular_range, end_y_normalized - start_y); + end_y = start_y + diff_y; + } + + // Return the cubic parameters. + return CubicInit(start_y, start_derivative, end_y, end_derivative, + blend_width); +} + +void BulkSplineEvaluator::BlendToSpline(const Index index, + const CompactSpline& spline, + const SplinePlayback& playback) { + // Calculate the spline that transitions from the current curve state + // to the target spline's state. + // Transition spline runs from x=0-->playback.blend_time. + const CubicInit blend_init = CalculateBlendInit(index, spline, playback); + + // Shift the transition spline so that it overlaps perfectly onto the target + // spline. Initialize all the x-parameters as if we were initializing the + // target spline. This will let us transition out of the transition spline + // straight into the target spline without special casing. + float blend_start_x = 0.0f; + const CompactSplineIndex blend_start_index = spline.IndexForXAllowingRepeat( + playback.start_x, kInvalidSplineIndex, playback.repeat, &blend_start_x); + const float cubic_start_x = blend_start_x - spline.NodeX(blend_start_index); + + Source& s = sources_[index]; + s.rate = playback.playback_rate; + s.y_offset = playback.y_offset; + s.y_scale = playback.y_scale; + s.spline = &spline; + s.x_index = blend_start_index; + s.repeat = playback.repeat; + cubic_xs_[index] = cubic_start_x; + cubic_x_ends_[index] = + cubic_start_x + playback.blend_x * playback.playback_rate; + cubics_[index].Init(blend_init); + cubics_[index].ShiftRight(cubic_start_x); +} + +void BulkSplineEvaluator::JumpToSpline(const Index index, + const CompactSpline& spline, + const SplinePlayback& playback) { + Source& s = sources_[index]; + s.rate = playback.playback_rate; + s.y_offset = playback.y_offset; + s.y_scale = playback.y_scale; + s.spline = &spline; + s.x_index = kInvalidSplineIndex; + s.repeat = playback.repeat; + InitCubic(index, playback.start_x); +} + +void BulkSplineEvaluator::SetSplines(const Index index, const Index count, + const CompactSpline* splines, + const SplinePlayback& playback) { + const CompactSpline* spline = splines; + for (Index i = index; i < index + count; ++i, spline = spline->Next()) { + // `splines` should specify `count` splines, but gracefully handle the + // case when it doesn't. + if (spline == nullptr) { + ClearSplines(i, count - i + index); + break; + } + + // If we're already playing a spline, and the blend time is specified, + // create a curve that blends from the current state to a point later in + // the new spline. + const Source& s = sources_[i]; + const bool should_blend = s.spline != nullptr && playback.blend_x > 0.0f; + if (should_blend) { + BlendToSpline(i, *spline, playback); + } else { + JumpToSpline(i, *spline, playback); + } + + // Update the results. + // TODO OPT: Evaluate these in bulk. + EvaluateIndex(i); + } +} + +void BulkSplineEvaluator::Splines(const Index index, const Index count, + const CompactSpline** splines) const { + for (Index i = 0; i < count; ++i) { + splines[i] = sources_[index + i].spline; + } +} + +void BulkSplineEvaluator::ClearSplines(const Index index, const Index count) { + for (Index i = index; i < index + count; ++i) { + sources_[i].spline = nullptr; + cubics_[i] = CubicCurve(0.0f, 0.0f, 0.0f, cubic_xs_[i]); + cubic_xs_[i] = 0.0f; + cubic_x_ends_[i] = std::numeric_limits::infinity(); + } +} + +void BulkSplineEvaluator::SetXs(const Index index, const Index count, + const float x) { + for (Index i = index; i < index + count; ++i) { + InitCubic(i, x); + EvaluateIndex(i); + } +} + +void BulkSplineEvaluator::SetPlaybackRates(const Index index, const Index count, + float playback_rate) { + for (Index i = index; i < index + count; ++i) { + sources_[i].rate = playback_rate; + } +} + +void BulkSplineEvaluator::SetRepeating(const Index index, const Index count, + bool repeat) { + for (Index i = index; i < index + count; ++i) { + sources_[i].repeat = repeat; + } +} + +void BulkSplineEvaluator::UpdateCubicXsAndGetMask_C(const float delta_x, + uint8_t* masks) { + const int num_xs = NumIndices(); + const float* x_ends = &cubic_x_ends_.front(); + float* xs = &cubic_xs_.front(); + + for (int i = 0; i < num_xs; ++i) { + xs[i] += delta_x * sources_[i].rate; + masks[i] = xs[i] > x_ends[i] ? 0xFF : 0x00; + } +} + +// For each non-zero mask[i], append 'i' to 'indices'. +// Returns: final length of indices. +// TODO OPT: Add assembly version if generated code is poor. +static size_t ConvertMaskToIndices(const uint8_t* mask, size_t length, + BulkSplineEvaluator::Index* indices) { + size_t num_indices = 0; + for (size_t i = 0; i < length; ++i) { + indices[num_indices] = static_cast(i); + if (mask[i] != 0) { + num_indices++; + } + } + return num_indices; +} + +// Get a byte mask for the indices to init, and then convert that byte mask +// into a list of indices. This algorithm is best for many SIMD implementations, +// since they have trouble converting masks into indices. +size_t BulkSplineEvaluator::UpdateCubicXs_TwoSteps(const float delta_x, + Index* indices_to_init) { + // Use last half of 'indices_to_init' as a scratch buffer for 'mask'. + // Must be the last half since we read 'mask' to write 'indices_to_init' + // in ConvertMaskToIndices(). + const Index num_indices = NumIndices(); + uint8_t* mask = reinterpret_cast(&indices_to_init[num_indices / 2]); + + // Add delta_x to each of the cubic_xs_. + // Set mask[i] to 0xFF if the cubic has gone past the end of its array. + UpdateCubicXsAndGetMask(delta_x, mask); + + // Get indices that are true 0xFF in the mask array. + return ConvertMaskToIndices(mask, num_indices, indices_to_init); +} + +// Record the indices, as we go along, for every index we need to re-init. +// This algorithm is fastest when we process indices serially. +size_t BulkSplineEvaluator::UpdateCubicXs_OneStep(const float delta_x, + Index* indices_to_init) { + const Index num_indices = NumIndices(); + size_t num_to_init = 0; + + for (Index i = 0; i < num_indices; ++i) { + // Increment each cubic x value by delta_x. + cubic_xs_[i] += delta_x * sources_[i].rate; + + // When x has gone past the end of the cubic, it should be reinitialized. + if (cubic_xs_[i] > cubic_x_ends_[i]) { + indices_to_init[num_to_init++] = i; + } + } + return num_to_init; +} + +void BulkSplineEvaluator::InitCubic(const Index index, const float start_x) { + // Do nothing if the requested index has no spline. + Source& s = sources_[index]; + if (s.spline == nullptr) return; + + // Get the spline index for start_x. + float new_start_x = 0.0f; + const CompactSplineIndex x_index = s.spline->IndexForXAllowingRepeat( + start_x, s.x_index + 1, s.repeat, &new_start_x); + + // Update the x values for the new index. + const Interval x_range = s.spline->IntervalX(x_index); + cubic_xs_[index] = new_start_x - x_range.min; + + // TODO OPT: Exit early if s.x_index == x_index, since we've already + // initialized the cubic. This is tricky, since if we're blending then the + // index might match, but the cubic curve will not mach. We should refactor + // to detect that case, so we can skip over the CreateCubicInit() call. + s.x_index = x_index; + + // Initialize the cubic to interpolate the new spline segment. + cubic_x_ends_[index] = x_range.Size(); + CubicCurve& c = cubics_[index]; + const CubicInit init = s.spline->CreateCubicInit(x_index); + c.Init(init); + + c.ScaleUp(s.y_scale); + c.ShiftUp(s.y_offset); +} + +void BulkSplineEvaluator::EvaluateIndex(const Index index) { + // Evaluate the cubic spline. + CubicCurve& c = cubics_[index]; + ys_[index] = c.Evaluate(cubic_xs_[index]); +} + +void BulkSplineEvaluator::EvaluateCubics_C() { + for (Index index = 0; index < NumIndices(); ++index) { + EvaluateIndex(index); + } +} + +void BulkSplineEvaluator::AdvanceFrame(const float delta_x) { + // Add 'delta_x' to 'cubic_xs'. + // Gather a list of indices that are now beyond the end of the cubic. + Index* indices_to_init = scratch_.size() == 0 ? nullptr : &scratch_.front(); + const size_t num_to_init = UpdateCubicXs(delta_x, indices_to_init); + + // Reinitialize indices that have traversed beyond the end of their cubic. + for (size_t i = 0; i < num_to_init; ++i) { + const Index index = indices_to_init[i]; + InitCubic(index, X(index)); + } + + // Update 'ys_' array. Also might affect the constant coefficients of + // 'cubics_', if we're adjusting for modular arithmetic. + EvaluateCubics(); +} + +bool BulkSplineEvaluator::Valid(const Index index) const { + return 0 <= index && index < NumIndices() && + sources_[index].spline != nullptr; +} + +// Form the assembly function name by appending "_Neon", "_SSD2", or whatever +// REDUX_ANIM_ASSEMBLY_TEST is defined to be. +#define REDUX_ANIM_TOKEN_PASTE_NESTED(a, b) a##b +#define REDUX_ANIM_TOKEN_PASTE(a, b) REDUX_ANIM_TOKEN_PASTE_NESTED(a, b) +#define REDUX_ANIM_ASSEMBLY_FUNCTION_NAME(name) \ + REDUX_ANIM_TOKEN_PASTE(name, REDUX_ANIM_ASSEMBLY_TEST) + +// These inline functions are used to redirect calls to the C or assembly +// versions, or to run both versions and compare the output. +inline void BulkSplineEvaluator::UpdateCubicXsAndGetMask(const float delta_x, + uint8_t* masks) { +#if defined(REDUX_ANIM_ASSEMBLY_TEST) + const int num_xs = NumIndices(); + std::vector xs_assembly(cubic_xs_); + std::vector masks_assembly(num_xs); + + UpdateCubicXsAndGetMask_C(delta_x, masks); + REDUX_ANIM_ASSEMBLY_FUNCTION_NAME(UpdateCubicXsAndGetMask_) + (delta_x, &cubic_x_ends_.front(), num_xs, &xs_assembly.front(), + &masks_assembly.front()); + + for (int i = 0; i < num_xs; ++i) { + assert(cubic_xs_[i] == xs_assembly[i]); + assert(masks[i] == masks_assembly[i]); + } + +#else // not defined(REDUX_ANIM_ASSEMBLY_TEST) + +#if defined(REDUX_ANIM_NEON) + if (optimization_ == kNeonOptimizations) { + UpdateCubicXsAndGetMask_Neon(delta_x, &cubic_x_ends_.front(), NumIndices(), + &cubic_xs_.front(), masks); + } else +#endif + { + UpdateCubicXsAndGetMask_C(delta_x, masks); + } + +#endif // not defined(REDUX_ANIM_ASSEMBLY_TEST) +} + +inline size_t BulkSplineEvaluator::UpdateCubicXs(const float delta_x, + Index* indices_to_init) { +#if defined(REDUX_ANIM_ASSEMBLY_TEST) + std::vector xs_original(cubic_xs_); + std::vector indices_one(NumIndices()); + + const size_t num_one = UpdateCubicXs_OneStep(delta_x, &indices_one.front()); + std::vector xs_one(cubic_xs_); + + cubic_xs_ = xs_original; + const size_t num_two = UpdateCubicXs_TwoSteps(delta_x, indices_to_init); + + assert(num_two == num_one); + for (size_t i = 0; i < num_two; ++i) { + assert(indices_to_init[i] == indices_one[i]); + } + for (int i = 0; i < NumIndices(); ++i) { + assert(cubic_xs_[i] == xs_one[i]); + } + return num_two; + +#else // not defined(REDUX_ANIM_ASSEMBLY_TEST) + +#if defined(REDUX_ANIM_NEON) + return UpdateCubicXs_TwoSteps(delta_x, indices_to_init); +#else + return UpdateCubicXs_OneStep(delta_x, indices_to_init); +#endif + +#endif // not defined(REDUX_ANIM_ASSEMBLY_TEST) +} + +inline void BulkSplineEvaluator::EvaluateCubics() { +#if defined(REDUX_ANIM_ASSEMBLY_TEST) + std::vector ys_assembly(NumIndices()); + std::vector cubics_assembly(cubics_); + + REDUX_ANIM_ASSEMBLY_FUNCTION_NAME(EvaluateCubics_) + (&cubics_assembly.front(), &cubic_xs_.front(), &y_ranges_.front(), + NumIndices(), &ys_assembly.front()); + EvaluateCubics_C(); + + for (int i = 0; i < NumIndices(); ++i) { + assert(ys_assembly[i] == ys_[i]); + } + for (int i = 0; i < NumIndices(); ++i) { + assert(cubics_assembly[i] == cubics_[i]); + } +#else // not defined(REDUX_ANIM_ASSEMBLY_TEST) + +#if defined(REDUX_ANIM_NEON) + EvaluateCubics_Neon(&cubics_.front(), &cubic_xs_.front(), &y_ranges_.front(), + NumIndices(), &ys_.front()); +#else + EvaluateCubics_C(); +#endif + +#endif // not defined(REDUX_ANIM_ASSEMBLY_TEST) +} + +} // namespace redux diff --git a/redux/redux/engines/animation/spline/bulk_spline_evaluator.h b/redux/redux/engines/animation/spline/bulk_spline_evaluator.h new file mode 100644 index 0000000..fd33e83 --- /dev/null +++ b/redux/redux/engines/animation/spline/bulk_spline_evaluator.h @@ -0,0 +1,405 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_SPLINE_BULK_SPLINE_EVALUATOR_H_ +#define REDUX_ENGINES_ANIMATION_SPLINE_BULK_SPLINE_EVALUATOR_H_ + +#include +#include "absl/time/time.h" +#include "redux/engines/animation/spline/compact_spline.h" +#include "redux/modules/math/bounds.h" + +namespace redux { + +// Traverses through a set of splines in a performant way. +// +// This class should be used if you have hundreds or more splines that you need +// to traverse in a uniform manner. It stores the spline data so that this +// traversal is very fast, when done in bulk, and so we can take advantage of +// SIMD on supported processors. +// +// This class maintains a current `x` value for each spline, and a current +// cubic-curve for the segment of the spline corresponding to that `x`. +// In AdvanceFrame, the `x`s are incremented. If this increment pushes us to +// the next segment of a spline, the cubic-curve is reinitialized to the next +// segment of the spline. The splines are evaluated at the current `x` in bulk. +// +// Similar to SplinePlayback, but everything is in spline time (ie. floats). +struct SplinePlayback { + float playback_rate = 1.f; + float blend_x = 0.f; + float start_x = 0.f; + float y_offset = 0.f; + float y_scale = 1.f; + bool repeat = false; +}; + +class BulkSplineEvaluator { + public: + using Index = int; + + BulkSplineEvaluator() = default; + + // Return the number of indices currently allocated. Each index is one + // spline that's being evaluated. + Index NumIndices() const { return static_cast(sources_.size()); } + + // Increase or decrease the total number of indices processed. + // + // This class holds a set of splines, each is given an index + // from 0 to size - 1. + // + // The number of splines can be increased or decreased with SetNumIndices(). + // - splines are allocated or removed at the highest indices + void SetNumIndices(const Index num_indices); + + // Move the data at `old_index` into `new_index`. Move `count` indices total. + // + // Unused indices are still processed every frame. You can fill these index + // holes with MoveIndex(), to move items from the last index into the hole. + // Once all holes have been moved to the highest indices, you can call + // SetNumIndices() to stop processing these highest indices. Note that this + // is exactly what fplutil::IndexAllocator does. You should use that class to + // keep your indices contiguous. + void MoveIndices(const Index old_index, const Index new_index, + const Index count); + + // Copy the data at `src` into `dst`, using `alloc` to allocate new + // CompactSplines. Copy `count` indices total. + // + // Because the BulkSplineEvaluator owns no CompactSpline memory, the caller + // must provide a function capable of allocating a CompactSpline that is a + // copy of the provided CompactSpline. The caller can use the provided + // Index to delete the CompactSpline when it is no longer in use. + template + void CopyIndices(Index dst, Index src, Index count, const AllocFn& alloc) { + MoveIndices(src, dst, count); + for (int i = 0; i < count; ++i) { + sources_[dst + i].spline = alloc(dst + i, sources_[src + i].spline); + } + } + + // Initialize `index` to normalize into the `modular_range` range, whenever + // the spline segment is initialized. While travelling along a segment, + // note that the value may exit the `modular_range` range. For example, you + // can ensure an angle stays near the [-pi, pi) range by passing that range + // as the `modular_range` for this `index`. + // If !modular_range.Valid(), then modular arithmetic is not used. + void SetYRanges(const Index index, const Index count, + const Interval& modular_range); + + // Initialize `index` to process `s.spline` starting from `s.start_x`. + // The Y() and Derivative() values are immediately available. + void SetSplines(const Index index, const Index count, + const CompactSpline* splines, const SplinePlayback& playback); + + // Mark spline range as invalid. + void ClearSplines(const Index index, const Index count); + + // Reposition the spline at `index` evaluate from `x`. + // Same as calling SetSpline() with the same spline and + // `playback.start_x = x`. + void SetXs(const Index index, const Index count, const float x); + + // Set conversion rate from AdvanceFrame's delta_x to the speed at which + // we traverse the spline. + // 0 ==> paused + // 0.5 ==> half speed (slow motion) + // 1 ==> authored speed + // 2 ==> double speed (fast forward) + void SetPlaybackRates(const Index index, const Index count, + float playback_rate); + + // Set repeat state for splines. + void SetRepeating(const Index index, const Index count, bool repeat); + + // Increment x and update the Y() and Derivative() values for all indices. + // Process all indices in bulk to efficiently traverse memory and allow SIMD + // instructions to be effective. + void AdvanceFrame(const float delta_x); + + // Return true if the spline for `index` has valid spline data. + bool Valid(const Index index) const; + + // Return the current x value for the spline at `index`. + float X(const Index index) const { + return CubicStartX(index) + cubic_xs_[index]; + } + + // Return the current y value for the spline at `index`. + float Y(const Index index) const { return ys_[index]; } + + // Return the current y value for the spline at `index`, normalized to be + // within the valid y_range. + float NormalizedY(const Index index) const { + return NormalizeY(index, ys_[index]); + } + + // Return the current y value for splines, from index onward. + // Since this is the most commonly called function, we keep it fast by + // returning a pointer to the pre-calculated array. Note that we don't + // recalculate the derivatives, etc., so that is why the interface is + // different. + const float* Ys(const Index index) const { return &ys_[index]; } + + // Return the current slope for the spline at `index`. + float Derivative(const Index index) const { + return PlaybackRate(index) * Cubic(index).Derivative(cubic_xs_[index]); + } + + // Return the slopes for the `count` splines starting at `index`. + // `out` is an array of length `count`. + // TODO OPT: Write assembly versions of this function. + void Derivatives(const Index index, const Index count, float* out) const { + assert(Valid(index) && Valid(index + count - 1)); + for (Index i = 0; i < count; ++i) { + out[i] = Derivative(index + i); + } + } + + // Return the current slope for the spline at `index`. Ignore the playback + // rate. This is useful for times when the playback rate is 0, but you + // still want to get information about the underlying spline. + float DerivativeWithoutPlayback(const Index index) const { + return Cubic(index).Derivative(cubic_xs_[index]); + } + + // Return the slopes for the `count` splines starting at `index`, ignoring + // the playback rate. + // `out` is an array of length `count`. + void DerivativesWithoutPlayback(const Index index, Index count, + float* out) const { + assert(Valid(index) && Valid(index + count - 1)); + for (Index i = 0; i < count; ++i) { + out[i] = DerivativeWithoutPlayback(index + i); + } + } + + // Return the current playback rate of the spline at `index`. + float PlaybackRate(const Index index) const { return sources_[index].rate; } + + // Return the spline that is currently being traversed at `index`. + const CompactSpline* SourceSpline(const Index index) const { + return sources_[index].spline; + } + + // Return the splines currently playing back from `index` to `index + count`. + // `splines` is an output array of length `count`. + void Splines(const Index index, const Index count, + const CompactSpline** splines) const; + + // Return the raw cubic curve for `index`. Useful if you need to calculate + // the second or third derivatives (which are not calculated in + // AdvanceFrame), or plot the curve for debug reasons. + const CubicCurve& Cubic(const Index index) const { return cubics_[index]; } + + // Return the current x value for the current cubic. Each spline segment + // is evaluated as a cubic that starts at x=0. + float CubicX(const Index index) const { return cubic_xs_[index]; } + + // Return x-value at the end of the spline. + float EndX(const Index index) const { return sources_[index].spline->EndX(); } + + // Return y-value at the end of the spline. + float EndY(const Index index) const { return sources_[index].spline->EndY(); } + + // TODO OPT: Write assembly versions of this function. + void EndYs(const Index index, const Index count, float* out) const { + assert(Valid(index) && Valid(index + count - 1)); + for (Index i = 0; i < count; ++i) { + out[i] = EndY(index + i); + } + } + + // Return slope at the end of the spline. + float EndDerivative(const Index index) const { + return PlaybackRate(index) * sources_[index].spline->EndDerivative(); + } + + // TODO OPT: Write assembly versions of this function. + void EndDerivatives(const Index index, const Index count, float* out) const { + assert(Valid(index) && Valid(index + count - 1)); + for (Index i = 0; i < count; ++i) { + out[i] = EndDerivative(index + i); + } + } + + // Return slope at the end of the spline, at `index`. Ignore the playback + // rate. This is useful for times when the playback rate is 0, but you + // still want to get information about the underlying spline. + float EndDerivativeWithoutPlayback(const Index index) const { + return sources_[index].spline->EndDerivative(); + } + + // Return y-distance between current-y and end-y. + // If using modular arithmetic, consider both paths to the target + // (directly and wrapping around), and return the length of the shorter path. + float YDifferenceToEnd(const Index index) const { + return NormalizeY(index, EndY(index) - Y(index)); + } + + // TODO OPT: Write assembly versions of this function. + void YDifferencesToEnd(const Index index, const Index count, + float* out) const { + assert(Valid(index) && Valid(index + count - 1)); + for (Index i = 0; i < count; ++i) { + out[i] = YDifferenceToEnd(index + i); + } + } + + // Apply modular arithmetic to ensure that `y` is within the valid y_range. + float NormalizeY(const Index index, const float y) const { + const YRange& r = y_ranges_[index]; + return r.modular_range.Size() > 0.f + ? NormalizeCloseValueWithinInterval(r.modular_range, y) + : y; + } + + // True if using modular arithmetic on this `index`. Modular arithmetic is + // used for types such as angles, which are equivalent modulo 2pi + // (e.g. -pi and +pi represent the same angle). + bool ModularArithmetic(const Index index) const { + return y_ranges_[index].modular_range.Size() > 0.f; + } + + // The modular range for values that use ModularArithmetic(). Note that Y() + // can be outside of this range. However, we always normalize to this range + // before blending to a new spline. + const Interval& ModularRange(const Index index) const { + return y_ranges_[index].modular_range; + } + + private: + void InitCubic(const Index index, const float start_x); + float SplineStartX(const Index index) const { + return sources_[index].spline->StartX(); + } + float CubicStartX(const Index index) const { + const Source& s = sources_[index]; + assert(s.spline != nullptr); + return s.spline->NodeX(s.x_index); + } + CubicInit CalculateBlendInit(const Index index, const CompactSpline& spline, + const SplinePlayback& playback) const; + void BlendToSpline(const Index index, const CompactSpline& spline, + const SplinePlayback& playback); + void JumpToSpline(const Index index, const CompactSpline& spline, + const SplinePlayback& playback); + + // These functions have C and assembly language variants. + void UpdateCubicXsAndGetMask(const float delta_x, uint8_t* masks); + void UpdateCubicXsAndGetMask_C(const float delta_x, uint8_t* masks); + size_t UpdateCubicXs(const float delta_x, Index* indices_to_init); + size_t UpdateCubicXs_TwoSteps(const float delta_x, Index* indices_to_init); + size_t UpdateCubicXs_OneStep(const float delta_x, Index* indices_to_init); + void EvaluateIndex(const Index index); + void EvaluateCubics(); + void EvaluateCubics_C(); + + struct Source { + Source() + : rate(1.0f), + y_offset(0.0f), + y_scale(1.0f), + spline(nullptr), + x_index(kInvalidSplineIndex), + repeat(false) {} + + Source(float rate, float y_offset, float y_scale) + : rate(rate), + y_offset(y_offset), + y_scale(y_scale), + spline(nullptr), + x_index(kInvalidSplineIndex), + repeat(false) {} + + // Speed at which time flows, relative to the spline's authored rate. + // 0 ==> paused + // 0.5 ==> half speed (slow motion) + // 1 ==> authored speed + // 2 ==> double speed (fast forward) + float rate; + + // Offset that we add to spline to shift it along the y-axis. + float y_offset; + + // Factor by which we scale the spline along the y-axis. We first scale + // the spline along the y-axis before shifting it. + float y_scale; + + // Pointer to the source spline node. Spline data is owned externally. + // We neither allocate or free this pointer here. + const CompactSpline* spline; + + // Current index into `spline`. The cubics_ valid is instantiated from + // spline[x_index]. + CompactSplineIndex x_index; + + // If true, start again at the beginning of the spline when we reach + // the end. + bool repeat; + }; + + struct YRange { + // If using modular arithmetic, hold the min and max extents of the + // modular range. Modular ranges are used for things like angles, + // which wrap around from -pi to +pi. + // By default, invalid. If invalid, do not use modular arithmetic. + Interval modular_range; + }; + + // Data is organized in struct-of-arrays format to match the algorithm`s + // consumption of the data. + // - The algorithm that updates x values, and detects when we must transition + // to the next segment of the spline looks only at data in `cubic_xs_` and + // `cubic_x_ends_`. + // - The algorithm that updates `ys_` looks only at the data in `cubic_xs_`, + // `cubics_`, and `y_ranges_`. It writes to `ys_`. + // These vectors grow when SetNumIndices() is called, but they never shrink. + // So, we`ll have a few reallocs (which are slow) until the highwater mark is + // reached. Then the cost of reallocs disappears. In this way we have a + // reasonable tradeoff between memory conservation and runtime performance. + + // Source spline nodes and our current index into these splines. + std::vector sources_; + + // Define the valid output values. We can clamp to a range, or wrap around to + // a range using modular arithmetic (two modes of operation). + std::vector y_ranges_; + + // The current `x` value at which `cubics_` are evaluated. + // ys_[i] = cubics_[i].Evaluate(cubic_xs_[i]) + std::vector cubic_xs_; + + // The last valid x value in `cubics_`. + std::vector cubic_x_ends_; + + // Currently active segment of sources_.spline. + // Instantiated from + // sources_[i].spline->CreateInitCubic(sources_[i].x_index). + std::vector cubics_; + + // Value of the spline at `cubic_xs_`, normalized and clamped to be within + // `y_ranges_`. Evaluated in AdvanceFrame. + std::vector ys_; + + // Stratch buffer used for internal calculations. + std::vector scratch_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_ANIMATION_SPLINE_BULK_SPLINE_EVALUATOR_H_ diff --git a/redux/redux/engines/animation/spline/bulk_spline_evaluator_neon.s b/redux/redux/engines/animation/spline/bulk_spline_evaluator_neon.s new file mode 100644 index 0000000..12cf37a --- /dev/null +++ b/redux/redux/engines/animation/spline/bulk_spline_evaluator_neon.s @@ -0,0 +1,160 @@ +@ Copyright 2015 Google Inc. All rights reserved. +@ +@ Licensed under the Apache License, Version 2.0 (the "License"); +@ you may not use this file except in compliance with the License. +@ You may obtain a copy of the License at +@ +@ http://www.apache.org/licenses/LICENSE-2.0 +@ +@ Unless required by applicable law or agreed to in writing, software +@ distributed under the License is distributed on an "AS IS" BASIS, +@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@ See the License for the specific language governing permissions and +@ limitations under the License. + + + .text + .syntax unified + + @ When used with the 'vtbl' instruction, grabs the first byte of every + @ word, and places it in the first word. Fills the second word with 0s. + @ For example, (0xFFFFFFFF, 0x00000000, 0x00000000, 0xFFFFFFFF) + @ ==> (0xFF0000FF, 0x00000000) +kFirstByteTableIndices: + .byte 0 + .byte 4 + .byte 8 + .byte 12 + .byte 0xFF + .byte 0xFF + .byte 0xFF + .byte 0xFF + + .balign 4 + .global UpdateCubicXsAndGetMask_Neon + .thumb_func +UpdateCubicXsAndGetMask_Neon: +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +@ +@ void UpdateCubicXsAndGetMask_Neon( +@ const float& delta_x, const float* x_ends, int num_xs, float* xs, +@ uint8_t* masks) +@ +@ Parameters +@ r0: *delta_x +@ r1: *x_ends +@ r2: *playback_rates +@ r3: num_xs +@ [sp]: *xs ==> r4 +@ [sp + 4]: *masks ==> r5 +@ +@ q15: kFirstByteTableIndices +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + + @ r5 <-- masks + ldr r4, [sp] + ldr r5, [sp, #4] + + @ q12 <-- delta_x splatted + vld1.f32 {d24[], d25[]}, [r0] + + @ q15 <-- kFirstByteTableIndices + adr r12, kFirstByteTableIndices + vld1.8 {d30}, [r12] + +.L_UpdateCubicXs_Loop: + @ num_xs -= 4; sets the 'gt' flag used in 'bgt' below + subs r3, r3, #4 + + @ q8 <-- xs[i] + @ q9 <-- x_ends[i], x_ends pointer += 4 + @ q11 <-- playback_rate[i], playback_rate pointer += 4 + vld1.f32 {d16, d17}, [r4:128] + vld1.f32 {d18, d19}, [r1:128]! + vld1.f32 {d22, d23}, [r2:128]! + + @ q8 <-- xs[i] + x_delta * playback_rate[i] + vmla.f32 q8, q12, q11 + + @ create comparison mask: 0xFFFFFFFF or 0x00000000. + @ q10 <-- q8 > q9 = xs[i] > x_ends[i] (mask) + vcgt.f32 q10, q8, q9 + + @ xs[i] <-- updated value: xs[i] + delta_x, xs pointer += 4 + vst1.f32 {d16, d17}, [r4:128]! + + @ pack mask into 4-byte word. + @ q10[0][1][2][3] <-- q10[0][4][8][12] (byte indices into q10) + vtbl.8 d20, {d20, d21}, d30 + + @ mask[i] <-- q10[0][1][2][3] (write one word), mask pointer += 4 + vst1.32 {d20[0]}, [r5:32]! + + @ continue loop if iterations still remain. + bgt .L_UpdateCubicXs_Loop + + @ Return to the address specified in the link register. + bx lr + + + .balign 4 + .global EvaluateCubics_Neon + .thumb_func +EvaluateCubics_Neon: +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +@ +@ void EvaluateCubics_Neon( +@ const CubicCurve* cubics, const float* xs, int num_cubics, float* ys) +@ +@ Parameters +@ r0: *cubics --> also r6 (offset by 32 bytes) +@ r1: *xs +@ r2: num_cubics +@ r3: *ys (out parameter) +@ +@ r10: 48 +@ r12: 16 +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + + @ save state to stack + push {r6, lr} @ 1 register x 4 bytes = 4 bytes + vpush {q6} @ 1 register x 16 bytes = 16 bytes + + @ r6 <-- cubics + 32 bytes (for efficient loading) + add r6, r0, #32 + + @ constants + mov r10, #48 + mov r12, #16 + + @ q6 <-- 0 + vmov.f32 q6, #0.0 + +.L_EvaluateCubic_Loop: + @ num_cubics -= 4 + @ sets the 'gt' flag used in 'bgt' below. + subs r2, r2, #4 + + @ q8, q9, q10, q11 <-- cubics[i].coeff[0], coeff[1], coeff[2], coefff[3] + @ deinterlaced the cubic coefficients so each one gets a register. + vld4.f32 {d16, d18, d20, d22}, [r0:256] + vld4.f32 {d17, d19, d21, d23}, [r6:256] + + @ q12 <-- xs[i] + vld1.f32 {d24, d25}, [r1]! + + @ q8 <-- y = c3*x^3 + c2*x^2 + c1*x + c0 + @ = ((c3*x + c2)*x + c1)*x + c0 + vmla.f32 q10, q11, q12 @ q10 = c3*x + c2 + vmla.f32 q9, q10, q12 @ q9 = (c3*x + c2)*x + c1 + vmla.f32 q8, q9, q12 @ q8 = ((c3*x + c2)*x + c1)*x + c0 + + @ ys[i] <-- q8 = final y, adjusted and clamped + vst1.f32 {d16, d17}, [r3:128]! + + @ continue loop if iterations still remain. + bgt .L_EvaluateCubic_Loop + + @ Return by popping the link register into the program counter. + vpop {q6} + pop {r6, pc} diff --git a/redux/redux/engines/animation/spline/bulk_spline_evaluator_tests.cc b/redux/redux/engines/animation/spline/bulk_spline_evaluator_tests.cc new file mode 100644 index 0000000..945e442 --- /dev/null +++ b/redux/redux/engines/animation/spline/bulk_spline_evaluator_tests.cc @@ -0,0 +1,227 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/engines/animation/spline/bulk_spline_evaluator.h" + +namespace redux { +namespace { + +using ::testing::Eq; +using ::testing::FloatNear; + +static const Interval kAngleInterval(-kPi, kPi); +static const float kFixedPointEpsilon = 0.02f; +static const float kDerivativePrecision = 0.01f; +static const float kSecondDerivativePrecision = 0.26f; +static const float kThirdDerivativePrecision = 6.0f; +static const float kXGranularityScale = 0.01f; + +// Data structure to hold the results of sampling a spline. +struct SampledSpline { + std::vector points; + std::vector derivatives; +}; + +// A collection of simple splines. We'll be using these to test the evaluator. +static const CubicInit kSimpleSplines[] = { + // start_y end_y width_x + // start_derivative end_derivative + CubicInit(0.0f, 1.0f, 0.1f, 0.0f, 1.0f), + CubicInit(1.0f, -8.0f, 0.0f, 0.0f, 1.0f), + CubicInit(1.0f, -8.0f, -1.0f, 0.0f, 1.0f), +}; +static const int kNumSimpleSplines = ABSL_ARRAYSIZE(kSimpleSplines); + +static Interval CubicInitYInterval(const CubicInit& init, + float buffer_percent) { + const auto min = std::min(init.start_y, init.end_y); + const auto max = std::max(init.start_y, init.end_y); + return Interval(min, max).Scaled(buffer_percent); +} + +SampledSpline SampleSpline(const CubicInit& init, bool is_angle = false, + float y_scale = 1.0f, float y_offset = 0.0f) { + // Create a spline using the `init` arguments. + CompactSpline spline; + const Interval y_range = CubicInitYInterval(init, 0.1f); + spline.Init(y_range, init.width_x * kXGranularityScale); + spline.AddNode(0.0f, init.start_y, init.start_derivative); + spline.AddNode(init.width_x, init.end_y, init.end_derivative); + + // Load the spline into the evaluator. + BulkSplineEvaluator evaluator; + evaluator.SetNumIndices(1); + if (is_angle) { + evaluator.SetYRanges(0, 1, kAngleInterval); + } + SplinePlayback playback; + playback.y_offset = y_offset; + playback.y_scale = y_scale; + evaluator.SetSplines(0, 1, &spline, playback); + + // Get the extents of the loaded spline to determine our precision. + const float y_precision = + evaluator.SourceSpline(0)->IntervalY().Size() * kFixedPointEpsilon; + const Interval range_x = evaluator.SourceSpline(0)->IntervalX(); + const float derivative_precision = fabs(y_scale) * kDerivativePrecision; + + // Figure out the sampling frequency. + static const int kNumSamples = 80; + const float delta_x = range_x.Size() / (kNumSamples - 1); + + // Sample the evaluator, storing the results. + SampledSpline samples; + for (int i = 0; i < kNumSamples; ++i) { + const CubicCurve& c = evaluator.Cubic(0); + const float x = evaluator.CubicX(0); + + // Verify that the spline and the evaluator agree on the values. + EXPECT_THAT(c.Evaluate(x), FloatNear(evaluator.Y(0), y_precision)); + EXPECT_THAT(c.Derivative(x), + FloatNear(evaluator.Derivative(0), kDerivativePrecision)); + + const vec2 point(evaluator.X(0), evaluator.Y(0)); + const vec3 derivatives(evaluator.Derivative(0), c.SecondDerivative(x), + c.ThirdDerivative(x)); + samples.points.push_back(point); + samples.derivatives.push_back(derivatives); + + evaluator.AdvanceFrame(delta_x); + } + + // Double-check start and end y values and derivitives, taking y-scale and + // y-offset into account. + const CubicCurve c(init); + EXPECT_THAT(c.Evaluate(0.0f) * y_scale + y_offset, + FloatNear(samples.points.front().y, y_precision)); + EXPECT_THAT(c.Evaluate(init.width_x) * y_scale + y_offset, + FloatNear(samples.points.back().y, y_precision)); + + EXPECT_THAT(c.Derivative(0.0f) * y_scale, + FloatNear(samples.derivatives.front().x, derivative_precision)); + EXPECT_THAT(c.Derivative(init.width_x) * y_scale, + FloatNear(samples.derivatives.back().x, derivative_precision)); + return samples; +} + +TEST(BulkSplineEvaluatorTests, SampleSimpleSplines) { + for (int i = 0; i < kNumSimpleSplines; ++i) { + // SampleSpline performs several internal consistency checks, so let's run + // those for the simple cases. + SampleSpline(kSimpleSplines[i]); + } +} + +TEST(BulkSplineEvaluatorTests, DoNotOvershoot) { + for (int i = 0; i < kNumSimpleSplines; ++i) { + const CubicInit& init = kSimpleSplines[i]; + const SampledSpline samples = SampleSpline(init); + + // Ensure the splines don't overshoot their mark. + const Interval x_range(-kXGranularityScale, + init.width_x * (1.0f + kXGranularityScale)); + const Interval y_range = CubicInitYInterval(init, 0.001f); + for (const vec2& point : samples.points) { + EXPECT_TRUE(x_range.Contains(point.x)); + EXPECT_TRUE(y_range.Contains(point.y)); + } + } +} + +// Ensure that the curves are mirrored in y when node y's are mirrored. +TEST(BulkSplineEvaluatorTests, MirrorY) { + for (int i = 0; i < kNumSimpleSplines; ++i) { + const CubicInit& init = kSimpleSplines[i]; + const CubicInit mirrored_init = + CubicInit(-init.start_y, -init.start_derivative, -init.end_y, + -init.end_derivative, init.width_x); + + const float y_precision = + fabs(init.start_y - init.end_y) * kFixedPointEpsilon; + + const SampledSpline lhs = SampleSpline(init); + const SampledSpline rhs = SampleSpline(mirrored_init); + + ASSERT_EQ(lhs.points.size(), rhs.points.size()); + for (size_t j = 0; j < lhs.points.size(); ++j) { + EXPECT_THAT(lhs.points[j].x, Eq(rhs.points[j].x)); + EXPECT_THAT(lhs.points[j].y, FloatNear(-rhs.points[j].y, y_precision)); + EXPECT_THAT(lhs.derivatives[j].x, + FloatNear(-rhs.derivatives[j].x, kDerivativePrecision)); + EXPECT_THAT(lhs.derivatives[j].y, + FloatNear(-rhs.derivatives[j].y, kSecondDerivativePrecision)); + EXPECT_THAT(lhs.derivatives[j].z, + FloatNear(-rhs.derivatives[j].z, kThirdDerivativePrecision)); + } + } +} + +// Ensure that the curves are scaled in x when node's x is scaled. +TEST(BulkSplineEvaluatorTests, ScaleX) { + static const float kScale = 100.0f; + for (int i = 0; i < kNumSimpleSplines; ++i) { + const CubicInit& init = kSimpleSplines[i]; + const CubicInit scaled_init = + CubicInit(init.start_y, init.start_derivative / kScale, init.end_y, + init.end_derivative / kScale, init.width_x * kScale); + + const float x_precision = init.width_x * kFixedPointEpsilon; + const float y_precision = + fabs(init.start_y - init.end_y) * kFixedPointEpsilon; + + const SampledSpline lhs = SampleSpline(init); + const SampledSpline rhs = SampleSpline(scaled_init); + + ASSERT_EQ(lhs.points.size(), rhs.points.size()); + for (size_t j = 0; j < lhs.points.size(); ++j) { + EXPECT_THAT(lhs.points[j].x, + FloatNear(rhs.points[j].x / kScale, x_precision)); + EXPECT_THAT(lhs.points[j].y, FloatNear(rhs.points[j].y, y_precision)); + + EXPECT_THAT(lhs.derivatives[j].x, FloatNear(rhs.derivatives[j].x * kScale, + kDerivativePrecision)); + EXPECT_THAT(lhs.derivatives[j].y, + FloatNear(rhs.derivatives[j].y * kScale * kScale, + kSecondDerivativePrecision)); + EXPECT_THAT(lhs.derivatives[j].z, + FloatNear(rhs.derivatives[j].z * kScale * kScale * kScale, + kThirdDerivativePrecision)); + } + } +} + +TEST(BulkSplineEvaluatorTests, YScaleAndOffset) { + static const float kOffsets[] = {0.0f, 2.0f, 0.111f, 10.0f, -1.5f, -1.0f}; + static const float kScales[] = {1.0f, 2.0f, 0.1f, 1.1f, 0.0f, -1.0f, -1.3f}; + + for (size_t k = 0; k < ABSL_ARRAYSIZE(kOffsets); ++k) { + for (size_t j = 0; j < ABSL_ARRAYSIZE(kScales); ++j) { + for (int i = 0; i < kNumSimpleSplines; ++i) { + // SampleSpline does some basic, internal consistency checks. We'll + // just rely on those for this test. + const CubicInit& init = kSimpleSplines[i]; + SampleSpline(init, false, kScales[j], kOffsets[k]); + } + } + } +} + +} // namespace +} // namespace redux diff --git a/redux/redux/engines/animation/spline/compact_spline.cc b/redux/redux/engines/animation/spline/compact_spline.cc new file mode 100644 index 0000000..5303703 --- /dev/null +++ b/redux/redux/engines/animation/spline/compact_spline.cc @@ -0,0 +1,403 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/animation/spline/compact_spline.h" + +#include "redux/engines/animation/spline/bulk_spline_evaluator.h" +#include "redux/engines/animation/spline/dual_cubic.h" + +namespace redux { + +using detail::CompactSplineNode; + +// static constants +const size_t CompactSpline::kBaseSize = + sizeof(CompactSpline) - + kDefaultMaxNodes * sizeof(detail::CompactSplineNode); +const CompactSplineXGrain CompactSplineNode::kMaxX = + std::numeric_limits::max(); +const CompactSplineYRung CompactSplineNode::kMaxY = + std::numeric_limits::max(); +const CompactSplineAngle CompactSplineNode::kMinAngle = + std::numeric_limits::min(); +const float CompactSplineNode::kYScale = 1.0f / static_cast(kMaxY); +const float CompactSplineNode::kAngleScale = + static_cast(-M_PI / static_cast(kMinAngle)); +static const float kYRangeBufferPercent = 1.05f; + +// YsBulkOutput records the evaluated y and derivative values into 2D arrays. +// Arrays are of length num_points * num_splines. +class YsBulkOutput : public CompactSpline::BulkOutput { + public: + YsBulkOutput(float* ys, float* derivatives, size_t num_splines) + : ys(ys), derivatives(derivatives), num_splines(num_splines) {} + + // Gets the Y and derivative values of the evaluator. + virtual void AddPoint(int i, const BulkSplineEvaluator& evaluator) { + assert(num_splines == static_cast(evaluator.NumIndices())); + + const size_t offset = i * num_splines; + float* y = ys + offset; + memcpy(y, evaluator.Ys(0), num_splines * sizeof(y[0])); + + if (derivatives) { + float* derivatives_for_i = derivatives + offset; + for (size_t j = 0; j < num_splines; ++j) { + derivatives_for_i[j] = + evaluator.Derivative(static_cast(j)); + } + } + } + + private: + float* ys; + float* derivatives; + size_t num_splines; +}; + +// `splines` is an array of length num_splines. +// AppendToSplineBulkOutput adds the evaluated x, y, and derivative values at +// index to the the corresponding spline in `splines`. +class AppendToSplineBulkOutput : public CompactSpline::BulkOutput { + public: + AppendToSplineBulkOutput(CompactSpline** splines, size_t num_splines) + : splines(splines), num_splines(num_splines) {} + + // Adds the current x, y, and derivative values of the evaluator + // to the spline. + virtual void AddPoint(int i, const BulkSplineEvaluator& evaluator) { + assert(num_splines == static_cast(evaluator.NumIndices())); + (void)i; + + for (size_t j = 0; j < num_splines; ++j) { + float x = evaluator.X(static_cast(j)); + float y = evaluator.Y(static_cast(j)); + float derivative = + evaluator.Derivative(static_cast(j)); + splines[j]->AddNode(x, y, derivative, kAddWithoutModification); + } + } + + private: + CompactSpline** splines; + size_t num_splines; +}; + +void CompactSpline::InitFromNodes(const UncompressedNode* nodes, + size_t num_nodes) { + const float end_x = nodes[num_nodes - 1].x; + const float x_granularity = CompactSpline::RecommendXGranularity(end_x); + Interval y_range = Interval::Empty(); + for (size_t i = 0; i < num_nodes; ++i) { + y_range = y_range.Included(nodes[i].y); + } + Init(y_range, x_granularity); + + AddUncompressedNodes(nodes, num_nodes); +} + +void CompactSpline::InitFromSpline(const CompactSpline& spline) { + assert(max_nodes_ > 1); + Init(spline.y_range().Scaled(kYRangeBufferPercent), spline.x_granularity()); + const float total_x = spline.EndX() - spline.StartX(); + const float delta_x = total_x / (max_nodes_ - 1); + CompactSpline* splines[] = {this}; + const size_t count = sizeof(splines) / sizeof(splines[0]); + AppendToSplineBulkOutput out(splines, count); + spline.BulkEvaluate(&spline, count, spline.StartX(), delta_x, max_nodes_, + &out); +} + +void CompactSpline::AddNode(const float x, const float y, + const float derivative, + const CompactSplineAddMethod method) { + const CompactSplineNode new_node(x, y, derivative, x_granularity_, y_range_); + + // Precondition: Nodes must come *after* or *at* the last node. + assert(num_nodes_ == 0 || new_node.x() >= Back().x()); + + // Early out when adding the same node. + const bool same_as_back = num_nodes_ > 0 && Back() == new_node; + if (same_as_back) return; + + // If we're adding a point at the same x, that means there will be a + // discontinuity in the curve at x (either in y or derivative). + const bool discontinuity = num_nodes_ > 0 && Back().x() == new_node.x(); + if (discontinuity) { + // No point in having three points with the same x, value. Two points makes + // a discontinuity, but for any more, the middle points will just take up + // space, so remove it. + const bool already_ends_in_discontinuity = + num_nodes_ >= 2 && Back().x() == nodes_[num_nodes_ - 2].x(); + if (already_ends_in_discontinuity) num_nodes_--; + } + + // Add a dual-cubic mid-node, if required, to keep cubic curves well behaved. + const bool add_middle_node = + !discontinuity && method == kEnsureCubicWellBehaved && num_nodes_ != 0; + if (add_middle_node) { + const CompactSplineNode& last_node = Back(); + const CubicInit init = CreateCubicInit(last_node, new_node); + const CubicCurve curve(init); + + // A curve is well behaved if it has uniform curvature. + if (!curve.UniformCurvature(Interval(0.0f, WidthX(last_node, new_node)))) { + // Find a suitable intermediate node using the math from the Dual Cubics + // document. + float mid_x; + float mid_y; + float mid_derivative; + CalculateDualCubicMidNode(init, &mid_x, &mid_y, &mid_derivative); + + // Add the intermediate node, as long as it + const CompactSplineNode mid_node(last_node.X(x_granularity_) + mid_x, + mid_y, mid_derivative, x_granularity_, + y_range_); + const bool is_unique_x = + mid_node.x() != last_node.x() && mid_node.x() != new_node.x(); + if (is_unique_x) { + AddNodeVerbatim(mid_node); + } + } + } + + // Add the new node. + AddNodeVerbatim(new_node); +} + +void CompactSpline::AddUncompressedNodes(const UncompressedNode* nodes, + size_t num_nodes) { + for (size_t i = 0; i < num_nodes; ++i) { + AddNode(nodes[i].x, nodes[i].y, nodes[i].derivative, + kAddWithoutModification); + } +} + +float CompactSpline::NodeX(const CompactSplineIndex index) const { + // Note that, when `index` is before the spline, we return x=0 instead of + // x=first node's x. This is because logically the spline always starts at + // x=0, so anything before the first node is in an implicit segment from + // x=0..first node's x. + if (index == kAfterSplineIndex) return EndX(); + if (index == kBeforeSplineIndex) return 0.0f; + assert(index < num_nodes_); + return nodes_[index].X(x_granularity_); +} + +float CompactSpline::NodeY(const CompactSplineIndex index) const { + if (index == kAfterSplineIndex) return EndY(); + if (index == kBeforeSplineIndex) return StartY(); + assert(index < num_nodes_); + return nodes_[index].Y(y_range_); +} + +float CompactSpline::YCalculatedSlowly(const float x) const { + const CompactSplineIndex index = IndexForX(x, 0); + + // Handle cases where `x` is outside the spline's domain. + if (index == kBeforeSplineIndex) + // The curve is flat outside the bounds, so all derivatives + // outside the bounds are 0. + return StartY(); + if (index == kAfterSplineIndex) return EndY(); + + // Create the cubic curve for `index` and evaluate it. + const CubicCurve cubic(CreateCubicInit(index)); + const float cubic_x = x - NodeX(index); + return cubic.Evaluate(cubic_x); +} + +void CompactSpline::Ys(const float start_x, const float delta_x, + const size_t num_points, float* ys, + float* derivatives) const { + // Use the BulkSplineEvaluator even though we're only evaluating one spline. + // Still faster, since it doesn't have to recreate the cubic for every x. + BulkYs(this, 1, start_x, delta_x, num_points, ys, derivatives); +} + +void CompactSpline::BulkEvaluate(const CompactSpline* const splines, + const size_t num_splines, const float start_x, + const float delta_x, const size_t num_points, + BulkOutput* out) { + BulkSplineEvaluator evaluator; + + // Initialize the evaluator with the splines. + // Note that we set `repeat` = false, so that we can accurately get the last + // value in the spline. + SplinePlayback playback; + playback.start_x = start_x; + evaluator.SetNumIndices(static_cast(num_splines)); + evaluator.SetSplines(0, static_cast(num_splines), splines, playback); + + // Grab y values, then advance spline evaluation by delta_x. + // Repeat num_points times. + for (CompactSplineIndex i = 0; i < num_points; ++i) { + out->AddPoint(i, evaluator); + evaluator.AdvanceFrame(delta_x); + } +} + +// static +void CompactSpline::BulkYs(const CompactSpline* const splines, + const size_t num_splines, const float start_x, + const float delta_x, const size_t num_points, + float* ys, float* derivatives) { + YsBulkOutput output(ys, derivatives, num_splines); + BulkEvaluate(splines, num_splines, start_x, delta_x, num_points, &output); +} + +Interval CompactSpline::IntervalX(const CompactSplineIndex index) const { + if (index == kBeforeSplineIndex) + // Return 0.0f for the start of the range instead of -inf. + // There is an implicit range from the start of the spline (x=0) to the + // start of the first segment. + return Interval(0.0f, StartX()); + + if (index == kAfterSplineIndex) + return Interval(EndX(), std::numeric_limits::infinity()); + + return Interval(nodes_[index].X(x_granularity_), + nodes_[index + 1].X(x_granularity_)); +} + +CompactSplineIndex CompactSpline::IndexForX( + const float x, const CompactSplineIndex guess_index) const { + const int quantized_x = CompactSplineNode::QuantizeX(x, x_granularity_); + + // Check bounds first. + // Return negative if before index 0. + if (quantized_x < Front().x()) return kBeforeSplineIndex; + + // When we are exactly on the last node, we want to return the index of the + // last segment (i.e. the second last node). This is so that the derivative + // at the end matches the derivative of the last node, and not 0 (since + // derivatives beyond the spline are forced to 0). + // This only makes sense if there is more than one node in the spline. + if (quantized_x == Back().x() && num_nodes_ >= 2) return num_nodes_ - 2; + + // Return index of the last index if beyond the last index. + if (quantized_x >= Back().x()) return kAfterSplineIndex; + + // Check the guess value first. Only return the guess index if it has a valid + // width. + const CompactSplineXGrain compact_x = + static_cast(quantized_x); + if (IndexContainsX(compact_x, guess_index) && guess_index < LastNodeIndex()) { + const CompactSplineIndex next_index = guess_index + 1; + if (WidthX(nodes_[guess_index], nodes_[next_index]) > 0.f) { + return guess_index; + } + } + + // Search for it, if the initial guess fails. + const CompactSplineIndex index = BinarySearchIndexForX(compact_x); + assert(IndexContainsX(compact_x, index)); + return index; +} + +CompactSplineIndex CompactSpline::IndexForXAllowingRepeat( + const float x, const CompactSplineIndex guess_index, const bool repeat, + float* final_x) const { + // Does not repeat, so return the index as is. + const CompactSplineIndex index = IndexForX(x, guess_index); + if (!repeat || index != kAfterSplineIndex) { + *final_x = x; + return index; + } + + // Repeats, so wrap `x` back to 0 and find the index again. + const Interval x_range(0.0f, EndX()); + const float repeat_x = NormalizeCloseValueWithinInterval(x_range, x); + const CompactSplineIndex repeat_index = IndexForX(repeat_x, 0); + *final_x = repeat_x; + return repeat_index; +} + +CompactSplineIndex CompactSpline::ClampIndex(const CompactSplineIndex index, + float* x) const { + if (index == kBeforeSplineIndex) { + *x = StartX(); + return 0; + } + if (index == kAfterSplineIndex) { + *x = EndX(); + return LastNodeIndex(); + } + assert(index < num_nodes_); + return index; +} + +bool CompactSpline::IndexContainsX(const CompactSplineXGrain compact_x, + const CompactSplineIndex index) const { + return index < LastNodeIndex() && nodes_[index].x() <= compact_x && + compact_x <= nodes_[index + 1].x(); +} + +static inline bool CompareSplineNodeX(const CompactSplineXGrain compact_x, + const CompactSplineNode& n) { + return compact_x < n.x(); +} + +CompactSplineIndex CompactSpline::BinarySearchIndexForX( + const CompactSplineXGrain compact_x) const { + // Binary search nodes by x. + // TODO OPT: avoid the pointer arithmetic (which is expensive on ARM since it + // requires an integer division) by searching with indices instead of + // iterators. + // int low = 0; + // int hi = max_hi; + // while (low + 1 < hi) { + // const int mid = (low + hi) / 2; + // + // if (compact_x < nodes_[mid].x()) { + // hi = mid; + // } else { + // low = mid; + // } + // } + const auto upper_it = std::upper_bound(nodes_, &nodes_[num_nodes_], compact_x, + CompareSplineNodeX); + const int low = static_cast(upper_it - nodes_) - 1; + assert(0 <= low && low < LastNodeIndex()); + + // We return the lower index: x is in the segment bt 'index' and 'index' + 1. + return static_cast(low); +} + +CubicInit CompactSpline::CreateCubicInit(const CompactSplineIndex index) const { + // Handle case where we are outside of the interpolatable range. + if (OutsideSpline(index)) { + const CompactSplineNode& n = index == kBeforeSplineIndex ? Front() : Back(); + const float constant_y = n.Y(y_range_); + return CubicInit(constant_y, 0.0f, constant_y, 0.0f, 1.0f); + } + + // Interpolate between the nodes at 'index' and 'index' + 1. + assert(index + 1 < num_nodes_); + return CreateCubicInit(nodes_[index], nodes_[index + 1]); +} + +CubicInit CompactSpline::CreateCubicInit(const CompactSplineNode& s, + const CompactSplineNode& e) const { + return CubicInit(s.Y(y_range_), s.Derivative(), e.Y(y_range_), e.Derivative(), + WidthX(s, e)); +} + +float CompactSpline::RecommendXGranularity(const float max_x) { + return max_x <= 0.0f ? 1.0f : max_x / CompactSplineNode::MaxX(); +} + +} // namespace redux diff --git a/redux/redux/engines/animation/spline/compact_spline.h b/redux/redux/engines/animation/spline/compact_spline.h new file mode 100644 index 0000000..97189b2 --- /dev/null +++ b/redux/redux/engines/animation/spline/compact_spline.h @@ -0,0 +1,608 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_SPLINE_COMPACT_SPLINE_H_ +#define REDUX_ENGINES_ANIMATION_SPLINE_COMPACT_SPLINE_H_ + +#include +#include + +#include "absl/base/attributes.h" +#include "redux/engines/animation/spline/compact_spline_node.h" +#include "redux/engines/animation/spline/cubic_curve.h" +#include "redux/modules/math/bounds.h" +#include "redux/modules/math/vector.h" + +namespace redux { + +class BulkSplineEvaluator; + +// Index into the spline. Some high values have special meaning (see below). +using CompactSplineIndex = uint16_t; +static constexpr CompactSplineIndex kInvalidSplineIndex = + static_cast(-1); +static constexpr CompactSplineIndex kBeforeSplineIndex = + static_cast(-2); +static constexpr CompactSplineIndex kAfterSplineIndex = + static_cast(-3); +static constexpr CompactSplineIndex kMaxSplineIndex = + static_cast(-4); + +// Return true if `index` is not an index into the spline. +inline bool OutsideSpline(CompactSplineIndex index) { + return index >= kAfterSplineIndex; +} + +inline float NormalizeWildValueWithinInterval(const Interval& range, float x) { + // Use (expensive) division to determine how many lengths we are away from + // the normalized range. + const float length = range.Size(); + const float units = (x - range.min) / length; + const float whole_units = floor(units); + + // Subtract off those units to get something that (mathematically) should + // be normalized. Due to Ting point error, it sometimes is slightly + // outside the bounds, so we need to do a standard normalization afterwards. + const float close = x - whole_units * length; + const float adjustment = x <= range.min ? length + : x > range.max ? -length + : 0.f; + const float normalized = close + adjustment; + // DCHECK(ContainsExcludingStart(normalized)); + return normalized; +} + +inline float NormalizeCloseValueWithinInterval(const Interval& range, float x) { + static const int kMaxAdjustments = 4; + + // Return without change if `x` is already normalized. + const bool below = x <= range.min; + const bool above = x > range.max; + if (!below && !above) { + return x; + } + + // Each time through the loop, we'll adjust by one length closer to the + // valid interval. + const float length = range.Size(); + int num_adjustments = 0; + + if (below) { + // Keep adding until we're in the range. + do { + x += length; + if (num_adjustments++ > kMaxAdjustments) { + return NormalizeWildValueWithinInterval(range, x); + } + } while (x <= range.min); + } else { + // Keep subtracting until we're in the range. + do { + x -= length; + if (num_adjustments++ > kMaxAdjustments) { + return NormalizeWildValueWithinInterval(range, x); + } + } while (x > range.max); + } + return x; +} + +enum CompactSplineAddMethod { + kAddWithoutModification, // Add node straight-up. No changes. + kEnsureCubicWellBehaved, // Insert an intermediate node, if required, + // to ensure cubic splines have uniform curvature. +}; + +// Float representation of a point on the spline. +// +// This node represents the x, y, and derivative values of a data point. +// Users can pass in an array of such nodes to CompactSpline::InitFromNodes(). +// Useful when you want to specify a reasonably short spline in code. +struct UncompressedNode { + float x = 0.f; + float y = 0.f; + float derivative = 0.f; +}; + +class CompactSpline; +using CompactSplinePtr = + std::unique_ptr>; + +// Represents a smooth curve in a small amount of memory. +// +// This spline interpolates a series of (x, y, derivative) nodes to create a +// smooth curve. +// +// This class holds a series of such nodes, and aids with the construction of +// that series by inserting extra nodes when extra smoothness is required. +// +// The data in this class is compacted as quantized values. It's not intended +// to be read directly. You should use the BulkSplineEvaluator to update +// and read values from the splines in a performant manner. +class CompactSpline { + public: + // When a `CompactSpline` is created on the stack, it will have this many + // nodes. This amount is sufficient for the vast majority of cases where + // you are procedurally generating a spline. We used a fixed number instead + // of an `std::vector` to avoid dynamic memory allocation. + static constexpr CompactSplineIndex kDefaultMaxNodes = 7; + + CompactSpline() + : x_granularity_(0.0f), num_nodes_(0), max_nodes_(kDefaultMaxNodes) {} + CompactSpline(const Interval& y_range, const float x_granularity) + : max_nodes_(kDefaultMaxNodes) { + Init(y_range, x_granularity); + } + + CompactSpline& operator=(const CompactSpline& rhs) { + DCHECK(rhs.num_nodes_ <= max_nodes_); + y_range_ = rhs.y_range_; + x_granularity_ = rhs.x_granularity_; + num_nodes_ = rhs.num_nodes_; + memcpy(nodes_, rhs.nodes_, rhs.num_nodes_ * sizeof(nodes_[0])); + return *this; + } + + // The range of values for x and y must be specified at spline creation time + // and cannot be changed afterwards. Empties all nodes, if we have any. + // + // @param y_range The upper and lower bounds for y-values in the nodes. + // The more narrow this is, the better the precision of the + // fixed point numbers. Note that you should add 10% padding + // here, since AddNode may insert a smoothing node that is + // slightly beyond the source y range. + // @param x_granularity The minimum increment of x-values. If you're working + // with a spline changes at most 30 times per second, + // and your x is in units of 1/1000th of a second, then + // x_granularity = 33 is a good baseline. You'll + // probably want granularity around 1/50th of that + // baseline value, though, since AddNode may insert + // smoothing nodes at intermediate x's. + // In our example here, you could set + // x_granularity near 33 / 50. For ease of debugging, + // an x_granularity of 0.5 or 1 is probably best. + void Init(const Interval& y_range, const float x_granularity) { + num_nodes_ = 0; + y_range_ = y_range; + x_granularity_ = x_granularity; + } + + // Initialize the CompactSpline and add curve in the `nodes` array. + // + // @param nodes An array of uncompressed nodes. + // @param num_nodes Length of the `nodes` array. + void InitFromNodes(const UncompressedNode* nodes, size_t num_nodes); + + // Evaluate `spline` at uniform x intervals, where the distance between + // consecutive x's is spline.LengthX() / (max_nodes() - 1). Initialize + // this spline with the results. + // + // @param spline The source spline to evaluate at uniform x intervals. + void InitFromSpline(const CompactSpline& spline); + + // Add a node to the end of the spline. Depending on the method, an + // intermediate node may also be inserted. + // + // @param x Must be greater than the x-value of the last spline node. If not, + // this call is a nop. + // @param y Must be within the `y_range` specified in Init(). + // @param derivative No restrictions, but excessively large values may still + // result in overshoot, even with an intermediate node. + // @param method If kAddWithoutModification, adds the node and does nothing + // else. If kEnsureCubicWellBehaved, adds the node and + // (if required) inserts another node in the middle so that + // the individual cubics have uniform curvature. + // Uniform curvature means always curving upward or always + // curving downward. See docs/dual_cubics.pdf for details. + void AddNode(const float x, const float y, const float derivative, + const CompactSplineAddMethod method = kEnsureCubicWellBehaved); + + // Add values without converting them. Useful when initializing from + // precalculated data. + void AddNodeVerbatim(const CompactSplineXGrain x, const CompactSplineYRung y, + const CompactSplineAngle angle) { + AddNodeVerbatim(detail::CompactSplineNode(x, y, angle)); + } + + // Compress `nodes` and append them to the spline. + // + // @param nodes An array of uncompressed nodes. + // @param num_nodes Length of the `nodes` array. + void AddUncompressedNodes(const UncompressedNode* nodes, size_t num_nodes); + + // Indicate that we have stopped adding nodes and want to release the + // remaining memory. Useful for when we have one giant buffer from which + // we want to add many splines of (potentially unknown) various sizes. + // We can do something like, + // \code{.cpp} + // size_t CreateSplines(char* memory_buffer, size_t memory_buffer_size) { + // char* buf = memory_buffer; + // const char* end = memory_buffer + memory_buffer_size; + // + // while (MoreSplinesToCreate()) { + // // Allocate a spline that can hold as many nodes as buf can hold. + // CompactSpline* spline = + // CompactSpline::CreateInPlaceMaxNodes(buf, end - buf); + // + // while (MoreNodesToAdd()) { + // // Ensure we haven't reached the end of the buffer. + // if (spline->num_splines() == spline->max_splines()) break; + // + // // ... spline creation logic ... + // spline->AddNode(...); + // } + // + // // Shrink `spline` to be the size that it actually is. + // spline->Finalize(); + // + // // Advance pointer so next spline starts where this one ends. + // buf += spline->Size(); + // } + // + // // Return the total bytes consumed from `memory_buffer`. + // return end - buf; + // } + // \endcode + void Finalize() { max_nodes_ = num_nodes_; } + + // Remove all nodes from the spline. + void Clear() { num_nodes_ = 0; } + + // Returns the memory occupied by this spline. + size_t Size() const { return Size(max_nodes_); } + + // Use on an array of splines created by CreateArrayInPlace(). + // Returns the next spline in the array. + CompactSpline* Next() { return NextAtIdx(1); } + const CompactSpline* Next() const { return NextAtIdx(1); } + + // Use on an array of splines created by CreateArrayInPlace(). + // Returns the idx'th spline in the array. + CompactSpline* NextAtIdx(int idx) { + // Use union to avoid potential aliasing bugs. + union { + CompactSpline* spline; + uint8_t* ptr; + } p; + p.spline = this; + p.ptr += idx * Size(); + return p.spline; + } + const CompactSpline* NextAtIdx(int idx) const { + return const_cast(this)->NextAtIdx(idx); + } + + // Return index of the first node before `x`. + // If `x` is before the first node, return kBeforeSplineIndex. + // If `x` is past the last node, return kAfterSplineIndex. + // + // @param x x-value in the spline. Most often, the x-axis represents time. + // @param guess_index Best guess at what the index for `x` will be. + // Often the caller will be traversing from low to high x, + // so a good guess is the index after the current index. + // If you have no idea, set to 0. + CompactSplineIndex IndexForX(const float x, + const CompactSplineIndex guess_index) const; + + // If `repeat` is true, loop to x = 0 when `x` >= EndX(). + // If `repeat` is false, same as IndexForX(). + CompactSplineIndex IndexForXAllowingRepeat( + const float x, const CompactSplineIndex guess_index, const bool repeat, + float* final_x) const; + + // Returns closest index between 0 and NumNodes() - 1. + // Clamps `x` to a value in the range of index. + // `index` must be a valid value: i.e. kBeforeSplineIndex, kAfterSplineIndex, + // or between 0..NumNodes()-1. + CompactSplineIndex ClampIndex(const CompactSplineIndex index, float* x) const; + + // First and last x, y, and derivatives in the spline. + float StartX() const { return Front().X(x_granularity_); } + float StartY() const { return Front().Y(y_range_); } + float StartDerivative() const { return nodes_[0].Derivative(); } + + float EndX() const { return Back().X(x_granularity_); } + float EndY() const { return Back().Y(y_range_); } + float EndDerivative() const { return Back().Derivative(); } + float NodeX(const CompactSplineIndex index) const; + float NodeY(const CompactSplineIndex index) const; + float NodeDerivative(const CompactSplineIndex index) const { + DCHECK(index < num_nodes_); + return nodes_[index].Derivative(); + } + float LengthX() const { return EndX() - StartX(); } + Interval IntervalX() const { return Interval(StartX(), EndX()); } + const Interval& IntervalY() const { return y_range_; } + + // Calls CalculatedSlowly at `x`, with `kCurveValue` to evaluate the y value. + // If calling from inside a loop, replace the loop with one call to Ys(), + // which is significantly faster. + float YCalculatedSlowly(const float x) const; + + // Fast evaluation of a subset of the x-domain of the spline. + // Spline is evaluated from `start_x` and subsequent intervals of `delta_x`. + // Evaluated values are returned in `ys` and, if not nullptr, `derivatives`. + void Ys(const float start_x, const float delta_x, const size_t num_points, + float* ys, float* derivatives = nullptr) const; + + // The start and end x-values covered by the segment after `index`. + Interval IntervalX(const CompactSplineIndex index) const; + + // Initialization parameters for a cubic curve that starts at `index` and + // ends at `index` + 1. Or a constant curve if `index` is kBeforeSplineIndex + // or kAfterSplineIndex. + CubicInit CreateCubicInit(const CompactSplineIndex index) const; + + // Returns the index of the last node in the spline. + CompactSplineIndex LastNodeIndex() const { + DCHECK(num_nodes_ >= 1); + return num_nodes_ - 1; + } + + // Returns the start index of the last segment in the spline. + CompactSplineIndex LastSegmentIndex() const { + DCHECK(num_nodes_ >= 2); + return num_nodes_ - 2; + } + + // Returns the number of nodes in this spline. + CompactSplineIndex num_nodes() const { return num_nodes_; } + CompactSplineIndex max_nodes() const { return max_nodes_; } + + // Return const versions of internal values. For serialization. + const detail::CompactSplineNode* nodes() const { return nodes_; } + const Interval& y_range() const { return y_range_; } + float x_granularity() const { return x_granularity_; } + + // Allocate memory for a spline using global `new`. + // @param max_nodes The maximum number of nodes that this spline class + // can hold. Memory is allocated so that these nodes are + // held contiguously in memory with the rest of the + // class. + static CompactSplinePtr Create(CompactSplineIndex max_nodes) { + uint8_t* buffer = new uint8_t[Size(max_nodes)]; + CompactSpline* spline = CreateInPlace(max_nodes, buffer); + return CompactSplinePtr(spline, [](CompactSpline* ptr) { + delete[] reinterpret_cast(ptr); + }); + } + + // Create a CompactSpline in the memory provided by `buffer`. + // @param buffer chunk of memory of size CompactSpline::Size(max_nodes) + // + // Useful for creating small splines on the stack. + static CompactSpline* CreateInPlace(CompactSplineIndex max_nodes, + void* buffer) { + CompactSpline* spline = new (buffer) CompactSpline(); + spline->max_nodes_ = max_nodes; + return spline; + } + + // Allocate memory using global `new`, and initialize it with `nodes`. + // @param nodes An array holding the curve, in uncompressed floats. + // @param num_nodes The length of the `nodes` array, and max nodes in the + // returned spline. + static CompactSplinePtr CreateFromNodes(const UncompressedNode* nodes, + size_t num_nodes) { + DCHECK(num_nodes <= kMaxSplineIndex); + CompactSplinePtr spline = + Create(static_cast(num_nodes)); + spline->InitFromNodes(nodes, num_nodes); + return spline; + } + + // Create a CompactSpline from `nodes` in the memory provided by `buffer`. + // @param nodes array of node data, uncompressed as floats. + // @param num_nodes length of the `nodes` array. + // @param buffer chunk of memory of size CompactSpline::Size(num_nodes). + // + // The returned CompactSpline does not need to be destroyed, but once the + // backing memory `buffer` disappears (e.g. if `buffer` is an array on the + // stack), you must stop referencing the returned CompactSpline. + static CompactSpline* CreateFromNodesInPlace(const UncompressedNode* nodes, + size_t num_nodes, void* buffer) { + DCHECK(num_nodes <= kMaxSplineIndex); + CompactSpline* spline = + CreateInPlace(static_cast(num_nodes), buffer); + spline->InitFromNodes(nodes, num_nodes); + return spline; + } + + // Allocate memory using global `new`, and initialize it by evaluating + // `source_spline` at a uniform x-interval. + // @param source_spline Spline to evaluate. The curve in the returned spline + // matches `source_spline` with its x points spaced + // uniformly. + // @param num_nodes The number of uniform x-intervals in the returned spline. + // Also the max_nodes of the returned spline. + static CompactSplinePtr CreateFromSpline(const CompactSpline& source_spline, + size_t num_nodes) { + DCHECK(num_nodes <= kMaxSplineIndex); + CompactSplinePtr spline = + Create(static_cast(num_nodes)); + spline->InitFromSpline(source_spline); + return spline; + } + + // Create a CompactSpline from `source_spline` in the memory provided by + // `buffer`. + // @param source_spline Spline to evaluate. The curve in the returned spline + // matches `source_spline` with its x points spaced + // uniformly. + // @param num_nodes The number of uniform x-intervals in the returned spline. + // Also the max_nodes of the returned spline. + // @param buffer chunk of memory of size CompactSpline::Size(num_nodes). + static CompactSpline* CreateFromSplineInPlace( + const CompactSpline& source_spline, size_t num_nodes, void* buffer) { + DCHECK(num_nodes <= kMaxSplineIndex); + CompactSpline* spline = + CreateInPlace(static_cast(num_nodes), buffer); + spline->InitFromSpline(source_spline); + return spline; + } + + // Returns the size, in bytes, of a CompactSpline class with `max_nodes` + // nodes. + // + // This function is useful when you want to provide your own memory buffer + // for splines, and then pass that buffer into CreateInPlace(). Your memory + // buffer must be at least Size(). + static size_t Size(CompactSplineIndex max_nodes) { + // Total size of the class must be rounded up to the nearest alignment + // so that arrays of the class are properly aligned. + // Largest type in the class is a float. + const size_t kAlignMask = sizeof(float) - 1; + const size_t size = + kBaseSize + max_nodes * sizeof(detail::CompactSplineNode); + const size_t aligned = (size + kAlignMask) & ~kAlignMask; + return aligned; + } + + // Returns the size, in bytes, of an array of CompactSplines (as allocated + // with CreateArray(), say). + // + // This function is useful when allocating a buffer for splines on your own, + // from which you can then call CreateArrayInPlace(). + static size_t ArraySize(size_t num_splines, size_t num_nodes) { + return num_splines * kBaseSize + + num_nodes * sizeof(detail::CompactSplineNode); + } + + // Recommend a granularity given a maximal-x value. We want to have the + // most precise granularity when quantizing x's. + static float RecommendXGranularity(const float max_x); + + // Callback interface for BulkEvaluate(). AddPoint() will be called + // `num_points` times, once for every x = start_x + n * delta_x, + // where n = 0..num_points-1. + class BulkOutput { + public: + virtual ~BulkOutput() {} + virtual void AddPoint(int point_index, + const BulkSplineEvaluator& evaluator) = 0; + }; + + // Called by BulkYs with the an additional BulkOutputInterface + // parameter. BulkOutputInterface specifies the type of evaluations + // on the splines. + static void BulkEvaluate(const CompactSpline* const splines, + const size_t num_splines, const float start_x, + const float delta_x, const size_t num_points, + BulkOutput* out); + + // Fast evaluation of several splines. + // @param splines input splines of length `num_splines`. + // @param num_splines number of splines to evaluate. + // @param start_x starting point for every spline. + // @param delta_x increment for each output y. + // @param num_points the upper dimension of the `ys` and `derivatives` + // arrays. + // @param ys two dimensional output array, ys[num_points][num_splines]. + // ys[0] are `splines` evaluated at start_x. + // ys[num_points - 1] are `splines` evaluated at + // start_x + delta_x * num_points. + // @param derivatives two dimensional output array, with the same indexing + // as `ys`. + static void BulkYs(const CompactSpline* const splines, + const size_t num_splines, const float start_x, + const float delta_x, const size_t num_points, float* ys, + float* derivatives = nullptr); + + // Fast evaluation of several splines, with mathfu::VectorPacked interface. + // Useful for evaluate three splines which together form a mathfu::vec3, + // for instance. + template + static void BulkYs(const CompactSpline* const splines, const float start_x, + const float delta_x, const size_t num_ys, + Vector* ys) { + BulkYs(splines, kDimensions, start_x, delta_x, num_ys, + reinterpret_cast(ys)); + } + + private: + static const size_t kBaseSize; + + CompactSpline(const CompactSpline& rhs) : max_nodes_(rhs.max_nodes_) { + *this = rhs; + } + + // All other AddNode() functions end up calling this one. + void AddNodeVerbatim(const detail::CompactSplineNode& node) + ABSL_ATTRIBUTE_NO_SANITIZE_ADDRESS /* nodes_ has variable size */ { + DCHECK(num_nodes_ < max_nodes_); + nodes_[num_nodes_++] = node; + } + + // Return true iff `x` is between the nodes at `index` and `index` + 1. + bool IndexContainsX(const CompactSplineXGrain compact_x, + const CompactSplineIndex index) const; + + // Search the nodes to find the index of the first node before `x`. + CompactSplineIndex BinarySearchIndexForX( + const CompactSplineXGrain compact_x) const; + + // Return e.x - s.x, converted from quantized to external units. + float WidthX(const detail::CompactSplineNode& s, + const detail::CompactSplineNode& e) const { + return (e.x() - s.x()) * x_granularity_; + } + + // Create the initialization parameters for a cubic running from `s` to `e`. + CubicInit CreateCubicInit(const detail::CompactSplineNode& s, + const detail::CompactSplineNode& e) const; + + const detail::CompactSplineNode& Front() const { + DCHECK_GT(num_nodes_, 0); + return nodes_[0]; + } + + const detail::CompactSplineNode& Back() const + ABSL_ATTRIBUTE_NO_SANITIZE_ADDRESS /* nodes_ has variable size */ { + DCHECK_GT(num_nodes_, 0); + return nodes_[num_nodes_ - 1]; + } + + // Extreme values for y. See comments on Init() for details. + Interval y_range_; + + // Minimum increment for x. See comments on Init() for details. + float x_granularity_; + + // Length of the `nodes_` array. + CompactSplineIndex num_nodes_; + + // Maximum length of the `nodes_` array. This may be different from + // `kDefaultMaxNodes` if CreateInPlace() was called. + CompactSplineIndex max_nodes_; + + // Array of key points (x, y, derivative) that describe the curve. + // The curve is interpolated smoothly between these key points. + // Key points are stored in quantized form, and converted back to world + // co-ordinates by using `y_range_` and `x_granularity_`. + // Note: This array can be longer or shorter than kDefaultMaxNodes if + // the class was created with CreateInPlace(). The actual length of + // this array is stored in max_nodes_. + // Note: We only access using nodes_ to silence --config=asan + // --copt=-fsanitize=bounds, but just point to the array with the + // default size. + // + detail::CompactSplineNode* nodes_ = nodes_buffer_; + detail::CompactSplineNode nodes_buffer_[kDefaultMaxNodes]; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_ANIMATION_SPLINE_COMPACT_SPLINE_H_ diff --git a/redux/redux/engines/animation/spline/compact_spline_node.h b/redux/redux/engines/animation/spline/compact_spline_node.h new file mode 100644 index 0000000..090f605 --- /dev/null +++ b/redux/redux/engines/animation/spline/compact_spline_node.h @@ -0,0 +1,176 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_SPLINE_COMPACT_SPLINE_NODE_H_ +#define REDUX_ENGINES_ANIMATION_SPLINE_COMPACT_SPLINE_NODE_H_ + +#include + +#include "redux/modules/math/bounds.h" +#include "redux/modules/math/interpolation.h" +#include "redux/modules/math/math.h" + +namespace redux { + +// X-axis is quantized into units of `x_granularity`. X values are represented +// by multiples of `x_granularity`. One unit of CompactSplineXGrain represents +// one multiple of `x_granularity`. +typedef uint16_t CompactSplineXGrain; + +// Y values within `y_range` can be represented. We quantize the `y_range` +// into equally-sized rungs, and round to the closest rung. +typedef uint16_t CompactSplineYRung; + +// Angles strictly between -90 and +90 can be represented. We record the angle +// instead of the slope for more uniform distribution. +typedef int16_t CompactSplineAngle; + +namespace detail { + +// A spline is composed of a series of spline nodes (x, y, derivative) that are +// interpolated to form a smooth curve. +// +// This class represents a single spline node in 6-bytes. It quantizes the +// valid ranges of x, y, and slope into three 16-bit integers = 6 bytes. +// +// The x and y values are quantized to the valid range. The valid range is +// stored externally and passed in to each call. Please see comments on +// CompactSplineXGrain and CompactSplineYRung for more detail. +// +// The derivative is stored as the angle from the x-axis. This is so that we +// can equally represent derivatives <= 1 (that is, <= 45 degrees) and +// derivatives >= 1 (that is, >= 45 degrees) with a quantized number. +// +class CompactSplineNode { + public: + // Don't initialize the data to save cycles. + CompactSplineNode() {} + + // Construct with values that have already been converted to quantized values. + // This constructor is useful when deserializing pre-converted data. + CompactSplineNode(const CompactSplineXGrain x, const CompactSplineYRung y, + const CompactSplineAngle angle) + : x_(x), y_(y), angle_(angle) {} + + // Construct with real-world values. Must pass in the valid x and y ranges. + CompactSplineNode(const float x, const float y, const float derivative, + const float x_granularity, const Interval& y_range) { + SetX(x, x_granularity); + SetY(y, y_range); + SetDerivative(derivative); + } + + // Set with real-world values. The valid range of x and y must be passed in. + // These values are passed in so that we don't have to store multiple copies + // of them. Memory compactness is the purpose of this class. + void SetX(const float x, const float x_granularity) { + x_ = CompactX(x, x_granularity); + } + void SetY(const float y, const Interval& y_range) { + y_ = CompactY(y, y_range); + } + void SetDerivative(const float derivative) { + angle_ = CompactDerivative(derivative); + } + + // Get real world values. The valid range of x and y must be passed in. + // The valid range must be the same as when x and y values were set. + float X(const float x_granularity) const { + return static_cast(x_) * x_granularity; + } + float Y(const Interval& y_range) const { + return Lerp(y_range.min, y_range.max, YPercent()); + } + float Derivative() const { return tan(Angle()); } + + // Get the quantized values. Useful for serializing a series of nodes. + CompactSplineXGrain x() const { return x_; } + CompactSplineYRung y() const { return y_; } + CompactSplineAngle angle() const { return angle_; } + + // Equivalence can be tested reasonably because internal types are integral, + // not floating point. + bool operator==(const CompactSplineNode& rhs) const { + return x_ == rhs.x_ && y_ == rhs.y_ && angle_ == rhs.angle_; + } + bool operator!=(const CompactSplineNode& rhs) const { + return !operator==(rhs); + } + + // Convert from real-world to quantized values. + // Please see type definitions for documentation on the quantized format. + static int QuantizeX(const float x, const float x_granularity) { + return static_cast(x / x_granularity + 0.5f); + } + + static CompactSplineXGrain CompactX(const float x, + const float x_granularity) { + const int x_quantized = QuantizeX(x, x_granularity); + assert(0 <= x_quantized && x_quantized <= kMaxX); + return static_cast(x_quantized); + } + + static CompactSplineYRung CompactY(const float y, const Interval& y_range) { + assert(y_range.Contains(y)); + + // Prevent a divide-by-zero if the range has zero length. + const float length = y_range.Size(); + if (length == 0.0f) { + return 0; + } + + const float percent = (y - y_range.min) / length; + const float clamped_percent = Clamp(percent, 0.f, 1.f); + const CompactSplineYRung compact_y = + static_cast(kMaxY * clamped_percent); + return compact_y; + } + + static CompactSplineAngle CompactDerivative(const float derivative) { + const float angle_radians = atan(derivative); + const CompactSplineAngle angle = + static_cast(angle_radians / kAngleScale); + return angle; + } + + static CompactSplineXGrain MaxX() { return kMaxX; } + + private: + static const CompactSplineXGrain kMaxX; + static const CompactSplineYRung kMaxY; + static const CompactSplineAngle kMinAngle; + static const float kYScale; + static const float kAngleScale; + + float YPercent() const { return static_cast(y_) * kYScale; } + float Angle() const { return static_cast(angle_) * kAngleScale; } + + // Position along x-axis. Multiplied by x-granularity to get actual domain. + // 0 ==> start. kMaxX ==> end, we should never reach the end. If we do, + // the x_granularity should be increased. + CompactSplineXGrain x_; + + // Position within y_range. 0 ==> y_range.start. kMaxY ==> y_range.end. + CompactSplineYRung y_; + + // Angle from x-axis. tan(angle) = rise / run = derivative. + CompactSplineAngle angle_; +}; + +} // namespace detail +} // namespace redux + +#endif // REDUX_ENGINES_ANIMATION_SPLINE_COMPACT_SPLINE_NODE_H_ diff --git a/redux/redux/engines/animation/spline/compact_spline_tests.cc b/redux/redux/engines/animation/spline/compact_spline_tests.cc new file mode 100644 index 0000000..30cfae6 --- /dev/null +++ b/redux/redux/engines/animation/spline/compact_spline_tests.cc @@ -0,0 +1,311 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/engines/animation/spline/compact_spline.h" + +namespace redux { +namespace { + +using ::testing::Eq; +using ::testing::FloatNear; + +static const Interval kAngleInterval(-kPi, kPi); +static const float kEpsilonDerivative = 0.01f; +static const float kEpsilonX = 0.0001f; +static const float kEpsilonY = 0.0001f; + +// Use a ridiculous index that will never hit when doing a search. +// We use this to test the binary search algorithm, not the cache. +static const CompactSplineIndex kRidiculousSplineIndex = 10000; + +static const UncompressedNode kUncompressedSpline[] = { + {0.0f, 0.0f, 0.0f}, + {1.0f, 0.5f, 0.03f}, + {1.5f, 0.6f, 0.02f}, + {3.0f, 0.0f, -0.04f}, +}; + +static const UncompressedNode kUniformSpline[] = { + {0.0f, 0.0f, 0.0f}, {1.0f, 0.5f, 0.03f}, {2.0f, 0.6f, 0.02f}, + {3.0f, 0.0f, -0.04f}, {4.0f, 0.03f, -0.02f}, {5.0f, 0.9f, -0.1f}, +}; + +inline constexpr auto kNumUncompressedNodes = + ABSL_ARRAYSIZE(kUncompressedSpline); + +inline constexpr auto kNumUniformSplineNodes = ABSL_ARRAYSIZE(kUniformSpline); + +class CompactSplineTests : public ::testing::Test { + protected: + void SetUp() override { + spline_.Init(Interval(0.0f, 1.0f), 0.01f); + spline_.AddNode(0.0f, 0.1f, 0.0f, kAddWithoutModification); + spline_.AddNode(1.0f, 0.4f, 0.0f, kAddWithoutModification); + spline_.AddNode(4.0f, 0.2f, 0.0f, kAddWithoutModification); + spline_.AddNode(40.0f, 0.2f, 0.0f, kAddWithoutModification); + spline_.AddNode(100.0f, 1.0f, 0.0f, kAddWithoutModification); + } + CompactSpline spline_; +}; + +// Test in-place creation and destruction. +TEST_F(CompactSplineTests, InPlaceCreation) { + // Create a buffer with a constant fill. + static const uint8_t kTestFill = 0xAB; + uint8_t buffer[1024]; + memset(buffer, kTestFill, sizeof(buffer)); + + // Dynamically create a spline in the buffer. + static const int kTestMaxNodes = 3; + static const size_t kSplineSize = CompactSpline::Size(kTestMaxNodes); + assert(kSplineSize < sizeof(buffer)); // Strictly less to test for overflow. + CompactSpline* spline = CompactSpline::CreateInPlace(kTestMaxNodes, buffer); + + EXPECT_THAT(spline->num_nodes(), Eq(0)); + EXPECT_THAT(spline->max_nodes(), Eq(kTestMaxNodes)); + + // Create spline and ensure it now has the max size. + spline->Init(kAngleInterval, 1.0f); + for (int i = 0; i < kTestMaxNodes; ++i) { + spline->AddNode(static_cast(i), 0.0f, 0.0f, kAddWithoutModification); + } + + EXPECT_THAT(spline->num_nodes(), Eq(kTestMaxNodes)); + EXPECT_THAT(spline->max_nodes(), Eq(kTestMaxNodes)); + + // Ensure the spline hasn't overflowed its buffer. + for (size_t j = kSplineSize; j < sizeof(buffer); ++j) { + EXPECT_THAT(buffer[j], Eq(kTestFill)); + } + + // Test node destruction. + spline->~CompactSpline(); +} + +// Ensure the index lookup is accurate for x's before the range. +TEST_F(CompactSplineTests, IndexForXBefore) { + EXPECT_THAT(spline_.IndexForX(-1.0f, kRidiculousSplineIndex), + Eq(kBeforeSplineIndex)); +} + +// Ensure the index lookup is accurate for x's barely before the range. +TEST_F(CompactSplineTests, IndexForXJustBefore) { + EXPECT_THAT(spline_.IndexForX(-0.0001f, kRidiculousSplineIndex), Eq(0)); +} + +// Ensure the index lookup is accurate for x's barely before the range. +TEST_F(CompactSplineTests, IndexForXBiggerThanGranularityAtStart) { + EXPECT_THAT(spline_.IndexForX(-0.011f, kRidiculousSplineIndex), Eq(0)); +} + +// Ensure the index lookup is accurate for x's after the range. +TEST_F(CompactSplineTests, IndexForXAfter) { + EXPECT_THAT(spline_.IndexForX(101.0f, kRidiculousSplineIndex), + Eq(kAfterSplineIndex)); +} + +// Ensure the index lookup is accurate for x's barely after the range. +TEST_F(CompactSplineTests, IndexForXJustAfter) { + EXPECT_THAT(spline_.IndexForX(100.0001f, kRidiculousSplineIndex), + Eq(spline_.LastSegmentIndex())); +} + +// Ensure the index lookup is accurate for x right at start. +TEST_F(CompactSplineTests, IndexForXStart) { + EXPECT_THAT(spline_.IndexForX(0.0f, kRidiculousSplineIndex), Eq(0)); +} + +// Ensure the index lookup is accurate for x right at end. +TEST_F(CompactSplineTests, IndexForXEnd) { + EXPECT_THAT(spline_.IndexForX(100.0f, kRidiculousSplineIndex), + Eq(spline_.LastSegmentIndex())); +} + +// Ensure the index lookup is accurate for x just inside end. +TEST_F(CompactSplineTests, IndexForXAlmostEnd) { + EXPECT_THAT(spline_.IndexForX(99.9999f, kRidiculousSplineIndex), + Eq(spline_.LastSegmentIndex())); +} + +// Ensure the index lookup is accurate for x just inside end. +TEST_F(CompactSplineTests, IndexForXBiggerThanGranularityAtEnd) { + EXPECT_THAT(spline_.IndexForX(99.99f, kRidiculousSplineIndex), Eq(3)); +} + +// Ensure the index lookup is accurate for x in middle, right on the node. +TEST_F(CompactSplineTests, IndexForXMidOnNode) { + EXPECT_THAT(spline_.IndexForX(1.0f, kRidiculousSplineIndex), Eq(1)); +} + +// Ensure the index lookup is accurate for x in middle, in middle of segment. +TEST_F(CompactSplineTests, IndexForXMidAfterNode) { + EXPECT_THAT(spline_.IndexForX(1.1f, kRidiculousSplineIndex), Eq(1)); +} + +// Ensure the index lookup is accurate for x in middle, in middle of segment. +TEST_F(CompactSplineTests, IndexForXMidSecondLast) { + EXPECT_THAT(spline_.IndexForX(4.1f, kRidiculousSplineIndex), Eq(2)); +} + +// YCalculatedSlowly should return the key-point Y values at key-point X values. +TEST_F(CompactSplineTests, YSlowAtNodes) { + for (CompactSplineIndex i = 0; i < spline_.num_nodes(); ++i) { + const float y = spline_.YCalculatedSlowly(spline_.NodeX(i)); + EXPECT_THAT(spline_.NodeY(i), FloatNear(y, kEpsilonY)); + } +} + +// BulkYs should return the proper start and end values. +TEST_F(CompactSplineTests, BulkYsStartAndEnd) { + // Get bulk data at several delta_xs, but always starting at the start of the + // spline and ending at the end of the spline. + // Then compare returned `ys` with start end end values of spline. + static const int kMaxBulkYs = 5; + for (size_t num = 2; num < kMaxBulkYs; ++num) { + const float delta_x = spline_.EndX() / (num - 1); + + std::vector ys(num); + std::vector ds(num); + CompactSpline::BulkYs(&spline_, 1, 0.0f, delta_x, num, ys.data(), + ds.data()); + + EXPECT_THAT(ys.front(), FloatNear(spline_.StartY(), kEpsilonY)); + EXPECT_THAT(ys.back(), FloatNear(spline_.EndY(), kEpsilonY)); + EXPECT_THAT(ds.front(), + FloatNear(spline_.StartDerivative(), kEpsilonDerivative)); + EXPECT_THAT(ds.back(), + FloatNear(spline_.EndDerivative(), kEpsilonDerivative)); + } +} + +// BulkYs should return the proper start and end values. +TEST_F(CompactSplineTests, BulkYsVsSlowYs) { + // Get bulk data at several delta_xs, but always starting at 3 delta_x + // prior to start of the spline and ending at 3 delta_x after the end + // of the spline. + // Then compare returned `ys` with start end end values of spline. + static const int kMaxBulkYs = 21; + static const int kExtraSamples = 6; + for (size_t num = 2; num < kMaxBulkYs - kExtraSamples; ++num) { + // Collect `num_ys` evenly-spaced samples from spline_. + const float delta_x = spline_.EndX() / (num - 1); + const float start_x = 0.0f - (kExtraSamples / 2) * delta_x; + const size_t num_points = num + kExtraSamples; + + std::vector ys(num_points); + std::vector ds(num_points); + CompactSpline::BulkYs(&spline_, 1, start_x, delta_x, num_points, ys.data(), + ds.data()); + + // Compare bulk samples to slowly calcuated samples. + float x = start_x; + for (size_t j = 0; j < ys.size(); ++j) { + const float expect_y = spline_.YCalculatedSlowly(x); + EXPECT_THAT(ys[j], FloatNear(expect_y, kEpsilonY)); + x += delta_x; + } + } +} + +// BulkYs should return the proper start and end values. +TEST_F(CompactSplineTests, BulkYsVec3) { + static const int kDimensions = 3; + static const int kNumYs = 16; + + // Make three copies of the spline data. + CompactSpline splines[kDimensions]; + for (size_t d = 0; d < kDimensions; ++d) { + splines[d] = spline_; + } + + // Collect `num_ys` evenly-spaced samples from spline_. + using float3 = Vector; + float3 ys[kNumYs]; + memset(ys, 0xFF, sizeof(ys)); + const float delta_x = spline_.EndX() / (kNumYs - 1); + CompactSpline::BulkYs<3>(splines, 0.0f, delta_x, kNumYs, ys); + + // Ensure all the values are being calculated. + for (int j = 0; j < kNumYs; ++j) { + const vec3 y(ys[j]); + EXPECT_THAT(y.x, Eq(y.y)); + EXPECT_THAT(y.y, Eq(y.z)); + } +} + +static void CheckUncompressedSplineNodes(const CompactSpline& spline, + const UncompressedNode* nodes, + size_t num_nodes) { + for (size_t i = 0; i < num_nodes; ++i) { + const UncompressedNode& n = nodes[i]; + const float x = spline.NodeX(static_cast(i)); + const float y = spline.NodeY(static_cast(i)); + const float d = spline.NodeDerivative(static_cast(i)); + + EXPECT_THAT(n.x, FloatNear(x, kEpsilonX)); + EXPECT_THAT(n.y, FloatNear(y, kEpsilonY)); + EXPECT_THAT(n.derivative, FloatNear(d, kEpsilonDerivative)); + } +} + +// Uncompressed nodes should be evaluated pretty much unchanged. +TEST_F(CompactSplineTests, InitFromUncompressedNodes) { + CompactSplinePtr spline = CompactSpline::CreateFromNodes( + kUncompressedSpline, kNumUncompressedNodes); + CheckUncompressedSplineNodes(*spline, kUncompressedSpline, + kNumUncompressedNodes); +} + +// In-place construction from uncompressed nodes should be evaluated pretty +// much unchanged. +TEST_F(CompactSplineTests, InitFromUncompressedNodesInPlace) { + uint8_t spline_buf[1024]; + assert(sizeof(spline_buf) >= CompactSpline::Size(kNumUncompressedNodes)); + CompactSpline* spline = CompactSpline::CreateFromNodesInPlace( + kUncompressedSpline, kNumUncompressedNodes, spline_buf); + CheckUncompressedSplineNodes(*spline, kUncompressedSpline, + kNumUncompressedNodes); +} + +// CreateFromSpline of an already uniform spline should evaluate to the same +// spline. +TEST_F(CompactSplineTests, InitFromSpline) { + CompactSplinePtr uniform_spline = + CompactSpline::CreateFromNodes(kUniformSpline, kNumUniformSplineNodes); + CompactSplinePtr spline = + CompactSpline::CreateFromSpline(*uniform_spline, kNumUniformSplineNodes); + CheckUncompressedSplineNodes(*spline, kUniformSpline, kNumUniformSplineNodes); +} + +// CreateFromSpline of an already uniform spline should evaluate to the same +// spline. Test in-place construction. +TEST_F(CompactSplineTests, InitFromSplineInPlace) { + uint8_t uniform_spline_buf[1024]; + uint8_t spline_buf[1024]; + assert(sizeof(spline_buf) >= CompactSpline::Size(kNumUniformSplineNodes) && + sizeof(uniform_spline_buf) >= + CompactSpline::Size(kNumUniformSplineNodes)); + CompactSpline* uniform_spline = CompactSpline::CreateFromNodesInPlace( + kUniformSpline, kNumUniformSplineNodes, uniform_spline_buf); + CompactSpline* spline = CompactSpline::CreateFromSplineInPlace( + *uniform_spline, kNumUniformSplineNodes, spline_buf); + CheckUncompressedSplineNodes(*spline, kUniformSpline, kNumUniformSplineNodes); +} + +} // namespace +} // namespace redux diff --git a/redux/redux/engines/animation/spline/cubic_curve.cc b/redux/redux/engines/animation/spline/cubic_curve.cc new file mode 100644 index 0000000..f373e8a --- /dev/null +++ b/redux/redux/engines/animation/spline/cubic_curve.cc @@ -0,0 +1,96 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/animation/spline/cubic_curve.h" + +#include "redux/modules/math/float.h" + +namespace redux { + +void CubicCurve::Init(const CubicInit& init) { + // f(x) = dx^3 + cx^2 + bx + a + // + // Solve for a and b by substituting with x = 0. + // y0 = f(0) = a + // s0 = f'(0) = b + // + // Solve for c and d by substituting with x = init.width_x = w. Gives two + // linear equations with unknowns 'c' and 'd'. + // y1 = f(x1) = dw^3 + cw^2 + bw + a + // s1 = f'(x1) = 3dw^2 + 2cw + b + // ==> 3*y1 - w*s1 = (3dw^3 + 3cw^2 + 3bw + 3a) - (3dw^3 + 2cw^2 + bw) + // 3*y1 - w*s1 = cw^2 - 2bw + 3a + // cw^2 = 3*y1 - w*s1 + 2bw - 3a + // cw^2 = 3*y1 - w*s1 + 2*s0*w - 3*y0 + // cw^2 = 3(y1 - y0) - w*(s1 + 2*s0) + // c = (3/w^2)*(y1 - y0) - (1/w)*(s1 + 2*s0) + // ==> 2*y1 - w*s1 = (2dw^3 + 2cw^2 + 2bw + 2a) - (3dw^3 + 2cw^2 + bw) + // 2*y1 - w*s1 = -dw^3 + bw + 2a + // dw^3 = -2*y1 + w*s1 + bw + 2a + // dw^3 = -2*y1 + w*s1 + s0*w + 2*y0 + // dw^3 = 2(y0 - y1) + w*(s1 + s0) + // d = (2/w^3)*(y0 - y1) + (1/w^2)*(s1 + s0) + const float one_over_w = init.width_x > 0.f ? (1.0f / init.width_x) : 1.f; + const float one_over_w_sq = one_over_w * one_over_w; + const float one_over_w_cubed = one_over_w_sq * one_over_w; + c_[0] = init.start_y; + c_[1] = init.width_x > 0.f ? init.start_derivative : 0.f; + c_[2] = 3.0f * one_over_w_sq * (init.end_y - init.start_y) - + one_over_w * (init.end_derivative + 2.0f * init.start_derivative); + c_[3] = 2.0f * one_over_w_cubed * (init.start_y - init.end_y) + + one_over_w_sq * (init.end_derivative + init.start_derivative); +} + +void CubicCurve::ShiftLeft(const float x_shift) { + // Early out optimization. + if (x_shift == 0.0f) return; + + // s = x_shift + // f(x) = dx^3 + cx^2 + bx + a + // f(x + s) = d(x+s)^3 + c(x+s)^2 + b(x+s) + a + // = d(x^3 + 3sx^2 + 3s^2x + s^3) + c(x^2 + 2sx + s^2) + b(x + s) + a + // = dx^3 + (3sd + c)x^2 + (3ds^2 + 2c + b)x + (ds^3 + cs^2 + bs + a) + // = dx^3 + (f''(s)/2) x^2 + f'(s) x + f(s) + // + // Or, for an more general formulation, see: + // http://math.stackexchange.com/questions/694565/polynomial-shift + const float new_c = SecondDerivative(x_shift) * 0.5f; + const float new_b = Derivative(x_shift); + const float new_a = Evaluate(x_shift); + c_[0] = new_a; + c_[1] = new_b; + c_[2] = new_c; +} + +bool CubicCurve::UniformCurvature(const Interval& x_limits) const { + // Curvature is given by the second derivative. The second derivative is + // linear. So, the curvature is uniformly positive or negative iff + // Sign(f''(x_limits.start)) == Sign(f''(x_limits.end)) + const float epsilon = Epsilon(); + const float start_second_derivative = + ClampNearZero(SecondDerivative(x_limits.min), epsilon); + const float end_second_derivative = + ClampNearZero(SecondDerivative(x_limits.max), epsilon); + return start_second_derivative * end_second_derivative >= 0.0f; +} + +bool CubicCurve::operator==(const CubicCurve& rhs) const { + for (int i = 0; i < kNumCoeff; ++i) { + if (c_[i] != rhs.c_[i]) return false; + } + return true; +} +} // namespace redux diff --git a/redux/redux/engines/animation/spline/cubic_curve.h b/redux/redux/engines/animation/spline/cubic_curve.h new file mode 100644 index 0000000..78166b8 --- /dev/null +++ b/redux/redux/engines/animation/spline/cubic_curve.h @@ -0,0 +1,145 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_SPLINE_CUBIC_CURVE_H_ +#define REDUX_ENGINES_ANIMATION_SPLINE_CUBIC_CURVE_H_ + +#include + +#include "redux/modules/math/bounds.h" +#include "redux/modules/math/vector.h" + +namespace redux { + +// Initialization parameters to create a cubic curve with start and end y-values +// and derivatives. Start is x = 0. End is x = width_x. +struct CubicInit { + CubicInit(const float start_y, const float start_derivative, + const float end_y, const float end_derivative, const float width_x) + : start_y(start_y), + start_derivative(start_derivative), + end_y(end_y), + end_derivative(end_derivative), + width_x(width_x) {} + + // Short-form in comments: + float start_y; // y0 + float start_derivative; // s0 + float end_y; // y1 + float end_derivative; // s1 + float width_x; // w +}; + +// Represents a cubic polynomial of the form: +// c_[3] * x^3 + c_[2] * x^2 + c_[1] * x + c_[0] +class CubicCurve { + public: + // 2^22 = the max precision of significand. + static constexpr float kEpsilonPrecision = static_cast(1 << 22); + static constexpr float kEpsilonScale = 1.0f / kEpsilonPrecision; + static const int kNumCoeff = 4; + + CubicCurve() { memset(c_, 0, sizeof(c_)); } + CubicCurve(const float c3, const float c2, const float c1, const float c0) { + c_[3] = c3; + c_[2] = c2; + c_[1] = c1; + c_[0] = c0; + } + explicit CubicCurve(const CubicInit& init) { Init(init); } + void Init(const CubicInit& init); + + // Shift the curve along the x-axis: x_shift to the left. + // That is x_shift becomes the curve's x=0. + void ShiftLeft(const float x_shift); + + // Shift the curve along the x-axis: x_shift to the right. + void ShiftRight(const float x_shift) { ShiftLeft(-x_shift); } + + // Shift the curve along the y-axis by y_offset: y_offset up the y-axis. + void ShiftUp(float y_offset) { c_[0] += y_offset; } + + // Scale the curve along the y-axis by a factor of y_scale. + void ScaleUp(float y_scale) { + for (int i = 0; i < kNumCoeff; ++i) { + c_[i] *= y_scale; + } + } + + // Return the cubic function's value at `x`. + // f(x) = c3*x^3 + c2*x^2 + c1*x + c0 + float Evaluate(const float x) const { + // Take advantage of multiply-and-add instructions that are common on FPUs. + return ((c_[3] * x + c_[2]) * x + c_[1]) * x + c_[0]; + } + + // Return the cubic function's slope at `x`. + // f'(x) = 3*c3*x^2 + 2*c2*x + c1 + float Derivative(const float x) const { + return (3.0f * c_[3] * x + 2.0f * c_[2]) * x + c_[1]; + } + + // Return the cubic function's second derivative at `x`. + // f''(x) = 6*c3*x + 2*c2 + float SecondDerivative(const float x) const { + return 6.0f * c_[3] * x + 2.0f * c_[2]; + } + + // Return the cubic function's constant third derivative. + // Even though `x` is unused, we pass it in for consistency with other + // curve classes. + // f'''(x) = 6*c3 + float ThirdDerivative(const float x) const { + (void)x; + return 6.0f * c_[3]; + } + + // Returns true if always curving upward or always curving downward on the + // specified x_limits. + // That is, returns true if the second derivative has the same sign over + // all of x_limits. + bool UniformCurvature(const Interval& x_limits) const; + + // Return a value below which floating point precision is unreliable. + // If we're testing for zero, for instance, we should test against this + // Epsilon(). + float Epsilon() const { + using std::fabs; + using std::max; + const float max_c = + max(max(max(fabs(c_[3]), fabs(c_[2])), fabs(c_[1])), fabs(c_[0])); + return max_c * kEpsilonScale; + } + + // Returns the coefficient for x to the ith power. + float Coeff(int i) const { return c_[i]; } + + // Overrides the coefficent for x to the ith power. + void SetCoeff(int i, float coeff) { c_[i] = coeff; } + + // Returns the number of coefficients in this curve. + int NumCoeff() const { return kNumCoeff; } + + // Equality. Checks for exact match. Useful for testing. + bool operator==(const CubicCurve& rhs) const; + bool operator!=(const CubicCurve& rhs) const { return !operator==(rhs); } + + private: + float c_[kNumCoeff]; // c_[3] * x^3 + c_[2] * x^2 + c_[1] * x + c_[0] +}; +} // namespace redux + +#endif // REDUX_ENGINES_ANIMATION_SPLINE_CUBIC_CURVE_H_ diff --git a/redux/redux/engines/animation/spline/cubic_curve_tests.cc b/redux/redux/engines/animation/spline/cubic_curve_tests.cc new file mode 100644 index 0000000..c5bbb7e --- /dev/null +++ b/redux/redux/engines/animation/spline/cubic_curve_tests.cc @@ -0,0 +1,84 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/engines/animation/spline/cubic_curve.h" + +namespace redux { +namespace { + +using ::testing::FloatNear; +using ::testing::Lt; + +typedef void ShiftFn(float shift, CubicCurve* c); +static void ShiftLeft(float shift, CubicCurve* c) { c->ShiftLeft(shift); } +static void ShiftRight(float shift, CubicCurve* c) { c->ShiftRight(shift); } + +static void TestShift(const CubicInit& init, float shift, ShiftFn* fn, + float direction) { + const CubicCurve c(init); + CubicCurve shifted(c); + fn(shift, &shifted); + + const float epsilon = shifted.Epsilon(); + const float offset = direction * shift; + EXPECT_THAT(shifted.Evaluate(offset), FloatNear(c.Evaluate(0.0f), epsilon)); + EXPECT_THAT(shifted.Evaluate(0.0f), FloatNear(c.Evaluate(-offset), epsilon)); + EXPECT_THAT(shifted.Derivative(offset), + FloatNear(c.Derivative(0.0f), epsilon)); + EXPECT_THAT(shifted.Derivative(0.0f), + FloatNear(c.Derivative(-offset), epsilon)); + EXPECT_THAT(shifted.SecondDerivative(offset), + FloatNear(c.SecondDerivative(0.0f), epsilon)); + EXPECT_THAT(shifted.SecondDerivative(0.0f), + FloatNear(c.SecondDerivative(-offset), epsilon)); +} + +static void TestShiftLeft(const CubicInit& init, float shift) { + TestShift(init, shift, ShiftLeft, -1.0f); +} + +static void TestShiftRight(const CubicInit& init, float shift) { + TestShift(init, shift, ShiftRight, 1.0f); +} + +TEST(CubicCurveTests, CubicWithWidth) { + const CubicInit init(1.0f, -8.0f, 0.3f, -4.0f, 1.0f); + const CubicCurve c(init); + const float epsilon = c.Epsilon(); + EXPECT_THAT(std::fabs(c.Evaluate(init.width_x) - init.end_y), Lt(epsilon)); +} + +TEST(CubicCurveTests, CubicShiftLeft) { + const CubicInit init(1.0f, -8.0f, 0.3f, -4.0f, 1.0f); + TestShiftLeft(init, 0.0f); + TestShiftLeft(init, 1.0f); + TestShiftLeft(init, -0.1f); + TestShiftLeft(init, 0.00001f); + TestShiftLeft(init, 10.0f); +} + +TEST(CubicCurveTests, CubicShiftRight) { + const CubicInit init(1.0f, -8.0f, 0.3f, -4.0f, 1.0f); + TestShiftRight(init, 0.0f); + TestShiftRight(init, 1.0f); + TestShiftRight(init, -0.1f); + TestShiftRight(init, 0.00001f); + TestShiftRight(init, 10.0f); +} +} // namespace +} // namespace redux diff --git a/redux/redux/engines/animation/spline/dual_cubic.cc b/redux/redux/engines/animation/spline/dual_cubic.cc new file mode 100644 index 0000000..3125192 --- /dev/null +++ b/redux/redux/engines/animation/spline/dual_cubic.cc @@ -0,0 +1,343 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/animation/spline/dual_cubic.h" + +#include + +#include "redux/engines/animation/spline/quadratic_curve.h" +#include "redux/modules/math/bounds.h" +#include "redux/modules/math/interpolation.h" + +namespace redux { + +static const float kMaxSteepness = 4.0f; +static const float kMinMidPercent = 0.1f; +static const float kMaxMidPercent = 1.0f - kMinMidPercent; +static const float kMinYDiff = 0.1f; // Prevent division by zero. + +// One node of a spline that specifies both first and second derivatives. +// Only used internally. +struct SplineControlNode { + SplineControlNode(float x, float y, float derivative, + float second_derivative = 0.0f) + : x(x), + y(y), + derivative(derivative), + second_derivative(second_derivative) {} + + float x; + float y; + float derivative; + float second_derivative; +}; + +static const Interval kZeroToOne(0.0f, 1.0f); + +static QuadraticCurve CalculateValidMidRangeSplineForStart( + const SplineControlNode& start, const SplineControlNode& end) { + const float yd = end.y - start.y; + const float sd = end.derivative - start.derivative; + const float wd = end.second_derivative - start.second_derivative; + const float w0 = start.second_derivative; + const float w1 = end.second_derivative; + const float s0 = start.derivative; + const float s1 = end.derivative; + + // r_g(k) = wd * k^2 + (4*sd - w0 - 2w1)k + 6yd - 2s0 - 4s1 + w1 + const float c2 = wd; + const float c1 = 4 * sd - w0 - 2 * w1; + const float c0 = 6 * yd - 2 * s0 - 4 * s1 + w1; + return QuadraticCurve(c2, c1, c0); +} + +static QuadraticCurve CalculateValidMidRangeSplineForEnd( + const SplineControlNode& start, const SplineControlNode& end) { + const float yd = end.y - start.y; + const float sd = end.derivative - start.derivative; + const float wd = end.second_derivative - start.second_derivative; + const float w1 = end.second_derivative; + const float s1 = end.derivative; + + // r_g(k) = -wd * k^2 + (-4*sd + 3w1)k - 6yd + 6s1 - 2w1 + const float c2 = -wd; + const float c1 = -4 * sd + 3 * w1; + const float c0 = -6 * yd + 6 * s1 - 2 * w1; + return QuadraticCurve(c2, c1, c0); +} + +template +struct IntervalArray { + Interval arr[kMaxLen]; + size_t len = 0; +}; + +// Return the index of the longest range in `ranges`. +static size_t IndexOfLongest(const Interval* ranges, size_t len) { + float longest_length = -1.0f; + size_t longest_index = 0; + for (size_t i = 0; i < len; ++i) { + const float length = ranges[i].Size(); + if (length > longest_length) { + longest_length = length; + longest_index = i; + } + } + return longest_index; +} + +// Return the index of the shortest range in `ranges`. +static size_t IndexOfShortest(const Interval* ranges, size_t len) { + float shortest_length = std::numeric_limits::infinity(); + size_t shortest_index = 0; + for (size_t i = 0; i < len; ++i) { + const float length = ranges[i].Size(); + if (length < shortest_length) { + shortest_length = length; + shortest_index = i; + } + } + return shortest_index; +} + +// Intersect every element of 'a' with every element of 'b'. Append +// intersections to 'intersections'. Note that 'intersections' is not reset +// at the start of the call. +template +static void IntersectIntervals(const IntervalArray& a, + const IntervalArray& b, + IntervalArray* intersections, + IntervalArray* gaps) { + for (size_t i = 0; i < N; ++i) { + for (size_t j = 0; j < N; ++j) { + const Interval intersection(Max(a.arr[i].min, b.arr[i].min), + Min(a.arr[i].max, b.arr[i].max)); + if (intersection.Size() > 0.f) { + intersections->arr[intersections->len] = intersection; + ++intersections->len; + } else { + // Invert invalid intersections to get the gap between the ranges. + gaps->arr[gaps->len] = Interval(intersection.max, intersection.min); + ++gaps->len; + } + } + } +} + +static Interval CalculateValidMidRange(const SplineControlNode& start, + const SplineControlNode& end, + bool* is_valid = nullptr) { + // The sign of these quadratics determine where the mid-node is valid. + // One quadratic for the start cubic, and one for the end cubic. + const QuadraticCurve start_spline = + CalculateValidMidRangeSplineForStart(start, end); + const QuadraticCurve end_spline = + CalculateValidMidRangeSplineForEnd(start, end); + + // The mid node is valid when the quadratic sign matches the second + // derivative's sign. + IntervalArray<2> start_ranges; + IntervalArray<2> end_ranges; + start_ranges.len = start_spline.IntervalsMatchingSign( + kZeroToOne, start.second_derivative, start_ranges.arr); + end_ranges.len = end_spline.IntervalsMatchingSign( + kZeroToOne, end.second_derivative, end_ranges.arr); + + // Find the valid overlapping ranges, or the gaps inbetween the ranges. + IntervalArray<4> intersections; + IntervalArray<4> gaps; + IntersectIntervals(start_ranges, end_ranges, &intersections, &gaps); + + // The mid-node is valid only if there is an overlapping range. + if (is_valid != nullptr) { + *is_valid = intersections.len > 0; + } + + // Take the largest overlapping range. If none, find the smallest gap + // between the ranges. + return intersections.len > 0 + ? intersections + .arr[IndexOfLongest(intersections.arr, intersections.len)] + : gaps.len > 0 ? gaps.arr[IndexOfShortest(gaps.arr, gaps.len)] + : kZeroToOne; +} + +static float CalculateMidPercent(const SplineControlNode& start, + const SplineControlNode& end) { + // The mid value we called 'k' in the dual cubic documentation. + // It's between 0~1 and determines where the start and end cubics are joined + // along the x-axis. + const Interval valid_range = CalculateValidMidRange(start, end); + + // Return the part of the range closest to the half-way mark. This seems to + // generate the smoothest looking curves. + const float mid_unclamped = Clamp(0.5f, valid_range.min, valid_range.max); + + // Clamp away from 0 and 1. The math requires the mid node to be strictly + // between 0 and 1. If we get to close to 0 or 1, some divisions are going to + // explode and we'll lose numerical precision. + const float mid = Clamp(mid_unclamped, kMinMidPercent, kMaxMidPercent); + return mid; +} + +static SplineControlNode CalculateMidNode(const SplineControlNode& start, + const SplineControlNode& end, + const float k) { + // The mid node is at x = Lerp(start.x, end.x, k) + // It has y value of 'y' and slope of 's', defined as: + // + // s = 3(y1-y0) - 2Lerp(s1,s0,k) - 1/2(k^2*w0 - (1-k)^2*w1) + // y = Lerp(y0,y1,k) + k(1-k)(-2/3(s1-s0) + 1/6 Lerp(w1,w0,k)) + // + // where (x0, y0, s0, w0) is the start control node's x, y, derivative, and + // second derivative, and (x1, y1, s1, w1) similarly represents the end + // control node. + // + // See the document on "Dual Cubics" for a derivation of this solution. + const float y_diff = end.y - start.y; + const float s_diff = end.derivative - start.derivative; + const float derivative_k = Lerp(end.derivative, start.derivative, k); + const float y_k = Lerp(start.y, end.y, k); + const float second_k = + Lerp(end.second_derivative, start.second_derivative, k); + const float j = 1.0f - k; + const float second_k_squared = + k * k * start.second_derivative - j * j * end.second_derivative; + + const float s = 3.0f * y_diff - 2.0f * derivative_k - 0.5f * second_k_squared; + const float y = + y_k + k * j * (-2.0f / 3.0f * s_diff + 1.0f / 6.0f * second_k); + const float x = Lerp(start.x, end.x, k); + + return SplineControlNode(x, y, s, 0.0f); +} + +// See the Dual Cubics document for a derivation of this equation. +static float ExtremeSecondDerivativeForStart(const SplineControlNode& start, + const SplineControlNode& end, + const float mid_percent) { + const float y_diff = end.y - start.y; + const float s_diff = end.derivative - start.derivative; + const float k = mid_percent; + const float extreme_second = + s_diff + + (1.0f / k) * (3.0f * y_diff - 2.0f * start.derivative - end.derivative); + return extreme_second; +} + +// See the Dual Cubics document for a derivation of this equation. +static float ExtremeSecondDerivativeForEnd(const SplineControlNode& start, + const SplineControlNode& end, + const float mid_percent) { + const float y_diff = end.y - start.y; + const float s_diff = end.derivative - start.derivative; + const float k = mid_percent; + const float extreme_second = + (1.0f / (k - 1.0f)) * + (s_diff * k + 3.0f * y_diff - 3.0f * end.derivative); + return extreme_second; +} + +// Android does not support log2 in its math.h. +static inline float Log2(const float x) { +#if defined(__ANDROID__) || defined(_WIN32) + static const float kOneOverLog2 = 3.32192809489f; + return log(x) * kOneOverLog2; // log2(x) = log(x) / log(2) +#else + return std::log2(x); +#endif +} + +// Steepness is a notion of how much the derivative has to change from the +// start (x=0) to the end (x=1). For derivatives under 1, we don't really care, +// since cubics can change fast enough to cover those differences. Only extreme +// differences in derivatives cause trouble. +static float CalculateSteepness(const float derivative) { + const float abs_derivative = fabs(derivative); + return abs_derivative <= 1.0f ? 0.0f : Log2(abs_derivative); +} + +static float ApproximateMidPercent(const SplineControlNode& start, + const SplineControlNode& end, + float* start_percent, float* end_percent) { + // The greater the difference in steepness, the more skewed the mid percent. + const float abs_y_diff = fabs(end.y - start.y); + const float y_diff_recip = 1.0f / std::max(abs_y_diff, kMinYDiff); + const float start_steepness = + CalculateSteepness(start.derivative * y_diff_recip); + const float end_steepness = CalculateSteepness(end.derivative * y_diff_recip); + const float diff_steepness = fabs(start_steepness - end_steepness); + const float percent_extreme = std::min(diff_steepness / kMaxSteepness, 1.0f); + + // We skew the mid percent towards the steeper side. + // If equally steep, the mid percent is right in the middle: 0.5. + const bool start_is_steeper = start_steepness >= end_steepness; + const float extreme_percent = + start_is_steeper ? kMinMidPercent : kMaxMidPercent; + const float mid_percent = Lerp(0.5f, extreme_percent, percent_extreme); + + // Later, when we calculate the second derivatives, we want to skew to the + // extreme second derivatives in the same manner (steeper side gets skewed to + // more). + *start_percent = start_is_steeper ? percent_extreme : 1.0f - percent_extreme; + *end_percent = start_is_steeper ? 1.0f - percent_extreme : percent_extreme; + return mid_percent; +} + +void CalculateDualCubicMidNode(const CubicInit& init, float* x, float* y, + float* derivative) { + // The initial y and derivative values of our node are given by the + // 'init' control nodes. We scale to x from 0~1, because all of our math + // assumes x on this domain. + SplineControlNode start(0.0f, init.start_y, + init.start_derivative * init.width_x); + SplineControlNode end(1.0f, init.end_y, init.end_derivative * init.width_x); + + // Use a heuristic to guess a reasonably close place to split the cubic into + // two cubics. + float start_percent, end_percent; + const float approx_mid_percent = + ApproximateMidPercent(start, end, &start_percent, &end_percent); + + // Given the start and end conditions and the place to split the cubic, + // find the extreme second derivatives for start and end curves. See the + // Dual Cubic document for a derivation of the math here. + const float start_extreme_second = + ExtremeSecondDerivativeForStart(start, end, approx_mid_percent); + const float end_extreme_second = + ExtremeSecondDerivativeForEnd(start, end, approx_mid_percent); + + // Don't just use the extreme values since this will create a curve that's + // flat in the middle. Skew the second derivative to favor the steeper side. + start.second_derivative = Lerp(0.0f, start_extreme_second, start_percent); + end.second_derivative = Lerp(0.0f, end_extreme_second, end_percent); + + // Now that we have the full characterization of the start end end nodes + // (including second derivatives), calculate the actual ideal mid percent + // (i.e. the place to split the curve). + const float mid_percent = CalculateMidPercent(start, end); + + // With a full characterization of start and end nodes, and a place to + // split the curve, we can uniquely calculate the mid node. + const SplineControlNode mid = CalculateMidNode(start, end, mid_percent); + + // Re-scale the output values to the proper x-width. + *x = mid.x * init.width_x; + *y = mid.y; + *derivative = mid.derivative / init.width_x; +} + +} // namespace redux diff --git a/redux/redux/engines/animation/spline/dual_cubic.h b/redux/redux/engines/animation/spline/dual_cubic.h new file mode 100644 index 0000000..870962a --- /dev/null +++ b/redux/redux/engines/animation/spline/dual_cubic.h @@ -0,0 +1,34 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_SPLINE_DUAL_CUBIC_H_ +#define REDUX_ENGINES_ANIMATION_SPLINE_DUAL_CUBIC_H_ + +#include "redux/engines/animation/spline/cubic_curve.h" + +namespace redux { + +// Finds a point (x, y) and its derivative in between init.start_x and +// init.end_x such that the two cubic functions look smoother than the one cubic +// function created by init. +// +// Please see docs/dual_cubic.pdf for the math. +void CalculateDualCubicMidNode(const CubicInit& init, float* x, float* y, + float* derivative); + +} // namespace redux + +#endif // REDUX_ENGINES_ANIMATION_SPLINE_DUAL_CUBIC_H_ diff --git a/redux/redux/engines/animation/spline/quadratic_curve.cc b/redux/redux/engines/animation/spline/quadratic_curve.cc new file mode 100644 index 0000000..d689aaa --- /dev/null +++ b/redux/redux/engines/animation/spline/quadratic_curve.cc @@ -0,0 +1,268 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/animation/spline/quadratic_curve.h" + +#include "redux/modules/math/float.h" + +#ifdef _DEBUG +#define REDUX_CURVE_SANITY_CHECKS +#endif // _DEBUG + +namespace redux { + +static bool InsideInvertablePowerOf2Range(float x) { + return kMinInvertablePowerOf2 <= x && x <= kMaxInvertablePowerOf2; +} + +void QuadraticCurve::ShiftLeft(const float x_shift) { + // Early out optimization. + if (x_shift == 0.0f) return; + + // s = x_shift + // f(x) = cx^2 + bx + a + // f(x + s) = c(x+s)^2 + b(x+s) + a + // = c(x^2 + 2sx + s^2) + b(x + s) + a + // = cx^2 + (2c + b)x + (cs^2 + bs + a) + // = cx^2 + f'(s) x + f(s) + // + // Or, for an more general formulation, see: + // http://math.stackexchange.com/questions/694565/polynomial-shift + const float new_b = Derivative(x_shift); + const float new_a = Evaluate(x_shift); + c_[0] = new_a; + c_[1] = new_b; +} + +float QuadraticCurve::ReliableDiscriminant(const float epsilon) const { + // When discriminant is (relative to coefficients) close to zero, we treat + // it as zero. It's possible that the discriminant is barely below zero due + // to floating point error. + const float discriminant = Discriminant(); + return ClampNearZero(discriminant, epsilon); +} + +size_t QuadraticCurve::Roots(float roots[2]) const { + // Leave a little headroom for arithmetic. + static const int kMaxExponentForRootCoeff = kMaxInvertableExponent - 1; + + // Scale in the x-axis so that c2 is in the range of the larger of c1 or c0. + // This eliminates numerical precision problems in cases where, for example, + // we have a tiny second derivative and a large constant. + // + // The x-axis scale is applied non-uniformly across the polynomial. + // f(x_scale * x) = x_scale^2 * c2 * x^2 + x_scale * c1 * x + c0 + // We use this to bring x_scale^2 * c2 in approximately equal to + // either x_scale * c1 or c0. + const QuadraticCurve abs = AbsCoeff(); + const bool scale_with_linear = abs.c_[1] >= abs.c_[0]; + const float comparison_coeff = std::max(abs.c_[1], abs.c_[0]); + const float x_scale_quotient = abs.c_[2] / comparison_coeff; + const float x_scale_reciprocal_unclamped = + !InsideInvertablePowerOf2Range(x_scale_quotient) ? 1.0f + : scale_with_linear ? ReciprocalExponent(x_scale_quotient) + : SqrtReciprocalExponent(x_scale_quotient); + + // Since we normalize through powers of 2, the scale can be large without + // losing precision. But we still have to worry about scaling to infinity. + // Note that in x-scale, only the linear (c1) and quadratic (c2) coefficients + // are + // scaled, and the quadratic coefficient is scaled to match an existing + // coefficient, + // so we only need to check the linear coefficient. + const float x_scale_reciprocal_max = + MaxPowerOf2Scale(abs.c_[1], kMaxInvertableExponent); + const float x_scale_reciprocal = + std::min(x_scale_reciprocal_unclamped, x_scale_reciprocal_max); + + // Create the quatratic scaled in x. + const QuadraticCurve x_scaled = ScaleInXByReciprocal(x_scale_reciprocal); + const QuadraticCurve x_scaled_abs = x_scaled.AbsCoeff(); + +#ifdef REDUX_CURVE_SANITY_CHECKS + // Sanity checks to ensure our math is correct. + if (InsideInvertablePowerOf2Range(x_scale_quotient)) { + const float x_scaled_quotient = + x_scaled_abs.c_[2] / + (scale_with_linear ? x_scaled_abs.c_[1] : x_scaled_abs.c_[0]); + assert(0.5f <= x_scaled_quotient && x_scaled_quotient <= 2.0f); + (void)x_scaled_quotient; + } +#endif // REDUX_CURVE_SANITY_CHECKS + + // Calculate the y-axis scale so that c2 is near 1. + // We need this because the quadratic equation divides by c2. + // + // The y-scale is applied evenly to all coefficients, and doesn't affect the + // roots. + // y_scale * f(x) = y_scale * c2 * x^2 + y_scale * c1 * x + y_scale * c0 + // + // Check need to clamp our y-scale so that the linear (c1) and constant (c0) + // coefficients + // don't go to infinity or denormalize. Note that the y-scale is calculated to + // bring the + // quadratic (c2) coefficient near 1, so we don't have to check the quadratic + // coefficient. + const float y_scale_unclamped = ReciprocalExponent(Clamp( + x_scaled_abs.c_[2], kMinInvertablePowerOf2, kMaxInvertablePowerOf2)); + const float y_scale_max = + std::min(MaxPowerOf2Scale(x_scaled_abs.c_[0], kMaxExponentForRootCoeff), + MaxPowerOf2Scale(x_scaled_abs.c_[1], kMaxExponentForRootCoeff)); + const float y_scale = std::min(y_scale_max, y_scale_unclamped); + + // Create a scaled version of our quadratic. + const QuadraticCurve x_and_y_scaled = x_scaled.ScaleInY(y_scale); + +#ifdef REDUX_CURVE_SANITY_CHECKS + // Sanity check to ensure our math is correct. + const QuadraticCurve x_and_y_scaled_abs = x_and_y_scaled.AbsCoeff(); + assert((Range(0.5f, 2.0f).Contains(x_and_y_scaled_abs.c_[2]) || + !kInvertablePowerOf2Range.Contains(x_scaled_abs.c_[2]) || + y_scale != y_scale_unclamped) && + x_and_y_scaled_abs.c_[1] <= std::numeric_limits::max() && + x_and_y_scaled_abs.c_[0] <= std::numeric_limits::max()); + (void)x_and_y_scaled_abs; +#endif // REDUX_CURVE_SANITY_CHECKS + + // Calculate the roots and then undo the x_scaling. + const size_t num_roots = x_and_y_scaled.RootsWithoutNormalizing(roots); + for (size_t i = 0; i < num_roots; ++i) { + roots[i] *= x_scale_reciprocal; + } + return num_roots; +} + +// See the Quadratic Formula for details: +// http://en.wikipedia.org/wiki/Quadratic_formula +// Roots returned in sorted order, smallest to largest. +size_t QuadraticCurve::RootsWithoutNormalizing(float roots[2]) const { + // x^2 coefficient of zero means that curve is linear or constant. + const float epsilon = EpsilonOfCoefficients(); + if (std::fabs(c_[2]) < epsilon) { + // If constant, even if zero, return no roots. This is arbitrary. + if (std::fabs(c_[1]) < epsilon) return 0; + + // Linear 0 = c1x + c0 ==> x = -c0 / c1. + roots[0] = -c_[0] / c_[1]; + return 1; + } + + // A negative discriminant means no real roots. + const float discriminant = ReliableDiscriminant(epsilon); + if (discriminant < 0.0f) return 0; + + // A zero discriminant means there is only one root. + const float divisor = (1.0f / c_[2]) * 0.5f; + if (discriminant == 0.0f) { + roots[0] = -c_[1] * divisor; + return 1; + } + + // Positive discriminant means two roots. We use the quadratic formula. + const float sqrt_discriminant = std::sqrt(discriminant); + const float root_minus = (-c_[1] - sqrt_discriminant) * divisor; + const float root_plus = (-c_[1] + sqrt_discriminant) * divisor; + assert(root_minus != root_plus); + roots[0] = std::min(root_minus, root_plus); + roots[1] = std::max(root_minus, root_plus); + return 2; +} + +// Only keep entries in 'values' if they are in +// (range.start - epsition, range.end + epsilon). +// Any values that are kept are clamped to 'range'. +// +// This function is useful when floating point precision error might put a +// value slightly outside 'range' even though mathematically it should be +// inside 'range'. This often happens with values right on the border of the +// valid range. +static size_t ValuesInInterval(const Interval& range, float epsilon, + size_t num_values, float* values) { + size_t num_returned = 0; + for (size_t i = 0; i < num_values; ++i) { + const float value = values[i]; + const float clamped = Clamp(value, range.min, range.max); + const float dist = fabs(value - clamped); + + // If the distance from the range is small, keep the clamped value. + if (dist <= epsilon) { + values[num_returned++] = clamped; + } + } + return num_returned; +} + +size_t QuadraticCurve::RootsInInterval(const Interval& valid_x, + float roots[2]) const { + const size_t num_roots = Roots(roots); + + // We allow the roots to be slightly outside the bounds, since this may + // happen due to floating point error. + const float epsilon_x = valid_x.Size() * kEpsilonScale; + + // Loop through each root and only return it if it is within the range + // [start_x - epsilon_x, end_x + epsilon_x]. Clamp to [start_x, end_x]. + return ValuesInInterval(valid_x, epsilon_x, num_roots, roots); +} + +size_t QuadraticCurve::IntervalsMatchingSign(const Interval& x_limits, + float sign, + Interval matching[2]) const { + // Gather the roots of the validity spline. These are transitions between + // valid and invalid regions. + float roots[2]; + const size_t num_roots = RootsInInterval(x_limits, roots); + + // We want ranges where the spline's sign equals valid_sign's. + const bool valid_at_start = sign * Evaluate(x_limits.min) >= 0.0f; + const bool valid_at_end = sign * Evaluate(x_limits.max) >= 0.0f; + + // If no roots, the curve never crosses zero, so the start and end validity + // must be the same. + // If two roots, the curve crosses zero twice, so the start and end validity + // must be the same. + assert(num_roots == 1 || valid_at_start == valid_at_end); + + // Starts invalid, and never crosses zero so never becomes valid. + if (num_roots == 0 && !valid_at_start) return 0; + + // Starts valid, crosses zero to invalid, crosses zero again back to valid, + // then ends valid. + if (num_roots == 2 && valid_at_start) { + matching[0] = Interval(x_limits.min, roots[0]); + matching[1] = Interval(roots[1], x_limits.max); + return 2; + } + + // If num_roots == 0: must be valid at both start and end so entire range. + // If num_roots == 1: crosses zero once, or just touches zero. + // If num_roots == 2: must start and end invalid, so valid range is between + // roots. + const float start = valid_at_start ? x_limits.min : roots[0]; + const float end = valid_at_end ? x_limits.max + : num_roots == 2 ? roots[1] + : roots[0]; + matching[0] = Interval(start, end); + return 1; +} + +bool QuadraticCurve::operator==(const QuadraticCurve& rhs) const { + for (int i = 0; i < kNumCoeff; ++i) { + if (c_[i] != rhs.c_[i]) return false; + } + return true; +} +} // namespace redux diff --git a/redux/redux/engines/animation/spline/quadratic_curve.h b/redux/redux/engines/animation/spline/quadratic_curve.h new file mode 100644 index 0000000..d215a56 --- /dev/null +++ b/redux/redux/engines/animation/spline/quadratic_curve.h @@ -0,0 +1,193 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_ANIMATION_SPLINE_QUADRATIC_CURVE_H_ +#define REDUX_ENGINES_ANIMATION_SPLINE_QUADRATIC_CURVE_H_ + +#include + +#include "redux/modules/base/logging.h" +#include "redux/modules/math/bounds.h" +#include "redux/modules/math/vector.h" + +namespace redux { + +// Represents a quadratic polynomial in the form: +// c_[2] * x^2 + c_[1] * x + c_[0] +class QuadraticCurve { + public: + // 2^22 = the max precision of significand. + static constexpr float kEpsilonPrecision = static_cast(1 << 22); + static constexpr float kEpsilonScale = 1.0f / kEpsilonPrecision; + static const int kNumCoeff = 3; + + QuadraticCurve() { memset(c_, 0, sizeof(c_)); } + + QuadraticCurve(const float c2, const float c1, const float c0) { + c_[2] = c2; + c_[1] = c1; + c_[0] = c0; + } + + QuadraticCurve(const QuadraticCurve& q, const float y_scale) { + for (int i = 0; i < kNumCoeff; ++i) { + c_[i] = y_scale * q.c_[i]; + } + } + + // Shift the curve along the x-axis: x_shift to the left. + // That is x_shift becomes the curve's x=0. + void ShiftLeft(const float x_shift); + + // Shift the curve along the x-axis: x_shift to the right. + void ShiftRight(const float x_shift) { ShiftLeft(-x_shift); } + + // Shift the curve along the y-axis by y_offset: y_offset up the y-axis. + void ShiftUp(const float y_offset) { c_[0] += y_offset; } + + // Scale the curve along the y-axis by a factor of y_scale. + void ScaleUp(const float y_scale) { + for (int i = 0; i < kNumCoeff; ++i) { + c_[i] *= y_scale; + } + } + + // Return the quadratic function's value at `x`. + // f(x) = c2*x^2 + c1*x + c0 + float Evaluate(const float x) const { + return (c_[2] * x + c_[1]) * x + c_[0]; + } + + // Return the quadratic function's slope at `x`. + // f'(x) = 2*c2*x + c1 + float Derivative(const float x) const { return 2.0f * c_[2] * x + c_[1]; } + + // Return the quadratic function's constant second derivative. + // f''(x) = 2*c2 + float SecondDerivative() const { return 2.0f * c_[2]; } + + // Return the quadratic function's constant second derivative. + // Even though `x` is unused, we pass it in for consistency with other + // curve classes. + float SecondDerivative(const float /*x*/) const { return SecondDerivative(); } + + // Return the quadratic function's constant third derivative: 0. + // Even though `x` is unused, we pass it in for consistency with other + // curve classes. + // f'''(x) = 0 + float ThirdDerivative(const float x) const { + (void)x; + return 0.0f; + } + + // Returns a value below which floating point precision is unreliable. + // If we're testing for zero, for instance, we should test against this + // value. Pass in the x-range as well, in case that coefficient dominates + // the others. + float EpsilonInInterval(const float max_x) const { + return Epsilon(std::max(std::fabs(max_x), MaxCoeff())); + } + + // Returns a value below which floating point precision is unreliable. + // If we're testing for zero, for instance, we should test against this + // value. We don't consider the x-range here, so this value is useful only + // when dealing with the equation itself. + float EpsilonOfCoefficients() const { return Epsilon(MaxCoeff()); } + + // Given values in the range of `x`, returns a value below which should be + // considered zero. + float Epsilon(const float x) const { + return x * kEpsilonScale; + } + + // Returns the largest absolute value of the coefficients. Gives a sense + // of the scale of the quaternion. If all the coefficients are tiny or + // huge, we'll need to renormalize around 1, since we take reciprocals of + // the coefficients, and floating point precision is poor for floats. + float MaxCoeff() const { + using std::max; + const QuadraticCurve abs = AbsCoeff(); + return max(max(abs.c_[2], abs.c_[1]), abs.c_[0]); + } + + // Used for finding roots, and more. + // See http://en.wikipedia.org/wiki/Discriminant + float Discriminant() const { return c_[1] * c_[1] - 4.0f * c_[2] * c_[0]; } + + // When Discriminant() is close to zero, set to zero. + // Often floating point precision problems can make the discriminant + // very slightly non-zero, even though mathematically it should be zero. + float ReliableDiscriminant(const float epsilon) const; + + // Return the x at which the derivative is zero. + float CriticalPoint() const { + DCHECK(std::fabs(c_[2]) >= EpsilonOfCoefficients()); + + // 0 = f'(x) = 2*c2*x + c1 ==> x = -c1 / 2c2 + return -(c_[1] / c_[2]) * 0.5f; + } + + // Returns the coefficient for x-to-the-ith -power. + float Coeff(int i) const { return c_[i]; } + + // Returns the number of coefficients in this curve. + int NumCoeff() const { return kNumCoeff; } + + // Returns the curve f(x / x_scale), where f is the current quadratic. + // This stretches the curve along the x-axis by x_scale. + QuadraticCurve ScaleInX(const float x_scale) const { + return ScaleInXByReciprocal(1.0f / x_scale); + } + + // Returns the curve f(x * x_scale_reciprocal), where f is the current + // quadratic. + // This stretches the curve along the x-axis by 1/x_scale_reciprocal + QuadraticCurve ScaleInXByReciprocal(const float x_scale_reciprocal) const { + return QuadraticCurve(c_[2] * x_scale_reciprocal * x_scale_reciprocal, + c_[1] * x_scale_reciprocal, c_[0]); + } + + // Returns the curve y_scale * f(x). + QuadraticCurve ScaleInY(const float y_scale) const { + return QuadraticCurve(*this, y_scale); + } + + // Returns the same curve but with all coefficients in absolute value. + // Useful for numerical precision work. + QuadraticCurve AbsCoeff() const { + using std::fabs; + return QuadraticCurve(fabs(c_[2]), fabs(c_[1]), fabs(c_[0])); + } + + // Equality. Checks for exact match. Useful for testing. + bool operator==(const QuadraticCurve& rhs) const; + bool operator!=(const QuadraticCurve& rhs) const { return !operator==(rhs); } + + size_t IntervalsMatchingSign(const Interval& x_limits, const float sign, + Interval matching[2]) const; + + size_t Roots(float roots[2]) const; + + private: + // Feel free to expose these versions in the external API if they're useful. + size_t RootsWithoutNormalizing(float roots[2]) const; + size_t RootsInInterval(const Interval& x_limits, float roots[2]) const; + + float c_[kNumCoeff]; // c_[2] * x^2 + c_[1] * x + c_[0] +}; +} // namespace redux + +#endif // REDUX_ENGINES_ANIMATION_SPLINE_QUADRATIC_CURVE_H_ diff --git a/redux/redux/engines/animation/spline/quadratic_curve_tests.cc b/redux/redux/engines/animation/spline/quadratic_curve_tests.cc new file mode 100644 index 0000000..a2d58f2 --- /dev/null +++ b/redux/redux/engines/animation/spline/quadratic_curve_tests.cc @@ -0,0 +1,188 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/engines/animation/spline/quadratic_curve.h" + +namespace redux { +namespace { + +using ::testing::Eq; +using ::testing::FloatNear; +using ::testing::Lt; + +static const float kMaxFloat = std::numeric_limits::max(); +static const float kMinFloat = std::numeric_limits::min(); + +static void CheckQuadraticRoots(const QuadraticCurve& s, + size_t num_expected_roots) { + // Ensure we have the correct number of roots. + float roots[2]; + const size_t num_roots = s.Roots(roots); + EXPECT_THAT(num_roots, Eq(num_expected_roots)); + + // Ensure roots are in ascending order. + EXPECT_TRUE(num_roots < 2 || roots[0] < roots[1]); + + // Ensure roots all evaluate to zero. + for (size_t i = 0; i < num_roots; ++i) { + float should_be_zero = s.Evaluate(roots[i]); + float epsilon = s.EpsilonInInterval(roots[i]); + + // If the quadratic has crazy coefficients and evalues to infinity, + // scale it down in y. The roots should be the same. + // Note that we don't want to do this in general because it's a less + // accurate test. + if (std::fabs(should_be_zero) > std::numeric_limits::max()) { + const QuadraticCurve s_shrunk(s, 1.0f / s.MaxCoeff()); + should_be_zero = s_shrunk.Evaluate(roots[i]); + epsilon = s_shrunk.EpsilonInInterval(roots[i]); + } + + EXPECT_THAT(should_be_zero, FloatNear(0.0f, epsilon)); + } +} + +static void CheckCriticalPoint(const QuadraticCurve& s) { + // Derivative should be zero at critical point. + const float critical_point_x = s.CriticalPoint(); + const float critical_point_derivative = s.Derivative(critical_point_x); + const float epsilon = s.EpsilonInInterval(critical_point_x); + EXPECT_THAT(std::fabs(critical_point_derivative), Lt(epsilon)); +} + +// Test for some coefficients as max float, one solution. +TEST(QuadraticCurveTests, QuadraticRoot_OneMaxOneSolution) { + CheckQuadraticRoots(QuadraticCurve(kMaxFloat, 0.0f, 0.0f), 1); +} + +// Test for all coefficients as max float, two solutions. +TEST(QuadraticCurveTests, QuadraticRoot_AllMaxTwoSolutions) { + CheckQuadraticRoots(QuadraticCurve(kMaxFloat, kMaxFloat, -kMaxFloat), 2); +} + +// Test for some coefficients as max float, two solutions. +TEST(QuadraticCurveTests, QuadraticRoot_TwoMaxTwoSolutions) { + CheckQuadraticRoots(QuadraticCurve(kMaxFloat, kMaxFloat, -1.0f), 2); +} + +// Test for all coefficients as max float, no solutions. +TEST(QuadraticCurveTests, QuadraticRoot_AllMaxNoSolutions) { + CheckQuadraticRoots(QuadraticCurve(kMaxFloat, kMaxFloat, kMaxFloat), 0); +} + +// Test for all coefficients as min float, no solutions. +TEST(QuadraticCurveTests, QuadraticRoot_AllMinNoSolutions) { + CheckQuadraticRoots(QuadraticCurve(kMinFloat, kMinFloat, kMinFloat), 0); +} + +// Test for all coefficients as min float, two solutions. +TEST(QuadraticCurveTests, QuadraticRoot_AllMinTwoSolutions) { + CheckQuadraticRoots(QuadraticCurve(-kMinFloat, kMinFloat, kMinFloat), 2); +} + +// Test for one coefficient as min float, one solution. +TEST(QuadraticCurveTests, QuadraticRoot_OneMinOneSolution) { + CheckQuadraticRoots(QuadraticCurve(-kMinFloat, 0.0f, 0.0f), 1); +} + +// Test for one coefficient as min float, one solution. +TEST(QuadraticCurveTests, QuadraticRoot_MinMaxMixOneSolution) { + CheckQuadraticRoots(QuadraticCurve(-kMinFloat, kMaxFloat, 1.0f), 1); +} + +// Test for one coefficient as min float, two solutions. +TEST(QuadraticCurveTests, QuadraticRoot_MaxMinMixOneSolution) { + CheckQuadraticRoots(QuadraticCurve(kMaxFloat, -kMinFloat, 0.0f), 1); +} + +// Test for zeros everywhere but the constant component. +TEST(QuadraticCurveTests, QuadraticRoot_Constant) { + CheckQuadraticRoots(QuadraticCurve(0.0f, 0.0f, 1.0f), 0); +} + +// Test for zeros everywhere but the linear component. +TEST(QuadraticCurveTests, QuadraticRoot_Linear) { + CheckQuadraticRoots(QuadraticCurve(0.0f, 1.0f, 0.0f), 1); +} + +// Test for zeros everywhere but the quadratic component. +TEST(QuadraticCurveTests, QuadraticRoot_Quadratic) { + CheckQuadraticRoots(QuadraticCurve(1.0f, 0.0f, 0.0f), 1); +} + +TEST(QuadraticCurveTests, QuadraticRoot_UpwardsAbove) { + // Curves upwards, critical point above zero ==> no real roots. + CheckQuadraticRoots(QuadraticCurve(60.0f, -32.0f, 6.0f), 0); +} + +TEST(QuadraticCurveTests, QuadraticRoot_UpwardsAt) { + // Curves upwards, critical point at zero ==> one real root. + CheckQuadraticRoots(QuadraticCurve(60.0f, -32.0f, 4.26666689f), 1); +} + +TEST(QuadraticCurveTests, QuadraticRoot_UpwardsBelow) { + // Curves upwards, critical point below zero ==> two real roots. + CheckQuadraticRoots(QuadraticCurve(60.0f, -32.0f, 4.0f), 2); +} + +TEST(QuadraticCurveTests, QuadraticRoot_DownwardsAbove) { + // Curves downwards, critical point above zero ==> two real roots. + CheckQuadraticRoots(QuadraticCurve(-0.00006f, -0.000028f, 0.0001f), 2); +} + +TEST(QuadraticCurveTests, QuadraticRoot_DownwardsAt) { + // Curves downwards, critical point at zero ==> one real root at critical + // point. + CheckQuadraticRoots(QuadraticCurve(-0.00006f, -0.000028f, -0.00000326666691f), + 1); +} + +TEST(QuadraticCurveTests, QuadraticRoot_DownwardsBelow) { + // Curves downwards, critical point below zero ==> no real roots. + CheckQuadraticRoots(QuadraticCurve(-0.00006f, -0.000028f, -0.000006f), 0); +} + +TEST(QuadraticCurveTests, QuadraticRoot_AllTinyCoefficients) { + // Curves upwards, critical point below zero ==> two real roots. + CheckQuadraticRoots( + QuadraticCurve(0.000000006f, -0.0000000032f, 0.0000000004f), 2); +} + +TEST(QuadraticCurveTests, QuadraticRoot_SmallSquareCoefficient) { + CheckQuadraticRoots(QuadraticCurve(-0.00000003f, 0.0f, 0.0008f), 2); +} + +TEST(QuadraticCurveTests, QuadraticRoot_TinySquareCoefficient) { + CheckQuadraticRoots(QuadraticCurve(0.000000001f, 1.0f, -0.00000001f), 2); +} + +TEST(QuadraticCurveTests, QuadraticCriticalPoint) { + // Curves upwards, critical point above zero ==> no real roots. + CheckCriticalPoint(QuadraticCurve(60.0f, -32.0f, 6.0f)); +} + +TEST(QuadraticCurveTests, QuadraticRangesMatchingSign_SmallValues) { + const Interval limits(0.0f, 1.0f); + const QuadraticCurve small(1.006107e-11f, -3.01832101e-11f, 1.006107e-11f); + + Interval matching[2]; + const int num_matches = small.IntervalsMatchingSign(limits, 1.0f, matching); + EXPECT_THAT(num_matches, Eq(1)); +} +} // namespace +} // namespace redux diff --git a/redux/redux/engines/audio/BUILD b/redux/redux/engines/audio/BUILD new file mode 100644 index 0000000..954b045 --- /dev/null +++ b/redux/redux/engines/audio/BUILD @@ -0,0 +1,28 @@ +# Audio Engine. + +licenses(["notice"]) + +package( + default_visibility = ["//redux:visibility"], +) + +cc_library( + name = "audio", + srcs = [ + ], + hdrs = [ + "audio_asset.h", + "audio_engine.h", + "sound.h", + "sound_room.h", + ], + deps = [ + "//redux/modules/audio:enums", + "//redux/modules/base:registry", + "//redux/modules/base:resource_manager", + "//redux/modules/base:typeid", + "//redux/modules/math:quaternion", + "//redux/modules/math:transform", + "//redux/modules/math:vector", + ], +) diff --git a/redux/redux/engines/audio/audio_asset.h b/redux/redux/engines/audio/audio_asset.h new file mode 100644 index 0000000..be9657f --- /dev/null +++ b/redux/redux/engines/audio/audio_asset.h @@ -0,0 +1,52 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_AUDIO_AUDIO_ASSET_H_ +#define REDUX_ENGINES_AUDIO_AUDIO_ASSET_H_ + +#include + +namespace redux { + +// An AudioAsset that provides data for sound playback. +// +// The AudioEngine uses AudioAssets to play Sounds. The same AudioAsset can be +// played by the AudioEngine multiple times resulting in multiple Sound +// instances. +class AudioAsset { + public: + using Id = uint64_t; + static constexpr Id kInvalidAsset = 0; + + ~AudioAsset() = default; + + AudioAsset(const AudioAsset&) = delete; + AudioAsset& operator=(const AudioAsset&) = delete; + + Id GetId() const { return id_; } + + protected: + explicit AudioAsset(Id id) : id_(id) {} + + private: + const Id id_; +}; + +using AudioAssetPtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_AUDIO_AUDIO_ASSET_H_ diff --git a/redux/redux/engines/audio/audio_engine.h b/redux/redux/engines/audio/audio_engine.h new file mode 100644 index 0000000..74318d8 --- /dev/null +++ b/redux/redux/engines/audio/audio_engine.h @@ -0,0 +1,105 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_AUDIO_AUDIO_ENGINE_H_ +#define REDUX_ENGINES_AUDIO_AUDIO_ENGINE_H_ + +#include "redux/engines/audio/audio_asset.h" +#include "redux/engines/audio/sound.h" +#include "redux/engines/audio/sound_room.h" +#include "redux/modules/audio/enums.h" +#include "redux/modules/base/registry.h" +#include "redux/modules/base/resource_manager.h" +#include "redux/modules/base/typeid.h" +#include "redux/modules/math/quaternion.h" +#include "redux/modules/math/vector.h" + +namespace redux { + +// Responsible for managing and playing sounds. +class AudioEngine { + public: + static void Create(Registry* registry); + virtual ~AudioEngine() = default; + + AudioEngine(const AudioEngine&) = delete; + AudioEngine& operator=(const AudioEngine&) = delete; + + // Sets the global volume of the AudioEngine itself, ranging from from 0 + // (mute) to 1 (max). + void SetGlobalVolume(float volume); + + // Sets the position and rotation of the listener's "head". + void SetListenerTransform(const vec3& position, const quat& rotation); + + enum StreamingPolicy { + // Only opens the audio asset for streaming. Once playback is finished, the + // handle to the asset is closed. + kStreamAndClose, + // Loads the entire audio data into memory when it is opened. The sound can + // then be played as often as desired, until it is explicitly closed. + kPreloadIntoMemory, + // Opens the asset for streaming, but as it is played, will also store the + // audio data into memory. + kStreamIntoMemory, + }; + + // Loads an AudioAsset from the given `uri`. Future requests for this + // AudioAsset will be cached as long as one instance is alive somewhere, or + // users can request the asset by calling GetAudioAsset with the Hash of the + // `uri`. + AudioAssetPtr LoadAudioAsset(std::string_view uri, StreamingPolicy policy); + + // Returns the AudioAsset associated with the given `key` that has previously + // been loaded and is still alive. + AudioAssetPtr GetAudioAsset(HashValue key); + + // Unloads the AudioAsset associated with the given `key`. + void UnloadAudioAsset(HashValue key); + + // Information about how to play a sound. + struct SoundPlaybackParams { + SoundType type = SoundType::Stereo; + float volume = 1.0f; + bool looping = false; + }; + + // Starts playing a sound using the given `asset` and play `params`. + SoundPtr PlaySound(AudioAssetPtr asset, const SoundPlaybackParams& params); + + // Similar to PlaySound, but the Sound starts in a paused state. + SoundPtr PrepareSound(AudioAssetPtr asset, const SoundPlaybackParams& params); + + // Updates all the active sounds. + void Update(); + + // Updates Audio rendering to simulate the listener being in an enclosed + // space (i.e. a room). + void EnableRoom(const SoundRoom& room, const vec3& position, + const quat& rotation); + void DisableRoom(); + + protected: + explicit AudioEngine(Registry* registry) : registry_(registry) {} + + Registry* registry_ = nullptr; +}; + +} // namespace redux + +REDUX_SETUP_TYPEID(redux::AudioEngine); + +#endif // REDUX_ENGINES_AUDIO_AUDIO_ENGINE_H_ diff --git a/redux/redux/engines/audio/resonance/BUILD b/redux/redux/engines/audio/resonance/BUILD new file mode 100644 index 0000000..0de291d --- /dev/null +++ b/redux/redux/engines/audio/resonance/BUILD @@ -0,0 +1,94 @@ +licenses(["notice"]) + +package( + default_visibility = ["//redux:visibility"], +) + +cc_library( + name = "resonance", + srcs = [ + "audio_asset_manager.cc", + "audio_asset_stream.cc", + "audio_planar_data.cc", + "audio_stream_manager.cc", + "audio_stream_renderer.cc", + "resonance_audio_asset.cc", + "resonance_audio_engine.cc", + "resonance_sound.cc", + "resonance_utils.cc", + ], + hdrs = [ + "audio_asset_manager.h", + "audio_asset_stream.h", + "audio_planar_data.h", + "audio_source_stream.h", + "audio_stream_manager.h", + "audio_stream_renderer.h", + "resonance_audio_asset.h", + "resonance_audio_engine.h", + "resonance_sound.h", + "resonance_utils.h", + ], + deps = [ + "@absl//absl/container:flat_hash_map", + "@absl//absl/functional:bind_front", + "@absl//absl/types:span", + "//redux/engines/audio", + "//redux/engines/platform:device_manager", + "//redux/engines/platform:device_profiles", + "//redux/modules/audio:audio_reader", + "//redux/modules/audio:enums", + "//redux/modules/audio:opus_reader", + "//redux/modules/audio:vorbis_reader", + "//redux/modules/audio:wav_reader", + "//redux/modules/base:asset_loader", + "//redux/modules/base:choreographer", + "//redux/modules/base:data_reader", + "//redux/modules/base:logging", + "//redux/modules/base:registry", + "//redux/modules/base:static_registry", + "@resonance//:resonance", + ], +) + +cc_test( + name = "audio_asset_manager_tests", + srcs = ["audio_asset_manager_tests.cc"], + data = [ + "//redux/modules/audio:test_data/speech.wav", + ], + deps = [ + ":resonance", + "@gtest//:gtest_main", + "//redux/modules/base:asset_loader", + "//redux/modules/testing", + ], +) + +cc_test( + name = "audio_asset_stream_tests", + srcs = ["audio_asset_stream_tests.cc"], + data = [ + "//redux/modules/audio:test_data/speech.wav", + ], + deps = [ + ":resonance", + "@gtest//:gtest_main", + "//redux/modules/audio:wav_reader", + "//redux/modules/testing", + ], +) + +cc_test( + name = "audio_planar_data_tests", + srcs = ["audio_planar_data_tests.cc"], + data = [ + "//redux/modules/audio:test_data/speech.wav", + ], + deps = [ + ":resonance", + "@gtest//:gtest_main", + "//redux/modules/audio:wav_reader", + "//redux/modules/testing", + ], +) diff --git a/redux/redux/engines/audio/resonance/audio_asset_manager.cc b/redux/redux/engines/audio/resonance/audio_asset_manager.cc new file mode 100644 index 0000000..7d11564 --- /dev/null +++ b/redux/redux/engines/audio/resonance/audio_asset_manager.cc @@ -0,0 +1,181 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/audio/resonance/audio_asset_manager.h" + +#include +#include + +#include "redux/engines/audio/resonance/resonance_audio_asset.h" +#include "redux/modules/audio/audio_reader.h" +#include "redux/modules/audio/opus_reader.h" +#include "redux/modules/audio/vorbis_reader.h" +#include "redux/modules/audio/wav_reader.h" +#include "redux/modules/base/asset_loader.h" +#include "redux/modules/base/logging.h" +#include "resonance_audio/dsp/resampler.h" + +namespace redux { + +AudioAssetManager::AudioAssetManager(Registry* registry, SpeakerProfile profile) + : registry_(registry), speaker_profile_(profile) { +} + +AudioAssetPtr AudioAssetManager::CreateAudioAsset( + std::string_view uri, AudioEngine::StreamingPolicy policy) { + const HashValue key = Hash(uri); + AudioAssetPtr asset = audio_assets_.Find(key); + if (asset == nullptr) { + AudioAsset::Id id = LoadAudioAsset(uri, policy); + asset = GetAssetById(id); + audio_assets_.Register(key, asset); + } + return asset; +} + +AudioAsset::Id AudioAssetManager::LoadAudioAsset( + std::string_view uri, AudioEngine::StreamingPolicy policy) { + auto asset_loader = registry_->Get(); + + const AudioAsset::Id id = asset_id_counter_++; + auto asset = std::make_shared( + id, policy == AudioEngine::kStreamIntoMemory); + asset_map_.insert(std::make_pair(id, asset)); + asset_uris_.insert(std::make_pair(id, std::string(uri))); + + auto on_open = [=](AssetLoader::StatusOrReader& reader) { + if (reader.ok()) { + std::unique_ptr audio_reader = CreateReader(reader.value()); + if (policy == AudioEngine::kPreloadIntoMemory) { + auto data = + AudioPlanarData::FromReader(*audio_reader, speaker_profile_); + asset->SetAudioPlanarData(std::move(data)); + } else { + asset->SetAudioReader(std::move(audio_reader)); + } + } else { + LOG(ERROR) << reader.status().message(); + asset->SetAudioReader(nullptr); + } + }; + + asset_loader->OpenAsync(uri, on_open, nullptr); + return id; +} + +AudioAssetPtr AudioAssetManager::FindAudioAsset(HashValue key) { + return audio_assets_.Find(key); +} + +std::shared_ptr AudioAssetManager::GetAssetById( + AudioAsset::Id asset_id) { + const auto iter = asset_map_.find(asset_id); + if (iter == asset_map_.end()) { + return nullptr; + } + + std::shared_ptr asset = iter->second; + DCHECK(asset); + return asset; +} + +void AudioAssetManager::UnloadAudioAsset(HashValue key) { + auto asset = FindAudioAsset(key); + asset_map_.erase(asset->GetId()); + asset_uris_.erase(asset->GetId()); + audio_assets_.Erase(key); +} + +std::shared_ptr AudioAssetManager::GetAssetForPlayback( + AudioAsset::Id asset_id) { + std::shared_ptr asset = GetAssetById(asset_id); + if (asset == nullptr) { + LOG(ERROR) << "Attempt to stream invalid asset"; + return nullptr; + } + + asset->WaitForInitialization(); + + if (asset->IsActivelyStreaming()) { + // Another AudioAssetStream is streaming this asset, so we need to create + // a temporary, stream-only asset from the same URI. If this is an issue + // consider loading the asset into memory so its always available. + asset = CreateTemporaryAudioAsset(asset_uris_[asset_id]); + } + + if (!asset->IsValid()) { + LOG(ERROR) << "Attempt to stream uninitialized asset."; + return nullptr; + } + + // This is a streaming-only asset (i.e. it's StreamingPolicy was + // kStreamAndClose), so let's forget about it here. + if (asset->GetPlanarData() == nullptr && + asset->ShouldStreamIntoMemory() == false) { + // UnloadAudioAsset doesn't actually destroy the AudioAsset; it just stops + // owning the shared_ptr. + UnloadAudioAsset(Hash(asset_uris_[asset_id])); + } + + return asset; +} + +std::shared_ptr +AudioAssetManager::CreateTemporaryAudioAsset(std::string_view uri) { + auto asset_loader = registry_->Get(); + auto reader = asset_loader->OpenNow(uri); + if (!reader.ok()) { + return nullptr; + } + + const AudioAsset::Id new_asset_id = asset_id_counter_++; + auto asset = std::make_shared(new_asset_id); + asset->SetAudioReader(CreateReader(reader.value())); + CHECK(asset->IsValid()) << "Failed to acquire reader for AudioAssetStream."; + return asset; +} + +std::unique_ptr AudioAssetManager::CreateReader( + DataReader& src) { + std::unique_ptr reader; + if (WavReader::CheckHeader(src)) { + reader = std::make_unique(std::move(src)); + } else if (OpusReader::CheckHeader(src)) { + reader = std::make_unique(std::move(src)); + } else if (VorbisReader::CheckHeader(src)) { + reader = std::make_unique(std::move(src)); + } + if (reader == nullptr) { + LOG(ERROR) << "Unable to determine audio format."; + return nullptr; + } + + const int source_sample_rate_hz = reader->GetSampleRateHz(); + const int target_sample_rate_hz = speaker_profile_.sample_rate_hz; + + if (source_sample_rate_hz!= target_sample_rate_hz) { + const bool supported = vraudio::Resampler::AreSampleRatesSupported( + source_sample_rate_hz, target_sample_rate_hz); + if (!supported) { + LOG(ERROR) << "Unsupported sampling rate: " << source_sample_rate_hz + << ". System sample rate: " << target_sample_rate_hz; + return nullptr; + } + } + return reader; +} + +} // namespace redux diff --git a/redux/redux/engines/audio/resonance/audio_asset_manager.h b/redux/redux/engines/audio/resonance/audio_asset_manager.h new file mode 100644 index 0000000..fd459ef --- /dev/null +++ b/redux/redux/engines/audio/resonance/audio_asset_manager.h @@ -0,0 +1,89 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_ASSET_MANAGER_H_ +#define REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_ASSET_MANAGER_H_ + +#include + +#include "absl/container/flat_hash_map.h" +#include "redux/engines/audio/audio_asset.h" +#include "redux/engines/audio/audio_engine.h" +#include "redux/engines/audio/resonance/resonance_audio_asset.h" +#include "redux/engines/platform/device_profiles.h" +#include "redux/modules/audio/audio_reader.h" +#include "redux/modules/base/data_reader.h" +#include "redux/modules/base/registry.h" + +namespace redux { + +// Provides methods for preloading and managing samples/sounds in memory and +// creating asset handles. +class AudioAssetManager { + public: + AudioAssetManager(Registry* registry, SpeakerProfile profile); + + // Initializes an audio asset by scheduling the decoding/initialization task + // for asynchronous loading, then returns immediately. + // + // The hash of the URI can be used as a key for looking up the asset. + AudioAssetPtr CreateAudioAsset(std::string_view uri, + AudioEngine::StreamingPolicy policy); + + // Removes a previously initialized audio asset from the asset cache. The key + // is the hash of the URI of the loaded asset. + void UnloadAudioAsset(HashValue key); + + // Returns a loaded/cached audio asset, using the hash of the URI as the key. + AudioAssetPtr FindAudioAsset(HashValue key); + + // Returns an 'AudioAsset' that can be used for playback by the engine. Note: + // this is not necessarily the same instance as that returned by LookupAsset. + // Specifically, if an asset is already in use for streaming, this will return + // a new instance (since a single asset cannot support multiple streaming + // playbacks). + std::shared_ptr GetAssetForPlayback( + AudioAsset::Id asset_id); + + private: + AudioAsset::Id LoadAudioAsset(std::string_view uri, + AudioEngine::StreamingPolicy policy); + + std::shared_ptr GetAssetById(AudioAsset::Id asset_id); + + std::shared_ptr CreateTemporaryAudioAsset( + std::string_view uri); + + std::unique_ptr CreateReader(DataReader& src); + + Registry* registry_ = nullptr; + + SpeakerProfile speaker_profile_; + AudioAsset::Id asset_id_counter_ = 0; + + // Maps |AudioAsset::Id| to |AudioAsset| instances. Shared pointers are used + // to safely remove assets that are currently played back or actively decoded. + ResourceManager audio_assets_; + absl::flat_hash_map asset_uris_; + absl::flat_hash_map> + asset_map_; +}; + +} // namespace redux + +REDUX_SETUP_TYPEID(redux::AudioAssetManager); + +#endif // REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_ASSET_MANAGER_H_ diff --git a/redux/redux/engines/audio/resonance/audio_asset_manager_tests.cc b/redux/redux/engines/audio/resonance/audio_asset_manager_tests.cc new file mode 100644 index 0000000..8d62ff3 --- /dev/null +++ b/redux/redux/engines/audio/resonance/audio_asset_manager_tests.cc @@ -0,0 +1,112 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/modules/base/asset_loader.h" +#include "redux/modules/testing/testing.h" +#include "redux/engines/audio/resonance/audio_asset_manager.h" + +namespace redux { +namespace { + +using ::testing::Eq; +using ::testing::Ne; + +static constexpr auto kDataPath = + "redux/modules/audio/test_data"; + +class AudioAssetManagerTest : public testing::Test { + protected: + void SetUp() override { + SpeakerProfile profile; + profile.num_channels = 2; + profile.frames_per_buffer = 256; + profile.sample_rate_hz = 48000; + + asset_loader_ = registry_.Create(®istry_); + asset_loader_->StopAsyncOperations(); + + audio_asset_manager_ = + registry_.Create(®istry_, profile); + } + + Registry registry_; + AssetLoader* asset_loader_; + AudioAssetManager* audio_asset_manager_; +}; + + +TEST_F(AudioAssetManagerTest, CreateAudioAssetForStreaming) { + const std::string uri = ResolveTestFilePath(kDataPath, "speech.wav"); + const auto asset = + audio_asset_manager_->CreateAudioAsset(uri, AudioEngine::kStreamAndClose); + EXPECT_TRUE(std::static_pointer_cast(asset)->IsValid()); +} + +TEST_F(AudioAssetManagerTest, CreateAudioAssetPreload) { + const std::string uri = ResolveTestFilePath(kDataPath, "speech.wav"); + const auto asset = audio_asset_manager_->CreateAudioAsset( + uri, AudioEngine::kPreloadIntoMemory); + EXPECT_TRUE(std::static_pointer_cast(asset)->IsValid()); +} + +TEST_F(AudioAssetManagerTest, CreateAudioAssetForStreamingAndLoading) { + const std::string uri = ResolveTestFilePath(kDataPath, "speech.wav"); + const auto asset = audio_asset_manager_->CreateAudioAsset( + uri, AudioEngine::kStreamIntoMemory); + EXPECT_TRUE(std::static_pointer_cast(asset)->IsValid()); +} + +TEST_F(AudioAssetManagerTest, InvalidAsset) { + const std::string uri = ResolveTestFilePath(kDataPath, "bad.wav"); + const auto asset = audio_asset_manager_->CreateAudioAsset( + uri, AudioEngine::kStreamIntoMemory); + EXPECT_FALSE(std::static_pointer_cast(asset)->IsValid()); +} + +TEST_F(AudioAssetManagerTest, UnloadAudioAsset) { + const std::string uri = ResolveTestFilePath(kDataPath, "speech.wav"); + auto asset = + audio_asset_manager_->CreateAudioAsset(uri, AudioEngine::kStreamAndClose); + EXPECT_THAT(asset, Ne(nullptr)); + + audio_asset_manager_->UnloadAudioAsset(Hash(uri)); + asset = audio_asset_manager_->FindAudioAsset(Hash(uri)); + EXPECT_THAT(asset, Eq(nullptr)); +} + +TEST_F(AudioAssetManagerTest, GetAssetForPlayback) { + const std::string uri = ResolveTestFilePath(kDataPath, "speech.wav"); + const auto asset = + audio_asset_manager_->CreateAudioAsset(uri, AudioEngine::kStreamAndClose); + const auto playback_asset = + audio_asset_manager_->GetAssetForPlayback(asset->GetId()); + EXPECT_THAT(playback_asset, Ne(nullptr)); +} + +TEST_F(AudioAssetManagerTest, GetAssetForPlaybackLocked) { + const std::string uri = ResolveTestFilePath(kDataPath, "speech.wav"); + const auto asset = audio_asset_manager_->CreateAudioAsset( + uri, AudioEngine::kStreamIntoMemory); + const auto asset1 = audio_asset_manager_->GetAssetForPlayback(asset->GetId()); + asset1->AcquireReader(); + const auto asset2 = audio_asset_manager_->GetAssetForPlayback(asset->GetId()); + EXPECT_THAT(asset1->GetId(), Ne(asset2->GetId())); +} + +} // namespace +} // namespace redux diff --git a/redux/redux/engines/audio/resonance/audio_asset_stream.cc b/redux/redux/engines/audio/resonance/audio_asset_stream.cc new file mode 100644 index 0000000..5d0eb1e --- /dev/null +++ b/redux/redux/engines/audio/resonance/audio_asset_stream.cc @@ -0,0 +1,465 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/audio/resonance/audio_asset_stream.h" + +#include + +#include "absl/functional/bind_front.h" +#include "resonance_audio/dsp/resampler.h" + +namespace redux { + +// 64 buffers correspond to 0.5 and 1.3 seconds of audio for 512 and 1024 +// frames per buffer and a sample rate of 48000 Hz respectively. +static const size_t kNumberFifoBuffers = 64; + +// Number of available buffers in which indicates a "low water mark" signal for +// the buffer stock to be refilled. +static const size_t kRefillOnBufferCount = kNumberFifoBuffers; + +AudioAssetStream::AudioAssetStream( + const std::shared_ptr& asset, + const SpeakerProfile& speaker_profile) + : asset_(asset), + looping_enabled_(false), + end_of_stream_(false), + playhead_position_(0), + stream_started_(false), + pending_seek_(false), + pending_seek_position_(0), + crossfade_size_frames_(0), + channel_count_(0), + frames_per_buffer_(speaker_profile.frames_per_buffer), + system_sample_rate_hz_(speaker_profile.sample_rate_hz), + num_stock_fifo_buffers_to_fill_(kNumberFifoBuffers), + active_stock_buffer_(nullptr) { + // Take over the asset's reader and the decoding process. + reader_ = asset_->AcquireReader(); + + if (reader_) { + channel_count_ = reader_->GetNumChannels(); + if (asset_->ShouldStreamIntoMemory()) { + planar_data_ = std::make_unique(channel_count_); + } + + // Make sure we're starting from the beginning. + reader_->Reset(); + } else { + CHECK(asset_->GetPlanarData() != nullptr); + channel_count_ = asset_->GetPlanarData()->GetNumChannels(); + } +} + +AudioAssetStream::~AudioAssetStream() { + if (planar_data_ != nullptr) { + if (end_of_stream_.load()) { + asset_->SetAudioPlanarData(std::move(planar_data_)); + reader_.reset(); + } + } + + // Return the reader to the asset. + asset_->SetAudioReader(std::move(reader_)); +} + +bool AudioAssetStream::Initialize() { + output_buffer_ = std::make_unique(channel_count_, + frames_per_buffer_); + + if (asset_->GetStatus() == ResonanceAudioAsset::Status::kLoadedInMemory) { + // Seamless loop size is set to 200ms (empirically obtained). + static const float kDefaultSeamlessCrossfadeLengthSec = 0.2f; + SetLoopCrossfadeDuration(kDefaultSeamlessCrossfadeLengthSec); + } else { + if (reader_ == nullptr) { + LOG(ERROR) << "AudioAssetStream failed to create decoder."; + return false; + } + total_frames_ = static_cast(reader_->GetTotalFrameCount()); + stock_fifo_ = + std::make_unique(kNumberFifoBuffers, *output_buffer_); + ConfigureResampler(); + } + + return true; +} + +void AudioAssetStream::ConfigureResampler() { + if (system_sample_rate_hz_ == reader_->GetSampleRateHz()) { + return; + } + + resampler_ = std::make_unique(); + resampler_->SetRateAndNumChannels(reader_->GetSampleRateHz(), + system_sample_rate_hz_, channel_count_); + const size_t max_resampled_frames_per_buffer = + resampler_->GetMaxOutputLength(frames_per_buffer_); + resampler_input_ = std::make_unique(channel_count_, + frames_per_buffer_); + resampler_output_ = std::make_unique( + channel_count_, max_resampled_frames_per_buffer); + partitioner_ = std::make_unique( + channel_count_, frames_per_buffer_, + absl::bind_front(&AudioAssetStream::PartitionedBufferCallback, this)); + + // Adjust the |stock_fifo_| fill to point so that buffer repartitioning + // will not cause |stock_fifo_| overflow. + num_stock_fifo_buffers_to_fill_ -= static_cast( + std::ceil(static_cast(max_resampled_frames_per_buffer) / + static_cast(frames_per_buffer_))); +} + +uint64_t AudioAssetStream::GetNumChannels() const { return channel_count_; } + +int AudioAssetStream::GetSampleRateHz() const { return system_sample_rate_hz_; } + +void AudioAssetStream::EnableLooping(bool looping_enabled) { + looping_enabled_ = looping_enabled; +} + +bool AudioAssetStream::GetNextAudioBuffer( + const vraudio::AudioBuffer** output_buffer) { + DCHECK(output_buffer); + if (asset_->GetStatus() == ResonanceAudioAsset::Status::kLoadedInMemory) { + return GetNextAudioBufferFromMemory(output_buffer); + } else { + return GetNextAudioBufferFromPrestockQueue(output_buffer); + } +} + +bool AudioAssetStream::GetNextAudioBufferFromMemory( + const vraudio::AudioBuffer** output_buffer) { + if (end_of_stream_.load()) { + return false; + } + + const AudioPlanarData* planar_data = asset_->GetPlanarData(); + const size_t total_frames = planar_data->GetFrameCount(); + DCHECK_LT(0, total_frames); + + size_t num_frames_read = 0; + while (num_frames_read < frames_per_buffer_) { + const size_t read_offset = playhead_position_.load(); + CHECK_LE(read_offset, total_frames); + + // Calculate the number of frames we can safely copy from the stream to the + // output buffer. + const size_t available_frames = total_frames - read_offset; + const size_t num_frames_left_in_buffer = + frames_per_buffer_ - num_frames_read; + const size_t num_frames_to_copy = + std::min(available_frames, num_frames_left_in_buffer); + + // Copy some frames. + for (size_t i = 0; i < output_buffer_->num_channels(); ++i) { + absl::Span src = planar_data->GetChannelData(i); + auto& dst = (*output_buffer_)[i]; + std::copy_n(&src[read_offset], num_frames_to_copy, &dst[num_frames_read]); + } + + // Do wrap-around seamless looping if looping is enabled. + if (looping_enabled_.load() && + available_frames - num_frames_to_copy < crossfade_size_frames_.load()) { + SeamlessLoopCrossfade(num_frames_to_copy, output_buffer_.get()); + } + + // Advance the playhead_position by the number of frames we've copied. + // Keep the position within the bounds, so a position of 0 is the end of + // the stream. + playhead_position_ = playhead_position_.load() + num_frames_to_copy; + playhead_position_ = playhead_position_.load() % total_frames; + if (playhead_position_.load() == 0 && !looping_enabled_.load()) { + end_of_stream_ = true; + } + + num_frames_read += num_frames_to_copy; + if (num_frames_read < frames_per_buffer_) { + if (looping_enabled_ && num_frames_to_copy > 0) { + // If looping is enabled, begin a crossfade near the end of the asset + // sample buffer. + playhead_position_ = crossfade_size_frames_.load(); + } else { + // If looping is disabled, silence the remaining buffer and mark the + // stream as invalid. + for (vraudio::AudioBuffer::Channel& channel : *output_buffer_) { + std::fill(channel.begin() + num_frames_read, channel.end(), 0.0f); + } + num_frames_read = frames_per_buffer_; + end_of_stream_ = true; + } + } + } + + *output_buffer = output_buffer_.get(); + stream_started_ = true; + return true; +} + +bool AudioAssetStream::GetNextAudioBufferFromPrestockQueue( + const vraudio::AudioBuffer** output_buffer) { + // Push the last buffer back into the fifo queue. + if (active_stock_buffer_ != nullptr) { + stock_fifo_->ReleaseOutputObject(active_stock_buffer_); + active_stock_buffer_ = nullptr; + } + + if (stock_fifo_->Size() == 0) { + // There are no more buffers because we are done. + if (end_of_stream_.load()) { + return false; + } + + if (stream_started_.load()) { + *output_buffer = nullptr; + LOG(WARNING) << "Stream underflow at play position " + << playhead_position_.load() << " of " + << total_frames_; + } + // Return true in the case of an under-run so that the stream isn't + // stopped. Under-runs may occur because of performance issues. + return true; + } + + // Get the next (filled) buffer from the fifo queue. + active_stock_buffer_ = stock_fifo_->AcquireOutputObject(); + DCHECK(active_stock_buffer_); + + if (planar_data_ != nullptr) { + planar_data_->AppendData(*active_stock_buffer_); + } + + // Advance the play position by the number of frames in the buffer. + playhead_position_ = playhead_position_.load() + frames_per_buffer_; + + // Loop the playhead position back to the start if we're looping. + if (looping_enabled_) { + if (total_frames_ > 0) { + playhead_position_ = + playhead_position_.load() % total_frames_; + } else { + playhead_position_ = 0; + } + } + + stream_started_ = true; + *output_buffer = active_stock_buffer_; + return true; +} + +bool AudioAssetStream::IsPrestockServiceNeeded() const { + if (asset_->GetStatus() == ResonanceAudioAsset::Status::kLoadedInMemory) { + // Do not prestock buffers if asset is in memory. + return false; + } + if (end_of_stream_.load()) { + return false; + } + + return pending_seek_.load() || stock_fifo_->Size() < kRefillOnBufferCount; +} + +void AudioAssetStream::ServicePrestock() { + DCHECK(asset_->GetStatus() != ResonanceAudioAsset::Status::kLoadedInMemory); + DCHECK(reader_ != nullptr); + + // Perform any pending seek before decoding more PCM data. + if (pending_seek_.load()) { + pending_seek_ = false; + reader_->SeekToFramePosition(pending_seek_position_.load()); + } + + // Decode additional data to refill the queue. + while (stock_fifo_->Size() < num_stock_fifo_buffers_to_fill_) { + if (!StockNextBufferFromReader()) { + return; + } + } +} + +bool AudioAssetStream::StockNextBufferFromReader() { + if (reader_->IsAtEndOfStream()) { + if (looping_enabled_.load()) { + // Rewind to beginning of asset if looping is enabled. + if (reader_->SeekToFramePosition(0) != 0) { + LOG(ERROR) << "Could not perform loop back to position zero."; + } + } else { + // Flush the partitioner, if necessary. + if (partitioner_ != nullptr) { + partitioner_->Flush(); + } + + // If end of stream has been reached and not in looping mode, prevent + // the decoder from being called for more data. Some decoders (namely + // SLES) will block until timeout if they have no more data available. + end_of_stream_ = true; + return false; + } + } + + if (resampler_ != nullptr) { + // |AudioBuffer| from decoder must be resampled and repartitioned, with + // the resulting buffer delivered via the |PartitionedBufferCallback| + // function. + const size_t frames_decoded = + AudioPlanarData::ReadNextAudioBufferFromReader(reader_.get(), + resampler_input_.get()); + if (frames_decoded == 0) { + end_of_stream_ = true; + return false; + } + + const size_t resampled_buffer_size = + resampler_->GetNextOutputLength(frames_decoded); + resampler_->Process(*resampler_input_, resampler_output_.get()); + + partitioner_->AddBuffer(resampled_buffer_size, *resampler_output_); + } else { + // No resampling is required. |AudioBuffer| from |stock_fifo_| can be + // directly passed into |GetNextBuffer|. + vraudio::AudioBuffer* buffer = stock_fifo_->AcquireInputObject(); + if (buffer != nullptr) { + const size_t frames_decoded = + AudioPlanarData::ReadNextAudioBufferFromReader(reader_.get(), buffer); + if (frames_decoded == 0) { + LOG(WARNING) << "DecodeToBuffer returned zero samples before EOS."; + buffer->Clear(); + } + stock_fifo_->ReleaseInputObject(buffer); + } + } + return true; +} + +vraudio::AudioBuffer* AudioAssetStream::PartitionedBufferCallback( + vraudio::AudioBuffer* buffer) { + if (buffer != nullptr) { + stock_fifo_->ReleaseInputObject(buffer); + } + vraudio::AudioBuffer* next = stock_fifo_->AcquireInputObject(); + if (next == nullptr) { + LOG(WARNING) << "Failed to get free buffer from stock_fifo_."; + } + return next; +} + +bool AudioAssetStream::Seek(float position_seconds) { + if (planar_data_ != nullptr) { + // Seeking is not allowed for assets of this type until they have streamed + // once from start to finish. + return false; + } else if (asset_->GetStatus() == + ResonanceAudioAsset::Status::kLoadedInMemory) { + const AudioPlanarData* planar_data = asset_->GetPlanarData(); + const size_t seek_frame_position = + static_cast(position_seconds * system_sample_rate_hz_); + const size_t total_frames = planar_data->GetFrameCount(); + if (seek_frame_position <= total_frames) { + playhead_position_ = seek_frame_position; + return true; + } + } else if (asset_->GetStatus() == + ResonanceAudioAsset::Status::kReadyForStreaming) { + // For a streamed asset, set a pending seek frame position for a seek + // which will occur when the next |ServicePrestock| call occurs. + const size_t tentative_seek_frame_position = + static_cast(position_seconds * reader_->GetSampleRateHz()); + if (tentative_seek_frame_position <= total_frames_) { + pending_seek_position_ = tentative_seek_frame_position; + pending_seek_ = true; + + // TODO(b/62629658) For instant seek operation, it would be preferable to + // clear the |stock_fifo_| to prevent already stocked data from being + // played before the seeked data. The fifo will be restocked from the + // |pending_seek_position_| on the next call to |ServicePrestock|. + + return true; + } + } + return false; +} + +void AudioAssetStream::SetLoopCrossfadeDuration(float loop_crossfade_seconds) { + crossfade_size_frames_ = 0; + + const AudioPlanarData* planar_data = asset_->GetPlanarData(); + if (planar_data == nullptr) { + return; + } + + const uint64_t total_frames = planar_data->GetFrameCount(); + if (total_frames == 0) { + return; + } + + const size_t num_crossfade_frames = static_cast( + loop_crossfade_seconds * static_cast(system_sample_rate_hz_)); + // To be able to render a seamless loop, we need at least two samples. + crossfade_size_frames_ = + std::min(num_crossfade_frames, total_frames - 1); +} + +void AudioAssetStream::SeamlessLoopCrossfade( + size_t num_frames_to_copy, vraudio::AudioBuffer* target_buffer) { + size_t source_buffer_play_position = playhead_position_.load(); + size_t crossfade_size_frames = crossfade_size_frames_.load(); + + // Beginning of loop tail in source buffer that fades out. + const AudioPlanarData* planar_data = asset_->GetPlanarData(); + const size_t tail_begin_in_source_buffer = + planar_data->GetFrameCount() - crossfade_size_frames; + + // Beginning of loop head in source buffer that fades in. + size_t head_begin_in_source_buffer = 0; + // Beginning of loop in target buffer. + size_t offset_in_target_buffer = + tail_begin_in_source_buffer - source_buffer_play_position; + // Crossfade percentage at beginning of loop in target buffer. + float crossfade_percentage = 0.0f; + + if (source_buffer_play_position >= tail_begin_in_source_buffer) { + // Loop started in one of the previous buffers. + // TODO(b/33060500) Consider an energy preserving crossfade window. + head_begin_in_source_buffer = + source_buffer_play_position - tail_begin_in_source_buffer; + crossfade_percentage = static_cast(head_begin_in_source_buffer) / + static_cast(crossfade_size_frames); + offset_in_target_buffer = 0; + } + + DCHECK_LE(static_cast(offset_in_target_buffer), num_frames_to_copy); + const size_t num_loop_frames_to_process = + num_frames_to_copy - offset_in_target_buffer; + + for (size_t channel = 0; channel < target_buffer->num_channels(); ++channel) { + const float* source_channel_ptr = + planar_data->GetChannelData(channel).data(); + float* target_channel_ptr = &(*target_buffer)[channel][0]; + for (size_t frame = 0; frame < num_loop_frames_to_process; ++frame) { + const float crossfade_factor = + crossfade_percentage + + static_cast(frame) / static_cast(crossfade_size_frames); + target_channel_ptr[offset_in_target_buffer + frame] = + crossfade_factor * + source_channel_ptr[head_begin_in_source_buffer + frame] + + (1.0f - crossfade_factor) * + target_channel_ptr[offset_in_target_buffer + frame]; + } + } +} + +} // namespace redux diff --git a/redux/redux/engines/audio/resonance/audio_asset_stream.h b/redux/redux/engines/audio/resonance/audio_asset_stream.h new file mode 100644 index 0000000..b4d2f12 --- /dev/null +++ b/redux/redux/engines/audio/resonance/audio_asset_stream.h @@ -0,0 +1,209 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_ASSET_STREAM_H_ +#define REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_ASSET_STREAM_H_ + +#include +#include +#include +#include + +#include "redux/engines/audio/resonance/audio_source_stream.h" +#include "redux/engines/audio/resonance/resonance_audio_asset.h" +#include "redux/modules/audio/audio_reader.h" +#include "resonance_audio/base/audio_buffer.h" +#include "resonance_audio/dsp/resampler.h" +#include "resonance_audio/utils/buffer_partitioner.h" +#include "resonance_audio/utils/threadsafe_fifo.h" + +namespace redux { + +// AudioSourceStream for an AudioAsset. +// +// There are two ways in which this AudioSourceStream can provide AudioBuffers +// for the audio device. +// +// The simplest is when the AudioAsset stores the decoded, planar audio data in +// a memory buffer. In this case, we can just read the data directly when +// needed. +// +// The more complicated case is when the AudioAsset is only able to stream data +// using an AudioReader. In this case, we try to asynchronously read and decode +// the audio stream and store a "stock" of audio buffers into a thread-safe +// FIFO queue. Then, when we get a request for data, we can return the next +// buffer from the queue. +// +// Managing this stock of buffers requires coordination with the streaming +// thread managed by the AudioStreamManager. +class AudioAssetStream : public AudioSourceStream { + public: + // Creates the AudioSourceStream around the AudioAsset. The audio will be + // streamed to match the speaker device requirements. + AudioAssetStream(const std::shared_ptr& asset, + const SpeakerProfile& speaker_profile); + + ~AudioAssetStream() override; + + AudioAssetStream(const AudioAssetStream&) = delete; + AudioAssetStream& operator=(const AudioAssetStream&) = delete; + + // Initializes the audio source stream. + bool Initialize() override; + + // Returns true if the end of the stream has been reached. + bool EndOfStreamReached() override { return end_of_stream_.load(); } + + // Populates `output_buffer` with data from the audio stream. Sets + // `output_buffer` to nullptr if a buffer underrun occurs. Returns false if + // there is no more data to be consumed. + // + // Should be called only from the audio thread. + bool GetNextAudioBuffer( + const vraudio::AudioBuffer** output_buffer) override; + + // Queries whether the asynchronously decoded stock of audio buffers needs to + // be refilled. Should be called only from the audio thread. + bool IsPrestockServiceNeeded() const override; + + // Refills the asynchronous stock of audio buffers. Should be called only from + // the decode thread. + void ServicePrestock() override; + + // Returns the number of audio channels in the audio stream. + uint64_t GetNumChannels() const override; + + // Returns the sample rate of the audio stream. This must be the same as the + // audio device sample rate. + int GetSampleRateHz() const override; + + // Seeks to a target time position in the audio stream. + bool Seek(float position_seconds) override; + + // Enables looped streaming from the audio source. Looping may not be + // meaningful for all audio source types; usage depends on subclass. + void EnableLooping(bool looping_enabled) override; + + // Sets the loop crossfade for the audio source if looping is enabled and + // meaningful for the audio source type. If not called, the source will use a + // default loop crossfade. + void SetLoopCrossfadeDuration(float loop_crossfade_seconds) override; + + private: + // Configures a Resampler and Partitioner if the sample rate of the input + // asset differs from the sample rate required by the audio engine. + void ConfigureResampler(); + + // Partitioner callback for when a new buffer is completed. + vraudio::AudioBuffer* PartitionedBufferCallback(vraudio::AudioBuffer* buffer); + + // Helper for `GetNextAudioBuffer` that gets the buffer from the asset's + // memory cache. + bool GetNextAudioBufferFromMemory(const vraudio::AudioBuffer** output_buffer); + + // Helper for `GetNextAudioBuffer` that gets the buffer from the prestock + // queue. + bool GetNextAudioBufferFromPrestockQueue( + const vraudio::AudioBuffer** output_buffer); + + // Reads an audio buffer from the asset reader and pushes it into the prestock + // queue. + bool StockNextBufferFromReader(); + + // Helper method which calculates a seamless loop by crossfading between the + // end and the beginning of asset. + void SeamlessLoopCrossfade(size_t num_frames_to_copy, + vraudio::AudioBuffer* target_buffer); + + // Shared pointer to the AudioAsset which will serve as the stream source. + // This asset is externally owned. + std::shared_ptr asset_; + + // The AudioReader that can be used to stream the asset data. + std::unique_ptr reader_; + + // The AudioPlanarData into which to store data as it is streamed and decoded. + // This is only used if the asset was configured for streaming into memory. + std::unique_ptr planar_data_; + + // A threadsafe FIFO queue that stores audio buffers that can be consumed by + // calls to GetNextAudioBuffer. + using AudioBufferFifo = vraudio::ThreadsafeFifo; + std::unique_ptr stock_fifo_; + + // Flag indicating looped playback. + std::atomic looping_enabled_; + + // Indication that an input stream has been completely decoded (end of file). + std::atomic end_of_stream_; + + // Current playhead frame position of the stream, as distinguished from the + // decode position. This would also be known as the next available frame in + // the stock_fifo_. + std::atomic playhead_position_; + + // Indicates that the audio stream has started, so that mid-stream underflows + // may be detected. This variable will be set to true once PCM data is + // produced by this |AudioAssetStream|. + std::atomic stream_started_; + + // Indicates that an asynchronous seek to frame position is pending. + std::atomic pending_seek_; + + // The pending seek position to be set asynchronously. The actual seek will + // occur in the |ServicePrestock| function. + std::atomic pending_seek_position_; + + // Size of seamless crossfade in frames. + std::atomic crossfade_size_frames_; + + // |AudioAsset| channel count. + uint64_t channel_count_; + + // Frames per buffer. + uint64_t frames_per_buffer_; + + // Audio system sample rate. + int system_sample_rate_hz_; + + // Overall duration of an audio asset. + size_t total_frames_; + + // The number of |stock_fifo_| buffers that the |ServicePrestock| routine + // should try to fill. This number will be smaller than |kNumberFifoBuffers| + // where a |partitioner_| is used. + size_t num_stock_fifo_buffers_to_fill_; + + // Resampler and related objects for when asset sample rate does not match the + // system sample rate. + std::unique_ptr resampler_; + std::unique_ptr resampler_input_; + std::unique_ptr resampler_output_; + std::unique_ptr partitioner_; + + // Preallocated output audio buffer. + std::unique_ptr output_buffer_; + + // |GetNextAudioBuffer| returns a pointer to the |stock_fifo_| front buffer. + // To ensure, the acquired and returned buffer is not reused, this pointer + // holds the most recent output |AudioBuffer| which is used to release it + // during the next |GetNextAudioBuffer| call. + const vraudio::AudioBuffer* active_stock_buffer_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_ASSET_STREAM_H_ diff --git a/redux/redux/engines/audio/resonance/audio_asset_stream_tests.cc b/redux/redux/engines/audio/resonance/audio_asset_stream_tests.cc new file mode 100644 index 0000000..dbf85ec --- /dev/null +++ b/redux/redux/engines/audio/resonance/audio_asset_stream_tests.cc @@ -0,0 +1,273 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/engines/audio/resonance/audio_asset_stream.h" +#include "redux/modules/audio/wav_reader.h" +#include "redux/modules/testing/testing.h" + +namespace redux { +namespace { + +using ::testing::Eq; +using ::testing::Gt; +using ::testing::Ne; + +static constexpr auto kDataPath = + "redux/modules/audio/test_data"; + +static std::unique_ptr CreateWavReader(std::string_view uri) { + const std::string fullpath = ResolveTestFilePath(kDataPath, uri); + FILE* file = fopen(fullpath.c_str(), "rb"); + return std::make_unique(DataReader::FromCFile(file)); +} + +static SpeakerProfile InitSpeakerProfile(const AudioReader* reader = nullptr) { + SpeakerProfile profile; + profile.frames_per_buffer = 256; + profile.num_channels = reader ? reader->GetNumChannels() : 2; + profile.sample_rate_hz = reader ? reader->GetSampleRateHz() : 48000; + return profile; +} + +static uint64_t RoundToFullFrames(uint64_t total_frames, + const SpeakerProfile& profile) { + const uint64_t frames_per_buffer = profile.frames_per_buffer; + const float num_buffers = + static_cast(total_frames) / static_cast(frames_per_buffer); + const uint64_t rounded = static_cast(num_buffers + 0.5f); + return rounded * frames_per_buffer; +} + +std::unique_ptr CreateAudioAssetStreamForStreaming( + std::unique_ptr reader, + std::shared_ptr asset) { + auto profile = InitSpeakerProfile(reader.get()); + + asset->SetAudioReader(std::move(reader)); + auto stream = std::make_unique(asset, profile); + EXPECT_TRUE(stream->Initialize()); + return stream; +} + +std::unique_ptr CreateAudioAssetStreamFromMemory( + std::unique_ptr reader, + std::shared_ptr asset) { + auto profile = InitSpeakerProfile(reader.get()); + + asset->SetAudioPlanarData(AudioPlanarData::FromReader(*reader, profile)); + auto stream = std::make_unique(asset, profile); + EXPECT_TRUE(stream->Initialize()); + return stream; +} + +TEST(AudioAssetStreamTest, BasicInfo) { + auto reader = CreateWavReader("speech.wav"); + auto profile = InitSpeakerProfile(reader.get()); + + auto asset = std::make_shared(1); + auto stream = CreateAudioAssetStreamForStreaming(std::move(reader), asset); + + EXPECT_THAT(stream->GetNumChannels(), Eq(profile.num_channels)); + EXPECT_THAT(stream->GetSampleRateHz(), Eq(profile.sample_rate_hz)); +} + +TEST(AudioAssetStreamTest, ReadFromMemory) { + auto reader = CreateWavReader("speech.wav"); + auto profile = InitSpeakerProfile(reader.get()); + + const uint64_t total_frames = reader->GetTotalFrameCount(); + const uint64_t expected_frames = RoundToFullFrames(total_frames, profile); + + auto asset = std::make_shared(1); + auto stream = CreateAudioAssetStreamFromMemory(std::move(reader), asset); + EXPECT_FALSE(stream->IsPrestockServiceNeeded()); + + vraudio::AudioBuffer buffer(profile.num_channels, profile.frames_per_buffer); + uint64_t frames_read = 0; + while (true) { + const vraudio::AudioBuffer* ptr = &buffer; + if (stream->GetNextAudioBuffer(&ptr)) { + frames_read += ptr->num_frames(); + } else { + break; + } + } + + EXPECT_THAT(frames_read, Eq(expected_frames)); +} + +TEST(AudioAssetStreamTest, ReadFromStreaming) { + auto reader = CreateWavReader("speech.wav"); + auto profile = InitSpeakerProfile(reader.get()); + + const uint64_t total_frames = reader->GetTotalFrameCount(); + const uint64_t expected_frames = RoundToFullFrames(total_frames, profile); + + auto asset = std::make_shared(1); + auto stream = CreateAudioAssetStreamForStreaming(std::move(reader), asset); + + vraudio::AudioBuffer buffer(profile.num_channels, profile.frames_per_buffer); + uint64_t frames_read = 0; + while (true) { + while (stream->IsPrestockServiceNeeded()) { + stream->ServicePrestock(); + } + + const vraudio::AudioBuffer* ptr = &buffer; + if (stream->GetNextAudioBuffer(&ptr)) { + frames_read += ptr->num_frames(); + } else { + break; + } + } + + EXPECT_THAT(frames_read, Eq(expected_frames)); +} + +TEST(AudioAssetStreamTest, PrestockBufferUnderrun) { + auto reader = CreateWavReader("speech.wav"); + auto profile = InitSpeakerProfile(reader.get()); + + const uint64_t total_frames = reader->GetTotalFrameCount(); + const uint64_t expected_frames = RoundToFullFrames(total_frames, profile); + + auto asset = std::make_shared(1); + auto stream = CreateAudioAssetStreamForStreaming(std::move(reader), asset); + // Only service the prestock once so that we get an underrun. + stream->ServicePrestock(); + + vraudio::AudioBuffer buffer(profile.num_channels, profile.frames_per_buffer); + + bool underrun = false; + uint64_t frames_read = 0; + while (frames_read < expected_frames) { + const vraudio::AudioBuffer* ptr = &buffer; + stream->GetNextAudioBuffer(&ptr); + underrun |= (ptr == nullptr); + frames_read += buffer.num_frames(); + } + + EXPECT_TRUE(underrun); +} + +TEST(AudioAssetStreamTest, ReadFromStreamingIntoMemory) { + auto reader = CreateWavReader("speech.wav"); + auto profile = InitSpeakerProfile(reader.get()); + + const uint64_t total_frames = reader->GetTotalFrameCount(); + const uint64_t expected_frames = RoundToFullFrames(total_frames, profile); + + auto asset = std::make_shared(1, true); + auto stream = CreateAudioAssetStreamForStreaming(std::move(reader), asset); + + vraudio::AudioBuffer buffer(profile.num_channels, profile.frames_per_buffer); + uint64_t frames_read = 0; + while (true) { + while (stream->IsPrestockServiceNeeded()) { + stream->ServicePrestock(); + } + + const vraudio::AudioBuffer* ptr = &buffer; + if (stream->GetNextAudioBuffer(&ptr)) { + frames_read += ptr->num_frames(); + } else { + break; + } + } + + EXPECT_THAT(frames_read, Eq(expected_frames)); + + auto asset_data = asset->GetPlanarData(); + EXPECT_THAT(asset_data, Eq(nullptr)); + stream.reset(); + + asset_data = asset->GetPlanarData(); + EXPECT_THAT(asset_data, Ne(nullptr)); + EXPECT_THAT(asset_data->GetFrameCount(), Eq(expected_frames)); +} + +TEST(AudioAssetStreamTest, SeekMemory) { + auto reader = CreateWavReader("speech.wav"); + auto profile = InitSpeakerProfile(reader.get()); + + const uint64_t total_frames = reader->GetTotalFrameCount(); + const uint64_t expected_frames = RoundToFullFrames(total_frames, profile); + + auto asset = std::make_shared(1); + auto stream = CreateAudioAssetStreamFromMemory(std::move(reader), asset); + + // Skip 1 second. + stream->Seek(1); + const uint64_t skipped_frames = + RoundToFullFrames(profile.sample_rate_hz, profile); + + vraudio::AudioBuffer buffer(profile.num_channels, profile.frames_per_buffer); + uint64_t frames_read = 0; + while (true) { + const vraudio::AudioBuffer* ptr = &buffer; + if (stream->GetNextAudioBuffer(&ptr)) { + frames_read += ptr->num_frames(); + } else { + break; + } + } + + EXPECT_THAT(frames_read + skipped_frames, Eq(expected_frames)); +} + +TEST(AudioAssetStreamTest, SeekFailsForStreamIntoMemory) { + auto reader = CreateWavReader("speech.wav"); + auto asset = std::make_shared(1, true); + auto stream = CreateAudioAssetStreamForStreaming(std::move(reader), asset); + EXPECT_FALSE(stream->Seek(0)); +} + +TEST(AudioAssetStreamTest, LoopMemory) { + auto reader = CreateWavReader("speech.wav"); + auto profile = InitSpeakerProfile(reader.get()); + + const uint64_t total_frames = reader->GetTotalFrameCount(); + const uint64_t expected_frames = RoundToFullFrames(total_frames, profile); + const uint64_t num_reads = expected_frames / profile.frames_per_buffer; + + auto asset = std::make_shared(1); + auto stream = CreateAudioAssetStreamFromMemory(std::move(reader), asset); + stream->EnableLooping(true); + + vraudio::AudioBuffer buffer(profile.num_channels, profile.frames_per_buffer); + + uint64_t frames_read = 0; + // Read "beyond" the end of the data, which is okay since we're looping. + for (int i = 0; i < num_reads + 3; ++i) { + while (stream->IsPrestockServiceNeeded()) { + stream->ServicePrestock(); + } + + const vraudio::AudioBuffer* ptr = &buffer; + if (stream->GetNextAudioBuffer(&ptr)) { + frames_read += buffer.num_frames(); + } else { + break; + } + } + + EXPECT_THAT(frames_read, Gt(expected_frames)); +} + +} // namespace +} // namespace redux diff --git a/redux/redux/engines/audio/resonance/audio_planar_data.cc b/redux/redux/engines/audio/resonance/audio_planar_data.cc new file mode 100644 index 0000000..d110f08 --- /dev/null +++ b/redux/redux/engines/audio/resonance/audio_planar_data.cc @@ -0,0 +1,139 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/audio/resonance/audio_planar_data.h" + +#include "redux/modules/audio/audio_reader.h" +#include "resonance_audio/dsp/resampler.h" +#include "resonance_audio/utils/planar_interleaved_conversion.h" + +namespace redux { + +AudioPlanarData::AudioPlanarData(size_t num_channels) + : channels_(num_channels) {} + +void AudioPlanarData::Reserve(size_t num_frames) { + for (auto& channel : channels_) { + channel.reserve(num_frames); + } +} + +uint64_t AudioPlanarData::GetFrameCount() const { + return channels_.empty() ? 0 : channels_.front().size(); +} + +uint64_t AudioPlanarData::GetNumChannels() const { + return static_cast(channels_.size()); +} + +absl::Span AudioPlanarData::GetChannelData(size_t index) const { + return index < channels_.size() ? channels_[index] + : absl::Span(); +} + +void AudioPlanarData::AppendData(const vraudio::AudioBuffer& source) { + AppendData(source, source.num_frames()); +} + +void AudioPlanarData::AppendData(const vraudio::AudioBuffer& source, + size_t num_frames) { + CHECK_EQ(source.num_channels(), channels_.size()); + for (size_t channel = 0; channel < channels_.size(); ++channel) { + auto& dst = channels_[channel]; + const auto& src = source[channel]; + dst.insert(dst.end(), src.begin(), src.begin() + num_frames); + } +} + +std::unique_ptr AudioPlanarData::FromReader( + AudioReader& reader, const SpeakerProfile& profile) { + const int num_channels = reader.GetNumChannels(); + auto planar_data = std::make_unique(num_channels); + + // See if we need to use a Resampler to match the sample rate of loaded audio + // samples with the system sample rate. + std::unique_ptr resampler; + std::unique_ptr resampled_buffer; + const int asset_sample_rate_hz = reader.GetSampleRateHz(); + if (asset_sample_rate_hz != profile.sample_rate_hz) { + resampler = std::make_unique(); + resampler->SetRateAndNumChannels(asset_sample_rate_hz, + profile.sample_rate_hz, + num_channels); + } + + // Loop to perform stream decoding, as not all reader types support a query + // for decoded frame count. + vraudio::AudioBuffer temp_buffer(num_channels, profile.frames_per_buffer); + + while (!reader.IsAtEndOfStream()) { + vraudio::AudioBuffer* source_buffer = &temp_buffer; + size_t frames_decoded = + ReadNextAudioBufferFromReader(&reader, source_buffer); + if (frames_decoded == 0) { + break; + } + + CHECK(reader.IsAtEndOfStream() || + frames_decoded == profile.frames_per_buffer); + + if (resampler != nullptr) { + const size_t resampled_size = + resampler->GetNextOutputLength(frames_decoded); + if (resampled_buffer == nullptr || + resampled_buffer->num_frames() != resampled_size) { + resampled_buffer = std::make_unique( + num_channels, resampled_size); + } + + if (frames_decoded != source_buffer->num_frames()) { + vraudio::AudioBuffer temp_buffer2(num_channels, frames_decoded); + for (size_t i = 0; i < num_channels; ++i) { + std::copy_n((*source_buffer)[i].begin(), frames_decoded, + temp_buffer2[i].begin()); + } + resampler->Process(temp_buffer2, resampled_buffer.get()); + } else { + resampler->Process(*source_buffer, resampled_buffer.get()); + } + + source_buffer = resampled_buffer.get(); + frames_decoded = resampled_buffer->num_frames(); + } + + planar_data->AppendData(*source_buffer, frames_decoded); + } + return planar_data; +} + +size_t AudioPlanarData::ReadNextAudioBufferFromReader( + AudioReader* reader, vraudio::AudioBuffer* buffer) { + const absl::Span bytes = + reader->ReadFrames(buffer->num_frames()); + const size_t num_frames = bytes.size() / reader->GetNumBytesPerFrame(); + const int num_channels = reader->GetNumChannels(); + + const auto format = reader->GetEncodingFormat(); + if (format == AudioReader::kFloat) { + const float* data = reinterpret_cast(bytes.data()); + vraudio::FillAudioBuffer(data, num_frames, num_channels, buffer); + } else if (format == AudioReader::kInt16) { + const int16_t* data = reinterpret_cast(bytes.data()); + vraudio::FillAudioBuffer(data, num_frames, num_channels, buffer); + } + return num_frames; +} +} // namespace redux diff --git a/redux/redux/engines/audio/resonance/audio_planar_data.h b/redux/redux/engines/audio/resonance/audio_planar_data.h new file mode 100644 index 0000000..3366ce9 --- /dev/null +++ b/redux/redux/engines/audio/resonance/audio_planar_data.h @@ -0,0 +1,70 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_PLANAR_DATA_H_ +#define REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_PLANAR_DATA_H_ + +#include +#include +#include + +#include "absl/types/span.h" +#include "redux/engines/platform/device_profiles.h" +#include "redux/modules/audio/audio_reader.h" +#include "resonance_audio/base/audio_buffer.h" + +namespace redux { + +// Storage for uncompressed planar audio data. +class AudioPlanarData { + public: + explicit AudioPlanarData(size_t num_channels); + + // Reserves memory in the buffer for the given number of frames. + void Reserve(size_t num_frames); + + // Returns the total number of frames of audio data. + uint64_t GetFrameCount() const; + + // Returns the number of channels of audio data. + uint64_t GetNumChannels() const; + + // Adds decoded frames to the internal buffers. + void AppendData(const vraudio::AudioBuffer& source); + + // Returns the data for the given channel. Returns an empty span if the + // channel index is invalid. + absl::Span GetChannelData(size_t index) const; + + // Decodes the entirety of an AudioReader into a AudioPlanarData object. + static std::unique_ptr FromReader( + AudioReader& reader, const SpeakerProfile& profile); + + // Populates the vraudio::AudioBuffer with data from the AudioReader. + static size_t ReadNextAudioBufferFromReader(AudioReader* reader, + vraudio::AudioBuffer* buffer); + + private: + // Adds decoded frames to the internal PCM buffer. + void AppendData(const vraudio::AudioBuffer& source, size_t num_frames); + + // Buffer containing uncompressed planar data. + std::vector> channels_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_PLANAR_DATA_H_ diff --git a/redux/redux/engines/audio/resonance/audio_planar_data_tests.cc b/redux/redux/engines/audio/resonance/audio_planar_data_tests.cc new file mode 100644 index 0000000..8c96dbf --- /dev/null +++ b/redux/redux/engines/audio/resonance/audio_planar_data_tests.cc @@ -0,0 +1,101 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/engines/audio/resonance/audio_planar_data.h" +#include "redux/modules/audio/wav_reader.h" +#include "redux/modules/testing/testing.h" + +namespace redux { +namespace { + +using ::testing::Eq; +using ::testing::ElementsAreArray; + +static constexpr auto kDataPath = + "redux/modules/audio/test_data"; + +static std::unique_ptr CreateWavReader(std::string_view uri) { + const std::string fullpath = ResolveTestFilePath(kDataPath, uri); + FILE* file = fopen(fullpath.c_str(), "rb"); + return std::make_unique(DataReader::FromCFile(file)); +} + +TEST(AudioPlanarDataTest, Append) { + const int num_channels = 2; + const int num_frames_per_append = 16; + + vraudio::AudioBuffer source(num_channels, num_frames_per_append); + source.Clear(); + for (int i = 0; i < num_channels; ++i) { + for (int j = 0; j < num_frames_per_append; ++j) { + source[i][j] = static_cast((i * num_frames_per_append) + j); + } + } + + AudioPlanarData data(num_channels); + EXPECT_THAT(data.GetNumChannels(), Eq(num_channels)); + + data.AppendData(source); + EXPECT_THAT(data.GetFrameCount(), Eq(num_frames_per_append)); + + data.AppendData(source); + EXPECT_THAT(data.GetFrameCount(), Eq(2 * num_frames_per_append)); + + auto channel0 = data.GetChannelData(0); + auto channel1 = data.GetChannelData(1); + EXPECT_THAT(channel0.size(), Eq(2 * num_frames_per_append)); + EXPECT_THAT(channel1.size(), Eq(2 * num_frames_per_append)); + + std::vector expect; + expect.insert(expect.end(), source[0].begin(), source[0].end()); + expect.insert(expect.end(), source[0].begin(), source[0].end()); + EXPECT_THAT(channel0, ElementsAreArray(expect.begin(), expect.end())); + + expect.clear(); + expect.insert(expect.end(), source[1].begin(), source[1].end()); + expect.insert(expect.end(), source[1].begin(), source[1].end()); + EXPECT_THAT(channel1, ElementsAreArray(expect.begin(), expect.end())); +} + +TEST(AudioPlanarDataTest, FromReader) { + auto reader = CreateWavReader("speech.wav"); + + SpeakerProfile profile; + profile.frames_per_buffer = 256; + profile.num_channels = reader->GetNumChannels(); + profile.sample_rate_hz = reader->GetSampleRateHz(); + + auto data = AudioPlanarData::FromReader(*reader, profile); + EXPECT_THAT(data->GetNumChannels(), Eq(reader->GetNumChannels())); + EXPECT_THAT(data->GetFrameCount(), Eq(reader->GetTotalFrameCount())); +} + +TEST(AudioPlanarDataTest, FromReaderResampled) { + auto reader = CreateWavReader("speech.wav"); + + SpeakerProfile profile; + profile.frames_per_buffer = 256; + profile.num_channels = reader->GetNumChannels(); + profile.sample_rate_hz = reader->GetSampleRateHz() / 2; + + auto data = AudioPlanarData::FromReader(*reader, profile); + EXPECT_THAT(data->GetNumChannels(), Eq(reader->GetNumChannels())); + EXPECT_THAT(data->GetFrameCount(), Eq(reader->GetTotalFrameCount() / 2)); +} +} // namespace +} // namespace redux diff --git a/redux/redux/engines/audio/resonance/audio_source_stream.h b/redux/redux/engines/audio/resonance/audio_source_stream.h new file mode 100644 index 0000000..fa92f74 --- /dev/null +++ b/redux/redux/engines/audio/resonance/audio_source_stream.h @@ -0,0 +1,80 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_SOURCE_STREAM_H_ +#define REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_SOURCE_STREAM_H_ + +#include + +#include "resonance_audio/base/audio_buffer.h" + +namespace redux { + +// Base interface of an audio stream capable of providing audio data +// synchronously or asynchronously to be fed into the audio device. +class AudioSourceStream { + public: + AudioSourceStream() = default; + virtual ~AudioSourceStream() = default; + + AudioSourceStream(const AudioSourceStream&) = delete; + AudioSourceStream& operator=(const AudioSourceStream&) = delete; + + // Initializes the audio source stream. + virtual bool Initialize() = 0; + + // Returns true if the end of the stream has been reached. + virtual bool EndOfStreamReached() = 0; + + // Populates `output_buffer` with data from the audio stream. Sets + // `output_buffer` to nullptr if a buffer underrun occurs. Returns false if + // there is no more data to be consumed. + // + // Should be called only from the audio thread. + virtual bool GetNextAudioBuffer( + const vraudio::AudioBuffer** output_buffer) = 0; + + // Queries whether the asynchronously decoded stock of audio buffers needs to + // be refilled. Should be called only from the audio thread. + virtual bool IsPrestockServiceNeeded() const = 0; + + // Refills the asynchronous stock of audio buffers. Should be called only from + // the decode thread. + virtual void ServicePrestock() = 0; + + // Returns the number of audio channels in the audio stream. + virtual uint64_t GetNumChannels() const = 0; + + // Returns the sample rate of the audio stream. This must be the same as the + // audio device sample rate. + virtual int GetSampleRateHz() const = 0; + + // Seeks to a target time position in the audio stream. + virtual bool Seek(float position_seconds) = 0; + + // Enables looped streaming from the audio source. Looping may not be + // meaningful for all audio source types; usage depends on subclass. + virtual void EnableLooping(bool looping_enabled) = 0; + + // Sets the loop crossfade for the audio source if looping is enabled and + // meaningful for the audio source type. If not called, the source will use a + // default loop crossfade. + virtual void SetLoopCrossfadeDuration(float loop_crossfade_seconds) = 0; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_SOURCE_STREAM_H_ diff --git a/redux/redux/engines/audio/resonance/audio_stream_manager.cc b/redux/redux/engines/audio/resonance/audio_stream_manager.cc new file mode 100644 index 0000000..bf0dc47 --- /dev/null +++ b/redux/redux/engines/audio/resonance/audio_stream_manager.cc @@ -0,0 +1,160 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/audio/resonance/audio_stream_manager.h" + +#include "resonance_audio/utils/task_thread_pool.h" +#include "resonance_audio/utils/threadsafe_fifo.h" + +namespace redux { + +// Number of streaming worker threads available for asynchronous decoding. +static const unsigned int kNumStreamingWorkerThreads = 64; + +// Maximum number of streaming requests in stream renderer pointer queue. +static const size_t kMaxStreamFifoElements = 64; + +AudioStreamManager::AudioStreamManager() + : streaming_thread_running_(false), + stream_renderer_ptr_fifo_(kMaxStreamFifoElements, nullptr) {} + +void AudioStreamManager::Start() { + stream_renderer_ptr_fifo_.EnableBlockingSleepUntilMethods(true); + if (!streaming_thread_running_.load()) { + streaming_thread_running_ = true; + streaming_thread_ = std::thread([this]() { StreamingThread(); }); + } +} + +void AudioStreamManager::Stop() { + stream_renderer_ptr_fifo_.EnableBlockingSleepUntilMethods(false); + if (streaming_thread_running_.load()) { + streaming_thread_running_ = false; + streaming_thread_.join(); + } + + while (stream_renderer_ptr_fifo_.Size() > 0) { + auto renderer = PopFromStreamRendererFifo(); + if (renderer) { + renderer->SetPrestockServicePending(false); + } + } +} + +bool AudioStreamManager::AddAudioStreamRenderer(RendererPtr renderer) { + const SourceId source_id = renderer->GetSourceId(); + const auto result = renderers_.emplace(source_id, std::move(renderer)); + return result.second; +} + +AudioStreamRenderer* AudioStreamManager::GetAudioStreamRenderer( + SourceId source_id) const { + const auto iter = renderers_.find(source_id); + if (iter != renderers_.end()) { + return iter->second.get(); + } + return nullptr; +} + +void AudioStreamManager::Render(std::vector* disabled_renderer_ids) { + if (disabled_renderer_ids) { + disabled_renderer_ids->clear(); + disabled_renderer_ids->reserve(renderers_.size()); + } + + for (auto iter = renderers_.begin(); iter != renderers_.end();) { + RendererPtr& renderer = iter->second; + // Check whether this Renderer requires a block of processing to continue + // supplying its stream of data to Resonance. If so, schedule an + // asynchronous task to do so. + if (streaming_thread_running_.load() && + renderer->IsPrestockServiceNeeded()) { + if (!stream_renderer_ptr_fifo_.Full()) { + renderer->SetPrestockServicePending(true); + const bool pushed = PushToStreamRendererFifo(renderer); + DCHECK(pushed); + } else { + renderer->SetPrestockServicePending(false); + LOG(WARNING) << "Overflow of asynchronous restock requests. Is the " + << "decoder thread blocked?"; + } + } + + if (renderer->Render()) { + ++iter; + } else { + // Remove any renderers that are no longer providing audio buffers. + if (disabled_renderer_ids) { + disabled_renderer_ids->push_back(iter->first); + } + renderers_.erase(iter++); + } + } +} + +void AudioStreamManager::StreamingThread() { + vraudio::TaskThreadPool worker_thread_pool; + if (!worker_thread_pool.StartThreadPool(kNumStreamingWorkerThreads)) { + LOG(ERROR) << "Could not start worker threads"; + return; + } + + while (streaming_thread_running_.load()) { + while (streaming_thread_running_.load() && + !worker_thread_pool.WaitUntilWorkerBecomesAvailable()) { + } + while (streaming_thread_running_.load() && + !stream_renderer_ptr_fifo_.SleepUntilOutputObjectIsAvailable()) { + } + + RendererPtr renderer = PopFromStreamRendererFifo(); + if (renderer != nullptr) { + if (streaming_thread_running_.load()) { + const auto task = [renderer]() { renderer->ServicePrestock(); }; + const bool worker_assigned = worker_thread_pool.RunOnWorkerThread(task); + DCHECK(worker_assigned); + } else { + renderer->SetPrestockServicePending(false); + } + } + } +} + +bool AudioStreamManager::PushToStreamRendererFifo( + const RendererPtr& audio_stream_renderer) { + auto* input_stream_renderer = stream_renderer_ptr_fifo_.AcquireInputObject(); + if (input_stream_renderer == nullptr) { + return false; + } + *input_stream_renderer = audio_stream_renderer; + stream_renderer_ptr_fifo_.ReleaseInputObject(input_stream_renderer); + return true; +} + +std::shared_ptr +AudioStreamManager::PopFromStreamRendererFifo() { + auto* output_stream_renderer = + stream_renderer_ptr_fifo_.AcquireOutputObject(); + if (output_stream_renderer == nullptr) { + return nullptr; + } + RendererPtr output = *output_stream_renderer; + output_stream_renderer->reset(); + stream_renderer_ptr_fifo_.ReleaseOutputObject(output_stream_renderer); + return output; +} + +} // namespace redux diff --git a/redux/redux/engines/audio/resonance/audio_stream_manager.h b/redux/redux/engines/audio/resonance/audio_stream_manager.h new file mode 100644 index 0000000..b4d3c38 --- /dev/null +++ b/redux/redux/engines/audio/resonance/audio_stream_manager.h @@ -0,0 +1,76 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_STREAM_MANAGER_H_ +#define REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_STREAM_MANAGER_H_ + +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "redux/engines/audio/resonance/audio_stream_renderer.h" +#include "resonance_audio/utils/threadsafe_fifo.h" + +namespace redux { + +// Manages the "streaming" thread in which an AudioStreamRenderer can stream and +// decode audio data. +class AudioStreamManager { + public: + using SourceId = vraudio::SourceId; + using RendererPtr = std::shared_ptr; + + AudioStreamManager(); + + // Starts the asynchronous streaming thread for decoding audio. + void Start(); + + // Stops the asynchronous streaming thread. Note: buffered tasks which have + // not begun will be discarded. + void Stop(); + + // Registers a new AudioStreamRenderer. The renderer will be automatically + // removed once it has completed playback. Returns `false` if the renderer's + // SourceId is already in use. + bool AddAudioStreamRenderer(RendererPtr renderer); + + // Returns a non-owning pointer to a registered AudioStreamRenderer. + AudioStreamRenderer* GetAudioStreamRenderer(SourceId source_id) const; + + // Triggers all Renderers to render new audio buffers with resonance. + // + // Any Renderers that have completed playback will be unregistered. The + // SourceIds of these renderers will added to `disabled_renderer_ids` + // argument, if one is provided. + void Render(std::vector* disabled_renderer_ids); + + private: + void StreamingThread(); + + bool PushToStreamRendererFifo(const RendererPtr& renderer); + + RendererPtr PopFromStreamRendererFifo(); + + std::thread streaming_thread_; + std::atomic streaming_thread_running_; + vraudio::ThreadsafeFifo stream_renderer_ptr_fifo_; + absl::flat_hash_map renderers_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_STREAM_MANAGER_H_ diff --git a/redux/redux/engines/audio/resonance/audio_stream_renderer.cc b/redux/redux/engines/audio/resonance/audio_stream_renderer.cc new file mode 100644 index 0000000..8c33bd8 --- /dev/null +++ b/redux/redux/engines/audio/resonance/audio_stream_renderer.cc @@ -0,0 +1,132 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/audio/resonance/audio_stream_renderer.h" + +#include "resonance_audio/utils/planar_interleaved_conversion.h" + +namespace redux { + +// Number of buffers to process to allow ResonanceAudioApi to fade out and ramp +// down to the target volume of zero. +static const size_t kNumBuffersToProcessDuringFadeOut = 3; + +AudioStreamRenderer::AudioStreamRenderer( + SoundType type, std::unique_ptr stream, + vraudio::ResonanceAudioApi* resonance, + vraudio::RenderingMode rendering_mode) + : stream_(std::move(stream)), + resonance_(resonance), + is_paused_(true), // initialize in paused state + shutdown_triggered_(false), + stream_volume_(1.0f), + fade_out_count_down_(0), + prestock_service_pending_(false) { + CHECK(stream_ != nullptr); + CHECK(resonance_ != nullptr); + + const auto num_channels = stream_->GetNumChannels(); + if (type == SoundType::Point) { + CHECK(num_channels == vraudio::kNumMonoChannels); + } else if (type == SoundType::Stereo) { + CHECK(num_channels <= vraudio::kNumStereoChannels); + } + output_channels_.resize(num_channels); + + switch (type) { + case SoundType::Point: + source_id_ = resonance_->CreateSoundObjectSource(rendering_mode); + break; + case SoundType::Field: + source_id_ = resonance_->CreateAmbisonicSource(num_channels); + break; + case SoundType::Stereo: + source_id_ = resonance_->CreateStereoSource(num_channels); + break; + } + CHECK(source_id_ != vraudio::kInvalidSourceId); +} + +AudioStreamRenderer::~AudioStreamRenderer() { + resonance_->DestroySource(source_id_); +} + +void AudioStreamRenderer::Resume() { + is_paused_ = false; + resonance_->SetSourceVolume(source_id_, stream_volume_); + fade_out_count_down_ = 0; +} + +void AudioStreamRenderer::Pause() { + resonance_->SetSourceVolume(source_id_, 0.0f); + fade_out_count_down_ = kNumBuffersToProcessDuringFadeOut; +} + +void AudioStreamRenderer::Shutdown() { + Pause(); + shutdown_triggered_ = true; +} + +void AudioStreamRenderer::SetVolume(float volume) { + stream_volume_ = volume; + if (!is_paused_) { + resonance_->SetSourceVolume(source_id_, stream_volume_); + } +} + +bool AudioStreamRenderer::Render() { + if (is_paused_) { + return !shutdown_triggered_; + } + + const vraudio::AudioBuffer* next_buffer = nullptr; + if (!stream_->GetNextAudioBuffer(&next_buffer) || next_buffer == nullptr) { + return !stream_->EndOfStreamReached(); + } + + // Get raw planar channel pointers from `next_buffer` to set input buffer in + // `ResonanceAudioApi`. + GetRawChannelDataPointersFromAudioBuffer(*next_buffer, &output_channels_); + resonance_->SetPlanarBuffer(source_id_, output_channels_.data(), + next_buffer->num_channels(), + next_buffer->num_frames()); + + if (fade_out_count_down_ > 0) { + --fade_out_count_down_; + is_paused_ = true; + } + return true; +} + +bool AudioStreamRenderer::IsPrestockServiceNeeded() const { + if (prestock_service_pending_.load()) { + // We're already queued up for a prestock service, so we return false to + // prevent being doubly-queued up. + return false; + } + return stream_->IsPrestockServiceNeeded(); +} + +void AudioStreamRenderer::SetPrestockServicePending(bool pending) { + prestock_service_pending_ = pending; +} + +void AudioStreamRenderer::ServicePrestock() { + stream_->ServicePrestock(); + prestock_service_pending_ = false; +} + +} // namespace redux diff --git a/redux/redux/engines/audio/resonance/audio_stream_renderer.h b/redux/redux/engines/audio/resonance/audio_stream_renderer.h new file mode 100644 index 0000000..4d36613 --- /dev/null +++ b/redux/redux/engines/audio/resonance/audio_stream_renderer.h @@ -0,0 +1,112 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_STREAM_RENDERER_H_ +#define REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_STREAM_RENDERER_H_ + +#include +#include + +#include "redux/engines/audio/resonance/audio_source_stream.h" +#include "redux/modules/audio/enums.h" +#include "resonance_audio/api/resonance_audio_api.h" + +namespace redux { + +// Reads audio data from a AudioSourceStream and pushes it into Resonance. +class AudioStreamRenderer { + public: + // Creates a Sound object in Resonance that will render audio data from the + // given stream. The sound will start in a paused state and must be explicitly + // unpaused. + AudioStreamRenderer(SoundType type, std::unique_ptr stream, + vraudio::ResonanceAudioApi* resonance, + vraudio::RenderingMode rendering_mode); + + ~AudioStreamRenderer(); + + // Starts rendering of source stream. + void Resume(); + + // Stops rendering of source stream. + void Pause(); + + // Shuts down source stream. Note that the audio stream is first faded-out + // before removing it from Resonance. + void Shutdown(); + + // Sets volume of source stream. + void SetVolume(float volume); + + // Returns the handle to the underlying vraudio source. + vraudio::SourceId GetSourceId() const { return source_id_; } + + // Writes a new chunk of audio data to the vraudio source. This method needs + // to be called before a new output audio buffer is requested from Resonance. + bool Render(); + + // Flags the renderer as having been added to the asynchronous block streaming + // queue by the AudioStreamManager. + void SetPrestockServicePending(bool pending); + + // Checks if the stream source requires block pre-processing to continue the + // flow of audio data. Used by the AudioStreamManager to check if the renderer + // should be added to the asynchronous block streaming thread. + bool IsPrestockServiceNeeded() const; + + // Runs any processing needed by the source stream to continue data flow. + // This function should only be called by the AudioStreamManager. Note that + // this function may be expensive, so invoking it from the audio thread + // should be avoided. + void ServicePrestock(); + + private: + // The stream source from which to render audio data. + std::unique_ptr stream_; + + // Pointer to the Resonance Audio API. + vraudio::ResonanceAudioApi* resonance_; + + // Handle to the sound source in Resonance. + vraudio::SourceId source_id_; + + // Indicates if the AudioSourceStream is paused. When audio streams are + // stopped they are first faded-out before this flag is set to true. + bool is_paused_; + + // Indicates a Shutdown call. + bool shutdown_triggered_; + + // Volume of audio stream. + float stream_volume_; + + // Number of silence buffers to be triggered before pausing the stream. + size_t fade_out_count_down_; + + // Indicates whether this renderer has already been staged for prestock. + // This flag is used only by the AudioStreamManager to co-ordinate the running + // of th prestock service. + std::atomic prestock_service_pending_; + + // Buffer to hold pointers to individual output channels. We need this buffer + // to bridge the API between vraudio::AudioBuffer and + // ResonanceAudioApi::SetPlanarBuffer. + std::vector output_channels_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_AUDIO_RESONANCE_AUDIO_STREAM_RENDERER_H_ diff --git a/redux/redux/engines/audio/resonance/resonance_audio_asset.cc b/redux/redux/engines/audio/resonance/resonance_audio_asset.cc new file mode 100644 index 0000000..770cc9c --- /dev/null +++ b/redux/redux/engines/audio/resonance/resonance_audio_asset.cc @@ -0,0 +1,84 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/audio/resonance/resonance_audio_asset.h" + +#include "redux/modules/audio/audio_reader.h" + +namespace redux { + +ResonanceAudioAsset::ResonanceAudioAsset(Id id, bool stream_into_memory) + : AudioAsset(id), + stream_into_memory_(stream_into_memory), + status_(Status::kWaitingForReader) {} + +void ResonanceAudioAsset::SetStatus(Status status) { + { + std::lock_guard lock(status_change_mutex_); + status_ = status; + } + status_change_conditional_.notify_all(); +} + +bool ResonanceAudioAsset::IsValid() const { + return status_.load() == ResonanceAudioAsset::Status::kLoadedInMemory || + status_.load() == ResonanceAudioAsset::Status::kReadyForStreaming; +} + +void ResonanceAudioAsset::SetAudioReader(std::unique_ptr reader) { + if (status_.load() == Status::kLoadedInMemory) { + // Audio data is loaded into memory, so we don't need a reader. + return; + } + if (reader == nullptr) { + SetStatus(Status::kInvalid); + return; + } + + CHECK(reader_ == nullptr) << "Reader already set!"; + reader_ = std::move(reader); + planar_data_.reset(); + SetStatus(Status::kReadyForStreaming); +} + +void ResonanceAudioAsset::SetAudioPlanarData( + std::unique_ptr planar_data) { + if (planar_data == nullptr) { + SetStatus(Status::kInvalid); + return; + } + + CHECK(planar_data_ == nullptr) << "Audio data already set!"; + planar_data_ = std::move(planar_data); + reader_.reset(); + SetStatus(Status::kLoadedInMemory); +} + +std::unique_ptr ResonanceAudioAsset::AcquireReader() { + return std::move(reader_); +} + +bool ResonanceAudioAsset::IsActivelyStreaming() const { + return status_.load() == Status::kReadyForStreaming && reader_ == nullptr; +} + +void ResonanceAudioAsset::WaitForInitialization() { + std::unique_lock lock(status_change_mutex_); + status_change_conditional_.wait(lock, [this]() { + return status_.load() != ResonanceAudioAsset::Status::kWaitingForReader; + }); +} +} // namespace redux diff --git a/redux/redux/engines/audio/resonance/resonance_audio_asset.h b/redux/redux/engines/audio/resonance/resonance_audio_asset.h new file mode 100644 index 0000000..9fbfe3c --- /dev/null +++ b/redux/redux/engines/audio/resonance/resonance_audio_asset.h @@ -0,0 +1,108 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_AUDIO_RESONANCE_RESONANCE_AUDIO_ASSET_H_ +#define REDUX_ENGINES_AUDIO_RESONANCE_RESONANCE_AUDIO_ASSET_H_ + +#include +#include +#include + +#include "redux/engines/audio/audio_asset.h" +#include "redux/engines/audio/resonance/audio_planar_data.h" +#include "redux/modules/audio/audio_reader.h" + +namespace redux { + +// Container that stores information related to an audio asset. +// +// The audio data for the asset will either be streamed from an AudioReader, or +// stored in memory in an AudioPlanarData buffer. +class ResonanceAudioAsset : public AudioAsset { + public: + enum class Status { + // Uninitialized asset. + kWaitingForReader, + // Audio data can be streamed using the an AudioReader. + kReadyForStreaming, + // Audio data is fully decoded to into a memory buffer. + kLoadedInMemory, + // Initialization failed. + kInvalid, + }; + + // Constructor. + explicit ResonanceAudioAsset(Id id, bool stream_into_memory = false); + + // Returns the current status of the asset. + Status GetStatus() const { return status_.load(); } + + // Blocks the calling thread until the asset is ready for use as a stream + // source, or if an unexpected failure has occurred. + void WaitForInitialization(); + + // Returns true if the asset is initialized and valid for use. + bool IsValid() const; + + // Binds an audio stream reader to the asset. This function may be called + // asynchronously, but is needed to initialize the asset. + void SetAudioReader(std::unique_ptr reader); + + // Sets the audio data for the asset. + void SetAudioPlanarData(std::unique_ptr planar_data); + + // Releases the internal AudioReader to the caller. We cannot have concurrent + // users attempting to stream data from the same AudioReader, so this + // effectively locks this ResonanceAudioAsset to the caller. The caller must + // either call SetAudioReader with this reader, or call SetAudioPlanarData + // with the decoded data, once they have finished streaming the data. + std::unique_ptr AcquireReader(); + + // Returns the audio data for the asset. Returns nullptr if the audio data is + // not stored in memory (i.e. the asset is streaming). + const AudioPlanarData* GetPlanarData() const { return planar_data_.get(); } + + // Returns true if the asset is being streamed (i.e. someone has called + // AcquireReader()). + bool IsActivelyStreaming() const; + + // Should the process that is streaming the asset also attempt to save the + // deocded data. + bool ShouldStreamIntoMemory() const { return stream_into_memory_; } + + private: + // Updates the status of the asset. + void SetStatus(Status status); + + // Flag indicating whether the audio data should be cached into memory as it + // is being streamed. + const bool stream_into_memory_; + + // AudioReader for streaming audio data. + std::unique_ptr reader_; + + // Buffer containing uncompressed, planar audio data. + std::unique_ptr planar_data_; + + // Mutex and conditional variable to signal status changes. + std::atomic status_; + std::mutex status_change_mutex_; + std::condition_variable status_change_conditional_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_AUDIO_RESONANCE_RESONANCE_AUDIO_ASSET_H_ diff --git a/redux/redux/engines/audio/resonance/resonance_audio_engine.cc b/redux/redux/engines/audio/resonance/resonance_audio_engine.cc new file mode 100644 index 0000000..ff70a5d --- /dev/null +++ b/redux/redux/engines/audio/resonance/resonance_audio_engine.cc @@ -0,0 +1,478 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/audio/resonance/resonance_audio_engine.h" + +#include "redux/engines/audio/resonance/audio_stream_renderer.h" +#include "redux/engines/audio/resonance/audio_asset_stream.h" +#include "redux/engines/audio/resonance/resonance_sound.h" +#include "redux/engines/audio/resonance/resonance_utils.h" +#include "redux/engines/platform/device_manager.h" +#include "redux/modules/base/choreographer.h" +#include "redux/modules/base/static_registry.h" +#include "resonance_audio/dsp/channel_converter.h" +#include "resonance_audio/graph/resonance_audio_api_impl.h" +#include "platforms/common/room_effects_utils.h" +#include "resonance_audio/utils/planar_interleaved_conversion.h" +#include "resonance_audio/utils/sample_type_conversion.h" + +namespace redux { + +// Flag to enable a separate audio processing thread. +static const bool kCreateAudioThread = true; + +// Size of FIFO to transmit buffers between audio and processing thread. +#if defined(__ANDROID__) +static const size_t kAudioThreadFifoBufferSize = 4; +#else +const size_t kAudioThreadFifoBufferSize = 1; +#endif + +// Maximum waiting period until FIFO slots are available. It is set to a +// duration significantly larger than the duration of a single buffer to avoid +// the output of silence buffers when no processed audio buffer is available in +// the audio callback. +static const size_t kAudioThreadFifoMaxWaitTimeMs = 100; + +// Maximum number of sources that can be simultaneously created. +static const size_t kMaxNumberOfSoundSources = 512; + +ResonanceAudioEngine::ResonanceAudioEngine(Registry* registry) + : AudioEngine(registry), + resonance_(nullptr), + audio_running_(false), + audio_thread_task_queue_(kMaxNumberOfSoundSources) { + auto* choreographer = registry->Get(); + if (choreographer) { + choreographer->Add<&AudioEngine::Update>(Choreographer::Stage::kRender); + } +} + +ResonanceAudioEngine::~ResonanceAudioEngine() { + // Audio I/O must be stopped before destroying the AudioStreamRenderers. + Stop(); + + // The task queue holds AudioStreamRenderer instances that access resonance + // during destruction, so they need to be cleared first. + audio_thread_task_queue_.Clear(); +} + +void ResonanceAudioEngine::Start() { + if (audio_running_) { + return; + } + audio_running_ = true; + + if (!audio_stream_manager_) { + audio_stream_manager_ = std::make_unique(); + } + if (!audio_asset_manager_) { + audio_asset_manager_ = + std::make_unique(registry_, *speaker_profile_); + } + if (!resonance_) { + resonance_.reset(vraudio::CreateResonanceAudioApi( + vraudio::kNumStereoChannels, speaker_profile_->frames_per_buffer, + speaker_profile_->sample_rate_hz)); + // Disable room effects by default. + resonance_->EnableRoomEffects(false); + } + + if (kCreateAudioThread) { + fifo_.EnableBlockingSleepUntilMethods(true); + audio_thread_ = + std::thread(&ResonanceAudioEngine::AudioProcessingThread, this); + } + audio_stream_manager_->Start(); + + auto device_manager = registry_->Get(); + device_manager->SetFillAudioBufferFn([=](Speaker::HwBuffer hw_buffer) { + const size_t size = speaker_profile_->num_channels * + speaker_profile_->frames_per_buffer * sizeof(uint16); + CHECK_EQ(hw_buffer.size(), size); + auto data_ptr = reinterpret_cast(hw_buffer.data()); + OnMoreData(data_ptr, speaker_profile_->num_channels, + speaker_profile_->frames_per_buffer); + }); +} + +void ResonanceAudioEngine::Stop() { + if (!audio_running_) { + return; + } + audio_running_ = false; + + auto device_manager = registry_->Get(); + device_manager->SetFillAudioBufferFn(nullptr); + + for (auto& sound : sounds_) { + sound.second->Stop(); + } + + audio_stream_manager_->Stop(); + + if (kCreateAudioThread && audio_thread_.joinable()) { + fifo_.EnableBlockingSleepUntilMethods(false); + audio_thread_.join(); + } +} + +void ResonanceAudioEngine::Update() { + auto device_manager = registry_->Get(); + const auto* speaker_profile = device_manager->Speaker(0).GetProfile(); + if (speaker_profile != nullptr) { + if (speaker_profile_.has_value()) { + // We can't tolerate a change here for now. + CHECK_EQ(speaker_profile_->num_channels, speaker_profile->num_channels); + CHECK_EQ(speaker_profile_->sample_rate_hz, + speaker_profile->sample_rate_hz); + CHECK_EQ(speaker_profile_->frames_per_buffer, + speaker_profile->frames_per_buffer); + } else { + speaker_profile_.emplace(); + speaker_profile_->num_channels = speaker_profile->num_channels; + speaker_profile_->sample_rate_hz = speaker_profile->sample_rate_hz; + speaker_profile_->frames_per_buffer = speaker_profile->frames_per_buffer; + CHECK_GT(speaker_profile_->num_channels, 0); + } + + if (!audio_running_) { + Start(); + } + } else { + if (audio_running_) { + Stop(); + } + } + + std::vector pending_delete; + { + std::lock_guard lock(pending_delete_mutex_); + pending_delete_.swap(pending_delete); + } + for (auto& source : pending_delete) { + // Stopping the sound will call this->StopSound() which will erase it + // from the sounds_ map. But, more importantly, it will invalidate the + // Sound object itself, prevent it from being double-deleted. + auto sound = sounds_.find(source)->second; + sound->Stop(); + } +} + +SoundPtr ResonanceAudioEngine::CreateSound(AudioAssetPtr asset, + const SoundPlaybackParams& params) { + auto playback_asset = + audio_asset_manager_->GetAssetForPlayback(asset->GetId()); + if (!playback_asset) { + return nullptr; + } + + auto stream = std::make_unique(playback_asset, + speaker_profile_.value()); + if (!stream->Initialize()) { + LOG(ERROR) << "AudioAssetStream failed to initialize."; + return nullptr; + } + + stream->EnableLooping(params.looping); + // if (params.looping_crossfade_seconds >= 0.f) { + // stream->SetLoopCrossfadeDuration(params.loop_crossfade_seconds); + // } + + auto mode = vraudio::RenderingMode::kBinauralHighQuality; + auto renderer = std::make_shared( + params.type, std::move(stream), resonance_.get(), mode); + + renderer->SetVolume(params.volume); + + const SourceId source_id = renderer->GetSourceId(); + CHECK(!sounds_.contains(source_id)) << "Duplicate source id: " << source_id; + + auto task = [=]() { + const bool success = + audio_stream_manager_->AddAudioStreamRenderer(renderer); + CHECK(success) << "Renderer is already registered: " << source_id; + }; + audio_thread_task_queue_.Post(task); + + auto sound = std::make_shared(params.type, source_id, this); + sounds_[source_id] = sound; + return std::static_pointer_cast(sound); +} + +void ResonanceAudioEngine::PauseSound(SourceId source_id) { + if (sounds_.contains(source_id)) { + RunRendererTask(source_id, + [](AudioStreamRenderer& renderer) { renderer.Pause(); }); + } +} + +void ResonanceAudioEngine::ResumeSound(SourceId source_id) { + if (sounds_.contains(source_id)) { + RunRendererTask(source_id, + [](AudioStreamRenderer& renderer) { renderer.Resume(); }); + } +} + +void ResonanceAudioEngine::StopSound(SourceId source_id) { + if (sounds_.contains(source_id)) { + RunRendererTask(source_id, + [](AudioStreamRenderer& renderer) { renderer.Shutdown(); }); + { + std::lock_guard lock(pending_delete_mutex_); + pending_delete_.push_back(source_id); + } + } +} + +void ResonanceAudioEngine::SetSoundObjectPosition(SourceId source_id, + const vec3& position) { + resonance_->SetSourcePosition(source_id, position.x, position.y, position.z); +} + +void ResonanceAudioEngine::SetSoundObjectDistanceRolloffModel( + SourceId source_id, Sound::DistanceRolloffModel rolloff, float min_distance, + float max_distance) { + resonance_->SetSourceDistanceModel(source_id, ToResonance(rolloff), + min_distance, max_distance); + if (rolloff == Sound::DistanceRolloffModel::NoRollof) { + // No distance attenuation should be applied. + resonance_->SetSourceDistanceAttenuation(source_id, 1.0f); + } +} + +void ResonanceAudioEngine::SetSoundfieldRotation(SourceId source_id, + const quat& rotation) { + resonance_->SetSourceRotation(source_id, rotation.x, rotation.y, rotation.z, + rotation.w); +} + +void ResonanceAudioEngine::SetSoundObjectDirectivity(SourceId source_id, + float alpha, float order) { + resonance_->SetSoundObjectDirectivity(source_id, alpha, order); +} + +void ResonanceAudioEngine::SetSoundObjectRotation(SourceId source_id, + const quat& rotation) { + resonance_->SetSourceRotation(source_id, rotation.x, rotation.y, rotation.z, + rotation.w); +} + +void ResonanceAudioEngine::SetSoundVolume(SourceId source_id, float volume) { + if (sounds_.contains(source_id)) { + RunRendererTask(source_id, [=](AudioStreamRenderer& renderer) { + renderer.SetVolume(volume); + }); + } +} + +void ResonanceAudioEngine::SetGlobalVolume(float volume) { + resonance_->SetMasterVolume(volume); +} + +void ResonanceAudioEngine::SetListenerTransform(const vec3& position, + const quat& rotation) { + resonance_->SetHeadPosition(position.x, position.y, position.z); + resonance_->SetHeadRotation(rotation.x, rotation.y, rotation.z, rotation.w); +} + +void ResonanceAudioEngine::EnableRoom(const SoundRoom& room, + const vec3& position, + const quat& rotation) { + auto room_properties = ToResonance(room, position, rotation); + const auto reflection = vraudio::ComputeReflectionProperties(room_properties); + const auto reverb = vraudio::ComputeReverbProperties(room_properties); + + resonance_->SetReflectionProperties(reflection); + resonance_->SetReverbProperties(reverb); + resonance_->EnableRoomEffects(true); +} + +void ResonanceAudioEngine::DisableRoom() { + resonance_->EnableRoomEffects(false); +} + +void ResonanceAudioEngine::RunRendererTask(SourceId source_id, + RendererTask fn) { + auto task = [this, source_id, fn = std::move(fn)]() { + auto renderer = audio_stream_manager_->GetAudioStreamRenderer(source_id); + if (renderer != nullptr) { + fn(*renderer); + } else { + std::lock_guard lock(pending_delete_mutex_); + pending_delete_.push_back(source_id); + } + }; + audio_thread_task_queue_.Post(task); +} + +void ResonanceAudioEngine::OnMoreData(int16_t* buffer_ptr, size_t num_channels, + size_t num_frames) { + const size_t num_samples = num_channels * num_frames; + if (num_frames != speaker_profile_->frames_per_buffer) { + LOG(ERROR) << "OnMoreData called with unexpected frames per buffer size: " + << num_frames; + std::fill(buffer_ptr, buffer_ptr + num_samples, 0); + return; + } + + const vraudio::AudioBuffer* export_buffer = nullptr; + std::unique_ptr audio_buffer_from_fifo; + if (kCreateAudioThread) { + // Wait till we have a buffer for sure. + const std::chrono::steady_clock::duration wait_time = + std::chrono::milliseconds(kAudioThreadFifoMaxWaitTimeMs); + if (audio_running_ && fifo_.SleepUntilNumElementsInQueue(1, wait_time)) { + audio_buffer_from_fifo = fifo_.PopFront(); + export_buffer = audio_buffer_from_fifo.get(); + } + } else { + export_buffer = BinauralProcessingOfSoundSources(); + } + + if (export_buffer == nullptr) { + std::fill(buffer_ptr, buffer_ptr + num_samples, 0); + } else if (num_channels == vraudio::kNumStereoChannels) { + ExportToStereoBuffer(*export_buffer, buffer_ptr, num_channels, num_frames); + } else if (num_channels == vraudio::kNumMonoChannels) { + DownmixToMonoBuffer(*export_buffer, buffer_ptr, num_channels, num_frames); + } else { + UpmixToSurroundBuffer(*export_buffer, buffer_ptr, num_channels, num_frames); + } +} + +void ResonanceAudioEngine::ExportToStereoBuffer( + const vraudio::AudioBuffer& input, int16_t* output, size_t num_channels, + size_t num_frames) { + FillExternalBuffer(input, output, num_frames, num_channels); +} + +void ResonanceAudioEngine::DownmixToMonoBuffer( + const vraudio::AudioBuffer& input, int16_t* output, size_t num_channels, + size_t num_frames) { + if (!mono_buffer_) { + mono_buffer_ = std::make_unique( + vraudio::kNumMonoChannels, speaker_profile_->frames_per_buffer); + } + ConvertMonoFromStereo(input, mono_buffer_.get()); + FillExternalBuffer(*mono_buffer_, output, num_frames, num_channels); +} + +void ResonanceAudioEngine::UpmixToSurroundBuffer( + const vraudio::AudioBuffer& input, int16_t* output, size_t num_channels, + size_t num_frames) { + const size_t num_samples = num_channels * num_frames; + std::fill(output, output + num_samples, 0); + + for (size_t channel = 0; channel < vraudio::kNumStereoChannels; ++channel) { + const auto& channel_view = input[channel]; + + size_t interleaved_index = channel; + for (size_t frame = 0; frame < num_frames; ++frame) { + vraudio::ConvertSampleFromFloatFormat(channel_view[frame], + &output[interleaved_index]); + interleaved_index += num_channels; + } + } +} + +const vraudio::AudioBuffer* +ResonanceAudioEngine::BinauralProcessingOfSoundSources() { + audio_thread_task_queue_.Execute(); + + std::vector disabled_renderers; + audio_stream_manager_->Render(&disabled_renderers); + { + std::lock_guard lock(pending_delete_mutex_); + pending_delete_.insert(pending_delete_.end(), disabled_renderers.begin(), + disabled_renderers.end()); + } + + auto impl = static_cast(resonance_.get()); + impl->ProcessNextBuffer(); + return impl->GetStereoOutputBuffer(); +} + +void ResonanceAudioEngine::AudioProcessingThread() { + const std::chrono::steady_clock::duration wait_time = + std::chrono::milliseconds(kAudioThreadFifoMaxWaitTimeMs); + + while (audio_running_) { + if (fifo_.SleepUntilBelowSizeTarget(kAudioThreadFifoBufferSize, + wait_time)) { + const vraudio::AudioBuffer* new_audio_buffer = + BinauralProcessingOfSoundSources(); + if (new_audio_buffer != nullptr) { + // Since |new_audio_buffer| is owned and reused by the audio graph, a + // copy is needed to push it to the output |fifo_|. + auto audio_buffer_copy = std::make_unique(); + *audio_buffer_copy = *new_audio_buffer; + fifo_.PushBack(std::move(audio_buffer_copy)); + } else { + // This will generate a silence output. + fifo_.PushBack(nullptr); + } + } + } +} + +// The following functions thunk the AudioEngine API to the ResonanceAudioEngine +// implementation. + +inline ResonanceAudioEngine* Upcast(AudioEngine* ptr) { + return static_cast(ptr); +} +void AudioEngine::Create(Registry* registry) { + auto ptr = new ResonanceAudioEngine(registry); + registry->Register(std::unique_ptr(ptr)); +} +void AudioEngine::SetGlobalVolume(float volume) { + Upcast(this)->SetGlobalVolume(volume); +} +void AudioEngine::EnableRoom(const SoundRoom& room, const vec3& position, + const quat& rotation) { + Upcast(this)->EnableRoom(room, position, rotation); +} +void AudioEngine::DisableRoom() { Upcast(this)->DisableRoom(); } +void AudioEngine::SetListenerTransform(const vec3& position, + const quat& rotation) { + Upcast(this)->SetListenerTransform(position, rotation); +} +void AudioEngine::Update() { Upcast(this)->Update(); } +AudioAssetPtr AudioEngine::GetAudioAsset(HashValue key) { + return Upcast(this)->GetAudioAssetManager()->FindAudioAsset(key); +} +AudioAssetPtr AudioEngine::LoadAudioAsset(std::string_view uri, + StreamingPolicy policy) { + return Upcast(this)->GetAudioAssetManager()->CreateAudioAsset(uri, policy); +} +void AudioEngine::UnloadAudioAsset(HashValue key) { + Upcast(this)->GetAudioAssetManager()->UnloadAudioAsset(key); +} +SoundPtr AudioEngine::PrepareSound(AudioAssetPtr asset, + const SoundPlaybackParams& params) { + return Upcast(this)->CreateSound(asset, params); +} +SoundPtr AudioEngine::PlaySound(AudioAssetPtr asset, + const SoundPlaybackParams& params) { + SoundPtr sound = PrepareSound(asset, params); + sound->Resume(); + return sound; +} + +static StaticRegistry Static_Register(AudioEngine::Create); + +} // namespace redux diff --git a/redux/redux/engines/audio/resonance/resonance_audio_engine.h b/redux/redux/engines/audio/resonance/resonance_audio_engine.h new file mode 100644 index 0000000..e853412 --- /dev/null +++ b/redux/redux/engines/audio/resonance/resonance_audio_engine.h @@ -0,0 +1,200 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_AUDIO_RESONANCE_RESONANCE_AUDIO_ENGINE_H_ +#define REDUX_ENGINES_AUDIO_RESONANCE_RESONANCE_AUDIO_ENGINE_H_ + +#include +#include +#include +#include + +#include "redux/engines/audio/audio_engine.h" +#include "redux/engines/audio/resonance/audio_asset_manager.h" +#include "redux/engines/audio/resonance/audio_stream_manager.h" +#include "redux/engines/audio/resonance/resonance_sound.h" +#include "redux/engines/platform/device_profiles.h" +#include "redux/modules/base/registry.h" +#include "resonance_audio/api/resonance_audio_api.h" +#include "resonance_audio/base/constants_and_types.h" +#include "resonance_audio/utils/lockless_task_queue.h" +#include "resonance_audio/utils/semi_lockless_fifo.h" + +namespace redux { + +// Audio engine that spatializes sound sources in 3D space, including distance +// and height cues, using the Resonance Audio library. +// +// The engine supports three types of sounds: +// +// 1. Sound Object: a sound source in 3D space. These sources, while +// spatialized, are fed with mono audio data. +// +// 2. Ambisonic Soundfield: multi-channel audio files which are spatialized all +// around the listener in 360 degrees. These can be thought of as recorded or +// prebaked soundfields. They can be of great use for background effects which +// sound perfectly spatial. Examples include rain noise, crowd noise or even the +// sound of the ocean off to one side. +// +// 3. Stereo Sounds: non-spatialized mono or stereo audio files played directly. +// This is useful for music and other such audio. +// +// Audio assets can be loaded using the AudioAssetManager. Once an asset is +// loaded, it can be used to create individual Sound instances that can be +// played. +// +// The ResonanceAudioEngine owns the "audio" thread on which audio rendering +// is performed using the AudioStreamManager. +class ResonanceAudioEngine : public AudioEngine { + public: + using SourceId = vraudio::SourceId; + + explicit ResonanceAudioEngine(Registry* registry); + ~ResonanceAudioEngine(); + + // Must be called from the main thread at a regular rate. It is used to + // execute background operations outside of the audio thread. + void Update(); + + // Creates and returns a new Sound object using the asset as a source. + // + // For Point sounds, the asset should contain a single (mono) audio channel. + // For ambisonic Field sounds, the asset must have 4 separate audio channels. + SoundPtr CreateSound(AudioAssetPtr asset, const SoundPlaybackParams& params); + + // Pauses the playback of a sound. + void PauseSound(SourceId source_id); + + // Resumes the playback of a sound. + void ResumeSound(SourceId source_id); + + // Stops the playback of a sound and invalidates the source. + void StopSound(SourceId source_id); + + // Repositions an existing sound object. + void SetSoundObjectPosition(SourceId source_id, const vec3& position); + + // Sets the given sound object source's distance attenuation method with + // minimum and maximum distances. Maximum distance must be greater than the + // minimum distance for the method to be set. + void SetSoundObjectDistanceRolloffModel(SourceId source_id, + Sound::DistanceRolloffModel rolloff, + float min_distance, + float max_distance); + + // Sets the given ambisonic soundfields's rotation. + void SetSoundfieldRotation(SourceId source, const quat& rotation); + + // Sets the given sound object's directivity pattern. + // + // `alpha` specifies the weighting balance between figure of eight pattern and + // omnidirectional pattern for source emission in range [0, 1]. A value of 0.5 + // results in a cardioid pattern. + // + // `order`is the order applied to computed directivity. Higher values will + // result in narrower and sharper directivity patterns. Range [1, inf). + void SetSoundObjectDirectivity(SourceId source_id, float alpha, float order); + + // Sets the given sound object's rotation. Note: This method only has an + // audible effect if the directivity pattern of the sound object is first set. + void SetSoundObjectRotation(SourceId source_id, const quat& rotation); + + // Changes the volume of an existing sound. + void SetSoundVolume(SourceId source_id, float volume); + + // Sets the global volume of the main audio output. Specify a volume (linear) + // amplitude in range [0, 1] for attenuation, range [1, inf) for gain boost. + void SetGlobalVolume(float volume); + + // Sets the position and rotation of the "listener" from which spatialized + // sounds will be related. + void SetListenerTransform(const vec3& position, const quat& rotation); + + // Turns on/off the room reverberation effect. + void EnableRoom(const SoundRoom& room, const vec3& position, + const quat& rotation); + void DisableRoom(); + + // Returrns the AudioAssetManager used for managing audio assets. + AudioAssetManager* GetAudioAssetManager() { + return audio_asset_manager_.get(); + } + + // Gets the underlying ResonanceAudioApi object for more functionality. + vraudio::ResonanceAudioApi* GetResonanceAudioApi() { + return resonance_.get(); + } + + private: + // Starts the audio playback. + void Start(); + + // Stops the audio playback. + void Stop(); + + // Callback from audio device requesting more audio data. + void OnMoreData(int16_t* buffer_ptr, size_t num_channels, size_t num_frames); + + void ExportToStereoBuffer(const vraudio::AudioBuffer& input, int16_t* output, + size_t num_channels, size_t num_frames); + void DownmixToMonoBuffer(const vraudio::AudioBuffer& input, int16_t* output, + size_t num_channels, size_t num_frames); + void UpmixToSurroundBuffer(const vraudio::AudioBuffer& input, int16_t* output, + size_t num_channels, size_t num_frames); + + // Performs an operation on the AudioStreamRenderer on the audio thread. + using RendererTask = std::function; + void RunRendererTask(SourceId source_id, RendererTask fn); + + // Adds new audio buffers for each source to |ResonanceAudioApi| and triggers + // the audio graph processing to obtain a binaurally rendered stereo + // |AudioBuffer|. + const vraudio::AudioBuffer* BinauralProcessingOfSoundSources(); + + // Performs audio processing in a separate thread and outputs to |fifo_|. + void AudioProcessingThread(); + + std::unique_ptr resonance_; + std::unique_ptr audio_asset_manager_; + std::unique_ptr audio_stream_manager_; + + std::optional speaker_profile_; + + absl::flat_hash_map> sounds_; + + // Audio processing thread. + std::thread audio_thread_; + std::atomic audio_running_; + + // Sources that have been completed on the audio thread are marked for + // deletion on the main thread. + mutable std::mutex pending_delete_mutex_; + std::vector pending_delete_; + + // Task queue of operations to be executed on the audio thread. + mutable vraudio::LocklessTaskQueue audio_thread_task_queue_; + + // FIFO queue to transport processed buffers between the processing thread and + // the audio thread. + vraudio::SemiLocklessFifo> fifo_; + + // Temporary AudioBuffer to for downmixing audio to mono output. + std::unique_ptr mono_buffer_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_AUDIO_RESONANCE_RESONANCE_AUDIO_ENGINE_H_ diff --git a/redux/redux/engines/audio/resonance/resonance_sound.cc b/redux/redux/engines/audio/resonance/resonance_sound.cc new file mode 100644 index 0000000..a5c41d7 --- /dev/null +++ b/redux/redux/engines/audio/resonance/resonance_sound.cc @@ -0,0 +1,107 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/audio/resonance/resonance_sound.h" + +#include "redux/engines/audio/resonance/resonance_audio_engine.h" + +namespace redux { + +ResonanceSound::ResonanceSound(SoundType type, Id id, + ResonanceAudioEngine* engine) + : Sound(type), id_(id), engine_(engine) {} + +ResonanceSound::~ResonanceSound() { + if (engine_) { + Stop(); + } +} + +bool ResonanceSound::IsValid() const { return engine_ != nullptr; } + +void ResonanceSound::Resume() { + engine_->ResumeSound(id_); + playing_ = true; +} + +void ResonanceSound::Pause() { + engine_->PauseSound(id_); + playing_ = false; +} + +void ResonanceSound::Stop() { + if (engine_) { + engine_->StopSound(id_); + playing_ = false; + engine_ = nullptr; + } +} + +void ResonanceSound::SetVolume(float volume) { + return engine_->SetSoundVolume(id_, volume); +} + +bool ResonanceSound::IsPlaying() const { return playing_; } + +void ResonanceSound::SetTransform(const Transform& transform) { + if (type_ == SoundType::Point) { + const vec3& pos = transform.translation; + const quat& rot = transform.rotation; + engine_->SetSoundObjectPosition(id_, pos); + engine_->SetSoundObjectRotation(id_, rot); + } else if (type_ == SoundType::Field) { + const quat& rot = transform.rotation; + engine_->SetSoundfieldRotation(id_, rot); + } +} + +void ResonanceSound::SetDirectivitiy(float alpha, float order) { + engine_->SetSoundObjectDirectivity(id_, alpha, order); +} + +void ResonanceSound::SetDistanceRolloffModel(DistanceRolloffModel rolloff, + float min_distance, + float max_distance) { + engine_->SetSoundObjectDistanceRolloffModel(id_, rolloff, min_distance, + max_distance); +} + +inline ResonanceSound* Upcast(Sound* ptr) { + return static_cast(ptr); +} + +inline const ResonanceSound* Upcast(const Sound* ptr) { + return static_cast(ptr); +} + +bool Sound::IsValid() const { return Upcast(this)->IsValid(); } +void Sound::Resume() { Upcast(this)->Resume(); } +void Sound::Pause() { Upcast(this)->Pause(); } +void Sound::Stop() { Upcast(this)->Stop(); } +void Sound::SetVolume(float volume) { Upcast(this)->SetVolume(volume); } +bool Sound::IsPlaying() const { return Upcast(this)->IsPlaying(); } +void Sound::SetTransform(const Transform& transform) { + Upcast(this)->SetTransform(transform); +} +void Sound::SetDirectivitiy(float alpha, float order) { + Upcast(this)->SetDirectivitiy(alpha, order); +} +void Sound::SetDistanceRolloffModel(DistanceRolloffModel rolloff, + float min_distance, float max_distance) { + Upcast(this)->SetDistanceRolloffModel(rolloff, min_distance, max_distance); +} + +} // namespace redux diff --git a/redux/redux/engines/audio/resonance/resonance_sound.h b/redux/redux/engines/audio/resonance/resonance_sound.h new file mode 100644 index 0000000..f5e253c --- /dev/null +++ b/redux/redux/engines/audio/resonance/resonance_sound.h @@ -0,0 +1,70 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_AUDIO_RESONANCE_RESONANCE_SOUND_H_ +#define REDUX_ENGINES_AUDIO_RESONANCE_RESONANCE_SOUND_H_ + +#include "redux/engines/audio/sound.h" + +namespace redux { + +class ResonanceAudioEngine; + +// Sounds are simply handles to "sources" that are managed by the VrAudioEngine. +class ResonanceSound : public Sound { + public: + using Id = int; + + ResonanceSound(SoundType type, Id id, ResonanceAudioEngine* engine); + ~ResonanceSound(); + + // Returns true if the Sound is valid (i.e. loaded in the AudioEngine). + bool IsValid() const; + + // Resumes (or starts) playing the sound. + void Resume(); + + // Pauses the sound that is playing. + void Pause(); + + // Stops the sound from playing, effectivey invalidating it. + void Stop(); + + // Sets the volume of the sound, ranging from from 0 (mute) to 1 (max). + void SetVolume(float volume); + + // Returns true if the Sound is playing. + bool IsPlaying() const; + + // Sets the position and rotation of the sound. + void SetTransform(const Transform& transform); + + // Sets the directivity of the sound. + void SetDirectivitiy(float alpha, float order); + + // Sets the distance rolloff model for the sound and the distances at which + // to apply the model. + void SetDistanceRolloffModel(DistanceRolloffModel rolloff, float min_distance, + float max_distance); + + private: + const Id id_; + bool playing_ = false; + ResonanceAudioEngine* engine_ = nullptr; +}; +} // namespace redux + +#endif // REDUX_ENGINES_AUDIO_RESONANCE_RESONANCE_SOUND_H_ diff --git a/redux/redux/engines/audio/resonance/resonance_utils.cc b/redux/redux/engines/audio/resonance/resonance_utils.cc new file mode 100644 index 0000000..a0e6d4c --- /dev/null +++ b/redux/redux/engines/audio/resonance/resonance_utils.cc @@ -0,0 +1,116 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/audio/resonance/resonance_utils.h" + +#include "platforms/common/room_properties.h" + +namespace redux { + +vraudio::MaterialName ToResonance(AudioSurfaceMaterial type) { + switch (type) { + case AudioSurfaceMaterial::Transparent: + return vraudio::MaterialName::kTransparent; + case AudioSurfaceMaterial::AcousticCeilingTiles: + return vraudio::MaterialName::kAcousticCeilingTiles; + case AudioSurfaceMaterial::BrickBare: + return vraudio::MaterialName::kBrickBare; + case AudioSurfaceMaterial::BrickPainted: + return vraudio::MaterialName::kBrickPainted; + case AudioSurfaceMaterial::ConcreteBlockCoarse: + return vraudio::MaterialName::kConcreteBlockCoarse; + case AudioSurfaceMaterial::ConcreteBlockPainted: + return vraudio::MaterialName::kConcreteBlockPainted; + case AudioSurfaceMaterial::CurtainHeavy: + return vraudio::MaterialName::kCurtainHeavy; + case AudioSurfaceMaterial::FiberGlassInsulation: + return vraudio::MaterialName::kFiberGlassInsulation; + case AudioSurfaceMaterial::GlassThin: + return vraudio::MaterialName::kGlassThin; + case AudioSurfaceMaterial::GlassThick: + return vraudio::MaterialName::kGlassThick; + case AudioSurfaceMaterial::Grass: + return vraudio::MaterialName::kGrass; + case AudioSurfaceMaterial::LinoleumOnConcrete: + return vraudio::MaterialName::kLinoleumOnConcrete; + case AudioSurfaceMaterial::Marble: + return vraudio::MaterialName::kMarble; + case AudioSurfaceMaterial::Metal: + return vraudio::MaterialName::kMetal; + case AudioSurfaceMaterial::ParquetOnConcrete: + return vraudio::MaterialName::kParquetOnConcrete; + case AudioSurfaceMaterial::PlasterRough: + return vraudio::MaterialName::kPlasterRough; + case AudioSurfaceMaterial::PlasterSmooth: + return vraudio::MaterialName::kPlasterSmooth; + case AudioSurfaceMaterial::PlywoodPanel: + return vraudio::MaterialName::kPlywoodPanel; + case AudioSurfaceMaterial::PolishedConcreteOrTile: + return vraudio::MaterialName::kPolishedConcreteOrTile; + case AudioSurfaceMaterial::Sheetrock: + return vraudio::MaterialName::kSheetrock; + case AudioSurfaceMaterial::WaterOrIceSurface: + return vraudio::MaterialName::kWaterOrIceSurface; + case AudioSurfaceMaterial::WoodCeiling: + return vraudio::MaterialName::kWoodCeiling; + case AudioSurfaceMaterial::WoodPanel: + return vraudio::MaterialName::kWoodPanel; + default: + LOG(FATAL) << "Unknown surface material"; + } + return vraudio::MaterialName::kTransparent; +} + +vraudio::DistanceRolloffModel ToResonance(Sound::DistanceRolloffModel model) { + switch (model) { + case Sound::NoRollof: + return vraudio::DistanceRolloffModel::kNone; + case Sound::LinearRolloff: + return vraudio::DistanceRolloffModel::kLinear; + case Sound::LogarithmicRolloff: + return vraudio::DistanceRolloffModel::kLogarithmic; + default: + CHECK(false); + return vraudio::DistanceRolloffModel::kNone; + } +} + +vraudio::RoomProperties ToResonance(const SoundRoom& room, const vec3& position, + const quat& rotation) { + vraudio::RoomProperties resonance; + resonance.position[0] = position.x; + resonance.position[1] = position.y; + resonance.position[2] = position.z; + + resonance.rotation[0] = rotation.x; + resonance.rotation[1] = rotation.y; + resonance.rotation[2] = rotation.z; + resonance.rotation[3] = rotation.w; + + resonance.dimensions[0] = room.size.x; + resonance.dimensions[1] = room.size.y; + resonance.dimensions[2] = room.size.z; + + for (int i = 0; i < SoundRoom::kNumWalls; ++i) { + resonance.material_names[i] = ToResonance(room.surface_materials[i]); + } + + resonance.reverb_gain = room.reverb_gain; + resonance.reverb_time = room.reverb_time; + resonance.reverb_brightness = room.reverb_brightness; + return resonance; +} +} // namespace redux diff --git a/redux/redux/engines/audio/resonance/resonance_utils.h b/redux/redux/engines/audio/resonance/resonance_utils.h new file mode 100644 index 0000000..9db04a0 --- /dev/null +++ b/redux/redux/engines/audio/resonance/resonance_utils.h @@ -0,0 +1,40 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_AUDIO_RESONANCE_RESONANCE_UTILS_H_ +#define REDUX_ENGINES_AUDIO_RESONANCE_RESONANCE_UTILS_H_ + +#include "redux/engines/audio/sound.h" +#include "redux/engines/audio/sound_room.h" +#include "redux/modules/audio/enums.h" +#include "resonance_audio/api/resonance_audio_api.h" +#include "platforms/common/room_properties.h" +#include "redux/modules/audio/audio_reader.h" + +namespace redux { + +// Returns the vraudio MaterialName matching the AudioSurfaceMaterial. +vraudio::MaterialName ToResonance(AudioSurfaceMaterial type); + +// Returns the vraudio DistanceRolloffModel matching the DistanceRolloffModel. +vraudio::DistanceRolloffModel ToResonance(Sound::DistanceRolloffModel model); + +// Returns the vraudio RoomPropereties matching the SoundRoom parameters. +vraudio::RoomProperties ToResonance(const SoundRoom& room, const vec3& position, + const quat& rotation); +} // namespace redux + +#endif // REDUX_ENGINES_AUDIO_RESONANCE_RESONANCE_UTILS_H_ diff --git a/redux/redux/engines/audio/sound.h b/redux/redux/engines/audio/sound.h new file mode 100644 index 0000000..4153613 --- /dev/null +++ b/redux/redux/engines/audio/sound.h @@ -0,0 +1,80 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_AUDIO_SOUND_H_ +#define REDUX_ENGINES_AUDIO_SOUND_H_ + +#include + +#include "redux/modules/audio/enums.h" +#include "redux/modules/math/transform.h" + +namespace redux { + +// A sound that is being played by the AudioEngine. +class Sound { + public: + virtual ~Sound() = default; + + Sound(const Sound&) = delete; + Sound& operator=(const Sound&) = delete; + + // Models used for distance attenuation. + enum DistanceRolloffModel { + NoRollof, + LinearRolloff, + LogarithmicRolloff, + }; + + // Returns true if the Sound is valid (i.e. loaded in the AudioEngine). + bool IsValid() const; + + // Resumes (or starts) playing the sound. + void Resume(); + + // Pauses the sound that is playing. + void Pause(); + + // Stops the sound from playing, effectivey invalidating it. + void Stop(); + + // Sets the volume of the sound, ranging from from 0 (mute) to 1 (max). + void SetVolume(float volume); + + // Returns true if the Sound is playing. + bool IsPlaying() const; + + // Sets the position and rotation of the sound. + void SetTransform(const Transform& transform); + + // Sets the directivity of the sound. + void SetDirectivitiy(float alpha, float order); + + // Sets the distance rolloff model for the sound and the distances at which + // to apply the model. + void SetDistanceRolloffModel(DistanceRolloffModel rolloff, float min_distance, + float max_distance); + + protected: + explicit Sound(SoundType type) : type_(type) {} + SoundType type_; +}; + +using SoundPtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_AUDIO_SOUND_H_ diff --git a/redux/redux/engines/audio/sound_room.h b/redux/redux/engines/audio/sound_room.h new file mode 100644 index 0000000..7223ddb --- /dev/null +++ b/redux/redux/engines/audio/sound_room.h @@ -0,0 +1,66 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_AUDIO_SOUND_ROOM_H_ +#define REDUX_ENGINES_AUDIO_SOUND_ROOM_H_ + +#include "redux/modules/audio/enums.h" +#include "redux/modules/math/vector.h" + +namespace redux { + +// Properties for a shoebox room that affect how sound propogates when the +// listener is inside the room. +struct SoundRoom { + enum WallTypes { + kLeftWall, + kRightWall, + kBottomWall, + kTopWall, + kFrontWall, + kBackWall, + kNumWalls, + }; + + // Size of the shoebox room in world space. + vec3 size = vec3::Zero(); + + // Material name of each surface of the room. + AudioSurfaceMaterial surface_materials[kNumWalls] = { + AudioSurfaceMaterial::Transparent, AudioSurfaceMaterial::Transparent, + AudioSurfaceMaterial::Transparent, AudioSurfaceMaterial::Transparent, + AudioSurfaceMaterial::Transparent, AudioSurfaceMaterial::Transparent, + }; + + // User defined uniform scaling factor for all reflection coefficients. + float reflection_scalar = 1.0f; + + // User defined reverb tail gain multiplier. + float reverb_gain = 1.0f; + + // Adjusts the reverberation time across all frequency bands. RT60 values + // are multiplied by this factor. Has no effect when set to 1.0f. + float reverb_time = 1.0f; + + // Controls the slope of a line from the lowest to the highest RT60 values + // (increases high frequency RT60s when positive, decreases when negative). + // Has no effect when set to 0.0f. + float reverb_brightness = 0.0f; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_AUDIO_SOUND_ROOM_H_ diff --git a/redux/redux/engines/physics/BUILD b/redux/redux/engines/physics/BUILD new file mode 100644 index 0000000..1c355cd --- /dev/null +++ b/redux/redux/engines/physics/BUILD @@ -0,0 +1,53 @@ +# Physics engine. + +load( + "//redux/tools:flatbuffer_cc_library.bzl", + "flatbuffer_cc_library", +) + +licenses(["notice"]) + +package( + default_visibility = ["//redux:visibility"], +) + +cc_library( + name = "physics", + srcs = [ + "collision_data.cc", + ], + hdrs = [ + "collision_data.h", + "collision_shape.h", + "physics_engine.h", + "rigid_body.h", + "trigger_volume.h", + ], + deps = [ + ":enums", + "@absl//absl/time", + "//redux/modules/base:bits", + "//redux/modules/base:data_container", + "//redux/modules/base:registry", + "//redux/modules/base:resource_manager", + "//redux/modules/ecs:entity", + "//redux/modules/math:bounds", + "//redux/modules/math:quaternion", + "//redux/modules/math:transform", + "//redux/modules/math:vector", + ], +) + +flatbuffer_cc_library( + name = "physics_enums_fbs", + srcs = ["physics_enums.fbs"], +) + +cc_library( + name = "enums", + hdrs = ["enums.h"], + deps = [ + ":physics_enums_fbs", + "//redux/modules/base:typeid", + ], +) diff --git a/redux/redux/engines/physics/bullet/BUILD b/redux/redux/engines/physics/bullet/BUILD new file mode 100644 index 0000000..bb9d37e --- /dev/null +++ b/redux/redux/engines/physics/bullet/BUILD @@ -0,0 +1,41 @@ +# Bullet physics backend for PhysicsEngine. + +licenses(["notice"]) + +package( + default_visibility = ["//redux:visibility"], +) + +cc_library( + name = "bullet", + srcs = [ + "bullet_collision_shape.cc", + "bullet_physics_engine.cc", + "bullet_rigid_body.cc", + "bullet_trigger_volume.cc", + "thunks.cc", + ], + hdrs = [ + "bullet_collision_shape.h", + "bullet_physics_engine.h", + "bullet_rigid_body.h", + "bullet_trigger_volume.h", + "bullet_utils.h", + ], + deps = [ + "@absl//absl/base", + "@bullet//:BulletCollision", + "@bullet//:BulletDynamics", + "@bullet//:LinearMath", + "//redux/engines/physics", + "//redux/engines/physics/thunks", + "//redux/modules/base:bits", + "//redux/modules/base:choreographer", + "//redux/modules/base:static_registry", + "//redux/modules/ecs:entity", + "//redux/modules/math:bounds", + "//redux/modules/math:quaternion", + "//redux/modules/math:transform", + "//redux/modules/math:vector", + ], +) diff --git a/redux/redux/engines/physics/bullet/bullet_collision_shape.cc b/redux/redux/engines/physics/bullet/bullet_collision_shape.cc new file mode 100644 index 0000000..ec3243c --- /dev/null +++ b/redux/redux/engines/physics/bullet/bullet_collision_shape.cc @@ -0,0 +1,123 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/physics/bullet/bullet_collision_shape.h" + +#include + +#include "BulletCollision/CollisionShapes/btBoxShape.h" +#include "BulletCollision/CollisionShapes/btCompoundShape.h" +#include "BulletCollision/CollisionShapes/btSphereShape.h" +#include "BulletCollision/CollisionShapes/btTriangleIndexVertexArray.h" +#include "BulletCollision/CollisionShapes/btUniformScalingShape.h" +#include "BulletCollision/Gimpact/btGImpactShape.h" +#include "redux/engines/physics/bullet/bullet_utils.h" + +namespace redux { + +BulletCollisionShape::BulletCollisionShape(std::shared_ptr data) + : data_(std::move(data)) { + CHECK(data_); + + const int num_shapes = data_->GetNumParts(); + CHECK_GT(num_shapes, 0); + + bool single_shape_at_origin = true; + if (num_shapes > 1) { + single_shape_at_origin = false; + } else if (data_->GetPosition(0) != vec3::Zero()) { + single_shape_at_origin = false; + } else if (data_->GetRotation(0) != quat::Identity()) { + single_shape_at_origin = false; + } + + btCompoundShape* compound = nullptr; + if (!single_shape_at_origin) { + compound = new btCompoundShape(true, num_shapes); + shapes_.emplace_back(compound); + bt_shape_ = compound; + } + + for (int i = 0; i < num_shapes; ++i) { + btCollisionShape* shape = nullptr; + const auto type = data_->GetPartType(i); + switch (type) { + case CollisionData::kBox: + shape = AddBoxShape(data_->GetBoxHalfExtents(i)); + break; + case CollisionData::kSphere: + shape = AddSphereShape(data_->GetSphereRadius(i)); + break; + case CollisionData::kMesh: + shape = AddMeshShape(data_->GetCollisionMesh(i)); + break; + case CollisionData::kNone: + LOG(FATAL) << "No shape."; + break; + } + + if (single_shape_at_origin) { + bt_shape_ = shape; + } else { + const btVector3 bt_position = ToBullet(data_->GetPosition(i)); + const btQuaternion bt_rotation = ToBullet(data_->GetRotation(i)); + const btTransform bt_transform(bt_rotation, bt_position); + compound->addChildShape(bt_transform, shape); + } + } + if (compound) { + compound->recalculateLocalAabb(); + } +} + +btCollisionShape* BulletCollisionShape::AddBoxShape(const vec3& half_extents) { + btVector3 bt_extents(half_extents.x, half_extents.y, half_extents.z); + auto ptr = new btBoxShape(bt_extents); + shapes_.emplace_back(ptr); + return ptr; +} + +btCollisionShape* BulletCollisionShape::AddSphereShape(float radius) { + auto ptr = new btSphereShape(radius); + shapes_.emplace_back(ptr); + return ptr; +} + +btCollisionShape* BulletCollisionShape::AddMeshShape( + const CollisionMesh& mesh) { + static constexpr size_t kVerticesPerTriangle = 3; + static constexpr size_t kFloatsPerVertex = 3; + static constexpr size_t kTriangleStride = kVerticesPerTriangle * sizeof(int); + static constexpr size_t kVertexStride = kFloatsPerVertex * sizeof(float); + + const int* triangles = reinterpret_cast(mesh.indices.GetBytes()); + const size_t num_triangles = mesh.indices.GetNumBytes() / kTriangleStride; + const float* vertices = + reinterpret_cast(mesh.vertices.GetBytes()); + const size_t num_vertices = mesh.indices.GetNumBytes() / kVertexStride; + + btTriangleIndexVertexArray* vertex_array = new btTriangleIndexVertexArray( + num_triangles, const_cast(triangles), kTriangleStride, num_vertices, + const_cast(vertices), kVertexStride); + vertices_.emplace_back(vertex_array); + + auto ptr = new btGImpactMeshShape(vertex_array); + ptr->postUpdate(); + ptr->updateBound(); + shapes_.emplace_back(ptr); + return ptr; +} +} // namespace redux diff --git a/redux/redux/engines/physics/bullet/bullet_collision_shape.h b/redux/redux/engines/physics/bullet/bullet_collision_shape.h new file mode 100644 index 0000000..68ba965 --- /dev/null +++ b/redux/redux/engines/physics/bullet/bullet_collision_shape.h @@ -0,0 +1,53 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PHYSICS_BULLET_BULLET_COLLISION_SHAPE_H_ +#define REDUX_ENGINES_PHYSICS_BULLET_BULLET_COLLISION_SHAPE_H_ + +#include +#include + +#include "BulletCollision/CollisionShapes/btCollisionShape.h" +#include "BulletCollision/CollisionShapes/btTriangleIndexVertexArray.h" +#include "redux/engines/physics/collision_data.h" +#include "redux/engines/physics/collision_shape.h" + +namespace redux { + +class BulletCollisionShape : public CollisionShape { + public: + explicit BulletCollisionShape(std::shared_ptr data); + + btCollisionShape* GetUnderlyingBtCollisionShape() { return bt_shape_; } + + private: + btCollisionShape* AddBoxShape(const vec3& half_extents); + btCollisionShape* AddSphereShape(float radius); + btCollisionShape* AddMeshShape(const CollisionMesh& mesh); + + btCollisionShape* bt_shape_ = nullptr; + std::shared_ptr data_; + std::vector> shapes_; + std::vector> vertices_; +}; + +inline BulletCollisionShape* Upcast(CollisionShape* ptr) { + return static_cast(ptr); +} + +} // namespace redux + +#endif // REDUX_ENGINES_PHYSICS_BULLET_BULLET_COLLISION_SHAPE_H_ diff --git a/redux/redux/engines/physics/bullet/bullet_physics_engine.cc b/redux/redux/engines/physics/bullet/bullet_physics_engine.cc new file mode 100644 index 0000000..0701c78 --- /dev/null +++ b/redux/redux/engines/physics/bullet/bullet_physics_engine.cc @@ -0,0 +1,195 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/physics/bullet/bullet_physics_engine.h" + +#include + +#include "btBulletDynamicsCommon.h" +#include "redux/engines/physics/bullet/bullet_collision_shape.h" +#include "redux/engines/physics/bullet/bullet_rigid_body.h" +#include "redux/engines/physics/bullet/bullet_trigger_volume.h" +#include "redux/engines/physics/bullet/bullet_utils.h" +#include "redux/engines/physics/physics_engine.h" +#include "redux/modules/base/choreographer.h" +#include "redux/modules/base/static_registry.h" +#include "redux/modules/ecs/entity.h" + +namespace redux { + +static void CreatePhysicsEngine(Registry* registry) { + auto ptr = new BulletPhysicsEngine(registry); + registry->Register(std::unique_ptr(ptr)); +} + +static StaticRegistry Static_Register(CreatePhysicsEngine); + +static void InternalTickCallback(btDynamicsWorld* world, btScalar time_step) { + BulletPhysicsEngine* engine = + static_cast(world->getWorldUserInfo()); + engine->OnSimTick(); +} + +BulletPhysicsEngine::BulletPhysicsEngine(Registry* registry) + : registry_(registry) { + bt_config_ = std::make_unique(); + bt_dispatcher_ = std::make_unique(bt_config_.get()); + bt_broadphase_ = std::make_unique(); + bt_solver_ = std::make_unique(); + bt_world_ = std::make_unique( + bt_dispatcher_.get(), bt_broadphase_.get(), bt_solver_.get(), + bt_config_.get()); + bt_world_->setGravity(ToBullet(gravity_)); + bt_world_->setInternalTickCallback(InternalTickCallback, + static_cast(this)); + + on_exit_collision_ = [](Entity, Entity) {}; + on_enter_collision_ = [](Entity, Entity) {}; +} + +void BulletPhysicsEngine::OnSimTick() { + contacts_.clear(); + current_collisions_.clear(); + + for (int i = 0; i < bt_dispatcher_->getNumManifolds(); ++i) { + const btPersistentManifold* manifold = + bt_dispatcher_->getManifoldByIndexInternal(i); + ProcessContactManifold(manifold); + } + + for (auto iter : current_collisions_) { + const bool new_collision = !previous_collisions_.contains(iter.first); + if (new_collision) { + on_enter_collision_(Entity(iter.first.entities[0]), + Entity(iter.first.entities[1])); + } + } + + for (auto iter : previous_collisions_) { + const bool expired_collision = !current_collisions_.contains(iter.first); + if (expired_collision) { + on_exit_collision_(Entity(iter.first.entities[0]), + Entity(iter.first.entities[1])); + } + } + + using std::swap; + swap(current_collisions_, previous_collisions_); +} + +void BulletPhysicsEngine::ProcessContactManifold( + const btPersistentManifold* manifold) { + const size_t num_contacts = manifold->getNumContacts(); + if (num_contacts == 0) { + return; + } + + auto body_a = manifold->getBody0(); + auto body_b = manifold->getBody1(); + Entity entity_a = EntityFromBulletUserIndex(body_a->getUserIndex()); + Entity entity_b = EntityFromBulletUserIndex(body_b->getUserIndex()); + CHECK(entity_a.get() && entity_b.get()); + + const BulletPhysicsCollisionKey key(entity_a, entity_b); + CHECK(key.entities[0] < key.entities[1]); + const bool flip_normal = entity_a.get() > entity_b.get(); + + const size_t start = contacts_.size(); + current_collisions_[key] = {manifold, start, num_contacts}; + for (size_t i = 0; i < num_contacts; ++i) { + const btManifoldPoint& bt_contact = manifold->getContactPoint(i); + + ContactPoint point; + point.world_position = FromBullet(bt_contact.getPositionWorldOnB()); + point.contact_normal = flip_normal + ? FromBullet(bt_contact.m_normalWorldOnB) + : -FromBullet(bt_contact.m_normalWorldOnB); + contacts_.emplace_back(point); + } +} + +absl::Span +BulletPhysicsEngine::GetActiveContacts(Entity entity_a, Entity entity_b) const { + const BulletPhysicsCollisionKey key(entity_a, entity_b); + auto iter = current_collisions_.find(key); + if (iter != current_collisions_.end()) { + return absl::Span( + &contacts_[iter->second.contact_index], iter->second.num_contacts); + } + return {}; +} + +void BulletPhysicsEngine::AdvanceFrame(absl::Duration timestep) { + // During one AdvanceFrame() call, do at most a set number of 1/60 second + // updates. Bullet will update the MotionStates of every Dynamic Entity that + // has a transform update. These Entities will be marked for synchronization. + const float dt = static_cast(absl::ToDoubleSeconds(timestep)); + bt_world_->stepSimulation(dt, max_substeps_, timestep_); +} + +void BulletPhysicsEngine::OnRegistryInitialize() { + auto* choreographer = registry_->Get(); + if (choreographer) { + choreographer->Add<&PhysicsEngine::AdvanceFrame>( + Choreographer::Stage::kPhysics); + } +} + +void BulletPhysicsEngine::SetOnEnterCollisionCallback(CollisionCallback cb) { + on_enter_collision_ = std::move(cb); +} + +void BulletPhysicsEngine::SetOnExitCollisionCallback(CollisionCallback cb) { + on_exit_collision_ = std::move(cb); +} + +RigidBodyPtr BulletPhysicsEngine::CreateRigidBody( + const RigidBodyParams& params) { + auto ptr = std::make_shared(params, bt_world_.get()); + return std::static_pointer_cast(ptr); +} + +TriggerVolumePtr BulletPhysicsEngine::CreateTriggerVolume( + const TriggerVolumeParams& params) { + auto ptr = std::make_shared(params, bt_world_.get()); + return std::static_pointer_cast(ptr); +} + +CollisionShapePtr BulletPhysicsEngine::CreateShape( + CollisionDataPtr shape_data) { + auto ptr = std::make_shared(std::move(shape_data)); + return std::static_pointer_cast(ptr); +} + +CollisionShapePtr BulletPhysicsEngine::CreateShape(HashValue name) { + auto ptr = shape_data_.Find(name); + if (ptr) { + auto impl = std::make_shared(ptr); + return std::static_pointer_cast(impl); + } + return nullptr; +} + +void BulletPhysicsEngine::CacheShapeData(HashValue name, + CollisionDataPtr data) { + shape_data_.Register(name, std::move(data)); +} + +void BulletPhysicsEngine::ReleaseShapeData(HashValue name) { + shape_data_.Release(name); +} + +} // namespace redux diff --git a/redux/redux/engines/physics/bullet/bullet_physics_engine.h b/redux/redux/engines/physics/bullet/bullet_physics_engine.h new file mode 100644 index 0000000..16f8af1 --- /dev/null +++ b/redux/redux/engines/physics/bullet/bullet_physics_engine.h @@ -0,0 +1,147 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PHYSICS_BULLET_BULLET_PHYSICS_ENGINE_H_ +#define REDUX_ENGINES_PHYSICS_BULLET_BULLET_PHYSICS_ENGINE_H_ + +#include + +#include "btBulletDynamicsCommon.h" +#include "redux/engines/physics/bullet/bullet_collision_shape.h" +#include "redux/engines/physics/bullet/bullet_rigid_body.h" +#include "redux/engines/physics/bullet/bullet_trigger_volume.h" +#include "redux/engines/physics/bullet/bullet_utils.h" +#include "redux/engines/physics/physics_engine.h" +#include "redux/modules/base/choreographer.h" +#include "redux/modules/base/static_registry.h" +#include "redux/modules/ecs/entity.h" + +namespace redux { + +struct BulletPhysicsCollisionKey { + BulletPhysicsCollisionKey(Entity a, Entity b) { + static_assert(sizeof(key) == sizeof(entities)); + entities[0] = std::min(a.get(), b.get()); + entities[1] = std::max(a.get(), b.get()); + } + + template + friend H AbslHashValue(H h, const BulletPhysicsCollisionKey& key) { + return H::combine(std::move(h), key.key); + } + + bool operator==(const BulletPhysicsCollisionKey& rhs) const { + return key == rhs.key; + } + + union { + uint64_t key; + Entity::Rep entities[2]; + }; +}; + +struct BulletPhysicsCollisionData { + const btPersistentManifold* manifold = nullptr; + size_t contact_index = 0; + size_t num_contacts = 0; +}; + +class BulletPhysicsEngine : public PhysicsEngine { + public: + explicit BulletPhysicsEngine(Registry* registry); + + void OnRegistryInitialize(); + + // The force of gravity that is applied to all rigid bodies in the world. + // The default gravity is (0, -9.81, 0). + void SetGravity(const vec3& gravity); + + // Physics works best when it is stepped with a consistent timestamp. + // Internally, the PhysicsEngine will keep track of any time accumalation + // between this timestep and the AdvanceFrame delta_time and may perform + // multiple physics steps in a AdvanceFrame call to keep things in sync. + // The default timestep is 1/60s with a max_substeps of 4. + void SetTimestep(absl::Duration timestep, int max_substeps); + + // Advances the physics simulation by the given timestep. + void AdvanceFrame(absl::Duration timestep); + + // Creates an active rigid body using the provided data. + RigidBodyPtr CreateRigidBody(const RigidBodyParams& params); + + // Creates an active trigger volume using the provided data. + TriggerVolumePtr CreateTriggerVolume(const TriggerVolumeParams& params); + + // Creates a CollisionShape using the provided data. + CollisionShapePtr CreateShape(CollisionDataPtr shape_data); + + // Creates a CollisionShape using the data associated with |name|. + CollisionShapePtr CreateShape(HashValue name); + + // Adds the given collision shape data to the cache using |name|. + // CollisionShapes can then be created from this data by just refering to its + // name. + void CacheShapeData(HashValue name, CollisionDataPtr data); + + // Releases the cached shape data associated with |name|. + void ReleaseShapeData(HashValue name); + + // Callback for when collisions occur between two volumes. The Entity values + // are specified in the trigger volume/rigid body construction params. + using CollisionCallback = std::function; + + // Sets the callback to invoke when two objects enter each others collision + // volumes. + void SetOnEnterCollisionCallback(CollisionCallback cb); + + // Sets the callback to invoke when two objects exit each others collision + // volumes. + void SetOnExitCollisionCallback(CollisionCallback cb); + + // Returns information about all the contacts between two Entities. Should be + // used in conjunction with the above collision callbacks. + absl::Span GetActiveContacts(Entity entity_a, + Entity entity_b) const; + + // Internal function used by the Bullet tick/step callback. + void OnSimTick(); + + private: + using CollisionMap = absl::flat_hash_map; + + void ProcessContactManifold(const btPersistentManifold* manifold); + + Registry* registry_ = nullptr; + ResourceManager shape_data_; + CollisionCallback on_enter_collision_; + CollisionCallback on_exit_collision_; + std::unique_ptr bt_config_; + std::unique_ptr bt_dispatcher_; + std::unique_ptr bt_broadphase_; + std::unique_ptr bt_solver_; + std::unique_ptr bt_world_; + CollisionMap current_collisions_; + CollisionMap previous_collisions_; + std::vector contacts_; + vec3 gravity_ = {0, -9.81, 0}; + float timestep_ = 1 / 60.f; + int max_substeps_ = 4; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_PHYSICS_BULLET_BULLET_PHYSICS_ENGINE_H_ diff --git a/redux/redux/engines/physics/bullet/bullet_rigid_body.cc b/redux/redux/engines/physics/bullet/bullet_rigid_body.cc new file mode 100644 index 0000000..88a65aa --- /dev/null +++ b/redux/redux/engines/physics/bullet/bullet_rigid_body.cc @@ -0,0 +1,197 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/physics/bullet/bullet_rigid_body.h" + +#include "LinearMath/btDefaultMotionState.h" +#include "redux/engines/physics/bullet/bullet_collision_shape.h" +#include "redux/engines/physics/bullet/bullet_utils.h" +#include "redux/modules/base/bits.h" + +namespace redux { + +BulletRigidBody::BulletRigidBody(const RigidBodyParams& params, + btDynamicsWorld* world) + : params_(params), world_(world) { + btCollisionShape* bt_shape = GetUnderlyingBtCollisionShape(); + + bt_motion_state_ = std::make_unique(); + + btVector3 inertia(0.f, 0.f, 0.f); + bt_shape->calculateLocalInertia(params.mass, inertia); + btRigidBody::btRigidBodyConstructionInfo info( + params.mass, bt_motion_state_.get(), bt_shape, inertia); + info.m_friction = params.sliding_friction; + info.m_restitution = params.restitution; + bt_rigid_body_ = std::make_unique(info); + bt_rigid_body_->setUserIndex(EntityToBulletUserIndex(params_.entity)); + + UpdateFlags(); + Activate(); +} + +BulletRigidBody::~BulletRigidBody() { Deactivate(); } + +void BulletRigidBody::UpdateFlags() { + static constexpr int kStaticFlag = + btCollisionObject::CollisionFlags::CF_STATIC_OBJECT; + static constexpr int kKinematicFlag = + btCollisionObject::CollisionFlags::CF_KINEMATIC_OBJECT; + + int flags = bt_rigid_body_->getCollisionFlags(); + if (params_.type == RigidBodyMotionType::Static) { + flags = SetBits(flags, kStaticFlag); + } else { + flags = ClearBits(flags, kStaticFlag); + } + if (params_.type == RigidBodyMotionType::Kinematic) { + flags = SetBits(flags, kKinematicFlag); + } else { + flags = ClearBits(flags, kKinematicFlag); + } + bt_rigid_body_->setFlags(flags); +} + +void BulletRigidBody::Activate() { + const int group = static_cast(params_.collision_group.Value()); + const int mask = static_cast(params_.collision_filter.Value()); + world_->addRigidBody(bt_rigid_body_.get(), group, mask); +} + +void BulletRigidBody::Deactivate() { + world_->removeRigidBody(bt_rigid_body_.get()); +} + +void BulletRigidBody::SetType(RigidBodyMotionType type) { + params_.type = type; + UpdateFlags(); +} + +bool BulletRigidBody::IsActive() const { return bt_rigid_body_->isInWorld(); } + +void BulletRigidBody::SetTransform(const Transform& transform) { + const auto bt_translation = ToBullet(transform.translation); + const auto bt_rotation = ToBullet(transform.rotation); + const auto bt_scale = ToBullet(transform.scale); + bt_rigid_body_->setWorldTransform(btTransform(bt_rotation, bt_translation)); + GetUnderlyingBtCollisionShape()->setLocalScaling(bt_scale); +} + +vec3 BulletRigidBody::GetPosition() const { + btTransform transform; + bt_rigid_body_->getMotionState()->getWorldTransform(transform); + return FromBullet(transform.getOrigin()); +} + +quat BulletRigidBody::GetRotation() const { + btTransform transform; + bt_rigid_body_->getMotionState()->getWorldTransform(transform); + return FromBullet(transform.getRotation()); +} + +void BulletRigidBody::SetMass(float mass_in_kg) { + btVector3 inertia(0.f, 0.f, 0.f); + GetUnderlyingBtCollisionShape()->calculateLocalInertia(mass_in_kg, inertia); + bt_rigid_body_->setMassProps(mass_in_kg, inertia); + // setMassProps() can change collision flags, so reset them to be sure. + UpdateFlags(); +} + +float BulletRigidBody::GetMass() const { return bt_rigid_body_->getMass(); } + +void BulletRigidBody::SetRestitution(float restitution) { + bt_rigid_body_->setRestitution(restitution); +} + +float BulletRigidBody::GetRestitution() const { + return bt_rigid_body_->getRestitution(); +} + +void BulletRigidBody::SetSlidingFriction(float friction) { + bt_rigid_body_->setFriction(friction); +} + +float BulletRigidBody::GetSlidingFriction() const { + return bt_rigid_body_->getFriction(); +} + +void BulletRigidBody::SetRollingFriction(float friction) { + bt_rigid_body_->setRollingFriction(friction); +} + +float BulletRigidBody::GetRollingFriction() const { + return bt_rigid_body_->getRollingFriction(); +} + +void BulletRigidBody::SetSpinningFriction(float friction) { + bt_rigid_body_->setSpinningFriction(friction); +} + +float BulletRigidBody::GetSpinningFriction() const { + return bt_rigid_body_->getSpinningFriction(); +} + +void BulletRigidBody::SetLinearDamping(float damping) { + const float angular_damping = bt_rigid_body_->getAngularDamping(); + bt_rigid_body_->setDamping(damping, angular_damping); +} + +float BulletRigidBody::GetLinearDamping() const { + return bt_rigid_body_->getLinearDamping(); +} + +void BulletRigidBody::SetAngularDamping(float damping) { + const float linear_damping = bt_rigid_body_->getLinearDamping(); + bt_rigid_body_->setDamping(linear_damping, damping); +} + +float BulletRigidBody::GetAngularDamping() const { + return bt_rigid_body_->getAngularDamping(); +} + +void BulletRigidBody::SetLinearVelocity(const vec3& velocity) { + bt_rigid_body_->setLinearVelocity(ToBullet(velocity)); +} + +vec3 BulletRigidBody::GetLinearVelocity() const { + return FromBullet(bt_rigid_body_->getLinearVelocity()); +} + +void BulletRigidBody::SetAngularVelocity(const vec3& velocity) { + bt_rigid_body_->setAngularVelocity(ToBullet(velocity)); +} + +vec3 BulletRigidBody::GetAngularVelocity() const { + return FromBullet(bt_rigid_body_->getAngularVelocity()); +} + +void BulletRigidBody::ApplyForce(const vec3& force, const vec3& position) { + bt_rigid_body_->applyForce(ToBullet(force), ToBullet(position)); +} + +void BulletRigidBody::ApplyImpulse(const vec3& impulse, const vec3& position) { + bt_rigid_body_->applyImpulse(ToBullet(impulse), ToBullet(position)); +} + +void BulletRigidBody::ApplyTorque(const vec3& torque) { + bt_rigid_body_->applyTorque(ToBullet(torque)); +} + +btCollisionShape* BulletRigidBody::GetUnderlyingBtCollisionShape() { + return Upcast(params_.shape.get())->GetUnderlyingBtCollisionShape(); +} + +} // namespace redux diff --git a/redux/redux/engines/physics/bullet/bullet_rigid_body.h b/redux/redux/engines/physics/bullet/bullet_rigid_body.h new file mode 100644 index 0000000..b4b8730 --- /dev/null +++ b/redux/redux/engines/physics/bullet/bullet_rigid_body.h @@ -0,0 +1,138 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PHYSICS_BULLET_BULLET_RIGID_BODY_H_ +#define REDUX_ENGINES_PHYSICS_BULLET_BULLET_RIGID_BODY_H_ + +#include "BulletDynamics/Dynamics/btDynamicsWorld.h" +#include "BulletDynamics/Dynamics/btRigidBody.h" +#include "redux/engines/physics/rigid_body.h" + +namespace redux { + +class PhysicsWorld; + +class BulletRigidBody : public RigidBody { + public: + BulletRigidBody(const RigidBodyParams& params, btDynamicsWorld* world); + ~BulletRigidBody() override; + + // Enables the rigid body to be included in any dynamics calculations and + // collision detection. + void Activate(); + + // Disables the rigid body from being included in any dynamics calculations or + // collision detection. + void Deactivate(); + + // Changes the type of the rigid body. + void SetType(RigidBodyMotionType type); + + // Returns true if the rigid body is active. + bool IsActive() const; + + // Applies a force onto the rigid body at the given position. + void ApplyForce(const vec3& force, const vec3& position); + + // Applies a force onto the rigid body at the given position. + void ApplyImpulse(const vec3& impulse, const vec3& position); + + // Applies toque onto the rigid body. + void ApplyTorque(const vec3& torque); + + // Sets the rigid body's transform. + void SetTransform(const Transform& transform); + + // Returns the rigid body's position. + vec3 GetPosition() const; + + // Returns the rigid body's rotation. + quat GetRotation() const; + + // Sets the rigid body's linear velocity. + void SetLinearVelocity(const vec3& velocity); + + // Returns the rigid body's linear velocity. + vec3 GetLinearVelocity() const; + + // Sets the rigid body's angular velocity (in radians/second per axis). + void SetAngularVelocity(const vec3& velocity); + + // Returns the rigid body's angular velocity. + vec3 GetAngularVelocity() const; + + // Sets the rigid body's mass (in kg). + void SetMass(float mass_in_kg); + + // Returns the rigid body's mass. + float GetMass() const; + + // Sets the rigid body's co-efficient of restitution (i.e. bounciness), with a + // range of 0.0 (i.e. will not bounce whatsoever) to 1.0 (i.e. does not lose + // any energy from bouncing). + void SetRestitution(float restitution); + + // Returns the rigid body's co-efficient of restitution (i.e. bounciness). + float GetRestitution() const; + + // Sets the rigid body's co-efficient of sliding friction, with a range of 0.0 + // (i.e. will not stop) to 1.0 (i.e. will not slide). + void SetSlidingFriction(float friction); + + // Returns the rigid body's co-efficient of sliding friction. + float GetSlidingFriction() const; + + // Sets the rigid body's co-efficient of rolling friction, with a range of 0.0 + // (i.e. will not stop) to 1.0 (i.e. will not slide). + void SetRollingFriction(float friction); + + // Returns the rigid body's co-efficient of rolling friction. + float GetRollingFriction() const; + + // Sets the rigid body's co-efficient of spinning friction, with a range of + // 0.0 (i.e. will not stop) to 1.0 (i.e. will not spin). + void SetSpinningFriction(float friction); + + // Returns the rigid body's co-efficient of spinning friction. + float GetSpinningFriction() const; + + // Sets the rigid body's linear damping, with a range of 0.0 (i.e. do not + // apply damping) to 1.0 (i.e. set the linear velocity to zero). + void SetLinearDamping(float damping); + + // Returns the rigid body's linear damping value. + float GetLinearDamping() const; + + // Sets the rigid body's angular damping, with a range of 0.0 (i.e. do not + // apply damping) to 1.0 (i.e. set the angular velocity to zero). + void SetAngularDamping(float damping); + + // Returns the rigid body's angular damping value. + float GetAngularDamping() const; + + private: + void UpdateFlags(); + btCollisionShape* GetUnderlyingBtCollisionShape(); + + RigidBodyParams params_; + btDynamicsWorld* world_ = nullptr; + std::unique_ptr bt_motion_state_; + std::unique_ptr bt_rigid_body_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_PHYSICS_BULLET_BULLET_RIGID_BODY_H_ diff --git a/redux/redux/engines/physics/bullet/bullet_trigger_volume.cc b/redux/redux/engines/physics/bullet/bullet_trigger_volume.cc new file mode 100644 index 0000000..c939285 --- /dev/null +++ b/redux/redux/engines/physics/bullet/bullet_trigger_volume.cc @@ -0,0 +1,101 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/physics/bullet/bullet_trigger_volume.h" + +#include + +#include "LinearMath/btDefaultMotionState.h" +#include "redux/engines/physics/bullet/bullet_collision_shape.h" +#include "redux/engines/physics/bullet/bullet_utils.h" + +namespace redux { + +BulletTriggerVolume::BulletTriggerVolume(const TriggerVolumeParams& params, + btDynamicsWorld* world) + : params_(params), world_(world) { + const btScalar mass = 0.f; + btCollisionShape* bt_shape = + Upcast(params_.shape.get())->GetUnderlyingBtCollisionShape(); + bt_motion_state_ = std::make_unique(); + bt_rigid_body_ = + std::make_unique(mass, bt_motion_state_.get(), bt_shape); + bt_rigid_body_->setUserIndex(EntityToBulletUserIndex(params_.entity)); + UpdateFlags(); + Activate(); +} + +BulletTriggerVolume::~BulletTriggerVolume() { Deactivate(); } + +void BulletTriggerVolume::Activate() { + const int group = static_cast(params_.collision_group.Value()); + const int mask = static_cast(params_.collision_filter.Value()); + world_->addRigidBody(bt_rigid_body_.get(), group, mask); +} + +void BulletTriggerVolume::Deactivate() { + world_->removeRigidBody(bt_rigid_body_.get()); +} + +bool BulletTriggerVolume::IsActive() const { + return bt_rigid_body_->isInWorld(); +} + +void BulletTriggerVolume::SetTransform(const Transform& transform) { + btCollisionShape* bt_shape = + Upcast(params_.shape.get())->GetUnderlyingBtCollisionShape(); + + const auto bt_translation = ToBullet(transform.translation); + const auto bt_rotation = ToBullet(transform.rotation); + const auto bt_scale = ToBullet(transform.scale); + bt_rigid_body_->setWorldTransform(btTransform(bt_rotation, bt_translation)); + bt_shape->setLocalScaling(bt_scale); +} + +vec3 BulletTriggerVolume::GetPosition() const { + btTransform transform; + bt_rigid_body_->getMotionState()->getWorldTransform(transform); + return FromBullet(transform.getOrigin()); +} + +quat BulletTriggerVolume::GetRotation() const { + btTransform transform; + bt_rigid_body_->getMotionState()->getWorldTransform(transform); + return FromBullet(transform.getRotation()); +} + +void BulletTriggerVolume::UpdateFlags() { + static constexpr int kStaticFlag = + btCollisionObject::CollisionFlags::CF_STATIC_OBJECT; + static constexpr int kKinematicFlag = + btCollisionObject::CollisionFlags::CF_KINEMATIC_OBJECT; + static constexpr int kNoResponseFlag = + btCollisionObject::CollisionFlags::CF_NO_CONTACT_RESPONSE; + static constexpr int kNoGravityFlag = + btRigidBodyFlags::BT_DISABLE_WORLD_GRAVITY; + + int collision_flags = bt_rigid_body_->getCollisionFlags(); + collision_flags = SetBits(collision_flags, kNoResponseFlag); + collision_flags = ClearBits(collision_flags, kStaticFlag); + collision_flags = ClearBits(collision_flags, kKinematicFlag); + bt_rigid_body_->setCollisionFlags(collision_flags); + + int rigid_body_flags = bt_rigid_body_->getFlags(); + rigid_body_flags = SetBits(rigid_body_flags, kNoGravityFlag); + bt_rigid_body_->setFlags(rigid_body_flags); +} + +} // namespace redux diff --git a/redux/redux/engines/physics/bullet/bullet_trigger_volume.h b/redux/redux/engines/physics/bullet/bullet_trigger_volume.h new file mode 100644 index 0000000..5e0c0e3 --- /dev/null +++ b/redux/redux/engines/physics/bullet/bullet_trigger_volume.h @@ -0,0 +1,65 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PHYSICS_BULLET_BULLET_TRIGGER_VOLUME_H_ +#define REDUX_ENGINES_PHYSICS_BULLET_BULLET_TRIGGER_VOLUME_H_ + +#include "BulletDynamics/Dynamics/btDynamicsWorld.h" +#include "BulletDynamics/Dynamics/btRigidBody.h" +#include "redux/engines/physics/trigger_volume.h" + +namespace redux { + +class PhysicsWorld; + +class BulletTriggerVolume : public TriggerVolume { + public: + BulletTriggerVolume(const TriggerVolumeParams& params, + btDynamicsWorld* world); + ~BulletTriggerVolume() override; + + // Enables the trigger volume to be included when performing any potential + // collision detection. + void Activate(); + + // Disables the trigger volume from being including in any collision + // detection. + void Deactivate(); + + // Returns true if the trigger volume is active. + bool IsActive() const; + + // Sets the trigger volume's transform. + void SetTransform(const Transform& transform); + + // Returns the trigger volume's position. + vec3 GetPosition() const; + + // Returns the trigger volume's rotation. + quat GetRotation() const; + + private: + void UpdateFlags(); + + TriggerVolumeParams params_; + btDynamicsWorld* world_ = nullptr; + std::unique_ptr bt_motion_state_; + std::unique_ptr bt_rigid_body_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_PHYSICS_BULLET_BULLET_TRIGGER_VOLUME_H_ diff --git a/redux/redux/engines/physics/bullet/bullet_utils.h b/redux/redux/engines/physics/bullet/bullet_utils.h new file mode 100644 index 0000000..6d72b32 --- /dev/null +++ b/redux/redux/engines/physics/bullet/bullet_utils.h @@ -0,0 +1,60 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PHYSICS_BULLET_BULLET_UTILS_H_ +#define REDUX_ENGINES_PHYSICS_BULLET_BULLET_UTILS_H_ + +#include "absl/base/casts.h" +#include "LinearMath/btQuaternion.h" +#include "LinearMath/btTransform.h" +#include "redux/modules/ecs/entity.h" +#include "redux/modules/math/quaternion.h" +#include "redux/modules/math/transform.h" +#include "redux/modules/math/vector.h" + +namespace redux { + +inline btVector3 ToBullet(const vec3& in) { + return btVector3(in.x, in.y, in.z); +} + +inline vec3 FromBullet(const btVector3& in) { + return vec3(in.x(), in.y(), in.z()); +} + +inline btQuaternion ToBullet(const quat& in) { + return btQuaternion(in.x, in.y, in.z, in.w); +} + +inline quat FromBullet(const btQuaternion& in) { + return quat(in.x(), in.y(), in.z(), in.w()); +} + +inline btTransform ToBullet(const Transform& in) { + return btTransform(ToBullet(in.rotation), ToBullet(in.translation)); +} + +inline int EntityToBulletUserIndex(Entity entity) { + return absl::bit_cast(entity.get()); +} + +inline Entity EntityFromBulletUserIndex(int index) { + return Entity(absl::bit_cast(index)); +} + +} // namespace redux + +#endif // REDUX_ENGINES_PHYSICS_BULLET_BULLET_UTILS_H_ diff --git a/redux/redux/engines/physics/bullet/thunks.cc b/redux/redux/engines/physics/bullet/thunks.cc new file mode 100644 index 0000000..1c193c2 --- /dev/null +++ b/redux/redux/engines/physics/bullet/thunks.cc @@ -0,0 +1,46 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/physics/bullet/bullet_physics_engine.h" +#include "redux/engines/physics/bullet/bullet_rigid_body.h" +#include "redux/engines/physics/bullet/bullet_trigger_volume.h" + +namespace redux { + +inline BulletPhysicsEngine* Upcast(PhysicsEngine* ptr) { + return static_cast(ptr); +} +inline const BulletPhysicsEngine* Upcast(const PhysicsEngine* ptr) { + return static_cast(ptr); +} +inline BulletTriggerVolume* Upcast(TriggerVolume* ptr) { + return static_cast(ptr); +} +inline const BulletTriggerVolume* Upcast(const TriggerVolume* ptr) { + return static_cast(ptr); +} +inline BulletRigidBody* Upcast(RigidBody* ptr) { + return static_cast(ptr); +} +inline const BulletRigidBody* Upcast(const RigidBody* ptr) { + return static_cast(ptr); +} + +} // namespace redux + +#include "redux/engines/physics/thunks/physics_engine.h" +#include "redux/engines/physics/thunks/rigid_body.h" +#include "redux/engines/physics/thunks/trigger_volume.h" diff --git a/redux/redux/engines/physics/collision_data.cc b/redux/redux/engines/physics/collision_data.cc new file mode 100644 index 0000000..24a3296 --- /dev/null +++ b/redux/redux/engines/physics/collision_data.cc @@ -0,0 +1,81 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/physics/collision_data.h" + +#include + +namespace redux { + +void CollisionData::AddBox(const vec3& position, const quat& rotation, + const vec3& half_extents) { + auto& shape = shape_parts_.emplace_back(); + shape.type = kBox; + shape.position = position; + shape.rotation = rotation; + shape.extents = half_extents; +} + +void CollisionData::AddSphere(const vec3& position, float radius) { + auto& shape = shape_parts_.emplace_back(); + shape.type = kSphere; + shape.position = position; + shape.extents = vec3(radius); +} + +void CollisionData::AddMesh(const vec3& position, const quat& rotation, + CollisionMesh mesh) { + auto& shape = shape_parts_.emplace_back(); + shape.type = kMesh; + shape.position = position; + shape.rotation = rotation; + shape.mesh = std::move(mesh); +} + +CollisionData::PartType CollisionData::GetPartType(size_t index) const { + CHECK(index < shape_parts_.size()); + return shape_parts_[index].type; +} + +const vec3& CollisionData::GetPosition(size_t index) const { + CHECK(index < shape_parts_.size()); + return shape_parts_[index].position; +} + +const quat& CollisionData::GetRotation(size_t index) const { + CHECK(index < shape_parts_.size()); + return shape_parts_[index].rotation; +} + +vec3 CollisionData::GetBoxHalfExtents(size_t index) const { + CHECK(index < shape_parts_.size()); + CHECK(shape_parts_[index].type == kBox); + return shape_parts_[index].extents; +} + +float CollisionData::GetSphereRadius(size_t index) const { + CHECK(index < shape_parts_.size()); + CHECK(shape_parts_[index].type == kSphere); + return shape_parts_[index].extents.x; +} + +const CollisionMesh& CollisionData::GetCollisionMesh(size_t index) const { + CHECK(index < shape_parts_.size()); + CHECK(shape_parts_[index].type == kMesh); + return shape_parts_[index].mesh; +} + +} // namespace redux diff --git a/redux/redux/engines/physics/collision_data.h b/redux/redux/engines/physics/collision_data.h new file mode 100644 index 0000000..c024ec7 --- /dev/null +++ b/redux/redux/engines/physics/collision_data.h @@ -0,0 +1,103 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PHYSICS_COLLISION_DATA_H_ +#define REDUX_ENGINES_PHYSICS_COLLISION_DATA_H_ + +#include +#include + +#include "redux/modules/base/data_container.h" +#include "redux/modules/math/quaternion.h" +#include "redux/modules/math/vector.h" + +namespace redux { + +// Collision shape represented using a convex triangle mesh. +struct CollisionMesh { + DataContainer vertices; // `vec3` point array. + DataContainer indices; // `int` indices array, 3 indices for each triangle. +}; + +// Collision data from which a CollisionShape can be created. +// +// A CollisionShape itself may be composed of multiple parts, each of which +// is either a primitive shape (e.g. box, sphere, etc.) or a CollisionMesh. +class CollisionData { + public: + CollisionData() = default; + + enum PartType { + kNone, + kBox, + kSphere, + kMesh, + }; + + // Adds a sphere shape part to the collision data. + void AddSphere(const vec3& position, float radius); + + // Adds a box shape part to the collision data. + void AddBox(const vec3& position, const quat& rotation, + const vec3& half_extents); + + // Adds a mesh shape part to the collision data. + void AddMesh(const vec3& position, const quat& rotation, CollisionMesh mesh); + + // Returns the total number of parts in the collision data. + size_t GetNumParts() const { return shape_parts_.size(); } + + // Returns the position of the shape part at the given `index` relative to the + // collision data's origin. + const vec3& GetPosition(size_t index) const; + + // Returns the rotation of the shape part at the given `index` relative to the + // collision data's origin. + const quat& GetRotation(size_t index) const; + + // Returns the PartType of the shape part at the given `index`. + PartType GetPartType(size_t index) const; + + // Returns the box half extents of the part at the `index`. Will check-fail + // if the part is not a box. + vec3 GetBoxHalfExtents(size_t index) const; + + // Returns the sphere radius of the part at the `index`. Will check-fail if + // the part is not a sphere. + float GetSphereRadius(size_t index) const; + + // Returns the collision mesh of the part at the `index`. Will check-fail if + // the part is not a mesh. + const CollisionMesh& GetCollisionMesh(size_t index) const; + + private: + struct ShapePart { + PartType type = kNone; + // Some values may not be set depending on the type. + vec3 position = vec3::Zero(); + quat rotation = quat::Identity(); + vec3 extents = vec3::Zero(); + CollisionMesh mesh; + }; + + std::vector shape_parts_; +}; + +using CollisionDataPtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_PHYSICS_COLLISION_DATA_H_ diff --git a/redux/redux/engines/physics/collision_shape.h b/redux/redux/engines/physics/collision_shape.h new file mode 100644 index 0000000..28171f2 --- /dev/null +++ b/redux/redux/engines/physics/collision_shape.h @@ -0,0 +1,40 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PHYSICS_COLLISION_SHAPE_H_ +#define REDUX_ENGINES_PHYSICS_COLLISION_SHAPE_H_ + +#include + +namespace redux { + +// The shape to be used for trigger volumes or rigid bodies. +// +// It is created by the PhysicsEngine using CollisionData. It has no +// functionality of its own. +class CollisionShape { + public: + virtual ~CollisionShape() = default; + + protected: + CollisionShape() = default; +}; + +using CollisionShapePtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_PHYSICS_COLLISION_SHAPE_H_ diff --git a/redux/redux/engines/physics/enums.h b/redux/redux/engines/physics/enums.h new file mode 100644 index 0000000..eed399e --- /dev/null +++ b/redux/redux/engines/physics/enums.h @@ -0,0 +1,36 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PHYSICS_ENUMS_H_ +#define REDUX_ENGINES_PHYSICS_ENUMS_H_ + +#include // for types missing from the autogen file. +#include // for types missing from the autogen file. + +#include "redux/engines/physics/physics_enums_generated.h" +#include "redux/modules/base/typeid.h" + +namespace redux { + +inline const char* ToString(RigidBodyMotionType e) { + return EnumNameRigidBodyMotionType(e); +} + +} // namespace redux + +REDUX_SETUP_TYPEID(redux::RigidBodyMotionType); + +#endif // REDUX_ENGINES_PHYSICS_ENUMS_H_ diff --git a/redux/redux/engines/physics/physics_engine.h b/redux/redux/engines/physics/physics_engine.h new file mode 100644 index 0000000..467de75 --- /dev/null +++ b/redux/redux/engines/physics/physics_engine.h @@ -0,0 +1,124 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PHYSICS_PHYSICS_ENGINE_H_ +#define REDUX_ENGINES_PHYSICS_PHYSICS_ENGINE_H_ + +#include + +#include "absl/time/time.h" +#include "redux/engines/physics/collision_data.h" +#include "redux/engines/physics/collision_shape.h" +#include "redux/engines/physics/rigid_body.h" +#include "redux/engines/physics/trigger_volume.h" +#include "redux/modules/base/registry.h" +#include "redux/modules/base/resource_manager.h" +#include "redux/modules/math/vector.h" + +namespace redux { + +// Holds and updates all physics objects. +// +// A physics object is essentially any object that occupies a volume in 3D +// space. There are two main types of physics objects: trigger volumes and +// rigid bodies. +// +// Trigger volumes are massless and exist solely to notify users when they are +// colliding/intersecting with other physics objects. (See trigger_volume.h +// for more details.) +// +// Rigid bodies, on the other hand, have mass which allows them to partake in +// dynamics, "the study of forces and their effects on motion." Simply put, +// rigid bodies can move on their own and bounce off each other. (See +// rigid_body.h for more details.) +// +// The PhysicsEngine is responsible for managing these objects, detecting +// collisions between these objects and, in the case of rigid bodies, updating +// their transforms. +class PhysicsEngine { + public: + virtual ~PhysicsEngine() = default; + + PhysicsEngine(const PhysicsEngine&) = delete; + PhysicsEngine& operator=(const PhysicsEngine&) = delete; + + void OnRegistryInitialize(); + + // The force of gravity that is applied to all rigid bodies in the world. + // The default gravity is (0, -9.81, 0). + void SetGravity(const vec3& gravity); + + // Physics works best when it is stepped with a consistent timestamp. + // Internally, the PhysicsEngine will keep track of any time accumalation + // between this timestep and the AdvanceFrame delta_time and may perform + // multiple physics steps in a AdvanceFrame call to keep things in sync. + // The default timestep is 1/60s with a max_substeps of 4. + void SetTimestep(absl::Duration timestep, int max_substeps); + + // Advances the physics simulation by the given timestep. + void AdvanceFrame(absl::Duration timestep); + + // Creates an active rigid body using the provided data. + RigidBodyPtr CreateRigidBody(const RigidBodyParams& params); + + // Creates an active trigger volume using the provided data. + TriggerVolumePtr CreateTriggerVolume(const TriggerVolumeParams& params); + + // Creates a CollisionShape using the provided data. + CollisionShapePtr CreateShape(CollisionDataPtr shape_data); + + // Creates a CollisionShape using the data associated with |name|. + CollisionShapePtr CreateShape(HashValue name); + + // Adds the given collision shape data to the cache using |name|. + // CollisionShapes can then be created from this data by just refering to its + // name. + void CacheShapeData(HashValue name, CollisionDataPtr data); + + // Releases the cached shape data associated with |name|. + void ReleaseShapeData(HashValue name); + + // Callback for when collisions occur between two volumes. The Entity values + // are specified in the trigger volume/rigid body construction params. + using CollisionCallback = std::function; + + // Sets the callback to invoke when two objects enter each others collision + // volumes. + void SetOnEnterCollisionCallback(CollisionCallback cb); + + // Sets the callback to invoke when two objects exit each others collision + // volumes. + void SetOnExitCollisionCallback(CollisionCallback cb); + + struct ContactPoint { + vec3 world_position = vec3::Zero(); + vec3 contact_normal = vec3::Zero(); + }; + + // Returns information about all the contacts between two Entities. Should be + // used in conjunction with the above collision callbacks. + absl::Span GetActiveContacts(Entity entity_a, + Entity entity_b) const; + + protected: + PhysicsEngine() = default; +}; + +} // namespace redux + +REDUX_SETUP_TYPEID(redux::PhysicsEngine); + +#endif // REDUX_ENGINES_PHYSICS_PHYSICS_ENGINE_H_ diff --git a/redux/redux/engines/physics/physics_enums.fbs b/redux/redux/engines/physics/physics_enums.fbs new file mode 100644 index 0000000..c603fd6 --- /dev/null +++ b/redux/redux/engines/physics/physics_enums.fbs @@ -0,0 +1,10 @@ +namespace redux; + +enum RigidBodyMotionType : ubyte { + // Moving, controlled by physics engine. + Dynamic, + // Moving, controller by user. + Kinematic, + // Non-moving. + Static, +} diff --git a/redux/redux/engines/physics/rigid_body.h b/redux/redux/engines/physics/rigid_body.h new file mode 100644 index 0000000..a4ba545 --- /dev/null +++ b/redux/redux/engines/physics/rigid_body.h @@ -0,0 +1,161 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PHYSICS_RIGID_BODY_H_ +#define REDUX_ENGINES_PHYSICS_RIGID_BODY_H_ + +#include + +#include "redux/engines/physics/collision_shape.h" +#include "redux/engines/physics/enums.h" +#include "redux/modules/base/bits.h" +#include "redux/modules/ecs/entity.h" +#include "redux/modules/math/transform.h" + +namespace redux { + +struct RigidBodyParams { + RigidBodyMotionType type = RigidBodyMotionType::Static; + + float mass = 0.f; // in kg + float restitution = 0.f; + float sliding_friction = 0.f; + float rolling_friction = 0.f; + float spinning_friction = 0.f; + + // The shape of the volume. + CollisionShapePtr shape; + + // The Entity to which the rigid body belongs. Used for collision callbacks. + Entity entity; + + // The groups to which the rigid body belongs. + Bits32 collision_group = Bits32::All(); + + // The groups against which the rigid body will collide. + Bits32 collision_filter = Bits32::All(); +}; + +// A rigid body is a physics object that has mass and shape/volume. +class RigidBody { + public: + virtual ~RigidBody() = default; + + RigidBody(const RigidBody&) = delete; + RigidBody& operator=(const RigidBody&) = delete; + + // Enables the rigid body to be included in any dynamics calculations and + // collision detection. + void Activate(); + + // Disables the rigid body from being included in any dynamics calculations or + // collision detection. + void Deactivate(); + + // Changes the type of the rigid body. + void SetType(RigidBodyMotionType type); + + // Returns true if the rigid body is active. + bool IsActive() const; + + // Applies a force onto the rigid body at the given position. + void ApplyForce(const vec3& force, const vec3& position); + + // Applies a force onto the rigid body at the given position. + void ApplyImpulse(const vec3& impulse, const vec3& position); + + // Applies toque onto the rigid body. + void ApplyTorque(const vec3& torque); + + // Sets the rigid body's transform. + void SetTransform(const Transform& transform); + + // Returns the rigid body's position. + vec3 GetPosition() const; + + // Returns the rigid body's rotation. + quat GetRotation() const; + + // Sets the rigid body's linear velocity. + void SetLinearVelocity(const vec3& velocity); + + // Returns the rigid body's linear velocity. + vec3 GetLinearVelocity() const; + + // Sets the rigid body's angular velocity (in radians/second per axis). + void SetAngularVelocity(const vec3& velocity); + + // Returns the rigid body's angular velocity. + vec3 GetAngularVelocity() const; + + // Sets the rigid body's mass (in kg). + void SetMass(float mass_in_kg); + + // Returns the rigid body's mass. + float GetMass() const; + + // Sets the rigid body's co-efficient of restitution (i.e. bounciness), with a + // range of 0.0 (i.e. will not bounce whatsoever) to 1.0 (i.e. does not lose + // any energy from bouncing). + void SetRestitution(float restitution); + + // Returns the rigid body's co-efficient of restitution (i.e. bounciness). + float GetRestitution() const; + + // Sets the rigid body's co-efficient of sliding friction, with a range of 0.0 + // (i.e. will not stop) to 1.0 (i.e. will not slide). + void SetSlidingFriction(float friction); + + // Returns the rigid body's co-efficient of sliding friction. + float GetSlidingFriction() const; + + // Sets the rigid body's co-efficient of rolling friction, with a range of 0.0 + // (i.e. will not stop) to 1.0 (i.e. will not slide). + void SetRollingFriction(float friction); + + // Returns the rigid body's co-efficient of rolling friction. + float GetRollingFriction() const; + + // Sets the rigid body's co-efficient of spinning friction, with a range of + // 0.0 (i.e. will not stop) to 1.0 (i.e. will not spin). + void SetSpinningFriction(float friction); + + // Returns the rigid body's co-efficient of spinning friction. + float GetSpinningFriction() const; + + // Sets the rigid body's linear damping, with a range of 0.0 (i.e. do not + // apply damping) to 1.0 (i.e. set the linear velocity to zero). + void SetLinearDamping(float damping); + + // Returns the rigid body's linear damping value. + float GetLinearDamping() const; + + // Sets the rigid body's angular damping, with a range of 0.0 (i.e. do not + // apply damping) to 1.0 (i.e. set the angular velocity to zero). + void SetAngularDamping(float damping); + + // Returns the rigid body's angular damping value. + float GetAngularDamping() const; + + protected: + RigidBody() = default; +}; + +using RigidBodyPtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_PHYSICS_RIGID_BODY_H_ diff --git a/redux/redux/engines/physics/thunks/BUILD b/redux/redux/engines/physics/thunks/BUILD new file mode 100644 index 0000000..b8858a2 --- /dev/null +++ b/redux/redux/engines/physics/thunks/BUILD @@ -0,0 +1,19 @@ +# PhysicsEngine thunks implementations. + +licenses(["notice"]) + +package( + default_visibility = ["//redux:visibility"], +) + +cc_library( + name = "thunks", + textual_hdrs = [ + "physics_engine.h", + "rigid_body.h", + "trigger_volume.h", + ], + deps = [ + "//redux/engines/physics", + ], +) diff --git a/redux/redux/engines/physics/thunks/README.md b/redux/redux/engines/physics/thunks/README.md new file mode 100644 index 0000000..5217b8b --- /dev/null +++ b/redux/redux/engines/physics/thunks/README.md @@ -0,0 +1,27 @@ +To support the option of multiple backends for a given interface, we do a bit of +manual "devirtualization" using thunks. The idea is basically: + +1) Declare the class API (ie. public function declarations) in the main package. +This class should not have any virtual functions _except_ it's destructor. It +should have a protected default constructor. + +2) Define an implementation class that inherits from the API class. The derived +class should declare the _same_ functions as the API class, but remember these +are not virtuals or overrides. (In other words, these functions will shadow the +base class functions.) + +3) Implement an Upcast function that takes a pointer to the base class and +returns the derived class. + +4) Include the various thunks header files. + +5) Provide a factory function that will create an instance of the derived class +and return it as a pointer to the base class. + +The way it works is that, for users of the module, they only get a pointer to +the base API class. When calling any function, it will call the "thunk" +implementation for that class which will "upcast" the object to the derived type +and invoke the function with the same name/signature in the derived class. The +advantage to this (over virtual functions) is that there is no dynamic dispatch; +the "thunk" functions can be inlined and so it will be like calling the derived +class directly using the base class pointer. diff --git a/redux/redux/engines/physics/thunks/physics_engine.h b/redux/redux/engines/physics/thunks/physics_engine.h new file mode 100644 index 0000000..17d8558 --- /dev/null +++ b/redux/redux/engines/physics/thunks/physics_engine.h @@ -0,0 +1,65 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PHYSICS_THUNKS_PHYSICS_ENGINE_H_ +#define REDUX_ENGINES_PHYSICS_THUNKS_PHYSICS_ENGINE_H_ + +#include + +#include "redux/engines/physics/physics_engine.h" + +namespace redux { + +// Thunk functions to call the actual implementation. +void PhysicsEngine::OnRegistryInitialize() { + Upcast(this)->OnRegistryInitialize(); +} +void PhysicsEngine::SetOnEnterCollisionCallback(CollisionCallback cb) { + Upcast(this)->SetOnEnterCollisionCallback(std::move(cb)); +} +void PhysicsEngine::SetOnExitCollisionCallback(CollisionCallback cb) { + Upcast(this)->SetOnExitCollisionCallback(std::move(cb)); +} +absl::Span PhysicsEngine::GetActiveContacts( + Entity entity_a, Entity entity_b) const { + return Upcast(this)->GetActiveContacts(entity_a, entity_b); +} +void PhysicsEngine::AdvanceFrame(absl::Duration timestep) { + Upcast(this)->AdvanceFrame(timestep); +} +RigidBodyPtr PhysicsEngine::CreateRigidBody(const RigidBodyParams& params) { + return Upcast(this)->CreateRigidBody(params); +} +TriggerVolumePtr PhysicsEngine::CreateTriggerVolume( + const TriggerVolumeParams& params) { + return Upcast(this)->CreateTriggerVolume(params); +} +CollisionShapePtr PhysicsEngine::CreateShape(CollisionDataPtr shape_data) { + return Upcast(this)->CreateShape(std::move(shape_data)); +} +CollisionShapePtr PhysicsEngine::CreateShape(HashValue name) { + return Upcast(this)->CreateShape(name); +} +void PhysicsEngine::CacheShapeData(HashValue name, CollisionDataPtr data) { + Upcast(this)->CacheShapeData(name, std::move(data)); +} +void PhysicsEngine::ReleaseShapeData(HashValue name) { + Upcast(this)->ReleaseShapeData(name); +} + +} // namespace redux + +#endif // REDUX_ENGINES_PHYSICS_THUNKS_PHYSICS_ENGINE_H_ diff --git a/redux/redux/engines/physics/thunks/rigid_body.h b/redux/redux/engines/physics/thunks/rigid_body.h new file mode 100644 index 0000000..4c0d1fd --- /dev/null +++ b/redux/redux/engines/physics/thunks/rigid_body.h @@ -0,0 +1,98 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PHYSICS_THUNKS_RIGID_BODY_H_ +#define REDUX_ENGINES_PHYSICS_THUNKS_RIGID_BODY_H_ + +#include "redux/engines/physics/rigid_body.h" + +namespace redux { + +// Thunk functions to call the actual implementation. +void RigidBody::Activate() { Upcast(this)->Activate(); } +void RigidBody::Deactivate() { Upcast(this)->Deactivate(); } +void RigidBody::SetType(RigidBodyMotionType type) { + Upcast(this)->SetType(type); +} +bool RigidBody::IsActive() const { return Upcast(this)->IsActive(); } +void RigidBody::SetTransform(const Transform& transform) { + Upcast(this)->SetTransform(transform); +} +vec3 RigidBody::GetPosition() const { return Upcast(this)->GetPosition(); } +quat RigidBody::GetRotation() const { return Upcast(this)->GetRotation(); } +void RigidBody::SetMass(float mass_in_kg) { Upcast(this)->SetMass(mass_in_kg); } +float RigidBody::GetMass() const { return Upcast(this)->GetMass(); } +void RigidBody::SetRestitution(float restitution) { + Upcast(this)->SetRestitution(restitution); +} +float RigidBody::GetRestitution() const { + return Upcast(this)->GetRestitution(); +} +void RigidBody::SetSlidingFriction(float friction) { + Upcast(this)->SetSlidingFriction(friction); +} +float RigidBody::GetSlidingFriction() const { + return Upcast(this)->GetSlidingFriction(); +} +void RigidBody::SetRollingFriction(float friction) { + Upcast(this)->SetRollingFriction(friction); +} +float RigidBody::GetRollingFriction() const { + return Upcast(this)->GetRollingFriction(); +} +void RigidBody::SetSpinningFriction(float friction) { + Upcast(this)->SetSpinningFriction(friction); +} +float RigidBody::GetSpinningFriction() const { + return Upcast(this)->GetSpinningFriction(); +} +void RigidBody::SetLinearDamping(float damping) { + Upcast(this)->SetLinearDamping(damping); +} +float RigidBody::GetLinearDamping() const { + return Upcast(this)->GetLinearDamping(); +} +void RigidBody::SetAngularDamping(float damping) { + Upcast(this)->SetAngularDamping(damping); +} +float RigidBody::GetAngularDamping() const { + return Upcast(this)->GetAngularDamping(); +} +void RigidBody::SetLinearVelocity(const vec3& velocity) { + Upcast(this)->SetLinearVelocity(velocity); +} +vec3 RigidBody::GetLinearVelocity() const { + return Upcast(this)->GetLinearVelocity(); +} +void RigidBody::SetAngularVelocity(const vec3& velocity) { + Upcast(this)->SetAngularVelocity(velocity); +} +vec3 RigidBody::GetAngularVelocity() const { + return Upcast(this)->GetAngularVelocity(); +} +void RigidBody::ApplyForce(const vec3& force, const vec3& position) { + Upcast(this)->ApplyForce(force, position); +} +void RigidBody::ApplyImpulse(const vec3& impulse, const vec3& position) { + Upcast(this)->ApplyImpulse(impulse, position); +} +void RigidBody::ApplyTorque(const vec3& torque) { + Upcast(this)->ApplyTorque(torque); +} + +} // namespace redux + +#endif // REDUX_ENGINES_PHYSICS_THUNKS_RIGID_BODY_H_ diff --git a/redux/redux/engines/physics/thunks/trigger_volume.h b/redux/redux/engines/physics/thunks/trigger_volume.h new file mode 100644 index 0000000..cacbc2f --- /dev/null +++ b/redux/redux/engines/physics/thunks/trigger_volume.h @@ -0,0 +1,36 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PHYSICS_THUNKS_TRIGGER_VOLUME_H_ +#define REDUX_ENGINES_PHYSICS_THUNKS_TRIGGER_VOLUME_H_ + +#include "redux/engines/physics/trigger_volume.h" + +namespace redux { + +// Thunk functions to call the actual implementation. +void TriggerVolume::Activate() { Upcast(this)->Activate(); } +void TriggerVolume::Deactivate() { Upcast(this)->Deactivate(); } +bool TriggerVolume::IsActive() const { return Upcast(this)->IsActive(); } +void TriggerVolume::SetTransform(const Transform& transform) { + Upcast(this)->SetTransform(transform); +} +vec3 TriggerVolume::GetPosition() const { return Upcast(this)->GetPosition(); } +quat TriggerVolume::GetRotation() const { return Upcast(this)->GetRotation(); } + +} // namespace redux + +#endif // REDUX_ENGINES_PHYSICS_THUNKS_TRIGGER_VOLUME_H_ diff --git a/redux/redux/engines/physics/trigger_volume.h b/redux/redux/engines/physics/trigger_volume.h new file mode 100644 index 0000000..67b89a9 --- /dev/null +++ b/redux/redux/engines/physics/trigger_volume.h @@ -0,0 +1,80 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PHYSICS_TRIGGER_VOLUME_H_ +#define REDUX_ENGINES_PHYSICS_TRIGGER_VOLUME_H_ + +#include + +#include "redux/engines/physics/collision_shape.h" +#include "redux/modules/base/bits.h" +#include "redux/modules/ecs/entity.h" +#include "redux/modules/math/transform.h" + +namespace redux { + +struct TriggerVolumeParams { + // The shape of the trigger volume. + CollisionShapePtr shape; + + // The Entity to which the trigger volume belongs. Used for collision + // callbacks. + Entity entity; + + // The groups to which the trigger volume belongs. + Bits32 collision_group = Bits32::All(); + + // The groups against which the trigger volume will collide. + Bits32 collision_filter = Bits32::All(); +}; + +// A trigger volume is a massless physics object with a shape/volume. +class TriggerVolume { + public: + virtual ~TriggerVolume() = default; + + TriggerVolume(const TriggerVolume&) = delete; + TriggerVolume& operator=(const TriggerVolume&) = delete; + + // Enables the trigger volume to be included when performing any potential + // collision detection. + void Activate(); + + // Disables the trigger volume from being including in any collision + // detection. + void Deactivate(); + + // Returns true if the trigger volume is active. + bool IsActive() const; + + // Sets the trigger volume's transform. + void SetTransform(const Transform& transform); + + // Returns the trigger volume's position. + vec3 GetPosition() const; + + // Returns the trigger volume's rotation. + quat GetRotation() const; + + protected: + TriggerVolume() = default; +}; + +using TriggerVolumePtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_PHYSICS_TRIGGER_VOLUME_H_ diff --git a/redux/redux/engines/platform/BUILD b/redux/redux/engines/platform/BUILD new file mode 100644 index 0000000..13f6174 --- /dev/null +++ b/redux/redux/engines/platform/BUILD @@ -0,0 +1,128 @@ +# Platform abstraction (i.e. platform-specific event processing loop) library. + +licenses(["notice"]) + +package( + default_visibility = ["//redux:visibility"], +) + +cc_library( + name = "buffered_state", + hdrs = ["buffered_state.h"], +) + +cc_library( + name = "device_manager", + srcs = ["device_manager.cc"], + hdrs = ["device_manager.h"], + deps = [ + ":device_profiles", + ":display", + ":keyboard", + ":mouse", + ":speaker", + "@absl//absl/synchronization", + "@absl//absl/types:span", + "//redux/modules/base:choreographer", + "//redux/modules/base:logging", + "//redux/modules/base:registry", + ], +) + +cc_library( + name = "device_profiles", + hdrs = ["device_profiles.h"], + deps = [ + "@absl//absl/container:flat_hash_map", + "//redux/modules/math:vector", + ], +) + +cc_library( + name = "keycodes", + srcs = ["keycodes.cc"], + hdrs = ["keycodes.h"], +) + +cc_library( + name = "display", + srcs = ["display.cc"], + hdrs = ["display.h"], + deps = [ + ":buffered_state", + ":device_profiles", + ":virtual_device", + "@absl//absl/time", + "//redux/modules/base:logging", + "//redux/modules/math:vector", + ], +) + +cc_library( + name = "keyboard", + srcs = ["keyboard.cc"], + hdrs = ["keyboard.h"], + deps = [ + ":buffered_state", + ":device_profiles", + ":keycodes", + ":virtual_device", + "@absl//absl/time", + "//redux/modules/base:logging", + "//redux/modules/math:vector", + ], +) + +cc_library( + name = "mainloop", + srcs = [ + "mainloop.cc", + ], + hdrs = [ + "mainloop.h", + ], + deps = [ + ":device_manager", + "@absl//absl/status", + "//redux/modules/base:asset_loader", + "//redux/modules/base:choreographer", + "//redux/modules/base:registry", + "//redux/modules/base:static_registry", + "//redux/modules/math:vector", + ], +) + +cc_library( + name = "mouse", + srcs = ["mouse.cc"], + hdrs = ["mouse.h"], + deps = [ + ":buffered_state", + ":device_profiles", + ":virtual_device", + "@absl//absl/time", + "//redux/modules/base:logging", + "//redux/modules/math:vector", + ], +) + +cc_library( + name = "speaker", + srcs = ["speaker.cc"], + hdrs = ["speaker.h"], + deps = [ + ":device_profiles", + ":virtual_device", + "@absl//absl/types:span", + ], +) + +cc_library( + name = "virtual_device", + srcs = ["virtual_device.cc"], + hdrs = ["virtual_device.h"], + deps = [ + "@absl//absl/time", + "//redux/modules/math:vector", + ], +) diff --git a/redux/redux/engines/platform/buffered_state.h b/redux/redux/engines/platform/buffered_state.h new file mode 100644 index 0000000..a3c4a28 --- /dev/null +++ b/redux/redux/engines/platform/buffered_state.h @@ -0,0 +1,73 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_BUFFERED_STATE_H_ +#define REDUX_ENGINES_PLATFORM_BUFFERED_STATE_H_ + +namespace redux { +namespace detail { + +// A container of type T that acts like a "triple-buffer". +// +// One way to picture this container is like a 3-element queue. The "top" of the +// queue is mutable/writable. Once the user is done mutating the element, it can +// be "committed". This will push the element further down into the container, +// making it the current read-only element. The previous read-only element +// gets pushed back as well, and the one prior to that is removed. And a new +// writable/mutable element is placed at the top. +template +class BufferedState { + public: + BufferedState() = default; + + // Initializes all internal states to the given |reference_state|. + void Initialize(const T& reference_state) { + write_buffer_ = reference_state; + for (T& state : read_buffers_) { + state = reference_state; + } + } + + // Makes the active mutable value the new "current" read-only value, pushing + // the other read-only elements down one-level of history. The new mutable + // element will contain stale data and should be re-written in its entirety. + void Commit() { + // Set the current buffer to be the new previous buffer. + curr_index_ = curr_index_ ? 0 : 1; + // Copy the contents of the write buffer into the current read buffer. + read_buffers_[curr_index_] = write_buffer_; + } + + // Returns the reference to writable state. + T& GetMutable() { return write_buffer_; } + + // Returns a read-only reference to most recent state. + const T& GetCurrent() const { return read_buffers_[curr_index_]; } + + // Returns a read-only reference to previous state. + const T& GetPrevious() const { return read_buffers_[curr_index_ ? 0 : 1]; } + + private: + static constexpr int kNumReadBuffers = 2; + T read_buffers_[kNumReadBuffers]; + T write_buffer_; + int curr_index_ = 0; +}; + +} // namespace detail +} // namespace redux + +#endif // REDUX_ENGINES_PLATFORM_BUFFERED_STATE_H_ diff --git a/redux/redux/engines/platform/device_manager.cc b/redux/redux/engines/platform/device_manager.cc new file mode 100644 index 0000000..7b8b7e1 --- /dev/null +++ b/redux/redux/engines/platform/device_manager.cc @@ -0,0 +1,101 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/platform/device_manager.h" + +#include "redux/modules/base/choreographer.h" + +namespace redux { + +DeviceManager::DeviceManager(Registry* registry) : registry_(registry) {} + +void DeviceManager::OnRegistryInitialize() { + auto* choreographer = registry_->Get(); + if (choreographer) { + choreographer->Add<&DeviceManager::AdvanceFrame>( + Choreographer::Stage::kInput); + } +} + +std::unique_ptr DeviceManager::Connect(DisplayProfile profile) { + return CreateDevice(std::move(profile)); +} + +std::unique_ptr DeviceManager::Connect(MouseProfile profile) { + return CreateDevice(std::move(profile)); +} + +std::unique_ptr DeviceManager::Connect(KeyboardProfile profile) { + return CreateDevice(std::move(profile)); +} + +std::unique_ptr DeviceManager::Connect(SpeakerProfile profile) { + return CreateDevice(std::move(profile)); +} + +Display::View DeviceManager::Display(size_t index) { + return GetView(index); +} + +Keyboard::View DeviceManager::Keyboard(size_t index) { + return GetView(index); +} + +Mouse::View DeviceManager::Mouse(size_t index) { + return GetView(index); +} + +Speaker::View DeviceManager::Speaker(size_t index) { + return GetView(index); +} + +void DeviceManager::SetFillAudioBufferFn(FillAudioBufferFn fn) { + absl::MutexLock lock(&audio_cb_mutex_); + audio_cb_ = std::move(fn); +} + +void DeviceManager::AudioHwCallback(absl::Span hw_buffer) { + absl::MutexLock lock(&audio_cb_mutex_); + if (audio_cb_) { + audio_cb_(hw_buffer); + } else { + // Fill with silence. + std::memset(hw_buffer.data(), 0, hw_buffer.size()); + } +} + +void DeviceManager::AdvanceFrame(absl::Duration delta_time) { + for (Device& device : devices_) { + if (device.device) { + device.device->Apply(delta_time); + } + } +} + +int DeviceManager::GetDeviceIndex(DeviceType type, int index) const { + for (int i = 0; i < devices_.size(); ++i) { + if (devices_[i].type == type) { + if (index <= 0) { + return i; + } else { + --index; + } + } + } + return -1; +} + +} // namespace redux diff --git a/redux/redux/engines/platform/device_manager.h b/redux/redux/engines/platform/device_manager.h new file mode 100644 index 0000000..a2df98d --- /dev/null +++ b/redux/redux/engines/platform/device_manager.h @@ -0,0 +1,142 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_DEVICE_MANAGER_H_ +#define REDUX_ENGINES_PLATFORM_DEVICE_MANAGER_H_ + +#include +#include +#include + +#include "absl/synchronization/mutex.h" +#include "redux/engines/platform/device_profiles.h" +#include "redux/engines/platform/display.h" +#include "redux/engines/platform/keyboard.h" +#include "redux/engines/platform/mouse.h" +#include "redux/engines/platform/speaker.h" +#include "redux/modules/base/logging.h" +#include "redux/modules/base/registry.h" + +namespace redux { + +// The DeviceManager is responsible for marshalling input events into a single, +// cohesive interface. Input events can be generated from arbitrary sources +// (ex. event loops, callbacks, polling threads, etc.) +// +// The DeviceManager keeps a small buffer of state for each connected input +// device, containing three frames: front, current, and previous. |front| is +// used for recording the incoming state for the device, i.e. from input events. +// |current| and |previous| are read-only and can be used to query the state of +// the device. This two-frame history allows for limited support of queries +// like "just pressed" and "touch delta". +// +// The AdvanceFrame function is used to update the buffer such that the |front| +// state becomes the |current| state and a new |front| state is made available +// for write operations. The DeviceManager allows multiple threads to generate +// input events by using a mutex. State information is also safe to read from +// multiple threads as they are read-only operations. However, it is assumed +// that no query operations will be performed during the AdvanceFrame call. +class DeviceManager { + public: + explicit DeviceManager(Registry* registry); + + DeviceManager(const DeviceManager&) = delete; + DeviceManager& operator=(const DeviceManager&) = delete; + + void OnRegistryInitialize(); + + // Connects a device. The DeviceManager will start tracking the state of this + // device for the duration of the connection. + std::unique_ptr Connect(DisplayProfile profile); + std::unique_ptr Connect(MouseProfile profile); + std::unique_ptr Connect(KeyboardProfile profile); + std::unique_ptr Connect(SpeakerProfile profile); + + // Returns a read-only "view" for a given device. + Display::View Display(size_t index = 0); + Keyboard::View Keyboard(size_t index = 0); + Mouse::View Mouse(size_t index = 0); + Speaker::View Speaker(size_t index = 0); + + // Updates the internal buffers such that the write-state is now the first + // read-only state and a new write-state is available. + // Important: No queries should be made concurrently while calling this + // function. + void AdvanceFrame(absl::Duration delta_time); + + // Register a function that will be called when the speaker's buffer needs + // to be filled with audio data. + // IMPORTANT! Do not make any assumptions about when this callback will be + // called (incl. from which thread). + using FillAudioBufferFn = std::function; + void SetFillAudioBufferFn(FillAudioBufferFn fn); + + // Request to fill the speaker's audio buffer. Must only be called by backend + // speaker implementation. + void AudioHwCallback(Speaker::HwBuffer hw_buffer); + + private: + struct Device { + Device(DeviceType type, VirtualDevice* device) + : type(type), device(device) {} + + template + const T* As() const { + if (type == T::Profile::kType) { + return static_cast(device); + } + return nullptr; + } + + DeviceType type; + VirtualDevice* device = nullptr; + }; + + template + std::unique_ptr CreateDevice(U profile) { + const size_t index = devices_.size(); + T* ptr = new T(std::move(profile), + [this, index]() { devices_[index].device = nullptr; }); + devices_.emplace_back(U::kType, ptr); + return std::unique_ptr(ptr); + } + + template + typename T::View GetView(size_t index) const { + typename T::View view; + const int n = GetDeviceIndex(T::Profile::kType, index); + if (n >= 0) { + view.getter = [this, n]() { return devices_[n].As(); }; + } else { + view.getter = []() { return nullptr; }; + } + return view; + } + + int GetDeviceIndex(DeviceType type, int index = 0) const; + + Registry* registry_; + std::vector devices_; + + absl::Mutex audio_cb_mutex_; + FillAudioBufferFn audio_cb_ ABSL_GUARDED_BY(audio_cb_mutex_); +}; + +} // namespace redux + +REDUX_SETUP_TYPEID(redux::DeviceManager); + +#endif // REDUX_ENGINES_PLATFORM_DEVICE_MANAGER_H_ diff --git a/redux/redux/engines/platform/device_profiles.h b/redux/redux/engines/platform/device_profiles.h new file mode 100644 index 0000000..32e23db --- /dev/null +++ b/redux/redux/engines/platform/device_profiles.h @@ -0,0 +1,158 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_DEVICE_PROFILES_H_ +#define REDUX_ENGINES_PLATFORM_DEVICE_PROFILES_H_ + +#include + +#include "absl/container/flat_hash_map.h" +#include "redux/modules/math/vector.h" + +namespace redux { + +// The different types of devices supported. +enum class DeviceType { + kDisplay, + kMouse, + kKeyboard, + kSpeaker, + kTouchpad, + kController, + kHmd, + kHand, +}; + +// Enum specifying how to handle degrees-of-freedom. +enum class DofProfile { + kNoDof, // Device does not provide dof. + kFakeDof, // dof values are emulated in software. + kRealDof, // dof values are obtained directly from hardware. +}; + +// Profile for a display device. +struct DisplayProfile { + static constexpr DeviceType kType = DeviceType::kDisplay; + + void* native_window = nullptr; + vec2i pixel_size = vec2i::Zero(); + vec2i display_size = vec2i::Zero(); + bool is_touchscreen = false; + bool supports_gestures = false; +}; + +// Profile for a mouse device. +struct MouseProfile { + static constexpr DeviceType kType = DeviceType::kMouse; + + enum Button { + kLeftButton, + kRightButton, + kMiddleButton, + kBackButton, + kForwardButton + }; + + size_t num_buttons = 0; + bool has_scroll_wheel = false; + std::optional long_press_time_ms; + + // An optional mapping between the button type and the button index. If not + // specified, we will use the enumeration value itself as the mapping. + absl::flat_hash_map button_map; +}; + +// Profile for a speaker device. +struct SpeakerProfile { + static constexpr DeviceType kType = DeviceType::kSpeaker; + + int sample_rate_hz = 0; + size_t num_channels = 0; + size_t frames_per_buffer = 0; +}; + +// Profile for a keyboard device. +struct KeyboardProfile { + static constexpr DeviceType kType = DeviceType::kKeyboard; +}; + +// Profile for a touchpad device. +struct TouchpadProfile { + static constexpr DeviceType kType = DeviceType::kTouchpad; + + bool supports_gestures = false; +}; + +// Profile for a hand tracking sensor. +struct HandProfile { + static constexpr DeviceType kType = DeviceType::kHand; + + enum Hand { kLeftHand, kRightHand, kEitherHand }; + + Hand hand = kEitherHand; + DofProfile rotation_dof = DofProfile::kNoDof; + DofProfile translation_dof = DofProfile::kNoDof; +}; + +// Profile for a head-mounted display (HMD) sensor. +struct HmdProfile { + static constexpr DeviceType kType = DeviceType::kHmd; + + enum Eye { kLeftEye, kRightEye }; + + size_t num_eyes = 2; + DofProfile rotation_dof = DofProfile::kNoDof; + DofProfile translation_dof = DofProfile::kNoDof; + + // An optional mapping between the eye type and the eye index. If not + // specified, we will use the enumeration value itself as the mapping. + absl::flat_hash_map eye_map; +}; + +// Profile for a generic controller; a collection of buttons (1D values ranging +// from 0 to 1) and sticks (2D values ranging from [-1,-1] to [1,1]). +struct ControllerProfile { + static constexpr DeviceType kType = DeviceType::kController; + + enum Button { + kPrimaryButton, + kSecondaryButton, + kTertiaryButton, + kCancelButton, + kLeftShoulder, + kRightShoulder, + kSecondaryLeftShoulder, + kSecondaryRightShoulder, + }; + + enum Stick { + kPrimaryStick, + kSecondaryStick, + kPrimaryDpad, + kSecondaryDpad, + }; + + size_t num_sticks = 0; + size_t num_buttons = 0; + absl::flat_hash_map stick_map; + absl::flat_hash_map button_map; + std::optional long_press_time_ms; + std::optional button_threshold; + std::optional dead_zone; +}; +} // namespace redux + +#endif // REDUX_ENGINES_PLATFORM_DEVICE_PROFILES_H_ diff --git a/redux/redux/engines/platform/display.cc b/redux/redux/engines/platform/display.cc new file mode 100644 index 0000000..e3ccef3 --- /dev/null +++ b/redux/redux/engines/platform/display.cc @@ -0,0 +1,41 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/platform/display.h" + +namespace redux { + +Display::Display(DisplayProfile profile, OnDestroy on_destroy) + : VirtualDevice(std::move(on_destroy)), profile_(std::move(profile)) { + DisplayState init; + init.size = profile_.display_size; + state_.Initialize(init); +} + +void Display::Apply(absl::Duration delta_time) { state_.Commit(); } + +void Display::SetSize(const vec2i& size) { state_.GetMutable().size = size; } + +const DisplayProfile* Display::View::GetProfile() const { + const Display* device = GetDevice(); + return device ? &device->profile_ : nullptr; +} + +vec2i Display::View::GetSize() const { + const Display* display = GetDevice(); + return display ? display->state_.GetCurrent().size : vec2i::Zero(); +} +} // namespace redux diff --git a/redux/redux/engines/platform/display.h b/redux/redux/engines/platform/display.h new file mode 100644 index 0000000..a72f04b --- /dev/null +++ b/redux/redux/engines/platform/display.h @@ -0,0 +1,57 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_DISPLAY_H_ +#define REDUX_ENGINES_PLATFORM_DISPLAY_H_ + +#include "redux/engines/platform/buffered_state.h" +#include "redux/engines/platform/device_profiles.h" +#include "redux/engines/platform/virtual_device.h" + +namespace redux { + +// Represents the visual device on which graphical rendering will be performed. +// +// For desktops, this represents the window. +class Display : public VirtualDevice { + public: + using Profile = DisplayProfile; + + // Records the dimensions of the display. + void SetSize(const vec2i& size); + + // The state of the display that will be exposed by the device manager. + struct View : VirtualView { + const Profile* GetProfile() const; + vec2i GetSize() const; + }; + + private: + friend class DeviceManager; + Display(DisplayProfile profile, OnDestroy on_destroy); + + void Apply(absl::Duration delta_time) override; + + struct DisplayState { + vec2i size = vec2i::Zero(); + }; + + DisplayProfile profile_; + detail::BufferedState state_; +}; +} // namespace redux + +#endif // REDUX_ENGINES_PLATFORM_DISPLAY_H_ diff --git a/redux/redux/engines/platform/keyboard.cc b/redux/redux/engines/platform/keyboard.cc new file mode 100644 index 0000000..6128e98 --- /dev/null +++ b/redux/redux/engines/platform/keyboard.cc @@ -0,0 +1,68 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/platform/keyboard.h" + +namespace redux { + +Keyboard::Keyboard(KeyboardProfile profile, OnDestroy on_destroy) + : VirtualDevice(std::move(on_destroy)), profile_(std::move(profile)) { + KeyboardState init; + state_.Initialize(init); +} + +void Keyboard::Apply(absl::Duration delta_time) { + state_.Commit(); + state_.GetMutable().keys.reset(); + state_.GetMutable().modifier = KEYMOD_NONE; +} + +void Keyboard::PressKey(KeyCode code) { state_.GetMutable().keys[code] = true; } + +void Keyboard::SetModifierState(KeyModifier modifier) { + state_.GetMutable().modifier = modifier; +} + +const KeyboardProfile* Keyboard::View::GetProfile() const { + const Keyboard* device = GetDevice(); + return device ? &device->profile_ : nullptr; +} + +Keyboard::TriggerFlag Keyboard::View::GetKeyState(KeyCode code) const { + const Keyboard* device = GetDevice(); + if (device) { + const bool curr = device->state_.GetCurrent().keys[code]; + const bool prev = device->state_.GetPrevious().keys[code]; + return DetermineTrigger(curr, prev); + } + return kReleased; +} + +std::string Keyboard::View::GetPressedKeys() const { + const Keyboard* device = GetDevice(); + if (device) { + return Chord(device->state_.GetCurrent().keys, + device->state_.GetCurrent().modifier); + } + return ""; +} + +KeyModifier Keyboard::View::GetModifierState() const { + const Keyboard* device = GetDevice(); + return device ? device->state_.GetCurrent().modifier : KEYMOD_NONE; +} + +} // namespace redux diff --git a/redux/redux/engines/platform/keyboard.h b/redux/redux/engines/platform/keyboard.h new file mode 100644 index 0000000..7d9a52a --- /dev/null +++ b/redux/redux/engines/platform/keyboard.h @@ -0,0 +1,64 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_KEYBOARD_H_ +#define REDUX_ENGINES_PLATFORM_KEYBOARD_H_ + +#include "redux/engines/platform/buffered_state.h" +#include "redux/engines/platform/device_profiles.h" +#include "redux/engines/platform/keycodes.h" +#include "redux/engines/platform/virtual_device.h" + +namespace redux { + +class Keyboard : public VirtualDevice { + public: + using Profile = KeyboardProfile; + + // Records a single keycode as being pressed. Key presses are valid only for a + // single frame and will be cleared on the next frame. We assume client code + // will handle key repeats on their own. + void PressKey(KeyCode code); + + // Records the active modifier keys that are set. The entire state must be + // set here (i.e. the modifiers should be bit-wise or'ed together). + void SetModifierState(KeyModifier modifier); + + // The state of the keyboard that will be exposed by the device manager. + struct View : VirtualView { + const Profile* GetProfile() const; + TriggerFlag GetKeyState(KeyCode code) const; + std::string GetPressedKeys() const; + KeyModifier GetModifierState() const; + }; + + private: + friend class DeviceManager; + Keyboard(KeyboardProfile profile, OnDestroy on_destroy); + + void Apply(absl::Duration delta_time) override; + + struct KeyboardState { + KeycodeBitset keys; + KeyModifier modifier; + }; + + KeyboardProfile profile_; + detail::BufferedState state_; +}; +} // namespace redux + +#endif // REDUX_ENGINES_PLATFORM_KEYBOARD_H_ diff --git a/redux/redux/engines/platform/keycodes.cc b/redux/redux/engines/platform/keycodes.cc new file mode 100644 index 0000000..9109d4a --- /dev/null +++ b/redux/redux/engines/platform/keycodes.cc @@ -0,0 +1,103 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/platform/keycodes.h" + +namespace redux { + +struct ChordMaker { + ChordMaker(const KeycodeBitset& keys, const KeyModifier& modifiers) + : keys(keys), modifiers(modifiers) {} + + std::string Build() const; + void TryAppend(std::string& str, KeyCode code, char c) const; + void TryRangedAppend(std::string& str, KeyCode first, KeyCode last, + char c) const; + + const KeycodeBitset& keys; + const KeyModifier& modifiers; +}; + +void ChordMaker::TryRangedAppend(std::string& str, KeyCode first, KeyCode last, + char c) const { + for (int i = first; i <= last; ++i) { + if (keys[i]) { + const char append_char = c + i - first; + str.push_back(append_char); + } + } +} + +void ChordMaker::TryAppend(std::string& str, KeyCode code, char c) const { + if (keys[code]) { + str.push_back(c); + } +} + +std::string ChordMaker::Build() const { + std::string str; + const bool capitalized = + modifiers & (KEYMOD_LSHIFT | KEYMOD_RSHIFT | KEYMOD_CAPS); + + TryRangedAppend(str, KEYCODE_A, KEYCODE_Z, capitalized ? 'A' : 'a'); + TryRangedAppend(str, KEYCODE_0, KEYCODE_9, '0'); + TryRangedAppend(str, KEYCODE_KP_0, KEYCODE_KP_9, '0'); + TryAppend(str, KEYCODE_KP_PLUS, '+'); + TryAppend(str, KEYCODE_KP_MINUS, '-'); + TryAppend(str, KEYCODE_KP_MULTIPLY, '*'); + TryAppend(str, KEYCODE_KP_DIVIDE, '/'); + TryAppend(str, KEYCODE_KP_ENTER, '\n'); + TryAppend(str, KEYCODE_KP_PERIOD, '.'); + TryAppend(str, KEYCODE_KP_EQUALS, '='); + TryAppend(str, KEYCODE_TAB, '\t'); + TryAppend(str, KEYCODE_RETURN, '\n'); + TryAppend(str, KEYCODE_SPACE, ' '); + TryAppend(str, KEYCODE_COMMA, ','); + TryAppend(str, KEYCODE_PERIOD, '.'); + TryAppend(str, KEYCODE_SLASH, '/'); + TryAppend(str, KEYCODE_BACKSLASH, '\\'); + TryAppend(str, KEYCODE_COLON, ':'); + TryAppend(str, KEYCODE_SEMICOLON, ';'); + TryAppend(str, KEYCODE_LEFT_BRACKET, '['); + TryAppend(str, KEYCODE_RIGHT_BRACKET, ']'); + TryAppend(str, KEYCODE_LEFT_PAREN, '('); + TryAppend(str, KEYCODE_RIGHT_PAREN, ')'); + TryAppend(str, KEYCODE_SINGLE_QUOTE, '\''); + TryAppend(str, KEYCODE_DOUBLE_QUOTE, '"'); + TryAppend(str, KEYCODE_BACK_QUOTE, '`'); + TryAppend(str, KEYCODE_EXCLAMATION, '!'); + TryAppend(str, KEYCODE_AT, '@'); + TryAppend(str, KEYCODE_HASH, '#'); + TryAppend(str, KEYCODE_DOLLAR, '$'); + TryAppend(str, KEYCODE_PERCENT, '%'); + TryAppend(str, KEYCODE_CARET, '^'); + TryAppend(str, KEYCODE_AMPERSAND, '&'); + TryAppend(str, KEYCODE_ASTERISK, '*'); + TryAppend(str, KEYCODE_QUESTION, '?'); + TryAppend(str, KEYCODE_PLUS, '+'); + TryAppend(str, KEYCODE_MINUS, '-'); + TryAppend(str, KEYCODE_LESS, '<'); + TryAppend(str, KEYCODE_EQUALS, '='); + TryAppend(str, KEYCODE_GREATER, '>'); + TryAppend(str, KEYCODE_UNDERSCORE, '_'); + return str; +} + +std::string Chord(const KeycodeBitset& keys, const KeyModifier& modifiers) { + ChordMaker c(keys, modifiers); + return c.Build(); +} +} // namespace redux diff --git a/redux/redux/engines/platform/keycodes.h b/redux/redux/engines/platform/keycodes.h new file mode 100644 index 0000000..393c530 --- /dev/null +++ b/redux/redux/engines/platform/keycodes.h @@ -0,0 +1,185 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_KEYCODES_H_ +#define REDUX_ENGINES_PLATFORM_KEYCODES_H_ + +#include +#include + +namespace redux { + +enum KeyCode { + KEYCODE_A, + KEYCODE_B, + KEYCODE_C, + KEYCODE_D, + KEYCODE_E, + KEYCODE_F, + KEYCODE_G, + KEYCODE_H, + KEYCODE_I, + KEYCODE_J, + KEYCODE_K, + KEYCODE_L, + KEYCODE_M, + KEYCODE_N, + KEYCODE_O, + KEYCODE_P, + KEYCODE_Q, + KEYCODE_R, + KEYCODE_S, + KEYCODE_T, + KEYCODE_U, + KEYCODE_V, + KEYCODE_W, + KEYCODE_X, + KEYCODE_Y, + KEYCODE_Z, + KEYCODE_0, + KEYCODE_1, + KEYCODE_2, + KEYCODE_3, + KEYCODE_4, + KEYCODE_5, + KEYCODE_6, + KEYCODE_7, + KEYCODE_8, + KEYCODE_9, + KEYCODE_KP_0, + KEYCODE_KP_1, + KEYCODE_KP_2, + KEYCODE_KP_3, + KEYCODE_KP_4, + KEYCODE_KP_5, + KEYCODE_KP_6, + KEYCODE_KP_7, + KEYCODE_KP_8, + KEYCODE_KP_9, + KEYCODE_KP_PLUS, + KEYCODE_KP_MINUS, + KEYCODE_KP_MULTIPLY, + KEYCODE_KP_DIVIDE, + KEYCODE_KP_ENTER, + KEYCODE_KP_PERIOD, + KEYCODE_KP_EQUALS, + KEYCODE_ESCAPE, + KEYCODE_TAB, + KEYCODE_RETURN, + KEYCODE_SPACE, + KEYCODE_BACKSPACE, + KEYCODE_COMMA, + KEYCODE_PERIOD, + KEYCODE_SLASH, + KEYCODE_BACKSLASH, + KEYCODE_COLON, + KEYCODE_SEMICOLON, + KEYCODE_LEFT_BRACKET, + KEYCODE_RIGHT_BRACKET, + KEYCODE_LEFT_PAREN, + KEYCODE_RIGHT_PAREN, + KEYCODE_SINGLE_QUOTE, + KEYCODE_DOUBLE_QUOTE, + KEYCODE_BACK_QUOTE, + KEYCODE_EXCLAMATION, + KEYCODE_AT, + KEYCODE_HASH, + KEYCODE_DOLLAR, + KEYCODE_PERCENT, + KEYCODE_CARET, + KEYCODE_AMPERSAND, + KEYCODE_ASTERISK, + KEYCODE_QUESTION, + KEYCODE_PLUS, + KEYCODE_MINUS, + KEYCODE_LESS, + KEYCODE_EQUALS, + KEYCODE_GREATER, + KEYCODE_UNDERSCORE, + KEYCODE_UP, + KEYCODE_DOWN, + KEYCODE_LEFT, + KEYCODE_RIGHT, + KEYCODE_CAPS_LOCK, + KEYCODE_NUM_LOCK, + KEYCODE_SCROLL_LOCK, + KEYCODE_PRINT_SCREEN, + KEYCODE_PAUSE, + KEYCODE_INSERT, + KEYCODE_DELETE, + KEYCODE_HOME, + KEYCODE_END, + KEYCODE_PAGEUP, + KEYCODE_PAGEDOWN, + KEYCODE_LEFT_CTRL, + KEYCODE_LEFT_SHIFT, + KEYCODE_LEFT_ALT, + KEYCODE_LEFT_GUI, + KEYCODE_RIGHT_CTRL, + KEYCODE_RIGHT_SHIFT, + KEYCODE_RIGHT_ALT, + KEYCODE_RIGHT_GUI, + KEYCODE_MODE, + KEYCODE_F1, + KEYCODE_F2, + KEYCODE_F3, + KEYCODE_F4, + KEYCODE_F5, + KEYCODE_F6, + KEYCODE_F7, + KEYCODE_F8, + KEYCODE_F9, + KEYCODE_F10, + KEYCODE_F11, + KEYCODE_F12, + KEYCODE_F13, + KEYCODE_F14, + KEYCODE_F15, + KEYCODE_F16, + KEYCODE_F17, + KEYCODE_F18, + KEYCODE_F19, + KEYCODE_F20, + KEYCODE_F21, + KEYCODE_F22, + KEYCODE_F23, + KEYCODE_F24, + NUM_KEYCODES, +}; + +enum KeyModifier { + KEYMOD_NONE = 0x0000, + KEYMOD_LSHIFT = 0x0001, + KEYMOD_RSHIFT = 0x0002, + KEYMOD_LCTRL = 0x0040, + KEYMOD_RCTRL = 0x0080, + KEYMOD_LALT = 0x0100, + KEYMOD_RALT = 0x0200, + KEYMOD_LGUI = 0x0400, + KEYMOD_RGUI = 0x0800, + KEYMOD_NUM = 0x1000, + KEYMOD_CAPS = 0x2000, + KEYMOD_MODE = 0x4000, +}; + +// Returns a string that represents the combination of all the keys (including +// modifiers) being pressed at the same time. +using KeycodeBitset = std::bitset; +std::string Chord(const KeycodeBitset& keys, const KeyModifier& modifiers); + +} // namespace redux + +#endif // REDUX_ENGINES_PLATFORM_KEYCODES_H_ diff --git a/redux/redux/engines/platform/mainloop.cc b/redux/redux/engines/platform/mainloop.cc new file mode 100644 index 0000000..e5e0c9e --- /dev/null +++ b/redux/redux/engines/platform/mainloop.cc @@ -0,0 +1,64 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/platform/mainloop.h" + +#ifdef REDUX_EDITOR +#include "redux/editor/editor.h" +#endif +#include "redux/engines/platform/device_manager.h" +#include "redux/modules/base/asset_loader.h" +#include "redux/modules/base/choreographer.h" +#include "redux/modules/base/static_registry.h" + +namespace redux { + +Mainloop::Mainloop() { + registry_.Create(GetRegistry()); + registry_.Create(GetRegistry()); + registry_.Create(GetRegistry()); + StaticRegistry::Create(GetRegistry()); +} + +void Mainloop::Initialize() { registry_.Initialize(); } + +absl::StatusCode Mainloop::Run(const PerFrameCallback& cb) { + absl::Time last_time = absl::Now(); + absl::StatusCode status = absl::StatusCode::kOk; + while (status == absl::StatusCode::kOk) { + status = PollEvents(); + if (status == absl::StatusCode::kOk && cb) { + status = cb(); + } + + if (status == absl::StatusCode::kOk) { + const absl::Time now = absl::Now(); + const absl::Duration delta = now - last_time; +#ifdef REDUX_EDITOR + // Editor will step the choreographer, but can do things like slow + // down or single step the framerate. + status = registry_.Get()->Update(delta); +#else + registry_.Get()->Step(delta); +#endif + last_time = now; + } + } + + return status; +} + +} // namespace redux diff --git a/redux/redux/engines/platform/mainloop.h b/redux/redux/engines/platform/mainloop.h new file mode 100644 index 0000000..97f90f4 --- /dev/null +++ b/redux/redux/engines/platform/mainloop.h @@ -0,0 +1,72 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_MAINLOOP_H_ +#define REDUX_ENGINES_PLATFORM_MAINLOOP_H_ + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "redux/modules/base/registry.h" +#include "redux/modules/math/vector.h" + +namespace redux { + +// The Main Event Loop. +// +// This is the main entry point for redux runtimes. It owns the Registry and is +// responsible for "running" the system event loop. It also provides the +// mechanism through which system devices (e.g. display, mouse, keyboard, etc.) +// are created. +class Mainloop { + public: + static std::unique_ptr Create(); + virtual ~Mainloop() = default; + + Mainloop(const Mainloop&) = delete; + Mainloop& operator=(const Mainloop&) = delete; + + virtual void CreateDisplay(std::string_view title, vec2i size) = 0; + virtual void CreateKeyboard() = 0; + virtual void CreateMouse() = 0; + virtual void CreateSpeaker() = 0; + + // Initializes the registry. + void Initialize(); + + // Runs the mainloop. A user-supplied callback can be provided which will be + // run "inside" the loop. If the callback returns a non-Ok status, the loop + // will be exited. + using PerFrameCallback = std::function; + absl::StatusCode Run(const PerFrameCallback& cb); + + // Returns the main registry. + Registry* GetRegistry() { return ®istry_; } + + protected: + Mainloop(); + + virtual absl::StatusCode PollEvents() = 0; + + Registry registry_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_PLATFORM_MAINLOOP_H_ diff --git a/redux/redux/engines/platform/mouse.cc b/redux/redux/engines/platform/mouse.cc new file mode 100644 index 0000000..7dc84ef --- /dev/null +++ b/redux/redux/engines/platform/mouse.cc @@ -0,0 +1,93 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/platform/mouse.h" + +#include + +#include "redux/modules/base/logging.h" + +namespace redux { + +Mouse::Mouse(MouseProfile profile, OnDestroy on_destroy) + : VirtualDevice(std::move(on_destroy)), profile_(std::move(profile)) { + MouseState init; + init.buttons.resize(profile_.num_buttons); + state_.Initialize(init); +} + +void Mouse::Apply(absl::Duration delta_time) { + timestamp_ += delta_time; + state_.Commit(); +} + +void Mouse::SetButton(Button button, TriggerFlag state) { + CHECK(state == kPressed || state == kReleased); + const bool active = (state & kPressed); + + const int idx = GetIndexForButton(button); + auto& button_state = state_.GetMutable().buttons[idx]; + if (button_state.active != active) { + button_state.toggle_time = timestamp_; + } + button_state.active = active; +} + +void Mouse::SetPosition(const vec2i& value) { + state_.GetMutable().position = value; +} + +void Mouse::SetScrollDelta(int delta) { + CHECK(profile_.has_scroll_wheel); + if (profile_.has_scroll_wheel) { + state_.GetMutable().scroll_value += delta; + } +} + +const MouseProfile* Mouse::View::GetProfile() const { + const Mouse* device = GetDevice(); + return device ? &device->profile_ : nullptr; +} + +vec2i Mouse::View::GetPosition() const { + const Mouse* mouse = GetDevice(); + return mouse ? mouse->state_.GetCurrent().position : vec2i::Zero(); +} + +int Mouse::View::GetScrollValue() const { + const Mouse* mouse = GetDevice(); + return mouse ? mouse->state_.GetCurrent().scroll_value : 0; +} + +Mouse::TriggerFlag Mouse::View::GetButtonState(Button button) const { + const Mouse* mouse = GetDevice(); + if (mouse) { + const int idx = mouse->GetIndexForButton(button); + const auto& curr = mouse->state_.GetCurrent().buttons[idx]; + const auto& prev = mouse->state_.GetPrevious().buttons[idx]; + return DetermineTrigger(curr, prev, mouse->profile_.long_press_time_ms); + } + return kReleased; +} + +int Mouse::GetIndexForButton(Button button) const { + CHECK(profile_.button_map.empty()); + const int index = static_cast(button); + CHECK(index < profile_.num_buttons); + return index; +} + +} // namespace redux diff --git a/redux/redux/engines/platform/mouse.h b/redux/redux/engines/platform/mouse.h new file mode 100644 index 0000000..3f0cdc6 --- /dev/null +++ b/redux/redux/engines/platform/mouse.h @@ -0,0 +1,68 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_MOUSE_H_ +#define REDUX_ENGINES_PLATFORM_MOUSE_H_ + +#include "redux/engines/platform/buffered_state.h" +#include "redux/engines/platform/device_profiles.h" +#include "redux/engines/platform/virtual_device.h" + +namespace redux { + +class Mouse : public VirtualDevice { + public: + using Profile = MouseProfile; + using Button = MouseProfile::Button; + + // Records the active buttons of the mouse. + void SetButton(Button button, TriggerFlag state); + + // Records the position of the mouse. + void SetPosition(const vec2i& value); + + // Records the amount the scroll wheel has been moved. + void SetScrollDelta(int delta); + + // The state of the mouse that will be exposed by the device manager. + struct View : VirtualView { + const Profile* GetProfile() const; + vec2i GetPosition() const; + int GetScrollValue() const; + TriggerFlag GetButtonState(Button button) const; + }; + + private: + friend class DeviceManager; + Mouse(MouseProfile profile, OnDestroy on_destroy); + + void Apply(absl::Duration delta_time) override; + + int GetIndexForButton(Button button) const; + + struct MouseState { + vec2i position = vec2i::Zero(); + int scroll_value = 0; + std::vector buttons; + }; + + MouseProfile profile_; + detail::BufferedState state_; + absl::Duration timestamp_; +}; +} // namespace redux + +#endif // REDUX_ENGINES_PLATFORM_MOUSE_H_ diff --git a/redux/redux/engines/platform/sdl2/BUILD b/redux/redux/engines/platform/sdl2/BUILD new file mode 100644 index 0000000..1e3ffea --- /dev/null +++ b/redux/redux/engines/platform/sdl2/BUILD @@ -0,0 +1,62 @@ +# SDL2 backend for platform abstraction library. + +licenses(["notice"]) + +package( + default_visibility = ["//redux:visibility"], +) + + +cc_library( + name = "sdl2", + srcs = [ + "sdl2_display.cc", + "sdl2_keyboard.cc", + "sdl2_mainloop.cc", + "sdl2_mouse.cc", + "sdl2_speaker.cc", + ], + hdrs = [ + "sdl2_display.h", + "sdl2_event_handler.h", + "sdl2_keyboard.h", + "sdl2_mainloop.h", + "sdl2_mouse.h", + "sdl2_speaker.h", + ], + deps = [ + "@sdl2//:sdl2", + "//redux/modules/math:vector", + "//redux/engines/platform:device_manager", + "//redux/engines/platform:display", + "//redux/engines/platform:mainloop", + "//redux/engines/platform:mouse", + "//redux/engines/platform:keyboard", + "//redux/engines/platform:speaker", + ] + select({ + "@platforms//os:osx": [ + ":get_native_window_osx", + ], + "//conditions:default": [ + ], + }), +) + +objc_library( + name = "get_native_window_osx", + srcs = select({ + "@platforms//os:osx": [ + "get_native_window_osx.mm", + ], + "//conditions:default": [ + ], + }), + deps = select({ + "@platforms//os:osx": [ + # "//third_party/apple_frameworks:Cocoa", + # "//third_party/apple_frameworks:Metal", + ], + "//conditions:default": [ + ], + }), +) diff --git a/redux/redux/engines/platform/sdl2/get_native_window_osx.mm b/redux/redux/engines/platform/sdl2/get_native_window_osx.mm new file mode 100644 index 0000000..2987a0a --- /dev/null +++ b/redux/redux/engines/platform/sdl2/get_native_window_osx.mm @@ -0,0 +1,24 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#import +#import + +void* GetNativeWindowOsx(void* window) { + NSWindow* ns_window = (__bridge NSWindow*)window; + NSView* ns_view = [ns_window contentView]; + return (__bridge void*)ns_view; +} diff --git a/redux/redux/engines/platform/sdl2/sdl2_display.cc b/redux/redux/engines/platform/sdl2/sdl2_display.cc new file mode 100644 index 0000000..3a96cbc --- /dev/null +++ b/redux/redux/engines/platform/sdl2/sdl2_display.cc @@ -0,0 +1,92 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/platform/sdl2/sdl2_display.h" + +#include "SDL_syswm.h" +#include "redux/engines/platform/device_manager.h" + +#if defined(__APPLE__) +extern void* GetNativeWindowOsx(void* window); +#endif + +namespace redux { + +std::unique_ptr Sdl2Display::Create(DeviceManager* dm, + std::string_view title, + const vec2i& size) { + auto window = SDL_CreateWindow(title.data(), SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, size.x, size.y, 0); + + auto display = std::make_unique(); + display->OnWindowCreated(dm, window, size); + return display; +} + +std::unique_ptr Sdl2Display::CreateHeadless(DeviceManager* dm, + const vec2i& size) { + auto window = + SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + size.x, size.y, SDL_WINDOW_HIDDEN); + + auto display = std::make_unique(); + display->OnWindowCreated(dm, window, size); + return display; +} + +Sdl2Display::~Sdl2Display() { + display_.reset(); + SDL_DestroyWindow(window_); +} + +void Sdl2Display::OnWindowCreated(DeviceManager* dm, SDL_Window* window, + const vec2i& size) { + CHECK(window != nullptr); + window_ = window; + + SDL_SysWMinfo wmi; + SDL_VERSION(&wmi.version); + SDL_GetWindowWMInfo(window_, &wmi); + +#if defined(__linux__) + native_window_ = reinterpret_cast(wmi.info.x11.window); +#elif defined(MSC_VER) + native_window_ = reinterpret_cast(wmi.info.win.window); +#elif defined(__APPLE__) + // Looks like a bug in SDL? https://hg.libsdl.org/SDL/rev/ab7529cb9558 + SDL_SetWindowSize(window_, size.x / 2, size.y / 2); + native_window_ = + GetNativeWindowOsx(reinterpret_cast(wmi.info.cocoa.window)); +#endif + + DisplayProfile profile; + profile.native_window = native_window_; + profile.display_size = size; + display_ = dm->Connect(profile); +} + +void Sdl2Display::Commit() { + int w = 0; + int h = 0; + SDL_GetWindowSize(window_, &w, &h); + display_->SetSize(vec2i(w, h)); + +#if defined(__APPLE__) + SDL_GL_GetDrawableSize(window_, &w, &h); + display_->SetSize(vec2i(w, h)); +#endif +} +} // namespace redux diff --git a/redux/redux/engines/platform/sdl2/sdl2_display.h b/redux/redux/engines/platform/sdl2/sdl2_display.h new file mode 100644 index 0000000..698e02e --- /dev/null +++ b/redux/redux/engines/platform/sdl2/sdl2_display.h @@ -0,0 +1,52 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_SDL2_SDL2_DISPLAY_H_ +#define REDUX_ENGINES_PLATFORM_SDL2_SDL2_DISPLAY_H_ + +#include "redux/engines/platform/device_manager.h" +#include "redux/engines/platform/display.h" +#include "redux/engines/platform/sdl2/sdl2_event_handler.h" + +namespace redux { + +class Sdl2Display : public Sdl2EventHandler { + public: + // Creates an SDL2 window with given title and size. + static std::unique_ptr Create(DeviceManager* dm, + std::string_view title, + const vec2i& size); + + // Creates a "headless" SDL2 display of the given size. + static std::unique_ptr CreateHeadless(DeviceManager* dm, + const vec2i& size); + + ~Sdl2Display() override; + + void Commit() override; + + private: + void OnWindowCreated(DeviceManager* dm, SDL_Window* window, + const vec2i& size); + + SDL_Window* window_ = nullptr; + void* native_window_ = nullptr; + std::unique_ptr display_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_PLATFORM_SDL2_SDL2_DISPLAY_H_ diff --git a/redux/redux/engines/platform/sdl2/sdl2_event_handler.h b/redux/redux/engines/platform/sdl2/sdl2_event_handler.h new file mode 100644 index 0000000..f167027 --- /dev/null +++ b/redux/redux/engines/platform/sdl2/sdl2_event_handler.h @@ -0,0 +1,33 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_SDL2_SDL2_EVENT_HANDLER_H_ +#define REDUX_ENGINES_PLATFORM_SDL2_SDL2_EVENT_HANDLER_H_ + +#include "SDL.h" + +namespace redux { + +class Sdl2EventHandler { + public: + virtual ~Sdl2EventHandler() = default; + virtual void HandleEvent(SDL_Event event) {} + virtual void Commit() {} +}; + +} // namespace redux + +#endif // REDUX_ENGINES_PLATFORM_SDL2_SDL2_EVENT_HANDLER_H_ diff --git a/redux/redux/engines/platform/sdl2/sdl2_keyboard.cc b/redux/redux/engines/platform/sdl2/sdl2_keyboard.cc new file mode 100644 index 0000000..49dde2c --- /dev/null +++ b/redux/redux/engines/platform/sdl2/sdl2_keyboard.cc @@ -0,0 +1,210 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/platform/sdl2/sdl2_keyboard.h" + +#include "redux/engines/platform/device_manager.h" + +namespace redux { + +#define KEYCODES \ + KEY(A, SDLK_a) \ + KEY(B, SDLK_b) \ + KEY(C, SDLK_c) \ + KEY(D, SDLK_d) \ + KEY(E, SDLK_e) \ + KEY(F, SDLK_f) \ + KEY(G, SDLK_g) \ + KEY(H, SDLK_h) \ + KEY(I, SDLK_i) \ + KEY(J, SDLK_j) \ + KEY(K, SDLK_k) \ + KEY(L, SDLK_l) \ + KEY(M, SDLK_m) \ + KEY(N, SDLK_n) \ + KEY(O, SDLK_o) \ + KEY(P, SDLK_p) \ + KEY(Q, SDLK_q) \ + KEY(R, SDLK_r) \ + KEY(S, SDLK_s) \ + KEY(T, SDLK_t) \ + KEY(U, SDLK_u) \ + KEY(V, SDLK_v) \ + KEY(W, SDLK_w) \ + KEY(X, SDLK_x) \ + KEY(Y, SDLK_y) \ + KEY(Z, SDLK_z) \ + KEY(0, SDLK_0) \ + KEY(1, SDLK_1) \ + KEY(2, SDLK_2) \ + KEY(3, SDLK_3) \ + KEY(4, SDLK_4) \ + KEY(5, SDLK_5) \ + KEY(6, SDLK_6) \ + KEY(7, SDLK_7) \ + KEY(8, SDLK_8) \ + KEY(9, SDLK_9) \ + KEY(KP_0, SDLK_KP_0) \ + KEY(KP_1, SDLK_KP_1) \ + KEY(KP_2, SDLK_KP_2) \ + KEY(KP_3, SDLK_KP_3) \ + KEY(KP_4, SDLK_KP_4) \ + KEY(KP_5, SDLK_KP_5) \ + KEY(KP_6, SDLK_KP_6) \ + KEY(KP_7, SDLK_KP_7) \ + KEY(KP_8, SDLK_KP_8) \ + KEY(KP_9, SDLK_KP_9) \ + KEY(KP_PLUS, SDLK_KP_PLUS) \ + KEY(KP_MINUS, SDLK_KP_MINUS) \ + KEY(KP_MULTIPLY, SDLK_KP_MULTIPLY) \ + KEY(KP_DIVIDE, SDLK_KP_DIVIDE) \ + KEY(KP_ENTER, SDLK_KP_ENTER) \ + KEY(KP_PERIOD, SDLK_KP_PERIOD) \ + KEY(KP_EQUALS, SDLK_KP_EQUALS) \ + KEY(ESCAPE, SDLK_ESCAPE) \ + KEY(TAB, SDLK_TAB) \ + KEY(RETURN, SDLK_RETURN) \ + KEY(SPACE, SDLK_SPACE) \ + KEY(BACKSPACE, SDLK_BACKSPACE) \ + KEY(COMMA, SDLK_COMMA) \ + KEY(PERIOD, SDLK_PERIOD) \ + KEY(SLASH, SDLK_SLASH) \ + KEY(BACKSLASH, SDLK_BACKSLASH) \ + KEY(COLON, SDLK_COLON) \ + KEY(SEMICOLON, SDLK_SEMICOLON) \ + KEY(LEFT_BRACKET, SDLK_LEFTBRACKET) \ + KEY(RIGHT_BRACKET, SDLK_RIGHTBRACKET) \ + KEY(LEFT_PAREN, SDLK_LEFTPAREN) \ + KEY(RIGHT_PAREN, SDLK_RIGHTPAREN) \ + KEY(SINGLE_QUOTE, SDLK_QUOTE) \ + KEY(DOUBLE_QUOTE, SDLK_QUOTEDBL) \ + KEY(BACK_QUOTE, SDLK_BACKQUOTE) \ + KEY(EXCLAMATION, SDLK_EXCLAIM) \ + KEY(AT, SDLK_AT) \ + KEY(HASH, SDLK_HASH) \ + KEY(DOLLAR, SDLK_DOLLAR) \ + KEY(PERCENT, SDLK_PERCENT) \ + KEY(CARET, SDLK_CARET) \ + KEY(AMPERSAND, SDLK_AMPERSAND) \ + KEY(ASTERISK, SDLK_ASTERISK) \ + KEY(QUESTION, SDLK_QUESTION) \ + KEY(PLUS, SDLK_PLUS) \ + KEY(MINUS, SDLK_MINUS) \ + KEY(LESS, SDLK_LESS) \ + KEY(EQUALS, SDLK_EQUALS) \ + KEY(GREATER, SDLK_GREATER) \ + KEY(UNDERSCORE, SDLK_UNDERSCORE) \ + KEY(UP, SDLK_UP) \ + KEY(DOWN, SDLK_DOWN) \ + KEY(LEFT, SDLK_LEFT) \ + KEY(RIGHT, SDLK_RIGHT) \ + KEY(CAPS_LOCK, SDLK_CAPSLOCK) \ + KEY(NUM_LOCK, SDLK_NUMLOCKCLEAR) \ + KEY(SCROLL_LOCK, SDLK_SCROLLLOCK) \ + KEY(PRINT_SCREEN, SDLK_PRINTSCREEN) \ + KEY(PAUSE, SDLK_PAUSE) \ + KEY(INSERT, SDLK_INSERT) \ + KEY(DELETE, SDLK_DELETE) \ + KEY(HOME, SDLK_HOME) \ + KEY(END, SDLK_END) \ + KEY(PAGEUP, SDLK_PAGEUP) \ + KEY(PAGEDOWN, SDLK_PAGEDOWN) \ + KEY(LEFT_CTRL, SDLK_LCTRL) \ + KEY(LEFT_SHIFT, SDLK_LSHIFT) \ + KEY(LEFT_ALT, SDLK_LALT) \ + KEY(LEFT_GUI, SDLK_LGUI) \ + KEY(RIGHT_CTRL, SDLK_RCTRL) \ + KEY(RIGHT_SHIFT, SDLK_RSHIFT) \ + KEY(RIGHT_ALT, SDLK_RALT) \ + KEY(RIGHT_GUI, SDLK_RGUI) \ + KEY(MODE, SDLK_MODE) \ + KEY(F1, SDLK_F1) \ + KEY(F2, SDLK_F2) \ + KEY(F3, SDLK_F3) \ + KEY(F4, SDLK_F4) \ + KEY(F5, SDLK_F5) \ + KEY(F6, SDLK_F6) \ + KEY(F7, SDLK_F7) \ + KEY(F8, SDLK_F8) \ + KEY(F9, SDLK_F9) \ + KEY(F10, SDLK_F10) \ + KEY(F11, SDLK_F11) \ + KEY(F12, SDLK_F12) \ + KEY(F13, SDLK_F13) \ + KEY(F14, SDLK_F14) \ + KEY(F15, SDLK_F15) \ + KEY(F16, SDLK_F16) \ + KEY(F17, SDLK_F17) \ + KEY(F18, SDLK_F18) \ + KEY(F19, SDLK_F19) \ + KEY(F20, SDLK_F20) \ + KEY(F21, SDLK_F21) \ + KEY(F22, SDLK_F22) \ + KEY(F23, SDLK_F23) \ + KEY(F24, SDLK_F24) + +// The mapping bewteen the redux name and the SDL name for a give key modifier. +#define KEYMODS \ + KEY(NONE, KMOD_NONE) \ + KEY(LSHIFT, KMOD_LSHIFT) \ + KEY(RSHIFT, KMOD_RSHIFT) \ + KEY(LCTRL, KMOD_LCTRL) \ + KEY(RCTRL, KMOD_RCTRL) \ + KEY(LALT, KMOD_LALT) \ + KEY(RALT, KMOD_RALT) \ + KEY(LGUI, KMOD_LGUI) \ + KEY(RGUI, KMOD_RGUI) \ + KEY(NUM, KMOD_NUM) \ + KEY(CAPS, KMOD_CAPS) \ + KEY(MODE, KMOD_MODE) + +static KeyCode ConvertSdlKeycode(SDL_Keycode sdl) { +#define KEY(X, Y) \ + case Y: \ + return KEYCODE_##X; + switch (sdl) { + KEYCODES + default: + CHECK(false) << "Unknown SDL keycode: " << sdl; + } +#undef KEY +} + +static KeyModifier ConvertSdlKeyModifier(SDL_Keymod sdl) { +#define KEY(X, Y) static_assert(static_cast(KEYMOD_##X) == Y); + KEYMODS +#undef KEY + return static_cast(sdl); +} + +Sdl2Keyboard::Sdl2Keyboard(DeviceManager* dm) { + KeyboardProfile profile; + keyboard_ = dm->Connect(profile); +} + +Sdl2Keyboard::~Sdl2Keyboard() { keyboard_.reset(); } + +void Sdl2Keyboard::HandleEvent(SDL_Event event) { + if (event.type == SDL_KEYDOWN) { + keyboard_->PressKey(ConvertSdlKeycode(event.key.keysym.sym)); + } +} + +void Sdl2Keyboard::Commit() { + const SDL_Keymod mod = SDL_GetModState(); + keyboard_->SetModifierState(ConvertSdlKeyModifier(mod)); +} +} // namespace redux diff --git a/redux/redux/engines/platform/sdl2/sdl2_keyboard.h b/redux/redux/engines/platform/sdl2/sdl2_keyboard.h new file mode 100644 index 0000000..d5b8284 --- /dev/null +++ b/redux/redux/engines/platform/sdl2/sdl2_keyboard.h @@ -0,0 +1,40 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_SDL2_SDL2_KEYBOARD_H_ +#define REDUX_ENGINES_PLATFORM_SDL2_SDL2_KEYBOARD_H_ + +#include "redux/engines/platform/device_manager.h" +#include "redux/engines/platform/keyboard.h" +#include "redux/engines/platform/sdl2/sdl2_event_handler.h" + +namespace redux { + +class Sdl2Keyboard : public Sdl2EventHandler { + public: + explicit Sdl2Keyboard(DeviceManager* dm); + ~Sdl2Keyboard() override; + + void HandleEvent(SDL_Event event) override; + void Commit() override; + + private: + std::unique_ptr keyboard_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_PLATFORM_SDL2_SDL2_KEYBOARD_H_ diff --git a/redux/redux/engines/platform/sdl2/sdl2_mainloop.cc b/redux/redux/engines/platform/sdl2/sdl2_mainloop.cc new file mode 100644 index 0000000..79df58e --- /dev/null +++ b/redux/redux/engines/platform/sdl2/sdl2_mainloop.cc @@ -0,0 +1,79 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/platform/sdl2/sdl2_mainloop.h" + +#include + +#include "redux/engines/platform/sdl2/sdl2_display.h" +#include "redux/engines/platform/sdl2/sdl2_keyboard.h" +#include "redux/engines/platform/sdl2/sdl2_mouse.h" +#include "redux/engines/platform/sdl2/sdl2_speaker.h" + +namespace redux { + +std::unique_ptr Mainloop::Create() { + auto ptr = new Sdl2Mainloop(); + return std::unique_ptr(ptr); +} + +Sdl2Mainloop::Sdl2Mainloop() { + const auto init = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS); + CHECK_EQ(init, 0); +} + +Sdl2Mainloop::~Sdl2Mainloop() { SDL_Quit(); } + +void Sdl2Mainloop::CreateDisplay(std::string_view title, vec2i size) { + auto device_manager = registry_.Get(); + handlers_.emplace_back(Sdl2Display::Create(device_manager, title, size)); +} + +void Sdl2Mainloop::CreateKeyboard() { + auto device_manager = registry_.Get(); + handlers_.emplace_back(new Sdl2Keyboard(device_manager)); +} + +void Sdl2Mainloop::CreateMouse() { + auto device_manager = registry_.Get(); + handlers_.emplace_back(new Sdl2Mouse(device_manager)); +} + +void Sdl2Mainloop::CreateSpeaker() { + auto device_manager = registry_.Get(); + handlers_.emplace_back(new Sdl2Speaker(device_manager)); +} + +absl::StatusCode Sdl2Mainloop::PollEvents() { + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + return absl::StatusCode::kCancelled; + } else if (event.type == SDL_APP_WILLENTERBACKGROUND) { + return absl::StatusCode::kCancelled; + } + + for (auto& iter : handlers_) { + iter->HandleEvent(event); + } + } + for (auto& iter : handlers_) { + iter->Commit(); + } + return absl::StatusCode::kOk; +} + +} // namespace redux diff --git a/redux/redux/engines/platform/sdl2/sdl2_mainloop.h b/redux/redux/engines/platform/sdl2/sdl2_mainloop.h new file mode 100644 index 0000000..c83126d --- /dev/null +++ b/redux/redux/engines/platform/sdl2/sdl2_mainloop.h @@ -0,0 +1,48 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_SDL2_SDL2_MAINLOOP_H_ +#define REDUX_ENGINES_PLATFORM_SDL2_SDL2_MAINLOOP_H_ + +#include +#include +#include + +#include "SDL.h" +#include "redux/engines/platform/mainloop.h" +#include "redux/engines/platform/sdl2/sdl2_event_handler.h" + +namespace redux { + +class Sdl2Mainloop : public Mainloop { + public: + Sdl2Mainloop(); + ~Sdl2Mainloop() override; + + void CreateDisplay(std::string_view title, vec2i size) override; + void CreateKeyboard() override; + void CreateMouse() override; + void CreateSpeaker() override; + + private: + absl::StatusCode PollEvents() override; + + std::vector> handlers_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_PLATFORM_SDL2_SDL2_MAINLOOP_H_ diff --git a/redux/redux/engines/platform/sdl2/sdl2_mouse.cc b/redux/redux/engines/platform/sdl2/sdl2_mouse.cc new file mode 100644 index 0000000..50aa6f7 --- /dev/null +++ b/redux/redux/engines/platform/sdl2/sdl2_mouse.cc @@ -0,0 +1,71 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/platform/sdl2/sdl2_mouse.h" + +#include "SDL_events.h" +#include "redux/engines/platform/device_manager.h" + +namespace redux { + +Sdl2Mouse::Sdl2Mouse(DeviceManager* dm) { + MouseProfile profile; + profile.num_buttons = 3; + profile.has_scroll_wheel = true; + mouse_ = dm->Connect(profile); +} + +Sdl2Mouse::~Sdl2Mouse() { mouse_.reset(); } + +static std::optional ButtonType(SDL_Event event) { + if (event.button.button == SDL_BUTTON_LEFT) { + return MouseProfile::kLeftButton; + } else if (event.button.button == SDL_BUTTON_RIGHT) { + return MouseProfile::kRightButton; + } else if (event.button.button == SDL_BUTTON_MIDDLE) { + return MouseProfile::kMiddleButton; + } else if (event.button.button == SDL_BUTTON_X1) { + return MouseProfile::kForwardButton; + } else if (event.button.button == SDL_BUTTON_X2) { + return MouseProfile::kBackButton; + } else { + return std::nullopt; + } +} + +void Sdl2Mouse::HandleEvent(SDL_Event event) { + if (event.type == SDL_MOUSEBUTTONDOWN) { + auto button = ButtonType(event); + if (button) { + mouse_->SetButton(button.value(), Mouse::kPressed); + } + } else if (event.type == SDL_MOUSEBUTTONUP) { + auto button = ButtonType(event); + if (button) { + mouse_->SetButton(button.value(), Mouse::kReleased); + } + } else if (event.type == SDL_MOUSEWHEEL) { + mouse_->SetScrollDelta(event.wheel.x); + } +} + +void Sdl2Mouse::Commit() { + int x = 0; + int y = 0; + SDL_GetMouseState(&x, &y); + mouse_->SetPosition(vec2i(x, y)); +} +} // namespace redux diff --git a/redux/redux/engines/platform/sdl2/sdl2_mouse.h b/redux/redux/engines/platform/sdl2/sdl2_mouse.h new file mode 100644 index 0000000..faf2882 --- /dev/null +++ b/redux/redux/engines/platform/sdl2/sdl2_mouse.h @@ -0,0 +1,39 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_SDL2_SDL2_MOUSE_H_ +#define REDUX_ENGINES_PLATFORM_SDL2_SDL2_MOUSE_H_ + +#include "redux/engines/platform/device_manager.h" +#include "redux/engines/platform/sdl2/sdl2_event_handler.h" + +namespace redux { + +class Sdl2Mouse : public Sdl2EventHandler { + public: + explicit Sdl2Mouse(DeviceManager* dm); + ~Sdl2Mouse() override; + + void HandleEvent(SDL_Event event) override; + void Commit() override; + + private: + std::unique_ptr mouse_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_PLATFORM_SDL2_SDL2_MOUSE_H_ diff --git a/redux/redux/engines/platform/sdl2/sdl2_speaker.cc b/redux/redux/engines/platform/sdl2/sdl2_speaker.cc new file mode 100644 index 0000000..771ffc3 --- /dev/null +++ b/redux/redux/engines/platform/sdl2/sdl2_speaker.cc @@ -0,0 +1,77 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/platform/sdl2/sdl2_speaker.h" + +#include "SDL.h" +#include "redux/engines/platform/device_manager.h" + +namespace redux { + +// Use standard 48 khz audio frequency for the speaker. +constexpr int kAudioFrequency = 48000; + +// Use stereo output for the speakers. +constexpr int kAudioChannels = 2; + +// Audio buffer will be signed 16-bit integer data. +constexpr auto kAudioFormat = AUDIO_S16; + +// The size of the audio buffer. 2kb provides reasonable performance. +constexpr int kAudioSamples = 2048; + +void SdlAudioCallback(void* userdata, uint8_t* stream, int len) { + Speaker::HwBuffer hw_buffer = absl::MakeSpan(stream, len); + reinterpret_cast(userdata)->AudioHwCallback(hw_buffer); +} + +Sdl2Speaker::Sdl2Speaker(DeviceManager* dm) : device_manager_(dm) { + if (!SDL_WasInit(SDL_INIT_AUDIO)) { + CHECK(SDL_Init(SDL_INIT_AUDIO) == 0) << SDL_GetError(); + } + + SDL_memset(&sdl_audio_spec_, 0, sizeof(sdl_audio_spec_)); + sdl_audio_spec_.freq = kAudioFrequency; + sdl_audio_spec_.format = kAudioFormat; + sdl_audio_spec_.channels = kAudioChannels; + sdl_audio_spec_.samples = kAudioSamples; + sdl_audio_spec_.callback = SdlAudioCallback; + sdl_audio_spec_.userdata = device_manager_; + + sdl_device_id_ = SDL_OpenAudioDevice(nullptr, 0, &sdl_audio_spec_, nullptr, + SDL_AUDIO_ALLOW_ANY_CHANGE); + CHECK(sdl_device_id_ != 0) << SDL_GetError(); + + SpeakerProfile profile; + profile.sample_rate_hz = kAudioFrequency; + profile.num_channels = kAudioChannels; + profile.frames_per_buffer = kAudioSamples; + + speaker_ = dm->Connect(profile); + + // Start the audio playback. The device manager will fill in empty data if + // there is no audio source. + SDL_PauseAudioDevice(sdl_device_id_, 0); +} + +Sdl2Speaker::~Sdl2Speaker() { + SDL_CloseAudioDevice(sdl_device_id_); + speaker_.reset(); +} + +void Sdl2Speaker::Commit() {} + +} // namespace redux diff --git a/redux/redux/engines/platform/sdl2/sdl2_speaker.h b/redux/redux/engines/platform/sdl2/sdl2_speaker.h new file mode 100644 index 0000000..af80405 --- /dev/null +++ b/redux/redux/engines/platform/sdl2/sdl2_speaker.h @@ -0,0 +1,43 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_SDL2_SDL2_SPEAKER_H_ +#define REDUX_ENGINES_PLATFORM_SDL2_SDL2_SPEAKER_H_ + +#include "redux/engines/platform/device_manager.h" +#include "redux/engines/platform/sdl2/sdl2_event_handler.h" +#include "redux/engines/platform/speaker.h" + +namespace redux { + +class Sdl2Speaker : public Sdl2EventHandler { + public: + explicit Sdl2Speaker(DeviceManager* dm); + + ~Sdl2Speaker() override; + + void Commit() override; + + private: + DeviceManager* device_manager_; + std::unique_ptr speaker_; + SDL_AudioDeviceID sdl_device_id_; + SDL_AudioSpec sdl_audio_spec_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_PLATFORM_SDL2_SDL2_SPEAKER_H_ diff --git a/redux/redux/engines/platform/speaker.cc b/redux/redux/engines/platform/speaker.cc new file mode 100644 index 0000000..6b1e9a3 --- /dev/null +++ b/redux/redux/engines/platform/speaker.cc @@ -0,0 +1,30 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/platform/speaker.h" + +namespace redux { + +Speaker::Speaker(SpeakerProfile profile, OnDestroy on_destroy) + : VirtualDevice(std::move(on_destroy)), profile_(std::move(profile)) {} + +void Speaker::Apply(absl::Duration delta_time) {} + +const SpeakerProfile* Speaker::View::GetProfile() const { + const Speaker* device = GetDevice(); + return device ? &device->profile_ : nullptr; +} +} // namespace redux diff --git a/redux/redux/engines/platform/speaker.h b/redux/redux/engines/platform/speaker.h new file mode 100644 index 0000000..2b020f9 --- /dev/null +++ b/redux/redux/engines/platform/speaker.h @@ -0,0 +1,48 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_SPEAKER_H_ +#define REDUX_ENGINES_PLATFORM_SPEAKER_H_ + +#include "absl/types/span.h" +#include "redux/engines/platform/device_profiles.h" +#include "redux/engines/platform/virtual_device.h" + +namespace redux { + +class Speaker : public VirtualDevice { + public: + using Profile = SpeakerProfile; + + // The state of the speaker that will be exposed by the device manager. + struct View : VirtualView { + const Profile* GetProfile() const; + }; + + // A chunk of audio data that will be fed into the hardware speaker. + using HwBuffer = absl::Span; + + private: + friend class DeviceManager; + Speaker(SpeakerProfile profile, OnDestroy on_destroy); + + void Apply(absl::Duration delta_time) override; + + SpeakerProfile profile_; +}; +} // namespace redux + +#endif // REDUX_ENGINES_PLATFORM_SPEAKER_H_ diff --git a/redux/redux/engines/platform/virtual_device.cc b/redux/redux/engines/platform/virtual_device.cc new file mode 100644 index 0000000..b16882f --- /dev/null +++ b/redux/redux/engines/platform/virtual_device.cc @@ -0,0 +1,84 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/platform/virtual_device.h" + +namespace redux { + +const VirtualDevice::TriggerFlag VirtualDevice::DetermineTrigger(bool curr, + bool prev) { + if (curr) { + return prev ? kPressed : (kPressed | kJustPressed); + } else { + return !prev ? kReleased : (kReleased | kJustReleased); + } +} + +const VirtualDevice::TriggerFlag VirtualDevice::DetermineTrigger( + const BooleanState& curr, const BooleanState& prev, + std::optional long_press_time_ms) { + // const absl::Duration long_press = + // absl::Milliseconds(long_press_time_ms.value_or(500.f)); + + TriggerFlag flag = 0; + if (curr.active) { + flag |= kPressed; + if (!prev.active) { + flag |= kJustPressed; + } + // if (repeat) { + // flag |= kRepeat; + // } + + // Check for long press: + // const absl::Duration curr_press_duration = + // curr_time_stamp - curr.toggle_time; + // const bool curr_long_press = curr_press_duration >= long_press; + // if (curr_long_press) { + // flag |= kLongPressed; + // if (prev.active) { + // const absl::Duration prev_press_duration = + // prev_time_stamp - prev.toggle_time; + // const bool prev_long_press = prev_press_duration >= long_press; + // if (!prev_long_press) { + // flag |= kJustLongPressed; + // } + // } else { + // flag |= kJustLongPressed; + // } + // } + } else { + flag |= kReleased; + if (prev.active) { + flag |= kJustReleased; + + // // Check if the released press was held for more than long_press_time. + // // If released on the first frame that would be a long_press, assume it + // // was released before the time limit passed and don't set + // kLongPressed. const absl::Duration prev_press_duration = + // prev_time_stamp - prev.toggle_time; + // const bool prev_long_press = prev_press_duration >= long_press; + // if (prev_long_press) { + // flag |= kLongPressed; + // } + // } else if (was) { + // flag |= kJustPressed | kJustReleased; + } + } + return flag; +} + +} // namespace redux diff --git a/redux/redux/engines/platform/virtual_device.h b/redux/redux/engines/platform/virtual_device.h new file mode 100644 index 0000000..f939d27 --- /dev/null +++ b/redux/redux/engines/platform/virtual_device.h @@ -0,0 +1,96 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_PLATFORM_VIRTUAL_DEVICE_H_ +#define REDUX_ENGINES_PLATFORM_VIRTUAL_DEVICE_H_ + +#include +#include +#include + +#include "absl/time/time.h" +#include "redux/modules/math/vector.h" + +namespace redux { + +// Virtual devices provide an API for platform-specific code to "push" data +// about a device into DeviceManager. Once all input events have been processed, +// the DeviceManager will make the current state of the devices available to +// the rest of the runtime to query. +class VirtualDevice { + public: + virtual ~VirtualDevice() { on_destroy_(); } + + VirtualDevice(const VirtualDevice&) = delete; + VirtualDevice& operator=(const VirtualDevice&) = delete; + + // The state for a binary trigger inputs (i.e. keys and buttons). + using TriggerFlag = uint8_t; + static constexpr TriggerFlag kReleased = 0x01 << 0; + static constexpr TriggerFlag kPressed = 0x01 << 1; + static constexpr TriggerFlag kLongPressed = 0x01 << 2; + static constexpr TriggerFlag kJustReleased = 0x01 << 3; + static constexpr TriggerFlag kJustPressed = 0x01 << 4; + static constexpr TriggerFlag kJustLongPressed = 0x01 << 5; + static constexpr TriggerFlag kRepeat = 0x01 << 6; + + // The state for a binary value. + struct BooleanState { + bool active = false; + absl::Duration toggle_time; + }; + + // The state for a single scalar value. + struct ScalarState { + bool active = false; + float value = 0.f; + absl::Duration toggle_time; + }; + + // The state for a 2D value. + struct Vec2State { + vec2i value = vec2i::Zero(); + }; + + // A view provides read-only information about a device's state. + template + struct VirtualView { + protected: + const Device* GetDevice() const { return getter(); } + friend class DeviceManager; + std::function getter; + }; + + // Returns the TriggerFlag based on current and previous states. + static const TriggerFlag DetermineTrigger(bool curr, bool prev); + static const TriggerFlag DetermineTrigger( + const BooleanState& curr, const BooleanState& prev, + std::optional long_press_time_ms); + + protected: + using OnDestroy = std::function; + explicit VirtualDevice(OnDestroy on_destroy) + : on_destroy_(std::move(on_destroy)) {} + + friend class DeviceManager; + virtual void Apply(absl::Duration delta_time) = 0; + + private: + OnDestroy on_destroy_; +}; +} // namespace redux + +#endif // REDUX_ENGINES_PLATFORM_VIRTUAL_DEVICE_H_ diff --git a/redux/redux/engines/render/BUILD b/redux/redux/engines/render/BUILD new file mode 100644 index 0000000..c6bbf2b --- /dev/null +++ b/redux/redux/engines/render/BUILD @@ -0,0 +1,49 @@ +# Render engine. + +licenses(["notice"]) + +package( + default_visibility = ["//redux:visibility"], +) + +cc_library( + name = "render", + hdrs = [ + "indirect_light.h", + "light.h", + "mesh.h", + "mesh_factory.h", + "render_engine.h", + "render_layer.h", + "render_scene.h", + "render_target.h", + "render_target_factory.h", + "renderable.h", + "shader.h", + "shader_factory.h", + "texture.h", + "texture_factory.h", + ], + deps = [ + "@absl//absl/container:flat_hash_map", + "@absl//absl/container:flat_hash_set", + "//redux/modules/base:bits", + "//redux/modules/base:data_buffer", + "//redux/modules/base:data_container", + "//redux/modules/base:hash", + "//redux/modules/base:registry", + "//redux/modules/base:resource_manager", + "//redux/modules/base:typeid", + "//redux/modules/graphics:color", + "//redux/modules/graphics:enums", + "//redux/modules/graphics:image_data", + "//redux/modules/graphics:material_data", + "//redux/modules/graphics:mesh_data", + "//redux/modules/graphics:texture_usage", + "//redux/modules/graphics:vertex_format", + "//redux/modules/math:bounds", + "//redux/modules/math:matrix", + "//redux/modules/math:vector", + "//redux/modules/var", + ], +) diff --git a/redux/redux/engines/render/filament/BUILD b/redux/redux/engines/render/filament/BUILD new file mode 100644 index 0000000..4ee6433 --- /dev/null +++ b/redux/redux/engines/render/filament/BUILD @@ -0,0 +1,68 @@ +# Filament backend for render engine. + +licenses(["notice"]) + +package( + default_visibility = ["//redux:visibility"], +) + +cc_library( + name = "filament", + srcs = [ + "filament_indirect_light.cc", + "filament_light.cc", + "filament_mesh.cc", + "filament_render_engine.cc", + "filament_render_layer.cc", + "filament_render_scene.cc", + "filament_render_target.cc", + "filament_renderable.cc", + "filament_shader.cc", + "filament_texture.cc", + "mesh_factory.cc", + "render_target_factory.cc", + "shader_factory.cc", + "texture_factory.cc", + "thunks.cc", + ], + hdrs = [ + "filament_indirect_light.h", + "filament_light.h", + "filament_mesh.h", + "filament_render_engine.h", + "filament_render_layer.h", + "filament_render_scene.h", + "filament_render_target.h", + "filament_renderable.h", + "filament_shader.h", + "filament_texture.h", + "filament_utils.h", + ], + deps = [ + "@absl//absl/container:btree", + "@absl//absl/container:flat_hash_map", + "@filament//:filament_hdrs", + "@filament//:math_hdrs", + "@filament//:utils_hdrs", + "//redux/data/asset_defs:shader_asset_def_fbs", + "//redux/engines/platform:device_manager", + "//redux/engines/render", + "//redux/engines/render/thunks", + "//redux/modules/base:asset_loader", + "//redux/modules/base:choreographer", + "//redux/modules/base:data_buffer", + "//redux/modules/base:hash", + "//redux/modules/base:logging", + "//redux/modules/base:registry", + "//redux/modules/base:static_registry", + "//redux/modules/codecs:decode_image", + "//redux/modules/graphics:enums", + "//redux/modules/graphics:image_data", + "//redux/modules/graphics:image_utils", + "//redux/modules/graphics:mesh_data", + "//redux/modules/graphics:texture_usage", + "//redux/modules/graphics:vertex_format", + "//redux/modules/math:bounds", + "//redux/modules/math:matrix", + ], +) diff --git a/redux/redux/engines/render/filament/filament_indirect_light.cc b/redux/redux/engines/render/filament/filament_indirect_light.cc new file mode 100644 index 0000000..88795ab --- /dev/null +++ b/redux/redux/engines/render/filament/filament_indirect_light.cc @@ -0,0 +1,120 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/render/filament/filament_indirect_light.h" + +#include "filament/Color.h" +#include "filament/IndirectLight.h" +#include "filament/LightManager.h" +#include "filament/TransformManager.h" +#include "utils/EntityManager.h" +#include "redux/engines/render/filament/filament_render_engine.h" +#include "redux/engines/render/filament/filament_texture.h" +#include "redux/engines/render/filament/filament_utils.h" + +namespace redux { + +FilamentIndirectLight::FilamentIndirectLight(Registry* registry, + const TexturePtr& reflection, + const TexturePtr& irradiance) + : reflection_(reflection), irradiance_(irradiance) { + CHECK(reflection_); + + fengine_ = GetFilamentEngine(registry); + filament::IndirectLight::Builder builder; + FilamentTexture* reflections = + static_cast(reflection_.get()); + builder.reflections(reflections->GetFilamentTexture()); + if (irradiance_) { + FilamentTexture* irradiance = + static_cast(irradiance_.get()); + builder.irradiance(irradiance->GetFilamentTexture()); + } else { + // clang-format off + std::array sh_coefficients = { + filament::math::float3{ 0.592915142902302, 0.580783147865357, 0.564906236122309}, // L00, irradiance, pre-scaled base + filament::math::float3{ 0.038230073440953, 0.040661612793765, 0.045912497583365}, // L1-1, irradiance, pre-scaled base + filament::math::float3{-0.306182569332798, -0.298728189882871, -0.292527808646246}, // L10, irradiance, pre-scaled base + filament::math::float3{-0.268674829827722, -0.258309969107310, -0.244936138194592}, // L11, irradiance, pre-scaled base + filament::math::float3{ 0.055981897791156, 0.053190319920282, 0.047808414744011}, // L2-2, irradiance, pre-scaled base + filament::math::float3{ 0.009835221123367, 0.006544190646597, 0.000350193519574}, // L2-1, irradiance, pre-scaled base + filament::math::float3{ 0.017525154215762, 0.017508716588022, 0.018218263542429}, // L20, irradiance, pre-scaled base + filament::math::float3{ 0.306912095635860, 0.292384283162994, 0.274657325943371}, // L21, irradiance, pre-scaled base + filament::math::float3{ 0.055928224084081, 0.051564836176893, 0.044938623517990}, // L22, irradiance, pre-scaled base + }; + // clang-format on + builder.irradiance(3, sh_coefficients.data()); + } + builder.intensity(30000); + fibl_ = builder.build(*fengine_); +} + +FilamentIndirectLight::~FilamentIndirectLight() { + for (auto* scene : scenes_) { + scene->setIndirectLight(nullptr); + } + scenes_.clear(); + fengine_->destroy(fibl_); +} + +void FilamentIndirectLight::Enable() { + if (!visible_) { + visible_ = true; + for (filament::Scene* scene : scenes_) { + scene->setIndirectLight(fibl_); + } + } +} + +void FilamentIndirectLight::Disable() { + if (visible_) { + visible_ = false; + for (filament::Scene* scene : scenes_) { + scene->setIndirectLight(nullptr); + } + } +} + +bool FilamentIndirectLight::IsEnabled() const { return visible_; } + +void FilamentIndirectLight::SetTransform(const mat4& transform) { + if (fibl_) { + fibl_->setRotation(ToFilament(transform).upperLeft()); + } +} + +void FilamentIndirectLight::AddToScene(FilamentRenderScene* scene) const { + auto fscene = scene->GetFilamentScene(); + if (!scenes_.contains(fscene)) { + if (visible_) { + fscene->setIndirectLight(fibl_); + } + scenes_.emplace(fscene); + } +} + +void FilamentIndirectLight::RemoveFromScene(FilamentRenderScene* scene) const { + auto fscene = scene->GetFilamentScene(); + auto iter = scenes_.find(fscene); + if (iter != scenes_.end()) { + if (visible_) { + fscene->setIndirectLight(nullptr); + } + scenes_.erase(iter); + } +} + +} // namespace redux diff --git a/redux/redux/engines/render/filament/filament_indirect_light.h b/redux/redux/engines/render/filament/filament_indirect_light.h new file mode 100644 index 0000000..da3ca21 --- /dev/null +++ b/redux/redux/engines/render/filament/filament_indirect_light.h @@ -0,0 +1,65 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_INDIRECT_LIGHT_H_ +#define REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_INDIRECT_LIGHT_H_ + +#include "filament/Engine.h" +#include "filament/LightManager.h" +#include "utils/Entity.h" +#include "redux/engines/render/filament/filament_render_scene.h" +#include "redux/engines/render/indirect_light.h" +#include "redux/modules/base/registry.h" + +namespace redux { + +// Manages filament IndirectLights. +class FilamentIndirectLight : public IndirectLight { + public: + FilamentIndirectLight(Registry* registry, const TexturePtr& reflection, + const TexturePtr& irradiance = nullptr); + ~FilamentIndirectLight() override; + + // Hides/disables the light from the scene. + void Disable(); + + // Shows/enables the light from the scene. + void Enable(); + + // Returns true if the light is enabled in the scene. + bool IsEnabled() const; + + // Sets the transform of the light. + void SetTransform(const mat4& transform); + + // Adds the light to a filament scene. + void AddToScene(FilamentRenderScene* scene) const; + + // Removes the light from a filament scene. + void RemoveFromScene(FilamentRenderScene* scene) const; + + private: + filament::Engine* fengine_ = nullptr; + filament::IndirectLight* fibl_ = nullptr; + TexturePtr reflection_; + TexturePtr irradiance_; + mutable absl::flat_hash_set scenes_; + bool visible_ = true; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_INDIRECT_LIGHT_H_ diff --git a/redux/redux/engines/render/filament/filament_light.cc b/redux/redux/engines/render/filament/filament_light.cc new file mode 100644 index 0000000..8731a01 --- /dev/null +++ b/redux/redux/engines/render/filament/filament_light.cc @@ -0,0 +1,138 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/render/filament/filament_light.h" + +#include "filament/Color.h" +#include "filament/LightManager.h" +#include "filament/TransformManager.h" +#include "utils/EntityManager.h" +#include "redux/engines/render/filament/filament_render_engine.h" +#include "redux/engines/render/filament/filament_texture.h" +#include "redux/engines/render/filament/filament_utils.h" + +namespace redux { + +FilamentLight::FilamentLight(Registry* registry, Type type) : type_(type) { + fengine_ = GetFilamentEngine(registry); + switch (type_) { + case Light::Directional: { + CreateLightEntity(filament::LightManager::Type::DIRECTIONAL); + break; + } + case Light::Spot: { + CreateLightEntity(filament::LightManager::Type::SPOT); + break; + } + case Light::Point: { + CreateLightEntity(filament::LightManager::Type::POINT); + break; + } + } +} + +FilamentLight::~FilamentLight() { + for (auto* scene : scenes_) { + scene->remove(fentity_); + } + scenes_.clear(); + + fengine_->getLightManager().destroy(fentity_); + fengine_->getTransformManager().destroy(fentity_); + utils::EntityManager::get().destroy(fentity_); +} + +void FilamentLight::CreateLightEntity(filament::LightManager::Type type) { + fentity_ = utils::EntityManager::get().create(); + filament::LightManager::Builder builder(type); + builder.direction({0, 0, -1}); // point lights in same direction as camera + builder.build(*fengine_, fentity_); +} + +void FilamentLight::Enable() { + if (!visible_) { + visible_ = true; + for (filament::Scene* scene : scenes_) { + scene->addEntity(fentity_); + } + } +} + +void FilamentLight::Disable() { + if (visible_) { + visible_ = false; + for (filament::Scene* scene : scenes_) { + scene->remove(fentity_); + } + } +} + +bool FilamentLight::IsEnabled() const { return visible_; } + +void FilamentLight::SetTransform(const mat4& transform) { + if (fentity_) { + auto ti = fengine_->getTransformManager().getInstance(fentity_); + fengine_->getTransformManager().setTransform(ti, ToFilament(transform)); + } +} + +void FilamentLight::SetColor(const Color4f& color) { + auto& lm = fengine_->getLightManager(); + auto li = lm.getInstance(fentity_); + filament::LinearColor fcolor(color.r, color.g, color.b); + lm.setColor(li, fcolor); +} + +void FilamentLight::SetIntensity(float intensity) { + auto& lm = fengine_->getLightManager(); + auto li = lm.getInstance(fentity_); + lm.setIntensity(li, intensity); +} + +void FilamentLight::SetFalloffDistance(float distance) { + auto& lm = fengine_->getLightManager(); + auto li = lm.getInstance(fentity_); + lm.setFalloff(li, distance); +} + +void FilamentLight::SetSpotLightConeAngles(float inner, float outer) { + auto& lm = fengine_->getLightManager(); + auto li = lm.getInstance(fentity_); + lm.setSpotLightCone(li, inner, outer); +} + +void FilamentLight::AddToScene(FilamentRenderScene* scene) const { + auto fscene = scene->GetFilamentScene(); + if (!scenes_.contains(fscene)) { + if (visible_) { + fscene->addEntity(fentity_); + } + scenes_.emplace(fscene); + } +} + +void FilamentLight::RemoveFromScene(FilamentRenderScene* scene) const { + auto fscene = scene->GetFilamentScene(); + auto iter = scenes_.find(fscene); + if (iter != scenes_.end()) { + if (visible_) { + fscene->remove(fentity_); + } + scenes_.erase(iter); + } +} + +} // namespace redux diff --git a/redux/redux/engines/render/filament/filament_light.h b/redux/redux/engines/render/filament/filament_light.h new file mode 100644 index 0000000..d43f8e1 --- /dev/null +++ b/redux/redux/engines/render/filament/filament_light.h @@ -0,0 +1,84 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_LIGHT_H_ +#define REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_LIGHT_H_ + +#include "filament/Engine.h" +#include "filament/LightManager.h" +#include "utils/Entity.h" +#include "redux/engines/render/filament/filament_render_scene.h" +#include "redux/engines/render/light.h" +#include "redux/modules/base/registry.h" + +namespace redux { + +// Manages filament light entities. +class FilamentLight : public Light { + public: + FilamentLight(Registry* registry, Type type); + ~FilamentLight() override; + + // Hides/disables the light from the scene. + void Disable(); + + // Shows/enables the light from the scene. + void Enable(); + + // Returns true if the light is enabled in the scene. + bool IsEnabled() const; + + // Sets the transform of the light. + void SetTransform(const mat4& transform); + + // Sets the color of the light. + void SetColor(const Color4f& color); + + // Sets the intensity of the light. For directional lights, it specifies the + // illuminance in lux. For point and spot lights, it specifies the luminous + // power in lumen. + void SetIntensity(float intensity); + + // Sets the spot light cone angles. The inner angle defines the light's + // falloff attenuation and the outer angle ddefines the light's influence. + // `inner` should be between 0 and pi/2, and `outer` should be beteen `inner` + // and pi/2. + void SetSpotLightConeAngles(float inner, float outer); + + // Sets the distance at which the lights stops being effective. + // For point lights, the intensity diminishes with the inverse square of the + // distance to the light. + void SetFalloffDistance(float distance); + + // Adds the light to a filament scene. + void AddToScene(FilamentRenderScene* scene) const; + + // Removes the light from a filament scene. + void RemoveFromScene(FilamentRenderScene* scene) const; + + private: + void CreateLightEntity(filament::LightManager::Type type); + + Type type_ = Directional; + filament::Engine* fengine_ = nullptr; + utils::Entity fentity_; + mutable absl::flat_hash_set scenes_; + bool visible_ = true; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_LIGHT_H_ diff --git a/redux/redux/engines/render/filament/filament_mesh.cc b/redux/redux/engines/render/filament/filament_mesh.cc new file mode 100644 index 0000000..66fe627 --- /dev/null +++ b/redux/redux/engines/render/filament/filament_mesh.cc @@ -0,0 +1,275 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/render/filament/filament_mesh.h" + +#include + +#include "redux/engines/render/filament/filament_render_engine.h" + +namespace redux { + +using MeshDataPtr = std::shared_ptr; + +static filament::VertexBuffer::AttributeType ToFilamentAttributeType( + VertexType type) { + switch (type) { + case VertexType::Scalar1f: + return filament::VertexBuffer::AttributeType::FLOAT; + case VertexType::Vec2f: + return filament::VertexBuffer::AttributeType::FLOAT2; + case VertexType::Vec3f: + return filament::VertexBuffer::AttributeType::FLOAT3; + case VertexType::Vec4f: + return filament::VertexBuffer::AttributeType::FLOAT4; + case VertexType::Vec2us: + return filament::VertexBuffer::AttributeType::USHORT2; + case VertexType::Vec4us: + return filament::VertexBuffer::AttributeType::USHORT4; + case VertexType::Vec4ub: + return filament::VertexBuffer::AttributeType::UBYTE4; + default: + LOG(FATAL) << "Unsupported vertex type: " << ToString(type); + return filament::VertexBuffer::AttributeType::FLOAT; + } +} + +static filament::VertexAttribute ToFilamentAttributeUsage(VertexUsage usage) { + switch (usage) { + case VertexUsage::Position: + return filament::VertexAttribute::POSITION; + case VertexUsage::Orientation: + return filament::VertexAttribute::TANGENTS; + case VertexUsage::Color0: + return filament::VertexAttribute::COLOR; + case VertexUsage::TexCoord0: + return filament::VertexAttribute::UV0; + case VertexUsage::TexCoord1: + return filament::VertexAttribute::UV1; + case VertexUsage::BoneIndices: + return filament::VertexAttribute::BONE_INDICES; + case VertexUsage::BoneWeights: + return filament::VertexAttribute::BONE_WEIGHTS; + default: + LOG(FATAL) << "Unsupported vertex usage: " << ToString(usage); + return filament::VertexAttribute::POSITION; + } +} + +static filament::VertexBuffer::BufferDescriptor CreateVertexBufferDescriptor( + const MeshDataPtr& mesh_data) { + auto* user_data = new MeshDataPtr(mesh_data); + const auto callback = [](void* buffer, size_t size, void* user) { + auto* ptr = reinterpret_cast(user); + delete ptr; + }; + + const auto bytes = mesh_data->GetVertexData(); + return filament::VertexBuffer::BufferDescriptor(bytes.data(), bytes.size(), + callback, user_data); +} + +static filament::IndexBuffer::BufferDescriptor CreateIndexBufferDescriptor( + const MeshDataPtr& mesh_data) { + auto* user_data = new MeshDataPtr(mesh_data); + const auto callback = [](void* buffer, size_t size, void* user) { + auto* ptr = reinterpret_cast(user); + delete ptr; + }; + + const auto bytes = mesh_data->GetIndexData(); + return filament::IndexBuffer::BufferDescriptor(bytes.data(), bytes.size(), + callback, user_data); +} + +template +static filament::IndexBuffer::BufferDescriptor +CreateIndexBufferDescriptorForRange(size_t count) { + T* arr = new T[count]; + for (T i = 0; i < static_cast(count); ++i) { + arr[i] = i; + } + const auto callback = [](void* buffer, size_t size, void* user) { + T* ptr = reinterpret_cast(user); + delete[] ptr; + }; + return filament::IndexBuffer::BufferDescriptor(arr, count * sizeof(T), + callback, arr); +} + +static FilamentResourcePtr CreateVertexBuffer( + filament::Engine* engine, const MeshDataPtr& mesh_data) { + const uint32_t count = static_cast(mesh_data->GetNumVertices()); + if (count == 0) { + return nullptr; + } + + filament::VertexBuffer::Builder builder; + builder.vertexCount(count); + builder.bufferCount(1); + + const VertexFormat& vertex_format = mesh_data->GetVertexFormat(); + const uint8_t vertex_size = + static_cast(vertex_format.GetVertexSize()); + + uint32_t offset = 0; + for (size_t index = 0; index < vertex_format.GetNumAttributes(); ++index) { + const VertexAttribute* attribute = vertex_format.GetAttributeAt(index); + + const auto ftype = ToFilamentAttributeType(attribute->type); + const auto fusage = ToFilamentAttributeUsage(attribute->usage); + builder.attribute(fusage, 0, ftype, offset, vertex_size); + if (ftype == filament::VertexBuffer::AttributeType::UBYTE4) { + if (fusage == filament::VertexAttribute::COLOR) { + builder.normalized(fusage); + } + } + offset += VertexFormat::GetAttributeSize(*attribute); + } + + filament::VertexBuffer* buffer = builder.build(*engine); + buffer->setBufferAt(*engine, 0, CreateVertexBufferDescriptor(mesh_data)); + return MakeFilamentResource(buffer, engine); +} + +static FilamentResourcePtr CreateIndexBuffer( + filament::Engine* engine, const MeshDataPtr& mesh_data) { + const uint32_t count = static_cast(mesh_data->GetNumIndices()); + const size_t vertex_count = mesh_data->GetNumVertices(); + if (count == 0 && vertex_count > 0) { + // Filament requires an index buffer, so create one here. + filament::IndexBuffer::Builder builder; + filament::IndexBuffer::BufferDescriptor desc; + + builder.indexCount(static_cast(vertex_count)); + if (vertex_count <= std::numeric_limits::max()) { + builder.bufferType(filament::IndexBuffer::IndexType::USHORT); + desc = CreateIndexBufferDescriptorForRange(vertex_count); + } else { + builder.bufferType(filament::IndexBuffer::IndexType::UINT); + desc = CreateIndexBufferDescriptorForRange(vertex_count); + } + filament::IndexBuffer* buffer = builder.build(*engine); + buffer->setBuffer(*engine, std::move(desc)); + return MakeFilamentResource(buffer, engine); + } + + if (count == 0) { + return nullptr; + } + + filament::IndexBuffer::Builder builder; + builder.indexCount(count); + switch (mesh_data->GetMeshIndexType()) { + case MeshIndexType::U16: + builder.bufferType(filament::IndexBuffer::IndexType::USHORT); + break; + case MeshIndexType::U32: + builder.bufferType(filament::IndexBuffer::IndexType::UINT); + break; + default: + LOG(FATAL) << "Unsupported index type."; + return nullptr; + } + + filament::IndexBuffer* buffer = builder.build(*engine); + buffer->setBuffer(*engine, CreateIndexBufferDescriptor(mesh_data)); + return MakeFilamentResource(buffer, engine); +} + +static size_t CalculateNumPrimitives(MeshPrimitiveType type, + const size_t count) { + switch (type) { + case MeshPrimitiveType::Points: + return count; + case MeshPrimitiveType::Lines: + return count / 2; + case MeshPrimitiveType::Triangles: + return count / 3; + case MeshPrimitiveType::TriangleFan: + return count - 2; + case MeshPrimitiveType::TriangleStrip: + return count - 2; + default: + LOG(FATAL) << "Invalid primitive type " << ToString(type); + return count; + } +} + +static size_t CalculateNumVertices(const VertexFormat& format, + absl::Span bytes) { + const size_t size = format.GetVertexSize(); + return size > 0 ? bytes.size() / size : 0; +} + +FilamentMesh::FilamentMesh(Registry* registry, + const std::shared_ptr& mesh_data) { + fengine_ = GetFilamentEngine(registry); + CHECK(fengine_); + if (mesh_data == nullptr) { + return; + } + + fvbuffer_ = CreateVertexBuffer(fengine_, mesh_data); + fibuffer_ = CreateIndexBuffer(fengine_, mesh_data); + + SubmeshData submesh; + submesh.vertex_format = mesh_data->GetVertexFormat(); + submesh.index_type = mesh_data->GetMeshIndexType(); + + auto parts = mesh_data->GetPartData(); + submeshes_.reserve(parts.size()); + for (const MeshData::PartData& part : parts) { + submesh.name = part.name; + submesh.primitive_type = part.primitive_type; + submesh.range_start = part.start; + submesh.range_end = part.end; + submesh.box = part.box; + submeshes_.push_back(submesh); + + num_primitives_ += + CalculateNumPrimitives(part.primitive_type, part.end - part.start); + } + + num_vertices_ = CalculateNumVertices(mesh_data->GetVertexFormat(), + mesh_data->GetVertexData()); + bounding_box_ = mesh_data->GetBoundingBox(); + NotifyReady(); +} + +size_t FilamentMesh::GetNumVertices() const { return num_vertices_; } + +size_t FilamentMesh::GetNumPrimitives() const { return num_primitives_; } + +size_t FilamentMesh::GetNumSubmeshes() const { return submeshes_.size(); } + +const FilamentMesh::SubmeshData& FilamentMesh::GetSubmeshData( + size_t index) const { + CHECK_LT(index, submeshes_.size()); + return submeshes_[index]; +} + +Box FilamentMesh::GetBoundingBox() const { return bounding_box_; } + +filament::VertexBuffer* FilamentMesh::GetFilamentVertexBuffer() const { + return fvbuffer_.get(); +} + +filament::IndexBuffer* FilamentMesh::GetFilamentIndexBuffer() const { + return fibuffer_.get(); +} + +} // namespace redux diff --git a/redux/redux/engines/render/filament/filament_mesh.h b/redux/redux/engines/render/filament/filament_mesh.h new file mode 100644 index 0000000..c05933c --- /dev/null +++ b/redux/redux/engines/render/filament/filament_mesh.h @@ -0,0 +1,69 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_MESH_H_ +#define REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_MESH_H_ + +#include + +#include "filament/IndexBuffer.h" +#include "filament/VertexBuffer.h" +#include "redux/engines/render/filament/filament_utils.h" +#include "redux/engines/render/mesh.h" +#include "redux/modules/base/registry.h" +#include "redux/modules/graphics/mesh_data.h" + +namespace redux { + +// Manages a filament VertexBuffer and IndexBuffer created using MeshData. +class FilamentMesh : public Mesh, public Readiable { + public: + FilamentMesh(Registry* registry, const std::shared_ptr& mesh_data); + + // Returns the number of vertices contained in the mesh. + size_t GetNumVertices() const; + + // Returns the number of primitives (eg. points, lines, triangles, etc.) + // contained in the mesh. + size_t GetNumPrimitives() const; + + // Gets the bounding box for the mesh. + Box GetBoundingBox() const; + + // Returns the number of submeshes in the mesh. + size_t GetNumSubmeshes() const; + + // Returns information about submesh of the mesh. + const SubmeshData& GetSubmeshData(size_t index) const; + + // Returns the underlying filament vertex buffer. + filament::VertexBuffer* GetFilamentVertexBuffer() const; + + // Returns the underlying filament index buffer. + filament::IndexBuffer* GetFilamentIndexBuffer() const; + + private: + filament::Engine* fengine_ = nullptr; + FilamentResourcePtr fvbuffer_; + FilamentResourcePtr fibuffer_; + Box bounding_box_; + size_t num_vertices_ = 0; + size_t num_primitives_ = 0; + std::vector submeshes_; +}; +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_MESH_H_ diff --git a/redux/redux/engines/render/filament/filament_render_engine.cc b/redux/redux/engines/render/filament/filament_render_engine.cc new file mode 100644 index 0000000..bc69cd7 --- /dev/null +++ b/redux/redux/engines/render/filament/filament_render_engine.cc @@ -0,0 +1,262 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/render/filament/filament_render_engine.h" + +#include "filament/Fence.h" +#include "redux/engines/platform/device_manager.h" +#include "redux/engines/render/filament/filament_indirect_light.h" +#include "redux/engines/render/filament/filament_light.h" +#include "redux/engines/render/filament/filament_render_target.h" +#include "redux/engines/render/filament/filament_renderable.h" +#include "redux/engines/render/renderable.h" +#include "redux/modules/base/choreographer.h" +#include "redux/modules/base/static_registry.h" +#include "redux/modules/graphics/image_utils.h" + +namespace redux { + +#ifdef __APPLE__ +constexpr auto kFilamentBackend = filament::Engine::Backend::OPENGL; +#else +constexpr auto kFilamentBackend = filament::Engine::Backend::VULKAN; +#endif + +constexpr HashValue kDefaultName = ConstHash("default"); + +static ImageFormat ToImageFormat(RenderTargetFormat format) { + switch (format) { + case RenderTargetFormat::Red8: + return ImageFormat::Alpha8; + case RenderTargetFormat::Rgb8: + return ImageFormat::Rgb888; + case RenderTargetFormat::Rgba8: + return ImageFormat::Rgba8888; + default: + LOG(FATAL) << "Unsupported format: " << static_cast(format); + } +} + +static filament::Texture::PixelBufferDescriptor::PixelDataFormat +ToFilamentPixelDataFormat(RenderTargetFormat format) { + switch (format) { + case RenderTargetFormat::Red8: + return filament::Texture::PixelBufferDescriptor::PixelDataFormat::R; + case RenderTargetFormat::Rgb8: + return filament::Texture::PixelBufferDescriptor::PixelDataFormat::RGB; + case RenderTargetFormat::Rgba8: + return filament::Texture::PixelBufferDescriptor::PixelDataFormat::RGBA; + default: + LOG(FATAL) << "Unsupported format: " << static_cast(format); + } +} + +FilamentRenderEngine::FilamentRenderEngine(Registry* registry) + : RenderEngine(registry) { + registry->RegisterDependency(static_cast(this), + true); +} + +void FilamentRenderEngine::CreateFactories() { + mesh_factory_ = registry_->Create(registry_); + shader_factory_ = registry_->Create(registry_); + texture_factory_ = registry_->Create(registry_); + render_target_factory_ = registry_->Create(registry_); +} + +FilamentRenderEngine::~FilamentRenderEngine() { + SyncWait(); + scenes_.clear(); + layers_.clear(); + default_render_target_.reset(); + if (fengine_ && frenderer_) { + fengine_->destroy(frenderer_); + } + if (fengine_ && fswapchain_) { + fengine_->destroy(fswapchain_); + } + filament::Engine::destroy(fengine_); +} + +void FilamentRenderEngine::OnRegistryInitialize() { + auto* choreographer = registry_->Get(); + if (choreographer) { + choreographer->Add<&RenderEngine::Render>(Choreographer::Stage::kRender); + } + + auto display = registry_->Get()->Display(); + void* native_window = display.GetProfile()->native_window; + + fengine_ = filament::Engine::create(kFilamentBackend); + frenderer_ = fengine_->createRenderer(); + fswapchain_ = fengine_->createSwapChain(native_window); + + filament::Renderer::ClearOptions clear_options; + clear_options.clearColor = {0.f, 0.f, 0.f, 1.f}; + clear_options.clear = true; + clear_options.discard = true; + frenderer_->setClearOptions(clear_options); + + auto target = std::make_shared(registry_); + default_render_target_ = std::static_pointer_cast(target); + + auto layer = CreateRenderLayer(kDefaultName); + auto scene = CreateRenderScene(kDefaultName); + layer->SetScene(scene); +} + +RenderLayerPtr FilamentRenderEngine::CreateRenderLayer(HashValue name) { + auto layer = + std::make_shared(registry_, default_render_target_); + + auto& ptr = layers_[name]; + CHECK(ptr == nullptr) << "Layer already exists: " << name.get(); + ptr = layer; + return ptr; +} + +RenderLayerPtr FilamentRenderEngine::GetRenderLayer(HashValue name) { + auto iter = layers_.find(name); + return iter != layers_.end() ? iter->second : nullptr; +} + +RenderLayerPtr FilamentRenderEngine::GetDefaultRenderLayer() { + return GetRenderLayer(kDefaultName); +} + +RenderScenePtr FilamentRenderEngine::CreateRenderScene(HashValue name) { + auto scene = std::make_shared(registry_); + + auto& ptr = scenes_[name]; + CHECK(ptr == nullptr); + ptr = std::static_pointer_cast(scene); + return ptr; +} + +RenderScenePtr FilamentRenderEngine::GetRenderScene(HashValue name) { + return scenes_[name]; +} + +RenderScenePtr FilamentRenderEngine::GetDefaultRenderScene() { + return GetRenderScene(kDefaultName); +} + +RenderablePtr FilamentRenderEngine::CreateRenderable() { + auto renderable = std::make_shared(registry_); + return std::static_pointer_cast(renderable); +} + +LightPtr FilamentRenderEngine::CreateLight(Light::Type type) { + auto light = std::make_shared(registry_, type); + return std::static_pointer_cast(light); +} + +IndirectLightPtr FilamentRenderEngine::CreateIndirectLight( + const TexturePtr& reflection, const TexturePtr& irradiance) { + auto light = std::make_shared(registry_, reflection, + irradiance); + return std::static_pointer_cast(light); +} + +bool FilamentRenderEngine::Render() { + std::vector layers; + layers.reserve(layers_.size()); + for (auto& iter : layers_) { + if (iter.second->IsEnabled()) { + auto impl = static_cast(iter.second.get()); + layers.emplace_back(impl); + } + } + if (layers.empty()) { + return false; + } + + absl::c_sort(layers, [](auto* lhs, auto* rhs) { + return lhs->GetPriority() < rhs->GetPriority(); + }); + + if (!frenderer_->beginFrame(fswapchain_)) { + return false; + } + + for (FilamentRenderLayer* layer : layers) { + frenderer_->render(layer->GetFilamentView()); + } + + frenderer_->endFrame(); + return true; +} + +bool FilamentRenderEngine::RenderLayer(HashValue name) { + auto iter = layers_.find(name); + if (iter == layers_.end()) { + return false; + } + if (!frenderer_->beginFrame(fswapchain_)) { + return false; + } + + auto* impl = static_cast(iter->second.get()); + frenderer_->render(impl->GetFilamentView()); + + frenderer_->endFrame(); + return true; +} + +ImageData FilamentRenderEngine::ReadPixels(FilamentRenderTarget* target) { + const RenderTargetFormat target_format = target->GetRenderTargetFormat(); + const int width = target->GetDimensions().x; + const int height = target->GetDimensions().y; + + const ImageFormat output_format = ToImageFormat(target_format); + const std::size_t bytes_per_pixel = GetBytesPerPixel(output_format); + ImageData image(output_format, {width, height}, + DataContainer::Allocate(width * height * bytes_per_pixel)); + + const auto format = ToFilamentPixelDataFormat(target_format); + const auto type = + filament::Texture::PixelBufferDescriptor::PixelDataType::UBYTE; + filament::Texture::PixelBufferDescriptor desc( + image.GetData(), image.GetNumBytes(), format, type); + + SyncWait(); + if (!frenderer_->beginFrame(fswapchain_)) { + LOG(FATAL) << "Unable to prepare renderer for reading pixels."; + } + + frenderer_->readPixels(target->GetFilamentRenderTarget(), 0, 0, width, + height, std::move(desc)); + frenderer_->endFrame(); + SyncWait(); + return image; +} + +void FilamentRenderEngine::SyncWait() { + filament::Fence::waitAndDestroy(fengine_->createFence()); +} + +void RenderEngine::Create(Registry* registry) { + auto ptr = new FilamentRenderEngine(registry); + registry->Register(std::unique_ptr(ptr)); + // We need to delay the creation of factories until after the engine is in + // the registry. Otherwise, the engine will get destroyed before the factories + // during shutdown. + ptr->CreateFactories(); +} + +static StaticRegistry Static_Register(RenderEngine::Create); + +} // namespace redux diff --git a/redux/redux/engines/render/filament/filament_render_engine.h b/redux/redux/engines/render/filament/filament_render_engine.h new file mode 100644 index 0000000..29d1a0f --- /dev/null +++ b/redux/redux/engines/render/filament/filament_render_engine.h @@ -0,0 +1,118 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_RENDER_ENGINE_H_ +#define REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_RENDER_ENGINE_H_ + +#include "filament/Engine.h" +#include "filament/Renderer.h" +#include "filament/SwapChain.h" +#include "redux/engines/render/filament/filament_render_layer.h" +#include "redux/engines/render/filament/filament_render_scene.h" +#include "redux/engines/render/filament/filament_render_target.h" +#include "redux/engines/render/mesh_factory.h" +#include "redux/engines/render/render_engine.h" +#include "redux/engines/render/render_target_factory.h" +#include "redux/engines/render/shader_factory.h" +#include "redux/engines/render/texture_factory.h" + +namespace redux { + +class FilamentRenderEngine : public RenderEngine { + public: + explicit FilamentRenderEngine(Registry* registry); + ~FilamentRenderEngine() override; + + void CreateFactories(); + + void OnRegistryInitialize(); + + // Creates a new RenderScene with the given name. + RenderScenePtr CreateRenderScene(HashValue name); + + // Returns the RenderScene with the given name. + RenderScenePtr GetRenderScene(HashValue name); + + // Returns a default RenderScene. + RenderScenePtr GetDefaultRenderScene(); + + // Creates a new RenderLayer with the given name. + RenderLayerPtr CreateRenderLayer(HashValue name); + + // Returns the RenderLayer with the given name. + RenderLayerPtr GetRenderLayer(HashValue name); + + // Returns a default RenderLayer. + RenderLayerPtr GetDefaultRenderLayer(); + + // Creates a new Renderable. + RenderablePtr CreateRenderable(); + + // Creates a new Light of the given type. + LightPtr CreateLight(Light::Type type); + + // Creates a new IndirectLight. + IndirectLightPtr CreateIndirectLight(const TexturePtr& reflection, + const TexturePtr& irradiance = nullptr); + + // Renders all active RenderLayers in priority order. + bool Render(); + + // Renders the specified layer (regardless of active state). + bool RenderLayer(HashValue name); + + // Waits until all rendering operations have completed. + void SyncWait(); + + // Accessors for the factories for various rendering asset types. + MeshFactory* GetMeshFactory() { return mesh_factory_; } + ShaderFactory* GetShaderFactory() { return shader_factory_; } + TextureFactory* GetTextureFactory() { return texture_factory_; } + RenderTargetFactory* GetRenderTargetFactory() { + return render_target_factory_; + } + + // Returns the image pixel data stored in the given render target. + ImageData ReadPixels(FilamentRenderTarget* target); + + filament::Engine* GetFilamentEngine() { return fengine_; } + filament::Renderer* GetFilamentRenderer() { return frenderer_; } + + private: + MeshFactory* mesh_factory_ = nullptr; + ShaderFactory* shader_factory_ = nullptr; + TextureFactory* texture_factory_ = nullptr; + RenderTargetFactory* render_target_factory_ = nullptr; + + filament::Engine* fengine_ = nullptr; + filament::Renderer* frenderer_ = nullptr; + filament::SwapChain* fswapchain_ = nullptr; + + RenderTargetPtr default_render_target_; + absl::flat_hash_map layers_; + absl::flat_hash_map scenes_; +}; + +inline filament::Engine* GetFilamentEngine(Registry* registry) { + auto engine = + static_cast(registry->Get()); + CHECK(engine); + return engine->GetFilamentEngine(); +} + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_RENDER_ENGINE_H_ diff --git a/redux/redux/engines/render/filament/filament_render_layer.cc b/redux/redux/engines/render/filament/filament_render_layer.cc new file mode 100644 index 0000000..04eb45b --- /dev/null +++ b/redux/redux/engines/render/filament/filament_render_layer.cc @@ -0,0 +1,118 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/render/filament/filament_render_layer.h" + +#include "utils/EntityManager.h" +#include "redux/engines/render/filament/filament_light.h" +#include "redux/engines/render/filament/filament_render_engine.h" +#include "redux/engines/render/filament/filament_render_target.h" +#include "redux/engines/render/filament/filament_renderable.h" +#include "redux/engines/render/filament/filament_utils.h" +#include "redux/modules/math/matrix.h" + +namespace redux { + +FilamentRenderLayer::FilamentRenderLayer(Registry* registry, + RenderTargetPtr target) + : render_target_(target) { + auto fengine = redux::GetFilamentEngine(registry); + fview_ = MakeFilamentResource(fengine->createView(), fengine); + + static const float kCameraAperture = 16.f; + static const float kCameraShutterSpeed = 1.0f / 125.0f; + static const float kCameraSensitivity = 100.0f; + auto camera_entity = utils::EntityManager::get().create(); + fcamera_ = + MakeFilamentResource(fengine->createCamera(camera_entity), fengine); + fcamera_->setExposure(kCameraAperture, kCameraShutterSpeed, + kCameraSensitivity); + fview_->setCamera(fcamera_.get()); + + SetViewport({vec2::Zero(), vec2::One()}); +} + +void FilamentRenderLayer::Enable() { enabled_ = true; } + +void FilamentRenderLayer::Disable() { enabled_ = false; } + +bool FilamentRenderLayer::IsEnabled() const { return enabled_; } + +void FilamentRenderLayer::SetPriority(int priority) { priority_ = priority; } + +int FilamentRenderLayer::GetPriority() const { return priority_; } + +void FilamentRenderLayer::EnableAntiAliasing() { + fview_->setAntiAliasing(filament::View::AntiAliasing::FXAA); +} + +void FilamentRenderLayer::DisableAntiAliasing() { + fview_->setAntiAliasing(filament::View::AntiAliasing::NONE); + fview_->setPostProcessingEnabled(false); +} + +void FilamentRenderLayer::DisablePostProcessing() { + fview_->setPostProcessingEnabled(false); +} + +void FilamentRenderLayer::SetClipPlaneDistances(float near, float far) { + near_plane_ = near; + far_plane_ = far; +} + +void FilamentRenderLayer::SetViewport(const Bounds2f& viewport) { + viewport_ = viewport; + + const vec2i target_size = render_target_->GetDimensions(); + filament::Viewport vp; + vp.left = static_cast(viewport.min.x * target_size.x); + vp.bottom = static_cast(viewport.min.y * target_size.y); + vp.width = static_cast(viewport.Size().x * target_size.x); + vp.height = static_cast(viewport.Size().y * target_size.y); + fview_->setViewport(vp); +} + +Bounds2i FilamentRenderLayer::GetAbsoluteViewport() const { + const filament::Viewport& vp = fview_->getViewport(); + Bounds2i res; + res.min = vec2i{vp.left, vp.bottom}; + res.max = vec2i{static_cast(vp.width), static_cast(vp.height)}; + return res; +} + +void FilamentRenderLayer::SetRenderTarget(RenderTargetPtr target) { + auto impl = static_cast(target.get()); + fview_->setRenderTarget(impl ? impl->GetFilamentRenderTarget() : nullptr); + render_target_ = target; + SetViewport(viewport_); +} + +void FilamentRenderLayer::SetScene(const RenderScenePtr& scene) { + auto impl = static_cast(scene.get()); + fview_->setScene(impl ? impl->GetFilamentScene() : nullptr); +} + +void FilamentRenderLayer::SetViewMatrix(const mat4& view_matrix) { + fcamera_->setModelMatrix(ToFilament(view_matrix)); +} + +void FilamentRenderLayer::SetProjectionMatrix(const mat4& projection_matrix) { + fcamera_->setCustomProjection( + filament::math::mat4(ToFilament(projection_matrix)), near_plane_, + far_plane_); +} + +} // namespace redux diff --git a/redux/redux/engines/render/filament/filament_render_layer.h b/redux/redux/engines/render/filament/filament_render_layer.h new file mode 100644 index 0000000..e7238b1 --- /dev/null +++ b/redux/redux/engines/render/filament/filament_render_layer.h @@ -0,0 +1,117 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_RENDER_LAYER_H_ +#define REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_RENDER_LAYER_H_ + +#include "absl/container/btree_set.h" +#include "filament/Camera.h" +#include "filament/Engine.h" +#include "filament/Renderer.h" +#include "filament/Scene.h" +#include "filament/View.h" +#include "filament/Viewport.h" +#include "redux/engines/render/filament/filament_utils.h" +#include "redux/engines/render/light.h" +#include "redux/engines/render/render_engine.h" +#include "redux/engines/render/renderable.h" +#include "redux/modules/base/registry.h" + +namespace redux { + +// Manages a filament Camera and View. +class FilamentRenderLayer : public RenderLayer { + public: + FilamentRenderLayer(Registry* registry, RenderTargetPtr target); + + // Adds the layer to the list of layers to be rendered, effectively enabling + // it. + void Enable(); + + // Removes the layer from the list of layers to be rendered, effectively + // disabling it. + void Disable(); + + // Returns true if the layer will be rendered. + bool IsEnabled() const; + + // Sets the priority at which the layer will be rendered. Higher priority + // layers will be rendered first. Two layers with the same priority will be + // rendered in arbitrary order. + void SetPriority(int priority); + + // Returns the render priority of the layer. + int GetPriority() const; + + // Associates a scene (which contains lights and renderables) with this layer. + // A layer can only have a single scene at a time. + void SetScene(const RenderScenePtr& scene); + + // Sets the render target on which to perform the drawing/rendering. + void SetRenderTarget(RenderTargetPtr target); + + // Sets the clip plane distances for rendering. + void SetClipPlaneDistances(float near, float far); + + // Sets the viewport (i.e. area) on the render target in which the rendering + // will be performed. + void SetViewport(const Bounds2f& viewport); + + // Returns the viewport (i.e. area) on the render target in which the + // rendering will be performed. + Bounds2i GetAbsoluteViewport() const; + + // Sets the view matrix that will be used for rendering. This is effectively + // the transform of the camera from which the scene will be rendered. + void SetViewMatrix(const mat4& view_matrix); + + // Sets the projection matrix that will be used for rendering. This is + // effectively the lens of the camera form which the scene will be rendered. + void SetProjectionMatrix(const mat4& projection_matrix); + + // Enables anti-aliasing when rendering the layer. + void EnableAntiAliasing(); + + // Disables anti-aliasing when rendering the layer. + void DisableAntiAliasing(); + + // Disables post-processing (like tone mapping) when rendering the layer. + void DisablePostProcessing(); + + // Returns the underlying filament view. + filament::View* GetFilamentView() { return fview_.get(); } + + // Returns the underlying filament camera. + filament::Camera* GetFilamentCamera() { return fcamera_.get(); } + + // Returns the underlying render target for this layer. + const RenderTargetPtr& GetRenderTarget() const { + return render_target_; + } + + private: + FilamentResourcePtr fcamera_ = nullptr; + FilamentResourcePtr fview_ = nullptr; + RenderTargetPtr render_target_; + Bounds2f viewport_; + float near_plane_ = 0.1f; + float far_plane_ = 1000.0f; + int priority_ = 0; + bool enabled_ = true; +}; +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_RENDER_LAYER_H_ diff --git a/redux/redux/engines/render/filament/filament_render_scene.cc b/redux/redux/engines/render/filament/filament_render_scene.cc new file mode 100644 index 0000000..b881510 --- /dev/null +++ b/redux/redux/engines/render/filament/filament_render_scene.cc @@ -0,0 +1,61 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/render/filament/filament_render_scene.h" + +#include "redux/engines/render/filament/filament_indirect_light.h" +#include "redux/engines/render/filament/filament_light.h" +#include "redux/engines/render/filament/filament_render_engine.h" +#include "redux/engines/render/filament/filament_renderable.h" + +namespace redux { + +FilamentRenderScene::FilamentRenderScene(Registry* registry) { + fengine_ = redux::GetFilamentEngine(registry); + fscene_ = MakeFilamentResource(fengine_->createScene(), fengine_); +} + +void FilamentRenderScene::Add(const Renderable& renderable) { + const auto* obj = static_cast(&renderable); + obj->AddToScene(this); +} + +void FilamentRenderScene::Remove(const Renderable& renderable) { + const auto* obj = static_cast(&renderable); + obj->RemoveFromScene(this); +} + +void FilamentRenderScene::Add(const Light& light) { + const auto* obj = static_cast(&light); + obj->AddToScene(this); +} + +void FilamentRenderScene::Remove(const Light& light) { + const auto* obj = static_cast(&light); + obj->RemoveFromScene(this); +} + +void FilamentRenderScene::Add(const IndirectLight& light) { + const auto* obj = static_cast(&light); + obj->AddToScene(this); +} + +void FilamentRenderScene::Remove(const IndirectLight& light) { + const auto* obj = static_cast(&light); + obj->RemoveFromScene(this); +} + +} // namespace redux diff --git a/redux/redux/engines/render/filament/filament_render_scene.h b/redux/redux/engines/render/filament/filament_render_scene.h new file mode 100644 index 0000000..6bf25e7 --- /dev/null +++ b/redux/redux/engines/render/filament/filament_render_scene.h @@ -0,0 +1,59 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_RENDER_SCENE_H_ +#define REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_RENDER_SCENE_H_ + +#include "filament/Scene.h" +#include "redux/engines/render/filament/filament_utils.h" +#include "redux/engines/render/render_scene.h" +#include "redux/modules/base/registry.h" + +namespace redux { + +// Manages a filament Scene. +class FilamentRenderScene : public RenderScene { + public: + explicit FilamentRenderScene(Registry* registry); + + // Adds a renderable to the scene. + void Add(const Renderable& renderable); + + // Removes a renderable from the scene. + void Remove(const Renderable& renderable); + + // Adds a light to the scene. + void Add(const Light& light); + + // Removes a light from the scene. + void Remove(const Light& light); + + // Adds an indirect light to the scene. + void Add(const IndirectLight& light); + + // Removes an indirect light from the scene. + void Remove(const IndirectLight& light); + + // Returns the underlying filament scene. + filament::Scene* GetFilamentScene() { return fscene_.get(); } + + private: + filament::Engine* fengine_ = nullptr; + FilamentResourcePtr fscene_ = nullptr; +}; +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_RENDER_SCENE_H_ diff --git a/redux/redux/engines/render/filament/filament_render_target.cc b/redux/redux/engines/render/filament/filament_render_target.cc new file mode 100644 index 0000000..f5701be --- /dev/null +++ b/redux/redux/engines/render/filament/filament_render_target.cc @@ -0,0 +1,133 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/render/filament/filament_render_target.h" + +#include "redux/engines/platform/device_manager.h" +#include "redux/engines/render/filament/filament_render_engine.h" + +namespace redux { + +static filament::Texture::InternalFormat ToFilamentTextureInternalFormat( + RenderTargetFormat format) { + switch (format) { + case RenderTargetFormat::Red8: + return filament::Texture::InternalFormat::R8; + case RenderTargetFormat::Rgb8: + return filament::Texture::InternalFormat::RGB8; + case RenderTargetFormat::Rgba8: + return filament::Texture::InternalFormat::RGBA8; + default: + LOG(FATAL) << "Unsupported format: " << static_cast(format); + } +} + +static filament::Texture::InternalFormat ToFilamentDepthStencilInternalFormat( + RenderTargetDepthStencilFormat format) { + switch (format) { + case RenderTargetDepthStencilFormat::Depth16: + case RenderTargetDepthStencilFormat::Depth24: + case RenderTargetDepthStencilFormat::Depth24Stencil8: + return filament::Texture::InternalFormat::DEPTH24_STENCIL8; + case RenderTargetDepthStencilFormat::Depth32f: + return filament::Texture::InternalFormat::DEPTH32F; + case RenderTargetDepthStencilFormat::Depth32fStencil8: + return filament::Texture::InternalFormat::DEPTH32F_STENCIL8; + default: + LOG(FATAL) << "Unsupported format: " << static_cast(format); + } + return filament::Texture::InternalFormat::UNUSED; +} + +FilamentRenderTarget::FilamentRenderTarget(Registry* registry) + : registry_(registry) { + auto display = registry_->Get()->Display(); + dimensions_ = display.GetProfile()->display_size; +} + +FilamentRenderTarget::FilamentRenderTarget(Registry* registry, + const RenderTargetParams& params) + : registry_(registry) { + fengine_ = redux::GetFilamentEngine(registry); + color_format_ = params.texture_format; + + CreateColorAttachment(params); + CreateDepthStencilAttachment(params); + + filament::RenderTarget::Builder builder; + CHECK(fcolor_); + builder.texture(filament::RenderTarget::AttachmentPoint::COLOR, + fcolor_.get()); + if (fdepth_stencil_) { + builder.texture(filament::RenderTarget::AttachmentPoint::DEPTH, + fdepth_stencil_.get()); + } + + dimensions_ = params.dimensions; + frender_target_ = MakeFilamentResource(builder.build(*fengine_), fengine_); +} + +void FilamentRenderTarget::CreateColorAttachment( + const RenderTargetParams& params) { + filament::Texture::Builder builder; + builder.width(params.dimensions.x); + builder.height(params.dimensions.y); + builder.format(ToFilamentTextureInternalFormat(params.texture_format)); + builder.sampler(filament::Texture::Sampler::SAMPLER_2D); + builder.usage(filament::Texture::Usage::COLOR_ATTACHMENT); + fcolor_ = MakeFilamentResource(builder.build(*fengine_), fengine_); +} + +void FilamentRenderTarget::CreateDepthStencilAttachment( + const RenderTargetParams& params) { + const filament::Texture::InternalFormat format = + ToFilamentDepthStencilInternalFormat(params.depth_stencil_format); + + if (format == filament::Texture::InternalFormat::UNUSED) { + return; + } + + static const filament::Texture::Usage kDepthStencilUsage = + static_cast( + static_cast( + filament::backend::TextureUsage::DEPTH_ATTACHMENT) | + static_cast( + filament::backend::TextureUsage::STENCIL_ATTACHMENT)); + + filament::Texture::Builder builder; + builder.format(format); + builder.width(params.dimensions.x); + builder.height(params.dimensions.y); + builder.levels(1); + builder.usage(kDepthStencilUsage); + fdepth_stencil_ = MakeFilamentResource(builder.build(*fengine_), fengine_); +} + +vec2i FilamentRenderTarget::GetDimensions() const { + return dimensions_; +} + +RenderTargetFormat FilamentRenderTarget::GetRenderTargetFormat() const { + return color_format_; +} + +ImageData FilamentRenderTarget::GetFrameBufferData() { + auto engine = + static_cast(registry_->Get()); + return engine->ReadPixels(this); +} + +} // namespace redux diff --git a/redux/redux/engines/render/filament/filament_render_target.h b/redux/redux/engines/render/filament/filament_render_target.h new file mode 100644 index 0000000..d705541 --- /dev/null +++ b/redux/redux/engines/render/filament/filament_render_target.h @@ -0,0 +1,63 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_RENDER_TARGET_H_ +#define REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_RENDER_TARGET_H_ + +#include "filament/RenderTarget.h" +#include "filament/Texture.h" +#include "redux/engines/render/filament/filament_utils.h" +#include "redux/engines/render/render_target.h" +#include "redux/engines/render/render_target_factory.h" +#include "redux/modules/base/registry.h" + +namespace redux { + +// Manages a filament RenderTarget. +class FilamentRenderTarget : public RenderTarget { + public: + explicit FilamentRenderTarget(Registry* registry); + FilamentRenderTarget(Registry* registry, const RenderTargetParams& params); + + // Returns the dimensions of the render target. + vec2i GetDimensions() const; + + // Returns the format of the underlying color buffer. + RenderTargetFormat GetRenderTargetFormat() const; + + // Returns the contents of the render buffer. + ImageData GetFrameBufferData(); + + filament::RenderTarget* GetFilamentRenderTarget() { + return frender_target_.get(); + } + + private: + void CreateColorAttachment(const RenderTargetParams& params); + void CreateDepthStencilAttachment(const RenderTargetParams& params); + + Registry* registry_ = nullptr; + filament::Engine* fengine_ = nullptr; + FilamentResourcePtr fcolor_; + FilamentResourcePtr fdepth_stencil_; + FilamentResourcePtr frender_target_; + RenderTargetFormat color_format_; + vec2i dimensions_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_RENDER_TARGET_H_ diff --git a/redux/redux/engines/render/filament/filament_renderable.cc b/redux/redux/engines/render/filament/filament_renderable.cc new file mode 100644 index 0000000..c5c517e --- /dev/null +++ b/redux/redux/engines/render/filament/filament_renderable.cc @@ -0,0 +1,439 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/render/filament/filament_renderable.h" + +#include "filament/RenderableManager.h" +#include "filament/TransformManager.h" +#include "utils/EntityManager.h" +#include "redux/engines/render/filament/filament_mesh.h" +#include "redux/engines/render/filament/filament_render_engine.h" +#include "redux/engines/render/filament/filament_shader.h" +#include "redux/engines/render/filament/filament_texture.h" +#include "redux/engines/render/filament/filament_utils.h" + +namespace redux { + +static constexpr HashValue kRootPart = HashValue(0); + +static filament::RenderableManager::PrimitiveType ToFilament( + MeshPrimitiveType type) { + switch (type) { + case MeshPrimitiveType::Triangles: + return filament::RenderableManager::PrimitiveType::TRIANGLES; + case MeshPrimitiveType::Points: + return filament::RenderableManager::PrimitiveType::POINTS; + case MeshPrimitiveType::Lines: + return filament::RenderableManager::PrimitiveType::LINES; + default: + LOG(FATAL) << "Unsupported primitive type: " << ToString(type); + } +} + +static MaterialPropertyType MaterialPropertyTypeFromTextureTarget( + TextureTarget target) { + switch (target) { + case TextureTarget::Normal2D: + return MaterialPropertyType::Sampler2D; + case TextureTarget::CubeMap: + return MaterialPropertyType::SamplerCubeMap; + default: + LOG(FATAL) << "Unsupported texture target: " << ToString(target); + } +} + +FilamentRenderable::FilamentRenderable(Registry* registry) { + fengine_ = GetFilamentEngine(registry); +} + +FilamentRenderable::~FilamentRenderable() { DestroyParts(); } + +void FilamentRenderable::PrepareToRender(const mat4& transform) { + auto& tm = fengine_->getTransformManager(); + auto& rm = fengine_->getRenderableManager(); + + const uint8_t global_visbility = IsHidden(kRootPart) ? 0x00 : 0xff; + const auto mx = ToFilament(transform); + for (auto& iter : parts_) { + auto ti = tm.getInstance(iter.second.fentity); + auto ri = rm.getInstance(iter.second.fentity); + tm.setTransform(ti, mx); + + const uint8_t local_visibility = IsHidden(iter.first) ? 0x00 : 0xff; + const uint8_t visibility = global_visbility & local_visibility; + rm.setLayerMask(ri, 0xff, visibility); + + if (iter.second.finstance) { + const bool updated = ApplyProperties(iter.first, iter.second); + if (updated) { + rm.setMaterialInstanceAt(ri, 0, iter.second.finstance.get()); + } + } + } +} + +bool FilamentRenderable::ApplyProperties(HashValue part_name, + PartInstance& part) { + bool updated = false; + + if (is_skinned_) { + const MaterialProperty* bones = + GetMaterialProperty(kRootPart, ConstHash("Bones")); + if (bones) { + CHECK(bones->type == MaterialPropertyType::Float4x4); + const auto bytes = bones->data.GetByteSpan(); + const auto* data = + reinterpret_cast(bytes.data()); + const size_t num_bones = bytes.size() / sizeof(filament::math::mat4f); + auto& rm = fengine_->getRenderableManager(); + auto ri = rm.getInstance(part.fentity); + rm.setBones(ri, data, num_bones); + } + } + { + const MaterialProperty* property = + GetMaterialProperty(part_name, ConstHash("Scissor")); + if (property) { + CHECK(property->type == MaterialPropertyType::Int4); + const vec4i* scissor = + reinterpret_cast(property->data.GetByteSpan().data()); + if (scissor->x < 0 || scissor->y < 0 || scissor->z < 0 || + scissor->w < 0) { + part.finstance->unsetScissor(); + } else { + part.finstance->setScissor(scissor->x, scissor->y, scissor->z, + scissor->w); + } + } + } + { + const MaterialProperty* property = + GetMaterialProperty(part_name, ConstHash("PolygonOffset")); + if (property) { + CHECK(property->type == MaterialPropertyType::Float2); + const vec2* offset = + reinterpret_cast(property->data.GetByteSpan().data()); + part.finstance->setPolygonOffset(offset->x, offset->y); + } + } + + FilamentShader* shader = static_cast(part.shader.get()); + shader->ForEachParameter( + part.variant_id, [&](const FilamentShader::ParameterInfo& param) { + const MaterialProperty* property = + GetMaterialProperty(part_name, param.key); + if (property == nullptr) { + return; + } + if (property->generation == part.property_generations[param.key]) { + return; + } + + part.property_generations[param.key] = property->generation; + if (property->texture) { + FilamentShader::SetParameter(part.finstance.get(), param.name.c_str(), + property->type, property->texture); + } else { + FilamentShader::SetParameter(part.finstance.get(), param.name.c_str(), + property->type, + property->data.GetByteSpan()); + } + updated = true; + }); + return updated; +} + +void FilamentRenderable::AddToScene(FilamentRenderScene* scene) const { + auto fscene = scene->GetFilamentScene(); + if (!scenes_.contains(fscene)) { + scenes_.emplace(fscene); + for (auto& part : parts_) { + fscene->addEntity(part.second.fentity); + } + } +} + +void FilamentRenderable::RemoveFromScene(FilamentRenderScene* scene) const { + auto fscene = scene->GetFilamentScene(); + if (scenes_.contains(fscene)) { + for (auto& part : parts_) { + fscene->remove(part.second.fentity); + } + scenes_.erase(fscene); + } +} + +MeshPtr FilamentRenderable::GetMesh() const { return mesh_; } + +void FilamentRenderable::SetMesh(MeshPtr mesh) { + if (mesh_ != mesh) { + mesh_ = mesh; + CreateParts(); + RebuildConditions(); + } +} + +void FilamentRenderable::CreateParts() { + DestroyParts(); + if (mesh_ == nullptr) { + return; + } + + FilamentMesh* impl = static_cast(mesh_.get()); + const auto vbuffer = impl->GetFilamentVertexBuffer(); + const auto ibuffer = impl->GetFilamentIndexBuffer(); + + const size_t count = mesh_->GetNumSubmeshes(); + for (size_t i = 0; i < count; ++i) { + const Mesh::SubmeshData& submesh = mesh_->GetSubmeshData(i); + CHECK(count == 1 || submesh.name.get()) << "Submeshes must have names."; + + // Ensure we always have a material for every part. + materials_[submesh.name]; + + is_skinned_ |= submesh.vertex_format.GetAttributeWithUsage( + VertexUsage::BoneWeights) != nullptr; + + // Create a filament Renderable for each part. + PartInstance& part = parts_[submesh.name]; + filament::RenderableManager::Builder builder(1); + const auto type = ToFilament(submesh.primitive_type); + builder.boundingBox(ToFilament(submesh.box)); + builder.geometry(0, type, vbuffer, ibuffer, submesh.range_start, + submesh.range_end - submesh.range_start); + + if (is_skinned_) { + builder.skinning(255); + } + + part.fentity = utils::EntityManager::get().create(); + builder.build(*fengine_, part.fentity); + + // Add the part to all the scenes to which this belongs. + for (filament::Scene* scene : scenes_) { + scene->addEntity(part.fentity); + } + } +} + +void FilamentRenderable::DestroyParts() { + auto& tm = fengine_->getTransformManager(); + auto& rm = fengine_->getRenderableManager(); + for (auto& iter : parts_) { + for (filament::Scene* scene : scenes_) { + scene->remove(iter.second.fentity); + } + rm.destroy(iter.second.fentity); + tm.destroy(iter.second.fentity); + utils::EntityManager::get().destroy(iter.second.fentity); + } + parts_.clear(); + is_skinned_ = false; +} + +void FilamentRenderable::SetShader(ShaderPtr shader, + std::optional part) { + Material& material = materials_[part ? part.value() : kRootPart]; + material.shader = std::move(shader); + ReacquireInstance(part ? part.value() : kRootPart); +} + +void FilamentRenderable::Show(std::optional part) { + Material& material = materials_[part ? part.value() : kRootPart]; + material.visible = true; +} + +void FilamentRenderable::Hide(std::optional part) { + Material& material = materials_[part ? part.value() : kRootPart]; + material.visible = false; +} + +bool FilamentRenderable::IsHidden(std::optional part) const { + auto iter = materials_.find(part ? part.value() : kRootPart); + return iter != materials_.end() ? !iter->second.visible : true; +} + +void FilamentRenderable::EnableVertexAttribute(VertexUsage usage) { + disabled_vertices_.erase(usage); + RebuildConditions(); +} + +void FilamentRenderable::DisableVertexAttribute(VertexUsage usage) { + disabled_vertices_.emplace(usage); + RebuildConditions(); +} + +bool FilamentRenderable::IsVertexAttributeEnabled(VertexUsage usage) const { + return !disabled_vertices_.contains(usage); +} + +void FilamentRenderable::SetTexture(TextureUsage usage, + const TexturePtr& texture) { + HashValue key = usage.Hash(); + Material& material = materials_[kRootPart]; + MaterialProperty& property = material.properties[key]; + property.data.Clear(); + property.texture = texture; + property.type = MaterialPropertyTypeFromTextureTarget(texture->GetTarget()); + + FilamentTexture* impl = static_cast(texture.get()); + impl->OnReady([=]() { RebuildConditions(); }); + ++property.generation; +} + +TexturePtr FilamentRenderable::GetTexture(TextureUsage usage) const { + const HashValue key = usage.Hash(); + const MaterialProperty* property = GetMaterialProperty(kRootPart, key); + return property ? property->texture : nullptr; +} + +void FilamentRenderable::SetProperty(HashValue name, MaterialPropertyType type, + absl::Span data) { + SetProperty(name, kRootPart, type, data); +} + +void FilamentRenderable::SetProperty(HashValue name, HashValue part, + MaterialPropertyType type, + absl::Span data) { + Material& material = materials_[part]; + if (type == MaterialPropertyType::Feature) { + CHECK(data.size() == sizeof(bool)); + const bool enable = *reinterpret_cast(data.data()); + if (enable) { + material.features.emplace(name); + } else { + material.features.erase(name); + } + ReacquireInstance(part); + } else { + MaterialProperty& property = material.properties[name]; + property.texture = nullptr; + property.data.Assign(data); + property.type = type; + ++property.generation; + } +} + +const FilamentRenderable::MaterialProperty* +FilamentRenderable::GetMaterialProperty(HashValue part, HashValue name) const { + bool is_root = part == kRootPart; + auto part_iter = materials_.find(part); + if (part_iter == materials_.end()) { + is_root = true; + part_iter = materials_.find(kRootPart); + } + if (part_iter == materials_.end()) { + return nullptr; + } + + const Material* material = &part_iter->second; + auto property_iter = material->properties.find(name); + if (property_iter == material->properties.end() && !is_root) { + is_root = true; + part_iter = materials_.find(kRootPart); + material = &part_iter->second; + property_iter = material->properties.find(name); + } + + return property_iter != material->properties.end() ? &property_iter->second + : nullptr; +} + +void FilamentRenderable::RebuildConditions() { + conditions_.clear(); + if (mesh_ == nullptr || mesh_->GetNumSubmeshes() == 0) { + return; + } + + const VertexFormat& format = mesh_->GetSubmeshData(0).vertex_format; + for (size_t i = 0; i < format.GetNumAttributes(); ++i) { + const VertexAttribute* attrib = format.GetAttributeAt(i); + if (!disabled_vertices_.contains(attrib->usage)) { + conditions_.insert(Hash(attrib->usage)); + } + } + const Material& root_material = materials_[kRootPart]; + for (const auto& iter : root_material.properties) { + if (iter.second.texture != nullptr) { + FilamentTexture* impl = + static_cast(iter.second.texture.get()); + if (impl->IsReady()) { + conditions_.insert(iter.first); + } + } + } + for (const auto& iter : parts_) { + ReacquireInstance(iter.first); + } +} + +void FilamentRenderable::ReacquireInstance(HashValue part) { + auto part_iter = parts_.find(part); + auto material_iter = materials_.find(part); + if (part_iter == parts_.end() || material_iter == materials_.end()) { + return; + } + + Material& part_material = material_iter->second; + Material& root_material = materials_[kRootPart]; + + // Use the root shader if the part does not have its own shader specified. + ShaderPtr shader = part_material.shader; + if (shader == nullptr) { + shader = root_material.shader; + } + + // Acquire the material instance from the shader. + if (shader) { + // Combine the features from the root and part into a single set. + absl::btree_set features; + for (auto& f : part_material.features) { + features.emplace(f); + } + for (auto& f : root_material.features) { + features.emplace(f); + } + + // Find a shader material instance that fulfills the requirements. + FilamentShader* impl = static_cast(shader.get()); + const auto variant = impl->DetermineVariantId(conditions_, features); + + // If the updated set of requirements requires a new variant instance, + // abandon the old one and create a new one. + PartInstance& part_instance = part_iter->second; + if (part_instance.variant_id != variant) { + part_instance.shader = shader; + part_instance.variant_id = variant; + part_instance.property_generations.clear(); + + const filament::Material* fmaterial = impl->GetFilamentMaterial(variant); + filament::MaterialInstance* finstance = fmaterial->createInstance(); + part_instance.finstance = MakeFilamentResource(finstance, fengine_); + } + } + + // If we're changing the root shader, then we also need to update any parts + // that don't have their own shader, but instead use the root shader. + if (part == kRootPart) { + for (const auto& iter : materials_) { + if (iter.first != kRootPart && iter.second.shader == nullptr) { + ReacquireInstance(iter.first); + } + } + } +} + +} // namespace redux diff --git a/redux/redux/engines/render/filament/filament_renderable.h b/redux/redux/engines/render/filament/filament_renderable.h new file mode 100644 index 0000000..53eb0c4 --- /dev/null +++ b/redux/redux/engines/render/filament/filament_renderable.h @@ -0,0 +1,147 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_RENDERABLE_H_ +#define REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_RENDERABLE_H_ + +#include "filament/Engine.h" +#include "utils/Entity.h" +#include "redux/engines/render/filament/filament_render_scene.h" +#include "redux/engines/render/filament/filament_shader.h" +#include "redux/engines/render/filament/filament_utils.h" +#include "redux/engines/render/renderable.h" +#include "redux/modules/base/data_buffer.h" +#include "redux/modules/base/registry.h" + +namespace redux { + +// Manages multiple filament Entities and MaterialInstances that are used for +// rendering. A Renderable requires a Mesh and a Shader to render. +// +// The Mesh defines the number of parts of the Renderable, each of which is +// assigned a filament Entity. Each part is also assigned a filament +// MaterialInstance, depending on the Shader variant associated with the part. +// +// Once setup, parts can be individually controlled (e.g. hidden, assigned +// material properties, etc.). In cases where a specific part is not specified, +// the change applies to the whole Renderable. +class FilamentRenderable : public Renderable { + public: + explicit FilamentRenderable(Registry* registry); + ~FilamentRenderable() override; + + // Prepares the renderable for rendering. The transform is used to place the + // renderable in all scenes to which it belongs. + void PrepareToRender(const mat4& transform); + + // Enables the renderable (or a part of the renderable) to be rendered. + void Show(std::optional part = std::nullopt); + + // Disables the renderable (or a part of the renderable) to be rendered. + void Hide(std::optional part = std::nullopt); + + // Returns true if the renderable (or a part of the renderable) is hidden. + bool IsHidden(std::optional part = std::nullopt) const; + + // Sets the mesh (i.e. shape) of the renderable. + void SetMesh(MeshPtr mesh); + + // Returns the mesh for the renderable. + MeshPtr GetMesh() const; + + // Enables a vertex attributes. All attributes are enabled by default. + void EnableVertexAttribute(VertexUsage usage); + + // Disables a specific vertex attribute which may affect how the renderable is + // drawn. For example, disabling a color vertex attribute will prevent the + // renderable's mesh color from being used when rendering. + void DisableVertexAttribute(VertexUsage usage); + + // Returns true if the vertex attribute is enabled. + bool IsVertexAttributeEnabled(VertexUsage usage) const; + + // Sets the shader that will be used to render the surface of the renderable + // for a specific part. + void SetShader(ShaderPtr shader, + std::optional part = std::nullopt); + + // Assigns a Texture for a given usage on the renderable. Textures are applied + // to the entirety of the renderable and not to individual parts. + void SetTexture(TextureUsage usage, const TexturePtr& texture); + + // Returns the Texture that was set for a given usage on the renderable. + TexturePtr GetTexture(TextureUsage usage) const; + + // Assigns a specific value to a material property with the given `name` which + // can be used by the shader when drawing the renderable. The shader will + // interpret the property `data` based on the material property `type`. + // Properties can be assigned to individual `part`s or the entire renderable. + void SetProperty(HashValue name, MaterialPropertyType type, + absl::Span data); + void SetProperty(HashValue name, HashValue part, MaterialPropertyType type, + absl::Span data); + + // Adds the renderable to a filament scene. + void AddToScene(FilamentRenderScene* scene) const; + + // Removes the renderable from a filament scene. + void RemoveFromScene(FilamentRenderScene* scene) const; + + private: + struct PartInstance { + utils::Entity fentity; + FilamentShader::VariantId variant_id = FilamentShader::kInvalidVariant; + FilamentResourcePtr finstance; + absl::flat_hash_map property_generations; + ShaderPtr shader; + }; + + struct MaterialProperty { + MaterialPropertyType type = MaterialPropertyType::Invalid; + DataBuffer data; + TexturePtr texture; + int generation = 0; + }; + + struct Material { + ShaderPtr shader; + bool visible = true; + absl::btree_set features; + absl::flat_hash_map properties; + }; + + void CreateParts(); + void DestroyParts(); + void RebuildConditions(); + void ReacquireInstance(HashValue part); + bool ApplyProperties(HashValue name, PartInstance& part); + const MaterialProperty* GetMaterialProperty(HashValue part, + HashValue name) const; + + filament::Engine* fengine_ = nullptr; + + MeshPtr mesh_; + absl::btree_set conditions_; + absl::flat_hash_set disabled_vertices_; + absl::flat_hash_map materials_; + absl::flat_hash_map parts_; + mutable absl::flat_hash_set scenes_; + bool is_skinned_ = false; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_RENDERABLE_H_ diff --git a/redux/redux/engines/render/filament/filament_shader.cc b/redux/redux/engines/render/filament/filament_shader.cc new file mode 100644 index 0000000..1102272 --- /dev/null +++ b/redux/redux/engines/render/filament/filament_shader.cc @@ -0,0 +1,178 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/render/filament/filament_shader.h" + +#include + +#include "redux/engines/render/filament/filament_render_engine.h" +#include "redux/engines/render/filament/filament_utils.h" +#include "redux/modules/base/logging.h" + +namespace redux { + +template +using FlatVector = flatbuffers::Vector; + +static absl::btree_set ReadFlags(const FlatVector* vec) { + absl::btree_set flags; + if (vec) { + for (const auto& flag : *vec) { + flags.emplace(flag); + } + } + return flags; +} + +template +absl::Span AsBytes(const FlatVector& vec) { + const std::byte* bytes = reinterpret_cast(vec.data()); + const size_t size = vec.size() * sizeof(T); + return {bytes, size}; +} + +static FilamentResourcePtr ReadFilamentMaterial( + filament::Engine* engine, const FlatVector* matc) { + CHECK(matc); + filament::Material::Builder builder; + builder.package(matc->data(), matc->size()); + return MakeFilamentResource(builder.build(*engine), engine); +} + +FilamentShader::FilamentShader(Registry* registry, const ShaderAssetDef* def) { + fengine_ = GetFilamentEngine(registry); + if (def && def->variants()) { + for (const auto& variant : *def->variants()) { + CHECK(variant); + BuildVariant(variant); + } + } +} + +void FilamentShader::BuildVariant(const ShaderVariantAssetDef* def) { + auto variant = std::make_unique(); + variant->fmaterial = ReadFilamentMaterial(fengine_, def->filament_material()); + variant->conditions = ReadFlags(def->conditions()); + variant->features = ReadFlags(def->features()); + + filament::MaterialInstance* default_instance = + variant->fmaterial->getDefaultInstance(); + + if (def->properties()) { + variant->params.reserve(def->properties()->size()); + for (const ShaderPropertyAssetDef* property : *def->properties()) { + CHECK(property->name() && property->name()->name()); + ParameterInfo param; + param.name = property->name()->name()->str(); + param.key = HashValue(property->name()->hash()); + param.type = property->type(); + if (property->texture_usage()) { + CHECK(param.type == MaterialPropertyType::Sampler2D || + param.type == MaterialPropertyType::SamplerCubeMap); + param.texture_usage = TextureUsage(*property->texture_usage()); + param.key = param.texture_usage.Hash(); + } + variant->params.emplace_back(param); + + if (property->default_floats()) { + absl::Span data = AsBytes(*property->default_floats()); + SetParameter(default_instance, param.name.c_str(), param.type, data); + } else if (property->default_ints()) { + absl::Span data = AsBytes(*property->default_ints()); + SetParameter(default_instance, param.name.c_str(), param.type, data); + } + } + } + + variants_.emplace_back(std::move(variant)); +} + +FilamentShader::VariantId FilamentShader::DetermineVariantId( + const FlagSet& conditions, const FlagSet& features) const { + for (int i = 0; i < variants_.size(); ++i) { + const auto& variant = variants_[i]; + if (absl::c_includes(conditions, variant->conditions) && + absl::c_includes(variant->features, features)) { + return i; + } + } + LOG(ERROR) << "Unable to find matching shader variant, falling back to " + "simplest variant."; + return static_cast(variants_.size()) - 1; +} + +const filament::Material* FilamentShader::GetFilamentMaterial( + VariantId id) const { + CHECK(id >= 0 && id < variants_.size()); + auto& variant = variants_[id]; + return variant->fmaterial.get(); +} + +void FilamentShader::ForEachParameter(VariantId id, const ForParameterFn& fn) { + CHECK(id >= 0 && id < variants_.size()); + auto& variant = variants_[id]; + for (const ParameterInfo& param : variant->params) { + fn(param); + } +} + +void FilamentShader::SetParameter(filament::MaterialInstance* instance, + const char* name, MaterialPropertyType type, + absl::Span data) { + using filament::math::float2; + using filament::math::float3; + using filament::math::float4; + switch (type) { + case MaterialPropertyType::Float1: { + CHECK_EQ(data.size(), sizeof(float)); + const float* value = reinterpret_cast(data.data()); + instance->setParameter(name, *value); + return; + } + case MaterialPropertyType::Float2: { + CHECK_EQ(data.size(), sizeof(float2)); + const float2* value = reinterpret_cast(data.data()); + instance->setParameter(name, *value); + return; + } + case MaterialPropertyType::Float3: { + CHECK_EQ(data.size(), sizeof(float3)); + const float3* value = reinterpret_cast(data.data()); + instance->setParameter(name, *value); + return; + } + case MaterialPropertyType::Float4: { + CHECK_EQ(data.size(), sizeof(float4)); + const float4* value = reinterpret_cast(data.data()); + instance->setParameter(name, *value); + return; + } + default: + LOG(FATAL) << "Unsupported material type: " << ToString(type); + } +} + +void FilamentShader::SetParameter(filament::MaterialInstance* instance, + const char* name, MaterialPropertyType type, + const TexturePtr& texture) { + FilamentTexture* impl = static_cast(texture.get()); + if (impl->IsReady()) { + instance->setParameter(name, impl->GetFilamentTexture(), + impl->GetFilamentSampler()); + } +} + +} // namespace redux diff --git a/redux/redux/engines/render/filament/filament_shader.h b/redux/redux/engines/render/filament/filament_shader.h new file mode 100644 index 0000000..a843aef --- /dev/null +++ b/redux/redux/engines/render/filament/filament_shader.h @@ -0,0 +1,104 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_SHADER_H_ +#define REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_SHADER_H_ + +#include +#include +#include +#include + +#include "absl/container/btree_set.h" +#include "absl/container/flat_hash_map.h" +#include "filament/Material.h" +#include "filament/MaterialInstance.h" +#include "redux/data/asset_defs/shader_asset_def_generated.h" +#include "redux/engines/render/filament/filament_texture.h" +#include "redux/engines/render/filament/filament_utils.h" +#include "redux/engines/render/shader.h" +#include "redux/modules/base/hash.h" +#include "redux/modules/base/registry.h" +#include "redux/modules/graphics/enums.h" +#include "redux/modules/graphics/texture_usage.h" + +namespace redux { + +// Defines how the "surface" of a renderable will be "colored in". +// +// A FilamentShader consists of multiple variants, each of which is basically a +// filament::Material. Each variant supports a set of features (e.g. skinning) +// and depends on a set of conditions (i.e. bone weights/indices vertex +// attributes). +class FilamentShader : public Shader { + public: + FilamentShader(Registry* registry, const ShaderAssetDef* def); + + // Internally, variants are stored in an array, and this is simply the index + // of the variant in that array. + using VariantId = int; + static constexpr VariantId kInvalidVariant = -1; + + // Finds the best matching variant given the `features` and `conditions`. + using FlagSet = absl::btree_set; + VariantId DetermineVariantId(const FlagSet& conditions, + const FlagSet& features) const; + + // Returns the filament::Material for the given variant. + const filament::Material* GetFilamentMaterial(VariantId id) const; + + // Information about a single parameter in a material. + struct ParameterInfo { + std::string name; + HashValue key; + MaterialPropertyType type; + TextureUsage texture_usage; + }; + + // Iterates over all the parameters in the material for the variant. + using ForParameterFn = std::function; + void ForEachParameter(VariantId id, const ForParameterFn& fn); + + // Sets the value of the parameter with the given `name` in the + // MaterialInstance based on the `type`. + static void SetParameter(filament::MaterialInstance* instance, + const char* name, MaterialPropertyType type, + absl::Span data); + + // Sets the value of the texture sampler with the given `name` in the + // MaterialInstance based on the `type`. + static void SetParameter(filament::MaterialInstance* instance, + const char* name, MaterialPropertyType type, + const TexturePtr& texture); + + private: + struct Variant { + FilamentResourcePtr fmaterial; + std::vector params; + FlagSet conditions; + FlagSet features; + }; + + void BuildVariant(const ShaderVariantAssetDef* def); + + filament::Engine* fengine_ = nullptr; + std::vector> variants_; + absl::flat_hash_map cache_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_SHADER_H_ diff --git a/redux/redux/engines/render/filament/filament_texture.cc b/redux/redux/engines/render/filament/filament_texture.cc new file mode 100644 index 0000000..2b8c2bf --- /dev/null +++ b/redux/redux/engines/render/filament/filament_texture.cc @@ -0,0 +1,227 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/render/filament/filament_texture.h" + +#include + +#include "redux/engines/render/filament/filament_render_engine.h" +#include "redux/modules/graphics/enums.h" + +namespace redux { + +using ImageDataPtr = std::shared_ptr; + +static filament::Texture::InternalFormat ToFilamentTextureInternalFormat( + ImageFormat format) { + switch (format) { + case ImageFormat::Rgba8888: + return filament::Texture::InternalFormat::RGBA8; + case ImageFormat::Rgb888: + return filament::Texture::InternalFormat::RGB8; + case ImageFormat::Rgba5551: + return filament::Texture::InternalFormat::RGB5_A1; + case ImageFormat::Rgb565: + return filament::Texture::InternalFormat::RGB565; + case ImageFormat::Alpha8: + return filament::Texture::InternalFormat::R8; + case ImageFormat::Luminance8: + return filament::Texture::InternalFormat::RG8; + case ImageFormat::LuminanceAlpha88: + return filament::Texture::InternalFormat::RG8; + default: + LOG(FATAL) << "Unhandled format: " << ToString(format); + return filament::Texture::InternalFormat::RGB8; + } +} + +static filament::Texture::Format ToFilamentTextureFormat(ImageFormat format) { + switch (format) { + case ImageFormat::Rgba8888: + return filament::Texture::Format::RGBA; + case ImageFormat::Rgb888: + return filament::Texture::Format::RGB; + case ImageFormat::Rgba5551: + return filament::Texture::Format::RGBA; + case ImageFormat::Rgb565: + return filament::Texture::Format::RGB; + case ImageFormat::Alpha8: + return filament::Texture::Format::R; + case ImageFormat::Luminance8: + return filament::Texture::Format::R; + case ImageFormat::LuminanceAlpha88: + return filament::Texture::Format::RG; + default: + LOG(FATAL) << "Unhandled format: " << ToString(format); + return filament::Texture::Format::RGBA; + } +} + +static filament::Texture::Type ToFilamentTextureType(ImageFormat format) { + switch (format) { + case ImageFormat::Rgba8888: + return filament::Texture::Type::UBYTE; + case ImageFormat::Rgb888: + return filament::Texture::Type::UBYTE; + case ImageFormat::Rgba5551: + return filament::Texture::Type::USHORT; + case ImageFormat::Rgb565: + return filament::Texture::Type::USHORT; + case ImageFormat::Alpha8: + return filament::Texture::Type::UBYTE; + case ImageFormat::Luminance8: + return filament::Texture::Type::UBYTE; + case ImageFormat::LuminanceAlpha88: + return filament::Texture::Type::UBYTE; + default: + LOG(FATAL) << "Unhandled format: " << ToString(format); + return filament::Texture::Type::UBYTE; + } +} + +static filament::TextureSampler::MinFilter ToFilamentMinFilter( + TextureFilter value) { + switch (value) { + case TextureFilter::Nearest: + return filament::TextureSampler::MinFilter::NEAREST; + case TextureFilter::Linear: + return filament::TextureSampler::MinFilter::LINEAR; + case TextureFilter::LinearMipmapLinear: + return filament::TextureSampler::MinFilter::LINEAR_MIPMAP_LINEAR; + case TextureFilter::LinearMipmapNearest: + return filament::TextureSampler::MinFilter::LINEAR_MIPMAP_NEAREST; + case TextureFilter::NearestMipmapNearest: + return filament::TextureSampler::MinFilter::NEAREST_MIPMAP_NEAREST; + case TextureFilter::NearestMipmapLinear: + return filament::TextureSampler::MinFilter::NEAREST_MIPMAP_LINEAR; + default: + LOG(FATAL) << "Unsupported filter: " << ToString(value); + } +} + +static filament::TextureSampler::MagFilter ToFilamentMagFilter( + TextureFilter value) { + switch (value) { + case TextureFilter::Nearest: + return filament::TextureSampler::MagFilter::NEAREST; + case TextureFilter::Linear: + return filament::TextureSampler::MagFilter::LINEAR; + default: + LOG(FATAL) << "Unsupported filter: " << ToString(value); + } +} + +static filament::TextureSampler::WrapMode ToFilamentWrapMode( + TextureWrap value) { + switch (value) { + case TextureWrap::Repeat: + return filament::TextureSampler::WrapMode::REPEAT; + case TextureWrap::ClampToEdge: + return filament::TextureSampler::WrapMode::CLAMP_TO_EDGE; + case TextureWrap::MirroredRepeat: + return filament::TextureSampler::WrapMode::MIRRORED_REPEAT; + default: + LOG(FATAL) << "Unsupported wrap mode: " << ToString(value); + } +} + +static filament::Texture::PixelBufferDescriptor CreatePixelBuffer( + const ImageDataPtr& image_data) { + using ImageDataPtr = std::shared_ptr; + + // We allocate a ImageDataPtr to extend the lifetime of the image data until + // filament is done with it. + void* user_data = new ImageDataPtr(image_data); + auto callback = [](void*, size_t, void* user) { + ImageDataPtr* image = reinterpret_cast(user); + delete image; + }; + + const std::byte* bytes = image_data->GetData(); + const size_t num_bytes = image_data->GetNumBytes(); + const auto format = ToFilamentTextureFormat(image_data->GetFormat()); + const auto type = ToFilamentTextureType(image_data->GetFormat()); + return filament::Texture::PixelBufferDescriptor(bytes, num_bytes, format, + type, callback, user_data); +} + +FilamentTexture::FilamentTexture(Registry* registry, std::string_view name) + : name_(name) { + fengine_ = GetFilamentEngine(registry); +} + +const std::string& FilamentTexture::GetName() const { return name_; } + +TextureTarget FilamentTexture::GetTarget() const { return target_; } + +vec2i FilamentTexture::GetDimensions() const { return dimensions_; } + +bool FilamentTexture::Update(ImageData image) { + Update(std::make_shared(std::move(image))); + return true; +} + +void FilamentTexture::Build(ImageData image_data, const TextureParams& params) { + Build(std::make_shared(std::move(image_data)), params); +} + +void FilamentTexture::Build(const std::shared_ptr& image_data, + const TextureParams& params) { + CHECK(image_data); + CHECK(ftexture_ == nullptr); + target_ = params.target; + + fsampler_.setMinFilter(ToFilamentMinFilter(params.min_filter)); + fsampler_.setMagFilter(ToFilamentMagFilter(params.mag_filter)); + fsampler_.setWrapModeR(ToFilamentWrapMode(params.wrap_r)); + fsampler_.setWrapModeS(ToFilamentWrapMode(params.wrap_s)); + fsampler_.setWrapModeT(ToFilamentWrapMode(params.wrap_t)); + + filament::Texture::Builder builder; + builder.width(image_data->GetSize().x); + builder.height(image_data->GetSize().y); + builder.format(ToFilamentTextureInternalFormat(image_data->GetFormat())); + builder.sampler(filament::Texture::Sampler::SAMPLER_2D); + if (target_ == TextureTarget::CubeMap) { + builder.sampler(filament::Texture::Sampler::SAMPLER_CUBEMAP); + } + + ftexture_ = MakeFilamentResource(builder.build(*fengine_), fengine_); + if (image_data->GetNumBytes() > 0) { + Update(image_data); + } + if (params.generate_mipmaps) { + ftexture_->generateMipmaps(*fengine_); + } + + dimensions_ = image_data->GetSize(); + NotifyReady(); +} + +void FilamentTexture::Update(const std::shared_ptr& image_data) { + CHECK(image_data); + CHECK_GT(image_data->GetNumBytes(), 0); + CHECK(ftexture_ != nullptr); + if (target_ == TextureTarget::CubeMap) { + const size_t face_size = image_data->GetNumBytes() / 6; + ftexture_->setImage(*fengine_, 0, CreatePixelBuffer(image_data), + filament::Texture::FaceOffsets(face_size)); + } else { + ftexture_->setImage(*fengine_, 0, CreatePixelBuffer(image_data)); + } +} + +} // namespace redux diff --git a/redux/redux/engines/render/filament/filament_texture.h b/redux/redux/engines/render/filament/filament_texture.h new file mode 100644 index 0000000..12ddd75 --- /dev/null +++ b/redux/redux/engines/render/filament/filament_texture.h @@ -0,0 +1,77 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_TEXTURE_H_ +#define REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_TEXTURE_H_ + +#include +#include + +#include "filament/Texture.h" +#include "filament/TextureSampler.h" +#include "redux/engines/render/filament/filament_utils.h" +#include "redux/engines/render/texture.h" +#include "redux/engines/render/texture_factory.h" +#include "redux/modules/base/registry.h" +#include "redux/modules/graphics/image_data.h" + +namespace redux { + +// Manages a filament Texture. +class FilamentTexture : public Texture, public Readiable { + public: + FilamentTexture(Registry* registry, std::string_view name); + + // Returns the name of the texture. + const std::string& GetName() const; + + // Returns the target type of the texture. + TextureTarget GetTarget() const; + + // Gets the dimensions of the underlying image. + vec2i GetDimensions() const; + + // Returns the underlying filament texture. + filament::Texture* GetFilamentTexture() { return ftexture_.get(); } + + // Returns the underlying filament sampler. + filament::TextureSampler GetFilamentSampler() { return fsampler_; } + + // Creates the actual underlying filament texture and sampler using the + // `image_data` and `params`. + void Build(ImageData image_data, const TextureParams& params); + + // Updates the entire contents of the texture. Image data is sent as-is. + // Returns false if the dimensions don't match or if the texture isn't + // internal and 2D. + bool Update(ImageData image); + + void Build(const std::shared_ptr& image_data, + const TextureParams& params); + void Update(const std::shared_ptr& image_data); + + private: + std::string name_; + vec2i dimensions_; + TextureTarget target_ = TextureTarget::Normal2D; + filament::Engine* fengine_ = nullptr; + filament::TextureSampler fsampler_; + FilamentResourcePtr ftexture_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_TEXTURE_H_ diff --git a/redux/redux/engines/render/filament/filament_utils.h b/redux/redux/engines/render/filament/filament_utils.h new file mode 100644 index 0000000..7c88e39 --- /dev/null +++ b/redux/redux/engines/render/filament/filament_utils.h @@ -0,0 +1,91 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_UTILS_H_ +#define REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_UTILS_H_ + +#include +#include +#include + +#include "filament/Box.h" +#include "filament/Engine.h" +#include "math/mat4.h" +#include "redux/modules/math/bounds.h" +#include "redux/modules/math/matrix.h" + +namespace redux { + +template +using FilamentResourcePtr = std::unique_ptr>; + +template +FilamentResourcePtr MakeFilamentResource(T* ptr, filament::Engine* engine) { + if constexpr (std::is_same_v) { + return {ptr, [=](T* camera) { + engine->destroyCameraComponent(camera->getEntity()); + }}; + } else { + return {ptr, [=](T* obj) { engine->destroy(obj); }}; + } +} + +inline filament::math::mat4f ToFilament(const mat4& src) { + return filament::math::mat4f{ + filament::math::float4{src(0, 0), src(1, 0), src(2, 0), src(3, 0)}, + filament::math::float4{src(0, 1), src(1, 1), src(2, 1), src(3, 1)}, + filament::math::float4{src(0, 2), src(1, 2), src(2, 2), src(3, 2)}, + filament::math::float4{src(0, 3), src(1, 3), src(2, 3), src(3, 3)}}; +} + +inline filament::Box ToFilament(const Box& src) { + filament::Box out; + const filament::math::float3 min{src.min.x, src.min.y, src.min.z}; + const filament::math::float3 max{src.max.x, src.max.y, src.max.z}; + out.set(min, max); + return out; +} + +class Readiable { + public: + using OnReadyFn = std::function; + + bool IsReady() const { return ready_; } + + void OnReady(const OnReadyFn& cb) { + if (ready_) { + cb(); + } else { + callbacks_.push_back(cb); + } + } + + protected: + void NotifyReady() { + ready_ = true; + for (auto& cb : callbacks_) { + cb(); + } + callbacks_.clear(); + } + + bool ready_ = false; + std::vector callbacks_; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_FILAMENT_FILAMENT_UTILS_H_ diff --git a/redux/redux/engines/render/filament/mesh_factory.cc b/redux/redux/engines/render/filament/mesh_factory.cc new file mode 100644 index 0000000..d421673 --- /dev/null +++ b/redux/redux/engines/render/filament/mesh_factory.cc @@ -0,0 +1,58 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/render/mesh_factory.h" + +#include + +#include "redux/engines/render/filament/filament_mesh.h" +#include "redux/engines/render/filament/filament_render_engine.h" + +namespace redux { + +MeshFactory::MeshFactory(Registry* registry) : registry_(registry) {} + +MeshPtr MeshFactory::CreateMesh(MeshData mesh_data) { + auto ptr = std::make_shared(std::move(mesh_data)); + auto impl = std::make_shared(registry_, ptr); + return std::static_pointer_cast(impl); +} + +MeshPtr MeshFactory::CreateMesh(HashValue name, MeshData mesh_data) { + MeshPtr mesh = CreateMesh(std::move(mesh_data)); + CacheMesh(name, mesh); + return mesh; +} + +void MeshFactory::CacheMesh(HashValue name, const MeshPtr& mesh) { + meshes_.Register(name, mesh); +} + +MeshPtr MeshFactory::GetMesh(HashValue name) const { + return meshes_.Find(name); +} + +void MeshFactory::ReleaseMesh(HashValue name) { meshes_.Release(name); } + +MeshPtr MeshFactory::EmptyMesh() { + if (!empty_) { + auto mesh = std::make_shared(registry_, nullptr); + empty_ = std::static_pointer_cast(mesh); + } + return empty_; +} + +} // namespace redux diff --git a/redux/redux/engines/render/filament/render_target_factory.cc b/redux/redux/engines/render/filament/render_target_factory.cc new file mode 100644 index 0000000..0bd1a6a --- /dev/null +++ b/redux/redux/engines/render/filament/render_target_factory.cc @@ -0,0 +1,42 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/render/render_target_factory.h" + +#include "redux/engines/render/filament/filament_render_target.h" + +namespace redux { + +RenderTargetFactory::RenderTargetFactory(Registry* registry) + : registry_(registry) {} + +RenderTargetPtr RenderTargetFactory::GetRenderTarget(HashValue name) const { + return render_targets_.Find(name); +} + +void RenderTargetFactory::ReleaseRenderTarget(HashValue name) { + render_targets_.Release(name); +} + +RenderTargetPtr RenderTargetFactory::CreateRenderTarget( + HashValue name, const RenderTargetParams& params) { + auto impl = std::make_shared(registry_, params); + auto target = std::static_pointer_cast(impl); + render_targets_.Register(name, target); + return target; +} + +} // namespace redux diff --git a/redux/redux/engines/render/filament/shader_factory.cc b/redux/redux/engines/render/filament/shader_factory.cc new file mode 100644 index 0000000..4467c2a --- /dev/null +++ b/redux/redux/engines/render/filament/shader_factory.cc @@ -0,0 +1,61 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/render/shader_factory.h" + +#include + +#include "redux/data/asset_defs/shader_asset_def_generated.h" +#include "redux/engines/render/filament/filament_render_engine.h" +#include "redux/engines/render/filament/filament_shader.h" +#include "redux/engines/render/shader.h" +#include "redux/modules/base/asset_loader.h" + +namespace redux { + +class ShaderFactory::ShaderAsset { + public: + ShaderPtr shader; +}; + +ShaderFactory::ShaderFactory(Registry* registry) : registry_(registry) {} + +ShaderPtr ShaderFactory::CreateShader(std::string_view shading_model) { + if (shading_model.empty()) { + auto shader = std::make_shared(registry_, nullptr); + return std::static_pointer_cast(shader); + } + + const std::string uri = std::string(shading_model) + ".rxshader"; + const HashValue key = Hash(uri); + + auto shader_asset = assets_.Find(key); + if (shader_asset == nullptr) { + auto asset_loader = registry_->Get(); + auto asset = asset_loader->LoadNow(uri); + CHECK(asset.ok()); + + const auto* def = flatbuffers::GetRoot(asset->GetBytes()); + auto shader = std::make_shared(registry_, def); + + shader_asset = std::make_shared(); + shader_asset->shader = std::static_pointer_cast(shader); + assets_.Register(key, shader_asset); + } + return shader_asset->shader; +} + +} // namespace redux diff --git a/redux/redux/engines/render/filament/texture_factory.cc b/redux/redux/engines/render/filament/texture_factory.cc new file mode 100644 index 0000000..1adb97f --- /dev/null +++ b/redux/redux/engines/render/filament/texture_factory.cc @@ -0,0 +1,171 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/render/texture_factory.h" + +#include + +#include "redux/engines/render/filament/filament_render_engine.h" +#include "redux/engines/render/filament/filament_texture.h" +#include "redux/modules/base/asset_loader.h" +#include "redux/modules/codecs/decode_image.h" +#include "redux/modules/graphics/enums.h" + +namespace redux { + +TextureFactory::TextureFactory(Registry* registry) : registry_(registry) {} + +TexturePtr TextureFactory::GetTexture(HashValue name) const { + return textures_.Find(name); +} + +void TextureFactory::CacheTexture(HashValue name, const TexturePtr& texture) { + textures_.Register(name, texture); +} + +void TextureFactory::ReleaseTexture(HashValue name) { textures_.Release(name); } + +TexturePtr TextureFactory::CreateTexture(ImageData image, + const TextureParams& params) { + auto texture = std::make_shared(registry_, ""); + texture->Build(std::move(image), params); + return std::static_pointer_cast(texture); +} + +TexturePtr TextureFactory::CreateTexture(HashValue name, ImageData image, + const TextureParams& params) { + TexturePtr texture = CreateTexture(std::move(image), params); + CacheTexture(name, texture); + return texture; +} + +TexturePtr TextureFactory::CreateTexture(const vec2i& size, ImageFormat format, + const TextureParams& params) { + DataContainer data(nullptr, 0); + ImageData empty(format, size, std::move(data)); + return CreateTexture(std::move(empty), params); +} + +TexturePtr TextureFactory::LoadTexture(std::string_view uri, + const TextureParams& params) { + const HashValue key = Hash(uri); + return textures_.Create(key, [=]() { + auto texture = std::make_shared(registry_, uri); + + std::shared_ptr image = std::make_shared(); + auto on_load = [=](AssetLoader::StatusOrData& asset) mutable { + if (asset.ok()) { + DecodeImageOptions opts; + opts.premultiply_alpha = params.premultiply_alpha; + *image = DecodeImage(*asset, opts); + } + }; + + auto on_finalize = [=](AssetLoader::StatusOrData& asset) mutable { + texture->Build(image, params); + }; + + auto asset_loader = registry_->Get(); + asset_loader->LoadAsync(uri, on_load, on_finalize); + + return std::static_pointer_cast(texture); + }); +} + +TexturePtr TextureFactory::MissingBlackTexture() { + if (missing_black_ == nullptr) { + // clang-format off + uint8_t data[] = { + 0, 0, 0, 255, 0, 0, 0, 255, + 0, 0, 0, 255, 0, 0, 0, 255, + }; + // clang-format on + ImageData image(ImageFormat::Rgba8888, vec2i(2, 2), + DataContainer::WrapData(data, sizeof(data))); + TextureParams params; + params.target = TextureTarget::Normal2D; + missing_black_ = CreateTexture(std::move(image), params); + } + return missing_black_; +} + +TexturePtr TextureFactory::MissingWhiteTexture() { + if (missing_white_ == nullptr) { + // clang-format off + uint8_t data[] = { + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + }; + // clang-format on + ImageData image(ImageFormat::Rgba8888, vec2i(2, 2), + DataContainer::WrapData(data, sizeof(data))); + TextureParams params; + params.target = TextureTarget::Normal2D; + missing_white_ = CreateTexture(std::move(image), params); + } + return missing_white_; +} + +TexturePtr TextureFactory::MissingNormalTexture() { + if (missing_normal_ == nullptr) { + // clang-format off + uint8_t data[] = { + 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, + }; + // clang-format on + ImageData image(ImageFormat::Rgb888, vec2i(2, 2), + DataContainer::WrapData(data, sizeof(data))); + TextureParams params; + params.target = TextureTarget::Normal2D; + missing_normal_ = CreateTexture(std::move(image), params); + } + return missing_normal_; +} + +TexturePtr TextureFactory::DefaultEnvReflectionTexture() { + if (default_env_reflection_ == nullptr) { + // clang-format off + uint8_t data[] = { + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + }; + // clang-format on + ImageData image(ImageFormat::Rgba8888, vec2i(2, 2), + DataContainer::WrapData(data, sizeof(data))); + TextureParams params; + params.target = TextureTarget::CubeMap; + default_env_reflection_ = CreateTexture(std::move(image), params); + } + return default_env_reflection_; +} + +} // namespace redux diff --git a/redux/redux/engines/render/filament/thunks.cc b/redux/redux/engines/render/filament/thunks.cc new file mode 100644 index 0000000..626e10c --- /dev/null +++ b/redux/redux/engines/render/filament/thunks.cc @@ -0,0 +1,95 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/render/filament/filament_indirect_light.h" +#include "redux/engines/render/filament/filament_light.h" +#include "redux/engines/render/filament/filament_mesh.h" +#include "redux/engines/render/filament/filament_render_engine.h" +#include "redux/engines/render/filament/filament_render_layer.h" +#include "redux/engines/render/filament/filament_render_scene.h" +#include "redux/engines/render/filament/filament_render_target.h" +#include "redux/engines/render/filament/filament_renderable.h" +#include "redux/engines/render/filament/filament_shader.h" +#include "redux/engines/render/filament/filament_texture.h" + +namespace redux { + +inline FilamentIndirectLight* Upcast(IndirectLight* ptr) { + return static_cast(ptr); +} +inline const FilamentIndirectLight* Upcast(const IndirectLight* ptr) { + return static_cast(ptr); +} +inline FilamentLight* Upcast(Light* ptr) { + return static_cast(ptr); +} +inline const FilamentLight* Upcast(const Light* ptr) { + return static_cast(ptr); +} +inline FilamentMesh* Upcast(Mesh* ptr) { + return static_cast(ptr); +} +inline const FilamentMesh* Upcast(const Mesh* ptr) { + return static_cast(ptr); +} +inline FilamentRenderEngine* Upcast(RenderEngine* ptr) { + return static_cast(ptr); +} +inline const FilamentRenderEngine* Upcast(const RenderEngine* ptr) { + return static_cast(ptr); +} +inline FilamentRenderLayer* Upcast(RenderLayer* ptr) { + return static_cast(ptr); +} +inline const FilamentRenderLayer* Upcast(const RenderLayer* ptr) { + return static_cast(ptr); +} +inline FilamentRenderScene* Upcast(RenderScene* ptr) { + return static_cast(ptr); +} +inline const FilamentRenderScene* Upcast(const RenderScene* ptr) { + return static_cast(ptr); +} +inline FilamentRenderTarget* Upcast(RenderTarget* ptr) { + return static_cast(ptr); +} +inline const FilamentRenderTarget* Upcast(const RenderTarget* ptr) { + return static_cast(ptr); +} +inline FilamentRenderable* Upcast(Renderable* ptr) { + return static_cast(ptr); +} +inline const FilamentRenderable* Upcast(const Renderable* ptr) { + return static_cast(ptr); +} +inline FilamentTexture* Upcast(Texture* ptr) { + return static_cast(ptr); +} +inline const FilamentTexture* Upcast(const Texture* ptr) { + return static_cast(ptr); +} + +} // namespace redux + +#include "redux/engines/render/thunks/indirect_light.h" +#include "redux/engines/render/thunks/light.h" +#include "redux/engines/render/thunks/mesh.h" +#include "redux/engines/render/thunks/render_engine.h" +#include "redux/engines/render/thunks/render_layer.h" +#include "redux/engines/render/thunks/render_scene.h" +#include "redux/engines/render/thunks/render_target.h" +#include "redux/engines/render/thunks/renderable.h" +#include "redux/engines/render/thunks/texture.h" diff --git a/redux/redux/engines/render/indirect_light.h b/redux/redux/engines/render/indirect_light.h new file mode 100644 index 0000000..16dadd9 --- /dev/null +++ b/redux/redux/engines/render/indirect_light.h @@ -0,0 +1,58 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_INDIRECT_LIGHT_H_ +#define REDUX_ENGINES_RENDER_INDIRECT_LIGHT_H_ + +#include + +#include "redux/engines/render/texture.h" +#include "redux/modules/graphics/color.h" +#include "redux/modules/graphics/texture_usage.h" +#include "redux/modules/math/matrix.h" +#include "redux/modules/var/var.h" + +namespace redux { + +// Simulates environmental light using cube maps. +class IndirectLight { + public: + virtual ~IndirectLight() = default; + + IndirectLight(const IndirectLight&) = delete; + IndirectLight& operator=(const IndirectLight&) = delete; + + // Hides/disables the light from the scene. + void Disable(); + + // Shows/enables the light from the scene. + void Enable(); + + // Returns true if the light is enabled in the scene. + bool IsEnabled() const; + + // Sets the transform of the light. + void SetTransform(const mat4& transform); + + protected: + explicit IndirectLight() = default; +}; + +using IndirectLightPtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_INDIRECT_LIGHT_H_ diff --git a/redux/redux/engines/render/light.h b/redux/redux/engines/render/light.h new file mode 100644 index 0000000..c5ee059 --- /dev/null +++ b/redux/redux/engines/render/light.h @@ -0,0 +1,93 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_LIGHT_H_ +#define REDUX_ENGINES_RENDER_LIGHT_H_ + +#include + +#include "redux/engines/render/texture.h" +#include "redux/modules/graphics/color.h" +#include "redux/modules/graphics/texture_usage.h" +#include "redux/modules/math/matrix.h" +#include "redux/modules/var/var.h" + +namespace redux { + +// Simulates light rays within a scene. Without lights, a physically-rendered +// scene will be black. +// +// A light can belong to multiple scenes. +class Light { + public: + enum Type { + // Simulates light rays that are travelling in parallel from the same + // direction uniformly across a scene. + Directional, + + // Simulates light rays eminating from a single point in the scene. + Point, + + // Simulates light rays as those coming from a spot light (i.e. a cone + // shape) in the scene. + Spot, + }; + + virtual ~Light() = default; + + Light(const Light&) = delete; + Light& operator=(const Light&) = delete; + + // Hides/disables the light from the scene. + void Disable(); + + // Shows/enables the light from the scene. + void Enable(); + + // Returns true if the light is enabled in the scene. + bool IsEnabled() const; + + // Sets the transform of the light. + void SetTransform(const mat4& transform); + + // Sets the color of the light. + void SetColor(const Color4f& color); + + // Sets the intensity of the light. For directional lights, it specifies the + // illuminance in lux. For point and spot lights, it specifies the luminous + // power in lumen. + void SetIntensity(float intensity); + + // Sets the spot light cone angles. The inner angle defines the light's + // falloff attenuation and the outer angle defines the light's influence. + // `inner` should be between 0 and pi/2, and `outer` should be beteen `inner` + // and pi/2. + void SetSpotLightConeAngles(float inner, float outer); + + // Sets the distance at which the lights stops being effective. + // For point lights, the intensity diminishes with the inverse square of the + // distance to the light. + void SetFalloffDistance(float distance); + + protected: + explicit Light() = default; +}; + +using LightPtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_LIGHT_H_ diff --git a/redux/redux/engines/render/mesh.h b/redux/redux/engines/render/mesh.h new file mode 100644 index 0000000..f19b4bc --- /dev/null +++ b/redux/redux/engines/render/mesh.h @@ -0,0 +1,74 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_MESH_H_ +#define REDUX_ENGINES_RENDER_MESH_H_ + +#include +#include +#include + +#include "redux/modules/graphics/mesh_data.h" + +namespace redux { + +// Geometry (eg. vertices, indicies, primitive type) used in a draw call. +// +// A mesh can be composed of multiple submeshes, each of which can be drawn +// independently of each other. +class Mesh { + public: + virtual ~Mesh() = default; + + Mesh(const Mesh&) = delete; + Mesh& operator=(const Mesh&) = delete; + + // Information about a submesh. + struct SubmeshData { + HashValue name; + VertexFormat vertex_format; + MeshPrimitiveType primitive_type = MeshPrimitiveType::Triangles; + MeshIndexType index_type = MeshIndexType::U16; + size_t range_start = 0; + size_t range_end = 0; + Box box; + }; + + // Returns the number of vertices contained in the mesh. + size_t GetNumVertices() const; + + // Returns the number of primitives (eg. points, lines, triangles, etc.) + // contained in the mesh. + size_t GetNumPrimitives() const; + + // Gets the bounding box for the mesh. + Box GetBoundingBox() const; + + // Returns the number of submeshes in the mesh. + size_t GetNumSubmeshes() const; + + // Returns information about submesh of the mesh. + const SubmeshData& GetSubmeshData(size_t index) const; + + protected: + Mesh() = default; +}; + +using MeshPtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_MESH_H_ diff --git a/redux/redux/engines/render/mesh_factory.h b/redux/redux/engines/render/mesh_factory.h new file mode 100644 index 0000000..17b72fa --- /dev/null +++ b/redux/redux/engines/render/mesh_factory.h @@ -0,0 +1,66 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_MESH_FACTORY_H_ +#define REDUX_ENGINES_RENDER_MESH_FACTORY_H_ + +#include "redux/engines/render/mesh.h" +#include "redux/modules/base/registry.h" +#include "redux/modules/base/resource_manager.h" +#include "redux/modules/graphics/mesh_data.h" + +namespace redux { + +// Creates and manages Mesh objects. +// +// Meshes will be automatically released along with the last external reference. +class MeshFactory { + public: + explicit MeshFactory(Registry* registry); + + MeshFactory(const MeshFactory&) = delete; + MeshFactory& operator=(const MeshFactory&) = delete; + + // Returns the mesh in the cache associated with |name|, else nullptr. + MeshPtr GetMesh(HashValue name) const; + + // Attempts to add |mesh| to the cache using |name|. + void CacheMesh(HashValue name, const MeshPtr& mesh); + + // Releases the cached mesh associated with |name|. + void ReleaseMesh(HashValue name); + + // Returns an empty mesh. + MeshPtr EmptyMesh(); + + // Creates a mesh using the specified data. + MeshPtr CreateMesh(MeshData mesh_data); + + // Creates a named mesh using the specified data; automatically registered + // with the factory. + MeshPtr CreateMesh(HashValue name, MeshData mesh_data); + + private: + Registry* registry_ = nullptr; + ResourceManager meshes_; + MeshPtr empty_; +}; + +} // namespace redux + +REDUX_SETUP_TYPEID(redux::MeshFactory); + +#endif // REDUX_ENGINES_RENDER_MESH_FACTORY_H_ diff --git a/redux/redux/engines/render/render_engine.h b/redux/redux/engines/render/render_engine.h new file mode 100644 index 0000000..8a64ac4 --- /dev/null +++ b/redux/redux/engines/render/render_engine.h @@ -0,0 +1,105 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_RENDER_ENGINE_H_ +#define REDUX_ENGINES_RENDER_RENDER_ENGINE_H_ + +#include + +#include "redux/engines/render/indirect_light.h" +#include "redux/engines/render/light.h" +#include "redux/engines/render/mesh.h" +#include "redux/engines/render/render_layer.h" +#include "redux/engines/render/render_target.h" +#include "redux/engines/render/renderable.h" +#include "redux/engines/render/shader.h" +#include "redux/engines/render/texture.h" +#include "redux/modules/base/bits.h" +#include "redux/modules/base/registry.h" +#include "redux/modules/base/typeid.h" +#include "redux/modules/graphics/color.h" +#include "redux/modules/math/bounds.h" + +namespace redux { + +class MeshFactory; +class ShaderFactory; +class TextureFactory; +class RenderTargetFactory; + +// Creates and manages the various rendering-related objects (e.g. layers, +// scenes, lights, and renderables) and provides the main API for rendering +// them. +class RenderEngine { + public: + static void Create(Registry* registry); + virtual ~RenderEngine() = default; + + void OnRegistryInitialize(); + + // Creates a new RenderScene with the given name. + RenderScenePtr CreateRenderScene(HashValue name); + + // Returns the RenderScene with the given name. + RenderScenePtr GetRenderScene(HashValue name); + + // Returns a default RenderScene. + RenderScenePtr GetDefaultRenderScene(); + + // Creates a new RenderLayer with the given name. + RenderLayerPtr CreateRenderLayer(HashValue name); + + // Returns the RenderLayer with the given name. + RenderLayerPtr GetRenderLayer(HashValue name); + + // Returns a default RenderLayer. + RenderLayerPtr GetDefaultRenderLayer(); + + // Creates a new Renderable. + RenderablePtr CreateRenderable(); + + // Creates a new Light of the given type. + LightPtr CreateLight(Light::Type type); + + // Creates a new IndirectLight. + IndirectLightPtr CreateIndirectLight(const TexturePtr& reflection, + const TexturePtr& irradiance = nullptr); + + // Renders all active RenderLayers in priority order. + bool Render(); + + // Renders the specified layer (regardless of active state). + bool RenderLayer(HashValue name); + + // Waits until all rendering operations have completed. + void SyncWait(); + + // Accessors for the factories for various rendering asset types. These are + // also available in the registry. + MeshFactory* GetMeshFactory(); + ShaderFactory* GetShaderFactory(); + TextureFactory* GetTextureFactory(); + RenderTargetFactory* GetRenderTargetFactory(); + + protected: + explicit RenderEngine(Registry* registry) : registry_(registry) {} + Registry* registry_ = nullptr; +}; +} // namespace redux + +REDUX_SETUP_TYPEID(redux::RenderEngine); + +#endif // REDUX_ENGINES_RENDER_RENDER_ENGINE_H_ diff --git a/redux/redux/engines/render/render_layer.h b/redux/redux/engines/render/render_layer.h new file mode 100644 index 0000000..1c18bc9 --- /dev/null +++ b/redux/redux/engines/render/render_layer.h @@ -0,0 +1,105 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_RENDER_LAYER_H_ +#define REDUX_ENGINES_RENDER_RENDER_LAYER_H_ + +#include +#include + +#include "redux/engines/render/light.h" +#include "redux/engines/render/render_scene.h" +#include "redux/engines/render/render_target.h" +#include "redux/engines/render/renderable.h" +#include "redux/modules/graphics/color.h" +#include "redux/modules/math/bounds.h" + +namespace redux { + +// Layers provide a high-level way to control the order in which rendering +// occurs. For example, one may want to render a UI on top of a 3D scene by +// having two layers. +class RenderLayer { + public: + virtual ~RenderLayer() = default; + + RenderLayer(const RenderLayer&) = delete; + RenderLayer& operator=(const RenderLayer&) = delete; + + // Adds the layer to the list of layers to be rendered, effectively enabling + // it. + void Enable(); + + // Removes the layer from the list of layers to be rendered, effectively + // disabling it. + void Disable(); + + // Returns true if the layer will be rendered. + bool IsEnabled() const; + + // Sets the priority at which the layer will be rendered. Higher priority + // layers will be rendered first. Two layers with the same priority will be + // rendered in arbitrary order. + void SetPriority(int priority); + + // Returns the render priority of the layer. + int GetPriority() const; + + // Associates a scene (which contains lights and renderables) with this layer. + // A layer can only have a single scene at a time. + void SetScene(const RenderScenePtr& scene); + + // Sets the render target on which to perform the drawing/rendering. + void SetRenderTarget(RenderTargetPtr target); + + // Sets the clip plane distances for rendering. + void SetClipPlaneDistances(float near, float far); + + // Sets the viewport (i.e. area) on the render target in which the rendering + // will be performed. The bounds should be specified in the range (0,0) + // (bottom-left) to (1,1) (top-right). + void SetViewport(const Bounds2f& viewport); + + // Returns the viewport (i.e. area) on the render target surface in which the + // rendering will be performed. + Bounds2i GetAbsoluteViewport() const; + + // Sets the view matrix that will be used for rendering. This is effectively + // the transform of the camera from which the scene will be rendered. + void SetViewMatrix(const mat4& view_matrix); + + // Sets the projection matrix that will be used for rendering. This is + // effectively the lens of the camera from which the scene will be rendered. + void SetProjectionMatrix(const mat4& projection_matrix); + + // Enables anti-aliasing when rendering the layer. + void EnableAntiAliasing(); + + // Disables anti-aliasing when rendering the layer. + void DisableAntiAliasing(); + + // Disables post-processing (like tone mapping) when rendering the layer. + void DisablePostProcessing(); + + protected: + explicit RenderLayer() = default; +}; + +using RenderLayerPtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_RENDER_LAYER_H_ diff --git a/redux/redux/engines/render/render_scene.h b/redux/redux/engines/render/render_scene.h new file mode 100644 index 0000000..a453fea --- /dev/null +++ b/redux/redux/engines/render/render_scene.h @@ -0,0 +1,65 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_RENDER_SCENE_H_ +#define REDUX_ENGINES_RENDER_RENDER_SCENE_H_ + +#include + +#include "redux/engines/render/indirect_light.h" +#include "redux/engines/render/light.h" +#include "redux/engines/render/render_target.h" +#include "redux/engines/render/renderable.h" +#include "redux/modules/graphics/color.h" +#include "redux/modules/math/bounds.h" + +namespace redux { + +// A collection of lights and renderables that will be rendered together. +class RenderScene { + public: + virtual ~RenderScene() = default; + + RenderScene(const RenderScene&) = delete; + RenderScene& operator=(const RenderScene&) = delete; + + // Adds a renderable to the scene. + void Add(const Renderable& renderable); + + // Removes a renderable from the scene. + void Remove(const Renderable& renderable); + + // Adds a light to the scene. + void Add(const Light& light); + + // Removes a light from the scene. + void Remove(const Light& light); + + // Adds an indirect light to the scene. + void Add(const IndirectLight& light); + + // Removes an indirect light from the scene. + void Remove(const IndirectLight& light); + + protected: + explicit RenderScene() = default; +}; + +using RenderScenePtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_RENDER_SCENE_H_ diff --git a/redux/redux/engines/render/render_target.h b/redux/redux/engines/render/render_target.h new file mode 100644 index 0000000..848e9de --- /dev/null +++ b/redux/redux/engines/render/render_target.h @@ -0,0 +1,49 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_RENDER_TARGET_H_ +#define REDUX_ENGINES_RENDER_RENDER_TARGET_H_ + +#include + +#include "redux/modules/graphics/enums.h" +#include "redux/modules/graphics/image_data.h" + +namespace redux { + +// The "pixels" onto which rendering operations take place. +class RenderTarget { + public: + virtual ~RenderTarget() = default; + + RenderTarget(const RenderTarget&) = delete; + RenderTarget& operator=(const RenderTarget&) = delete; + + // Returns the dimensions of the render target. + vec2i GetDimensions() const; + + // Returns the contents of the render buffer. + ImageData GetFrameBufferData(); + + protected: + RenderTarget() = default; +}; + +using RenderTargetPtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_RENDER_TARGET_H_ diff --git a/redux/redux/engines/render/render_target_factory.h b/redux/redux/engines/render/render_target_factory.h new file mode 100644 index 0000000..5c801ee --- /dev/null +++ b/redux/redux/engines/render/render_target_factory.h @@ -0,0 +1,105 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_RENDER_TARGET_FACTORY_H_ +#define REDUX_ENGINES_RENDER_RENDER_TARGET_FACTORY_H_ + +#include "redux/engines/render/render_target.h" +#include "redux/modules/base/registry.h" +#include "redux/modules/base/resource_manager.h" + +namespace redux { + +enum class RenderTargetFormat { + None, + Red8, + Rgb8, + Rgba8, +}; + +enum class RenderTargetDepthStencilFormat { + None, + Depth16, + Depth24, + Depth32f, + Depth24Stencil8, + Depth32fStencil8, + Stencil8, +}; + +struct RenderTargetParams { + // The width and height of the render target. + vec2i dimensions = {0, 0}; + + // The format for the render target. + RenderTargetFormat texture_format = RenderTargetFormat::None; + + // The depth stencil format for an accompanying depth stencil buffer. + // |DepthStencilFormat_None| means no depth stencil buffer will be + // generated. + RenderTargetDepthStencilFormat depth_stencil_format = + RenderTargetDepthStencilFormat::None; + + // |num_mip_levels| controls the number of mips the texture will be created + // with. A value of 0 will lead to an automatic generation of mips. + unsigned int num_mip_levels = 1; + + // The texture minifying function is used whenever the pixel being textured + // maps to an area greater than one texture element. There are six defined + // minifying functions. Two of them use the nearest one or nearest four + // texture elements to compute the texture value. The other four use + // mipmaps. + TextureFilter min_filter = TextureFilter::Nearest; + + // The texture magnification function is used when the pixel being textured + // maps to an area less than or equal to one texture element. + TextureFilter mag_filter = TextureFilter::Nearest; + + // Wrap parameter for texture coordinate s. + TextureWrap wrap_s = TextureWrap::ClampToEdge; + + // Wrap parameter for texture coordinate t. + TextureWrap wrap_t = TextureWrap::ClampToEdge; +}; + +// Creates and manages RenderTarget objects. +class RenderTargetFactory { + public: + explicit RenderTargetFactory(Registry* registry); + + RenderTargetFactory(const RenderTargetFactory&) = delete; + RenderTargetFactory& operator=(const RenderTargetFactory&) = delete; + + // Returns the RenderTarget in the cache associated with |name|, else nullptr. + RenderTargetPtr GetRenderTarget(HashValue name) const; + + // Releases the cached RenderTarget associated with |name|. + void ReleaseRenderTarget(HashValue name); + + // Creates a RenderTarget using the specified data. + RenderTargetPtr CreateRenderTarget(HashValue name, + const RenderTargetParams& params); + + private: + Registry* registry_ = nullptr; + ResourceManager render_targets_; +}; + +} // namespace redux + +REDUX_SETUP_TYPEID(redux::RenderTargetFactory); + +#endif // REDUX_ENGINES_RENDER_RENDER_TARGET_FACTORY_H_ diff --git a/redux/redux/engines/render/renderable.h b/redux/redux/engines/render/renderable.h new file mode 100644 index 0000000..4d79593 --- /dev/null +++ b/redux/redux/engines/render/renderable.h @@ -0,0 +1,109 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_RENDERABLE_H_ +#define REDUX_ENGINES_RENDER_RENDERABLE_H_ + +#include +#include + +#include "redux/engines/render/mesh.h" +#include "redux/engines/render/shader.h" +#include "redux/engines/render/texture.h" +#include "redux/modules/graphics/texture_usage.h" +#include "redux/modules/math/matrix.h" + +namespace redux { + +// Represents an "object" that will be drawn in a scene. +// +// Renderables consist of two main concepts: the shape and the surface. The +// shape of a renderable is defined by a Mesh and its surface is defined by a +// Shader. A Shader will use information from the Mesh as well as any Textures +// or Properties that are set on the renderable to "color-in" the surface of +// renderable as described by its Mesh shape. +// +// A renderable may have multiple parts as defined by the Mesh object. Each +// part can then be individually assigned a Shader and given its own set of +// properties. +// +// Renderables can belong to multiple scenes. See RenderScene and RenderLayer +// for more information. +class Renderable { + public: + virtual ~Renderable() = default; + + Renderable(const Renderable&) = delete; + Renderable& operator=(const Renderable&) = delete; + + // Prepares the renderable for rendering. The transform is used to place the + // renderable in all scenes to which it belongs. + void PrepareToRender(const mat4& transform); + + // Enables the renderable (or a part of the renderable) to be rendered. + void Show(std::optional part = std::nullopt); + + // Disables the renderable (or a part of the renderable) to be rendered. + void Hide(std::optional part = std::nullopt); + + // Returns true if the renderable (or a part of the renderable) is hidden. + bool IsHidden(std::optional part = std::nullopt) const; + + // Sets the mesh (i.e. shape) of the renderable. + void SetMesh(MeshPtr mesh); + + // Returns the mesh for the renderable. + MeshPtr GetMesh() const; + + // Enables a vertex attributes. All attributes are enabled by default. + void EnableVertexAttribute(VertexUsage usage); + + // Disables a specific vertex attribute which may affect how the renderable is + // drawn. For example, disabling a color vertex attribute will prevent the + // renderable's mesh color from being used when rendering. + void DisableVertexAttribute(VertexUsage usage); + bool IsVertexAttributeEnabled(VertexUsage usage) const; + + // Sets the shader that will be used to render the surface of the renderable + // for a specific part. + void SetShader(ShaderPtr shader, + std::optional part = std::nullopt); + + // Assigns a Texture for a given usage on the renderable. Textures are applied + // to the entirety of the renderable and not to individual parts. + void SetTexture(TextureUsage usage, const TexturePtr& texture); + + // Returns the Texture that was set for a given usage on the renderable. + TexturePtr GetTexture(TextureUsage usage) const; + + // Assigns a specific value to a material property with the given `name` which + // can be used by the shader when drawing the renderable. The shader will + // interpret the property `data` based on the material property `type`. + // Properties can be assigned to individual `part`s or the entire renderable. + void SetProperty(HashValue name, MaterialPropertyType type, + absl::Span data); + void SetProperty(HashValue name, HashValue part, MaterialPropertyType type, + absl::Span data); + + protected: + Renderable() = default; +}; + +using RenderablePtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_RENDERABLE_H_ diff --git a/redux/redux/engines/render/shader.h b/redux/redux/engines/render/shader.h new file mode 100644 index 0000000..1a48ca5 --- /dev/null +++ b/redux/redux/engines/render/shader.h @@ -0,0 +1,44 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_SHADER_H_ +#define REDUX_ENGINES_RENDER_SHADER_H_ + +#include + +namespace redux { + +// Shaders are used to "color in" the surface of a renderable in a particular +// way. For example, some shaders can be "flat" which will color the entire +// surface of a renderable with a single color. Other shaders can use +// "physically-based rendering" which uses complex algorithms to simulate the +// physical properties of light bouncing of the surface of a renderable. +class Shader { + public: + virtual ~Shader() = default; + + Shader(const Shader&) = delete; + Shader& operator=(const Shader&) = delete; + + protected: + Shader() = default; +}; + +using ShaderPtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_SHADER_H_ diff --git a/redux/redux/engines/render/shader_factory.h b/redux/redux/engines/render/shader_factory.h new file mode 100644 index 0000000..cce7418 --- /dev/null +++ b/redux/redux/engines/render/shader_factory.h @@ -0,0 +1,51 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_SHADER_FACTORY_H_ +#define REDUX_ENGINES_RENDER_SHADER_FACTORY_H_ + +#include "redux/engines/render/shader.h" +#include "redux/modules/base/registry.h" +#include "redux/modules/base/resource_manager.h" + +namespace redux { + +// Creates Shader objects. +class ShaderFactory { + public: + explicit ShaderFactory(Registry* registry); + + ShaderFactory(const ShaderFactory&) = delete; + ShaderFactory& operator=(const ShaderFactory&) = delete; + + // Creates the shader asset associated with the shading model. If the shader + // was previously loaded, then this will return the cached shader (since they + // are shareable). The shader will only be unloaded once all references to it + // are released. + ShaderPtr CreateShader(std::string_view shading_model); + + private: + class ShaderAsset; + + Registry* registry_ = nullptr; + ResourceManager assets_; +}; + +} // namespace redux + +REDUX_SETUP_TYPEID(redux::ShaderFactory); + +#endif // REDUX_ENGINES_RENDER_SHADER_FACTORY_H_ diff --git a/redux/redux/engines/render/texture.h b/redux/redux/engines/render/texture.h new file mode 100644 index 0000000..ccf9f86 --- /dev/null +++ b/redux/redux/engines/render/texture.h @@ -0,0 +1,66 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_TEXTURE_H_ +#define REDUX_ENGINES_RENDER_TEXTURE_H_ + +#include +#include +#include +#include + +#include "redux/modules/graphics/enums.h" +#include "redux/modules/graphics/image_data.h" +#include "redux/modules/math/vector.h" + +namespace redux { + +// Textures contain pixel data that can be used for a variety of purposes by a +// Shader when drawing a Renderable. +// +// The simplest example is where the entire contents of the texture are +// rendered "as-is" by a shader onto the screen. More complex situations are +// used for things like bump mapping, environmental lighting, etc. +class Texture { + public: + virtual ~Texture() = default; + + Texture(const Texture&) = delete; + Texture& operator=(const Texture&) = delete; + + // Returns the name of the texture. + const std::string& GetName() const; + + // Returns the target type of the texture. + TextureTarget GetTarget() const; + + // Gets the dimensions of the underlying image. + vec2i GetDimensions() const; + + // Updates the entire contents of the texture. Image data is sent as-is. + // Returns false if the dimensions don't match or if the texture isn't + // internal and 2D. + bool Update(ImageData image); + + protected: + Texture() = default; +}; + +using TexturePtr = std::shared_ptr; + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_TEXTURE_H_ diff --git a/redux/redux/engines/render/texture_factory.h b/redux/redux/engines/render/texture_factory.h new file mode 100644 index 0000000..dee7da6 --- /dev/null +++ b/redux/redux/engines/render/texture_factory.h @@ -0,0 +1,95 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_TEXTURE_FACTORY_H_ +#define REDUX_ENGINES_RENDER_TEXTURE_FACTORY_H_ + +#include "redux/engines/render/texture.h" +#include "redux/modules/base/registry.h" +#include "redux/modules/base/resource_manager.h" +#include "redux/modules/graphics/image_data.h" + +namespace redux { + +struct TextureParams { + TextureParams() = default; + + TextureFilter min_filter = TextureFilter::Linear; + TextureFilter mag_filter = TextureFilter::Linear; + TextureWrap wrap_s = TextureWrap::Repeat; + TextureWrap wrap_t = TextureWrap::Repeat; + TextureWrap wrap_r = TextureWrap::Repeat; + TextureTarget target = TextureTarget::Normal2D; + bool premultiply_alpha = false; + bool generate_mipmaps = false; +}; + +class TextureFactory { + public: + explicit TextureFactory(Registry* registry); + + TextureFactory(const TextureFactory&) = delete; + TextureFactory& operator=(const TextureFactory&) = delete; + + // Returns the texture in the cache associated with |name|, else nullptr. + TexturePtr GetTexture(HashValue name) const; + + // Attempts to add |texture| to the cache using |name|. + void CacheTexture(HashValue name, const TexturePtr& texture); + + // Releases the cached texture associated with |name|. + void ReleaseTexture(HashValue name); + + // Creates a texture using the specified data. + TexturePtr CreateTexture(ImageData image, const TextureParams& params); + + // Creates a named texture using the specified data; automatically registered + // with the factory. + TexturePtr CreateTexture(HashValue name, ImageData image, + const TextureParams& params); + + // Creates an "empty" texture of the specified size. The |params| must + // specify a format for the texture. + TexturePtr CreateTexture(const vec2i& size, ImageFormat format, + const TextureParams& params); + + // Loads a texture off disk with the given |filename| and uses the creation + // |params| to configure it for the GPU. The filename is also used as the + // "name" of the texture. Subsequent calls to this function with the same + // |filename| will return the original texture as long as any references to + // that texture are still alive. + TexturePtr LoadTexture(std::string_view uri, const TextureParams& params); + + // Some useful default/hard-coded textures. + TexturePtr MissingBlackTexture(); + TexturePtr MissingWhiteTexture(); + TexturePtr MissingNormalTexture(); + TexturePtr DefaultEnvReflectionTexture(); + + private: + Registry* registry_ = nullptr; + ResourceManager textures_; + TexturePtr missing_black_; + TexturePtr missing_white_; + TexturePtr missing_normal_; + TexturePtr default_env_reflection_; +}; + +} // namespace redux + +REDUX_SETUP_TYPEID(redux::TextureFactory); + +#endif // REDUX_ENGINES_RENDER_TEXTURE_FACTORY_H_ diff --git a/redux/redux/engines/render/thunks/BUILD b/redux/redux/engines/render/thunks/BUILD new file mode 100644 index 0000000..f8aa5d5 --- /dev/null +++ b/redux/redux/engines/render/thunks/BUILD @@ -0,0 +1,25 @@ +# RenderEngine thunks implementations. + +licenses(["notice"]) + +package( + default_visibility = ["//redux:visibility"], +) + +cc_library( + name = "thunks", + textual_hdrs = [ + "indirect_light.h", + "light.h", + "mesh.h", + "render_engine.h", + "render_layer.h", + "render_scene.h", + "render_target.h", + "renderable.h", + "texture.h", + ], + deps = [ + "//redux/engines/render", + ], +) diff --git a/redux/redux/engines/render/thunks/README.md b/redux/redux/engines/render/thunks/README.md new file mode 100644 index 0000000..5217b8b --- /dev/null +++ b/redux/redux/engines/render/thunks/README.md @@ -0,0 +1,27 @@ +To support the option of multiple backends for a given interface, we do a bit of +manual "devirtualization" using thunks. The idea is basically: + +1) Declare the class API (ie. public function declarations) in the main package. +This class should not have any virtual functions _except_ it's destructor. It +should have a protected default constructor. + +2) Define an implementation class that inherits from the API class. The derived +class should declare the _same_ functions as the API class, but remember these +are not virtuals or overrides. (In other words, these functions will shadow the +base class functions.) + +3) Implement an Upcast function that takes a pointer to the base class and +returns the derived class. + +4) Include the various thunks header files. + +5) Provide a factory function that will create an instance of the derived class +and return it as a pointer to the base class. + +The way it works is that, for users of the module, they only get a pointer to +the base API class. When calling any function, it will call the "thunk" +implementation for that class which will "upcast" the object to the derived type +and invoke the function with the same name/signature in the derived class. The +advantage to this (over virtual functions) is that there is no dynamic dispatch; +the "thunk" functions can be inlined and so it will be like calling the derived +class directly using the base class pointer. diff --git a/redux/redux/engines/render/thunks/indirect_light.h b/redux/redux/engines/render/thunks/indirect_light.h new file mode 100644 index 0000000..7b27cb5 --- /dev/null +++ b/redux/redux/engines/render/thunks/indirect_light.h @@ -0,0 +1,34 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_THUNKS_INDIRECT_LIGHT_H_ +#define REDUX_ENGINES_RENDER_THUNKS_INDIRECT_LIGHT_H_ + +#include "redux/engines/render/indirect_light.h" + +namespace redux { + +// Thunk functions to call the actual implsementation. +void IndirectLight::Disable() { Upcast(this)->Disable(); } +void IndirectLight::Enable() { Upcast(this)->Enable(); } +bool IndirectLight::IsEnabled() const { return Upcast(this)->IsEnabled(); } +void IndirectLight::SetTransform(const mat4& transform) { + Upcast(this)->SetTransform(transform); +} + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_THUNKS_INDIRECT_LIGHT_H_ diff --git a/redux/redux/engines/render/thunks/light.h b/redux/redux/engines/render/thunks/light.h new file mode 100644 index 0000000..95cb452 --- /dev/null +++ b/redux/redux/engines/render/thunks/light.h @@ -0,0 +1,44 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_THUNKS_LIGHT_H_ +#define REDUX_ENGINES_RENDER_THUNKS_LIGHT_H_ + +#include "redux/engines/render/light.h" + +namespace redux { + +// Thunk functions to call the actual implsementation. +void Light::Disable() { Upcast(this)->Disable(); } +void Light::Enable() { Upcast(this)->Enable(); } +bool Light::IsEnabled() const { return Upcast(this)->IsEnabled(); } +void Light::SetTransform(const mat4& transform) { + Upcast(this)->SetTransform(transform); +} +void Light::SetColor(const Color4f& color) { Upcast(this)->SetColor(color); } +void Light::SetIntensity(float intensity) { + Upcast(this)->SetIntensity(intensity); +} +void Light::SetFalloffDistance(float distance) { + Upcast(this)->SetFalloffDistance(distance); +} +void Light::SetSpotLightConeAngles(float inner, float outer) { + Upcast(this)->SetSpotLightConeAngles(inner, outer); +} + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_THUNKS_LIGHT_H_ diff --git a/redux/redux/engines/render/thunks/mesh.h b/redux/redux/engines/render/thunks/mesh.h new file mode 100644 index 0000000..bb0682c --- /dev/null +++ b/redux/redux/engines/render/thunks/mesh.h @@ -0,0 +1,37 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_THUNKS_MESH_H_ +#define REDUX_ENGINES_RENDER_THUNKS_MESH_H_ + +#include "redux/engines/render/renderable.h" + +namespace redux { + +// Thunk functions to call the actual implementation. +size_t Mesh::GetNumVertices() const { return Upcast(this)->GetNumVertices(); } +size_t Mesh::GetNumPrimitives() const { + return Upcast(this)->GetNumPrimitives(); +} +Box Mesh::GetBoundingBox() const { return Upcast(this)->GetBoundingBox(); } +size_t Mesh::GetNumSubmeshes() const { return Upcast(this)->GetNumSubmeshes(); } +const Mesh::SubmeshData& Mesh::GetSubmeshData(size_t index) const { + return Upcast(this)->GetSubmeshData(index); +} + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_THUNKS_MESH_H_ diff --git a/redux/redux/engines/render/thunks/render_engine.h b/redux/redux/engines/render/thunks/render_engine.h new file mode 100644 index 0000000..412a25c --- /dev/null +++ b/redux/redux/engines/render/thunks/render_engine.h @@ -0,0 +1,77 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_THUNKS_RENDER_ENGINE_H_ +#define REDUX_ENGINES_RENDER_THUNKS_RENDER_ENGINE_H_ + +#include "redux/engines/render/render_engine.h" + +namespace redux { + +// Thunk functions to call the actual implementation. + +void RenderEngine::OnRegistryInitialize() { + Upcast(this)->OnRegistryInitialize(); +} +RenderScenePtr RenderEngine::GetRenderScene(HashValue name) { + return Upcast(this)->GetRenderScene(name); +} +RenderScenePtr RenderEngine::GetDefaultRenderScene() { + return Upcast(this)->GetDefaultRenderScene(); +} +RenderLayerPtr RenderEngine::GetRenderLayer(HashValue name) { + return Upcast(this)->GetRenderLayer(name); +} +RenderLayerPtr RenderEngine::GetDefaultRenderLayer() { + return Upcast(this)->GetDefaultRenderLayer(); +} +RenderScenePtr RenderEngine::CreateRenderScene(HashValue name) { + return Upcast(this)->CreateRenderScene(name); +} +RenderLayerPtr RenderEngine::CreateRenderLayer(HashValue name) { + return Upcast(this)->CreateRenderLayer(name); +} +RenderablePtr RenderEngine::CreateRenderable() { + return Upcast(this)->CreateRenderable(); +} +LightPtr RenderEngine::CreateLight(Light::Type type) { + return Upcast(this)->CreateLight(type); +} +IndirectLightPtr RenderEngine::CreateIndirectLight( + const TexturePtr& reflection, const TexturePtr& irradiance) { + return Upcast(this)->CreateIndirectLight(reflection, irradiance); +} +bool RenderEngine::Render() { return Upcast(this)->Render(); } +bool RenderEngine::RenderLayer(HashValue name) { + return Upcast(this)->RenderLayer(name); +} +void RenderEngine::SyncWait() { Upcast(this)->SyncWait(); } +MeshFactory* RenderEngine::GetMeshFactory() { + return Upcast(this)->GetMeshFactory(); +} +ShaderFactory* RenderEngine::GetShaderFactory() { + return Upcast(this)->GetShaderFactory(); +} +TextureFactory* RenderEngine::GetTextureFactory() { + return Upcast(this)->GetTextureFactory(); +} +RenderTargetFactory* RenderEngine::GetRenderTargetFactory() { + return Upcast(this)->GetRenderTargetFactory(); +} + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_THUNKS_RENDER_ENGINE_H_ diff --git a/redux/redux/engines/render/thunks/render_layer.h b/redux/redux/engines/render/thunks/render_layer.h new file mode 100644 index 0000000..a7f1fa8 --- /dev/null +++ b/redux/redux/engines/render/thunks/render_layer.h @@ -0,0 +1,61 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_THUNKS_RENDER_LAYER_H_ +#define REDUX_ENGINES_RENDER_THUNKS_RENDER_LAYER_H_ + +#include "redux/engines/render/render_layer.h" + +namespace redux { + +// Thunk functions to call the actual implementation. +void RenderLayer::Enable() { Upcast(this)->Enable(); } +void RenderLayer::Disable() { Upcast(this)->Disable(); } +bool RenderLayer::IsEnabled() const { return Upcast(this)->IsEnabled(); } +void RenderLayer::SetPriority(int priority) { + Upcast(this)->SetPriority(priority); +} +int RenderLayer::GetPriority() const { return Upcast(this)->GetPriority(); } +void RenderLayer::EnableAntiAliasing() { Upcast(this)->EnableAntiAliasing(); } +void RenderLayer::DisableAntiAliasing() { Upcast(this)->DisableAntiAliasing(); } +void RenderLayer::DisablePostProcessing() { + Upcast(this)->DisablePostProcessing(); +} +void RenderLayer::SetScene(const RenderScenePtr& scene) { + Upcast(this)->SetScene(scene); +} +void RenderLayer::SetRenderTarget(RenderTargetPtr target) { + Upcast(this)->SetRenderTarget(target); +} +void RenderLayer::SetClipPlaneDistances(float near, float far) { + Upcast(this)->SetClipPlaneDistances(near, far); +} +void RenderLayer::SetViewport(const Bounds2f& viewport) { + Upcast(this)->SetViewport(viewport); +} +Bounds2i RenderLayer::GetAbsoluteViewport() const { + return Upcast(this)->GetAbsoluteViewport(); +} +void RenderLayer::SetViewMatrix(const mat4& view_matrix) { + Upcast(this)->SetViewMatrix(view_matrix); +} +void RenderLayer::SetProjectionMatrix(const mat4& projection_matrix) { + Upcast(this)->SetProjectionMatrix(projection_matrix); +} + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_THUNKS_RENDER_LAYER_H_ diff --git a/redux/redux/engines/render/thunks/render_scene.h b/redux/redux/engines/render/thunks/render_scene.h new file mode 100644 index 0000000..f268f3d --- /dev/null +++ b/redux/redux/engines/render/thunks/render_scene.h @@ -0,0 +1,40 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_THUNKS_RENDER_SCENE_H_ +#define REDUX_ENGINES_RENDER_THUNKS_RENDER_SCENE_H_ + +#include "redux/engines/render/render_scene.h" + +namespace redux { + +// Thunk functions to call the actual implementation. +void RenderScene::Add(const Renderable& renderable) { + Upcast(this)->Add(renderable); +} +void RenderScene::Remove(const Renderable& renderable) { + Upcast(this)->Remove(renderable); +} +void RenderScene::Add(const Light& light) { Upcast(this)->Add(light); } +void RenderScene::Remove(const Light& light) { Upcast(this)->Remove(light); } +void RenderScene::Add(const IndirectLight& light) { Upcast(this)->Add(light); } +void RenderScene::Remove(const IndirectLight& light) { + Upcast(this)->Remove(light); +} + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_THUNKS_RENDER_SCENE_H_ diff --git a/redux/redux/engines/render/thunks/render_target.h b/redux/redux/engines/render/thunks/render_target.h new file mode 100644 index 0000000..5265ca6 --- /dev/null +++ b/redux/redux/engines/render/thunks/render_target.h @@ -0,0 +1,34 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_THUNKS_RENDER_TARGET_H_ +#define REDUX_ENGINES_RENDER_THUNKS_RENDER_TARGET_H_ + +#include "redux/engines/render/render_target.h" + +namespace redux { + +// Thunk functions to call the actual implementation. +ImageData RenderTarget::GetFrameBufferData() { + return Upcast(this)->GetFrameBufferData(); +} +vec2i RenderTarget::GetDimensions() const { + return Upcast(this)->GetDimensions(); +} + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_THUNKS_RENDER_TARGET_H_ diff --git a/redux/redux/engines/render/thunks/renderable.h b/redux/redux/engines/render/thunks/renderable.h new file mode 100644 index 0000000..78c8fcb --- /dev/null +++ b/redux/redux/engines/render/thunks/renderable.h @@ -0,0 +1,71 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_THUNKS_RENDERABLE_H_ +#define REDUX_ENGINES_RENDER_THUNKS_RENDERABLE_H_ + +#include "redux/engines/render/renderable.h" + +namespace redux { + +// Thunk functions to call the actual implementation. +void Renderable::PrepareToRender(const mat4& transform) { + Upcast(this)->PrepareToRender(transform); +} +void Renderable::SetMesh(MeshPtr mesh) { + Upcast(this)->SetMesh(std::move(mesh)); +} +MeshPtr Renderable::GetMesh() const { return Upcast(this)->GetMesh(); } +void Renderable::EnableVertexAttribute(VertexUsage usage) { + Upcast(this)->EnableVertexAttribute(usage); +} +void Renderable::DisableVertexAttribute(VertexUsage usage) { + Upcast(this)->DisableVertexAttribute(usage); +} +bool Renderable::IsVertexAttributeEnabled(VertexUsage usage) const { + return Upcast(this)->IsVertexAttributeEnabled(usage); +} +void Renderable::SetShader(ShaderPtr shader, std::optional part) { + Upcast(this)->SetShader(std::move(shader), part); +} +void Renderable::Show(std::optional part) { + Upcast(this)->Show(part); +} +void Renderable::Hide(std::optional part) { + Upcast(this)->Hide(part); +} +bool Renderable::IsHidden(std::optional part) const { + return Upcast(this)->IsHidden(part); +} +void Renderable::SetTexture(TextureUsage usage, const TexturePtr& texture) { + Upcast(this)->SetTexture(usage, texture); +} +TexturePtr Renderable::GetTexture(TextureUsage usage) const { + return Upcast(this)->GetTexture(usage); +} +void Renderable::SetProperty(HashValue name, MaterialPropertyType type, + absl::Span data) { + Upcast(this)->SetProperty(name, type, data); +} +void Renderable::SetProperty(HashValue name, HashValue part, + MaterialPropertyType type, + absl::Span data) { + Upcast(this)->SetProperty(name, part, type, data); +} + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_THUNKS_RENDERABLE_H_ diff --git a/redux/redux/engines/render/thunks/texture.h b/redux/redux/engines/render/thunks/texture.h new file mode 100644 index 0000000..0a2bc98 --- /dev/null +++ b/redux/redux/engines/render/thunks/texture.h @@ -0,0 +1,34 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_RENDER_THUNKS_TEXTURE_H_ +#define REDUX_ENGINES_RENDER_THUNKS_TEXTURE_H_ + +#include "redux/engines/render/texture.h" + +namespace redux { + +// Thunk functions to call the actual implementation. +const std::string& Texture::GetName() const { return Upcast(this)->GetName(); } +TextureTarget Texture::GetTarget() const { return Upcast(this)->GetTarget(); } +vec2i Texture::GetDimensions() const { return Upcast(this)->GetDimensions(); } +bool Texture::Update(ImageData image) { + return Upcast(this)->Update(std::move(image)); +} + +} // namespace redux + +#endif // REDUX_ENGINES_RENDER_THUNKS_TEXTURE_H_ diff --git a/redux/redux/engines/script/BUILD b/redux/redux/engines/script/BUILD new file mode 100644 index 0000000..5f622bc --- /dev/null +++ b/redux/redux/engines/script/BUILD @@ -0,0 +1,53 @@ +# Scripting engine. + +licenses(["notice"]) + +package( + default_visibility = ["//redux:visibility"], +) + +cc_library( + name = "script", + hdrs = [ + "script.h", + "script_call_context.h", + "script_engine.h", + ], + deps = [ + ":call_native_function", + "@absl//absl/status", + "//redux/modules/base:hash", + "//redux/modules/base:registry", + "//redux/modules/base:typeid", + "//redux/modules/var", + "//redux/modules/var:var_convert", + "@magic_enum//:magic_enum", + ], +) + +cc_library( + name = "call_native_function", + hdrs = ["call_native_function.h"], + deps = [ + "@absl//absl/status", + "//redux/modules/base:function_traits", + "//redux/modules/base:logging", + ], +) + +cc_test( + name = "call_native_function_tests", + srcs = ["call_native_function_tests.cc"], + deps = [ + ":call_native_function", + "@gtest//:gtest_main", + "@absl//absl/types:any", + ], +) + +cc_library( + name = "function_binder", + srcs = ["function_binder.cc"], + hdrs = ["function_binder.h"], + deps = [":script"], +) diff --git a/redux/redux/engines/script/call_native_function.h b/redux/redux/engines/script/call_native_function.h new file mode 100644 index 0000000..688356d --- /dev/null +++ b/redux/redux/engines/script/call_native_function.h @@ -0,0 +1,118 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef THIRD_PARTY_REDUX_MODULES_BASE_CALL_NATIVE_FUNCTION_H_ +#define THIRD_PARTY_REDUX_MODULES_BASE_CALL_NATIVE_FUNCTION_H_ + +#include + +#include "absl/status/status.h" +#include "redux/modules/base/function_traits.h" +#include "redux/modules/base/logging.h" + +namespace redux { + +// "Calls" a function of type Fn using the Context object. The Context object is +// required to provide two functions: +// +// absl::StatusCode ArgFromCpp(size_t index, T& arg_out); +// This function should set `arg_out` to the value that will be passed as the +// n-th argument (as specified by `index`) to the function Fn. An error code +// can be returned (if, for example, the argument index is out of range of the +// argument is not of type T.) +// +// absl::StatusCode ReturnFromCpp(T&&); +// This function should store the given value into the context. This value is +// the result of invoking the function Fn. An error code can be returned if +// the context is unable to handle the return value. +// +// This function will return the error code returned by any of the above calls +// if an error is encountered, otherwise it will return absl::StatusCode::kOk. +template +absl::StatusCode CallNativeFunction(Context* context, const Fn& fn); + +namespace detail { + +// Generates a NativeFunction object that wraps the given functor. +template +class NativeFunctionCaller; + +template +class NativeFunctionCaller { + public: + explicit NativeFunctionCaller(Context* context) : context_(context) { + CHECK(context != nullptr) << "Must provide a context!"; + } + + template + absl::StatusCode Invoke(const Fn& fn) { + ExpandArgumentsAndCall(fn, std::index_sequence_for{}); + return status_; + } + + private: + template + void ExpandArgumentsAndCall(const Fn& fn, std::index_sequence) { + // Expansion will first call ReadArg for all arguments, then pass the + // resulting values to `CallFunction` which will then invoke `fn` with the + // arg values. + CallFunction(fn, ReadArg(S)...); + } + + // Attempts to evaluate the value of the Argument and the given `index`. + template + std::decay_t ReadArg(size_t index) { + std::decay_t value = {}; + if (status_ == absl::StatusCode::kOk) { + status_ = context_->ArgFromCpp(index, &value); + CHECK(status_ == absl::StatusCode::kOk) + << "Unable to read arg " << index << ": " << status_; + } + return value; + } + + template + void CallFunction(const Fn& fn, Args&&... args) { + CHECK(status_ == absl::StatusCode::kOk) + << "Error evaluating arguments; will not invoke function."; + + if constexpr (std::is_void_v) { + // Function returns void, just call it with the arguments. + fn(std::forward(args)...); + } else { + // Call the function with the arguments, storing the result in the + // context or any error that may have occurred in the status. + status_ = context_->ReturnFromCpp(fn(std::forward(args)...)); + CHECK(status_ == absl::StatusCode::kOk) << "Unable to set return value."; + } + } + + Context* context_ = nullptr; + absl::StatusCode status_ = absl::StatusCode::kOk; +}; + +} // namespace detail + +template +absl::StatusCode CallNativeFunction(Context* context, const Fn& fn) { + using Signature = typename FunctionTraits::Signature; + detail::NativeFunctionCaller caller(context); + return caller.Invoke(fn); +} + +} // namespace redux + +#endif // THIRD_PARTY_REDUX_MODULES_BASE_CALL_NATIVE_FUNCTION_H_ diff --git a/redux/redux/engines/script/call_native_function_tests.cc b/redux/redux/engines/script/call_native_function_tests.cc new file mode 100644 index 0000000..b09b445 --- /dev/null +++ b/redux/redux/engines/script/call_native_function_tests.cc @@ -0,0 +1,78 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/engines/script/call_native_function.h" + +namespace redux { +namespace { + +using ::testing::Eq; + +struct TestContext { + absl::StatusCode ArgFromCpp(size_t index, int* out) { + if (index < args.size()) { + *out = args[index]; + return absl::StatusCode::kOk; + } + return absl::StatusCode::kOutOfRange; + } + + absl::StatusCode ReturnFromCpp(int value) { + return_value = value; + return absl::StatusCode::kOk; + } + + std::vector args; + int return_value = 0; +}; + +static int Add(int x, int y) { return x + y; } + +TEST(CallNativeFunction, Function) { + TestContext context; + context.args.push_back(1); + context.args.push_back(2); + CallNativeFunction(&context, Add); + EXPECT_THAT(context.return_value, Eq(3)); +} + +TEST(CallNativeFunction, Lambda) { + auto lambda = [](int x, int y) { return x + y; }; + + TestContext context; + context.args.push_back(1); + context.args.push_back(2); + CallNativeFunction(&context, lambda); + EXPECT_THAT(context.return_value, Eq(3)); +} + +TEST(CallNativeFunction, StdFunction) { + std::function add = [](int x, int y) { return x + y; }; + + TestContext context; + context.args.push_back(1); + context.args.push_back(2); + CallNativeFunction(&context, add); + EXPECT_THAT(context.return_value, Eq(3)); +} + +} // namespace +} // namespace redux diff --git a/redux/redux/engines/script/function_binder.cc b/redux/redux/engines/script/function_binder.cc new file mode 100644 index 0000000..2d98fbd --- /dev/null +++ b/redux/redux/engines/script/function_binder.cc @@ -0,0 +1,38 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/script/function_binder.h" + +namespace redux { + +FunctionBinder::FunctionBinder(Registry* registry) : registry_(registry) {} + +FunctionBinder::~FunctionBinder() { + auto* engine = GetScriptEngine(); + for (const std::string& name : functions_) { + engine->UnregisterFunction(name); + } +} + +ScriptEngine* FunctionBinder::GetScriptEngine() { + if (script_engine_ == nullptr) { + script_engine_ = registry_->Get(); + CHECK(script_engine_ != nullptr); + } + return script_engine_; +} + +} // namespace redux diff --git a/redux/redux/engines/script/function_binder.h b/redux/redux/engines/script/function_binder.h new file mode 100644 index 0000000..794adca --- /dev/null +++ b/redux/redux/engines/script/function_binder.h @@ -0,0 +1,97 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_SCRIPT_FUNCTION_BINDER_H_ +#define REDUX_ENGINES_SCRIPT_FUNCTION_BINDER_H_ + +#include +#include +#include + +#include "redux/engines/script/script_engine.h" + +namespace redux { + +// Manages the binding and (automatic) unbinding of functions with the +// ScriptEngine. +// +// Any functions bound to the ScriptEngine via the FunctionBinder will be +// unregistered when the FunctionBinder is destroyed. +class FunctionBinder { + public: + explicit FunctionBinder(Registry* registry); + ~FunctionBinder(); + + FunctionBinder(const FunctionBinder&) = delete; + FunctionBinder& operator=(const FunctionBinder&) = delete; + + // Registers a function with the ScriptEngine. + template + void RegisterFn(std::string_view name, const Fn& fn) { + GetScriptEngine()->RegisterFunction(name, fn); + functions_.emplace_back(name); + } + + // Registers an enum with the ScriptEngine. + template + void RegisterEnum(std::string_view prefix) { + static_assert(std::is_enum_v); + GetScriptEngine()->RegisterEnum(prefix); + } + + // Registers a member function call with the ScriptEngine. The caller is + // required to provide the pointer-to-member-function and the instance of the + // object with which to invoke it. + template + void RegisterMemFn(std::string_view name, T* obj, const Fn& fn) { + static_assert(std::is_member_function_pointer_v); + RegisterMemberFunctionBinder binder; + GetScriptEngine()->RegisterFunction(name, binder.Bind(obj, fn)); + functions_.emplace_back(name); + } + + private: + ScriptEngine* GetScriptEngine(); + + template + struct RegisterMemberFunctionBinder; + + Registry* registry_; + ScriptEngine* script_engine_ = nullptr; + std::vector functions_; +}; + +// Specialization for non-const method with return type R. +template +struct FunctionBinder::RegisterMemberFunctionBinder { + static auto Bind(T* obj, R (T::*fn)(Args...)) { + return + [=](Args... args) { return (obj->*fn)(std::forward(args)...); }; + } +}; + +// Specialization for const method with return type R. +template +struct FunctionBinder::RegisterMemberFunctionBinder { + static auto Bind(T* obj, R (T::*fn)(Args...) const) { + return + [=](Args... args) { return (obj->*fn)(std::forward(args)...); }; + } +}; + +} // namespace redux + +#endif // REDUX_ENGINES_SCRIPT_FUNCTION_BINDER_H_ diff --git a/redux/redux/engines/script/redux/BUILD b/redux/redux/engines/script/redux/BUILD new file mode 100644 index 0000000..c625ab1 --- /dev/null +++ b/redux/redux/engines/script/redux/BUILD @@ -0,0 +1,216 @@ +# Redux's internal Lisp-based scripting backend for ScriptEngine. + +licenses(["notice"]) + +package( + default_visibility = ["//redux:visibility"], +) + +cc_library( + name = "redux", + srcs = ["script_engine.cc"], + deps = [ + ":script_env", + "@absl//absl/status", + "//redux/engines/script", + "//redux/modules/base:asset_loader", + "//redux/modules/base:registry", + "//redux/modules/base:static_registry", + "//redux/modules/ecs:entity", + "//redux/modules/math:matrix", + "//redux/modules/math:quaternion", + "//redux/modules/math:vector", + "//redux/modules/var", + ], +) + +cc_test( + name = "script_tests", + srcs = ["script_tests.cc"], + deps = [ + "@gtest//:gtest_main", + "//redux/engines/script", + "//redux/engines/script/redux", + ], +) + +cc_test( + name = "script_engine_tests", + srcs = ["script_engine_tests.cc"], + deps = [ + "@gtest//:gtest_main", + "//redux/engines/script", + "//redux/engines/script/redux", + ], +) + +cc_library( + name = "script_env", + srcs = [ + "script_ast_builder.cc", + "script_compiler.cc", + "script_env.cc", + "script_frame.cc", + "script_parser.cc", + "script_stack.cc", + "script_value.cc", + "stringify.cc", + ], + hdrs = [ + "functions/array.h", + "functions/cond.h", + "functions/hash.h", + "functions/map.h", + "functions/math.h", + "functions/message.h", + "functions/operators.h", + "functions/typeof.h", + "script_ast_builder.h", + "script_compiler.h", + "script_env.h", + "script_frame.h", + "script_frame_context.h", + "script_parser.h", + "script_stack.h", + "script_types.h", + "script_value.h", + ], + deps = [ + "@absl//absl/container:flat_hash_map", + "@absl//absl/status", + "@absl//absl/strings", + "@absl//absl/types:span", + "//redux/engines/script:call_native_function", + "//redux/modules/base:function_traits", + "//redux/modules/base:hash", + "//redux/modules/base:logging", + "//redux/modules/base:typed_ptr", + "//redux/modules/base:typeid", + "//redux/modules/dispatcher:message", + "//redux/modules/ecs:entity", + "//redux/modules/math:matrix", + "//redux/modules/math:quaternion", + "//redux/modules/math:vector", + "//redux/modules/var", + "//redux/modules/var:var_convert", + ], +) + +cc_library( + name = "testing", + hdrs = ["testing.h"], +) + +cc_test( + name = "script_compiler_tests", + srcs = ["script_compiler_tests.cc"], + deps = [ + ":script_env", + ":testing", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "script_env_tests", + srcs = ["script_env_tests.cc"], + deps = [ + ":script_env", + ":testing", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "script_parser_tests", + srcs = ["script_parser_tests.cc"], + deps = [ + ":script_env", + ":testing", + "@gtest//:gtest_main", + "//redux/modules/var", + ], +) + +cc_test( + name = "script_stack_tests", + srcs = ["script_stack_tests.cc"], + deps = [ + ":script_env", + ":testing", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "array_tests", + srcs = ["functions/array_tests.cc"], + deps = [ + ":script_env", + ":testing", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "cond_tests", + srcs = ["functions/cond_tests.cc"], + deps = [ + ":script_env", + ":testing", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "map_tests", + srcs = ["functions/map_tests.cc"], + deps = [ + ":script_env", + ":testing", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "math_tests", + srcs = ["functions/math_tests.cc"], + deps = [ + ":script_env", + ":testing", + "@gtest//:gtest_main", + "//redux/modules/math:quaternion", + "//redux/modules/math:vector", + ], +) + +cc_test( + name = "message_tests", + srcs = ["functions/message_tests.cc"], + deps = [ + ":script_env", + ":testing", + "@gtest//:gtest_main", + "//redux/modules/dispatcher:message", + ], +) + +cc_test( + name = "operators_tests", + srcs = ["functions/operators_tests.cc"], + deps = [ + ":script_env", + ":testing", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "typeof_tests", + srcs = ["functions/typeof_tests.cc"], + deps = [ + ":script_env", + ":testing", + "@gtest//:gtest_main", + ], +) diff --git a/redux/redux/engines/script/redux/README.md b/redux/redux/engines/script/redux/README.md new file mode 100644 index 0000000..e367403 --- /dev/null +++ b/redux/redux/engines/script/redux/README.md @@ -0,0 +1,151 @@ +A simple, redux-centric LISP interpreter. + +Redux Scripting can be used to create simple programs that are evaluated and +executed at runtime. A single statement is structured like so: + +``` +(function arg0 arg1 arg2 ...) +``` + +For example, the statement `(+ 1 1)` will evaluate to the value `2`. + +Commas are treated as whitespace, so you can do this: + +``` +(function arg0, arg1, arg2, ...) +``` + +Single line comments start with a semicolon: + +``` +; this is a comment +(+ 1 1) ; this is also a comment +``` + +The `?` function is a short cut for `LOG(INFO)` and can take a variable number +of arguments. + +``` +(? 'hello world') +``` + + +Variables can be declared using the `var` keyword: + +``` +(var one 1) +(var two 2.0) +(var hello 'hello') +``` + +Redux scripting uses the C++ type system behind the scenes, so you get access +to the same standard types. + +* `true` and `false` literals are treated as booleans +* whole numbers (e.g. 1, 5, 15, 39) are treated as int32_t +* decimal numbers (e.g. 1.2, 3.456, 0.87) are treated as doubles +* numbers suffixed with `f` (e.g. 1f, 2.3f) are treated as floats +* numbers suffixed with `u` (e.g. 2u) are treated as uint32_t +* numbers suffixed with `l` (e.g. 3l) are treated as int64_t +* numbers suffixed with `ul` (e.g. 4ul) are treated as uint64_t +* strings can be enclosed in single `'` or double quotes `"` +* the `null` literal is treated as a special "null" type + +Values can be explicitly cast to other types using the following functions: +`int8`, `int16`, `int32`, `int64`, `uint8`, `uint16`, `uint32`, `uint64`, +`float`, and `double`. + +For example, `(uint8 214)` will return a `uin8_t` with the value `214`. + + +A set of expressions can be evaluated in sequence using the `do` command. + +``` +(do + (+ 1 1) + (+ 2 2) +) +``` + +The result of the last expression within the `do`-block will be the result of +the block itself. You can use a `return` statement to stop execution of a +block. + +``` +(do + (+ 1 1) + (return (+ 2 2)) + (+ 3 3) ; will not be evaluated +) +``` + +You can define functions using `def`: + +``` +(def add (x y) ( + (+ x y) +)) +``` + +The body of the function is treated like a `do`-block and can contain multiple +statements. + +Similarly, you can define a macro using `macro`: + +``` +(def maco (x y) ( + (+ x y) +)) +``` + +The difference between a function and a macro is that the arguments to a +function are evaluated before they are passed to the function, whereas the +arguments to a macro are passed to the macro in an unevaluated form. + +For example, consider the following: + +``` +(def my_function (x) (+ x x)) +(macro my_macro (x) (+ x x)) +``` + +Calling + +``` +(var my_var 1) +(my_function (= my_var (+ my_var 1))) +``` + +will return a value of `4`. This is because the value of `my_var` will be +assigned to `2` before the function is called. + +Calling + +``` +(var my_var 1) +(my_macro (= my_var (+ my_var 1))) +``` + +In this case, the result is `5`. That is because the entire expression +`(= my_var (+ my_var 1))` is passed into the macro which it then evaluated +twice before being added. In other words, the macro is doing: + +``` +(+ + (= my_var (+ my_var 1)) ; sets my_var to 2 + (= my_var (+ my_var 1)) ; sets my_var to 3 +) +``` + +Redux scripting provides several other built-in functions as well, but they +are described in the individual header files under `redux/functions`. These +include (but are not limited to): + +* Conditional/branching using `if` and `cond` +* Boolean algebra operators `and`, `or`, and `not` +* Comparison operators `==`, `!=`, `<`, `>`, `<=`, and `>=` +* Mathematical operators `+`, `-`, `*`, `/`, and `%` +* Vector math types `vec2`, `vec2i`, `vec3`, `vec3i`, `vec4`, `vec4i`, and + `quat` +* Time types which can be created using `seconds` and `milliseconds` +* Entity IDs using `entity` diff --git a/redux/redux/engines/script/redux/functions/array.h b/redux/redux/engines/script/redux/functions/array.h new file mode 100644 index 0000000..5f4fa08 --- /dev/null +++ b/redux/redux/engines/script/redux/functions/array.h @@ -0,0 +1,169 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_ARRAY_H_ +#define REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_ARRAY_H_ + +#include "redux/engines/script/redux/script_env.h" + +// This file implements the following script functions: +// +// (make-array [value] [value] ... ) +// Creates an array with the optional list of values. The values can be of +// any supported type. +// +// (array-size [array]) +// Returns the number of elements in the array. +// +// (array-empty [array]) +// Returns true if the array is empty (ie. contains no elements). +// +// (array-push [array] [value]) +// Adds a new value to the end of the array. +// +// (array-pop [array]) +// Removes and returns a value from the end of the array (or does nothing if +// the array is empty). +// +// (array-insert [array] [index] [value]) +// Inserts the value into the array at the given index, pushing all elements +// after the index "backwards". The index must be an integer type. +// +// (array-erase [array] [index]) +// Removes the element in the array at the specified index, moving all +// elements after the index "forwards". The index must be an integer type. +// +// (array-at [array] [index]) +// Returns the value at the specified index in the array. The index must be +// an integer type. +// +// (array-set [array] [index] [value]) +// Sets the value of the specified index in the array. The index must be an +// integer type. +// +// (array-foreach [array] ([index-name?] [value-name]) [expressions...]) +// Passes each element of the array to expressions with the value bound to +// [value-name] and if supplied, index bound to [index-name]. +// +// Note that redux script parsing uses [ and ] as a short-cut for make-array. +// In other words, the following are equivalent: +// (make-array 1 2 3) +// [1 2 3] + +#include "redux/modules/var/var_array.h" + +namespace redux { +namespace script_functions { + +inline void ArrayCreateFn(ScriptFrame* frame) { + VarArray array; + while (frame->HasNext()) { + array.PushBack(*frame->EvalNext().Get()); + } + frame->Return(array); +} + +inline int ArraySizeFn(const VarArray* array) { + return static_cast(array->Count()); +} + +inline bool ArrayEmptyFn(const VarArray* array) { return array->Count() == 0; } + +inline void ArrayPushFn(VarArray* array, const Var* value) { + array->PushBack(*value); +} + +inline Var ArrayPopFn(VarArray* array) { + Var value; + if (array->Count() > 0) { + value = array->At(array->Count() - 1); + array->PopBack(); + } + return value; +} + +inline void ArrayInsertFn(VarArray* array, int index, const Var* value) { + array->Insert(index, *value); +} + +inline void ArrayEraseFn(VarArray* array, int index) { array->Erase(index); } + +inline Var ArrayAtFn(const VarArray* array, int index) { + return array->At(index); +} + +inline Var ArraySetFn(VarArray* array, int index, const Var* value) { + return (*array)[index] = *value; +} + +inline void ArrayForEachFn(ScriptFrame* frame) { + if (!frame->HasNext()) { + frame->Error("array-foreach: expect [array] ([args]) [body]."); + return; + } + ScriptValue array_arg = frame->EvalNext(); + if (!array_arg.Is()) { + frame->Error("array-foreach: first argument should be an array."); + return; + } + const VarArray* array = array_arg.Get(); + + const AstNode* node = frame->GetArgs().Get(); + if (!node) { + frame->Error("array-foreach: expected parameters after array."); + return; + } + + const AstNode* params = node->first.Get(); + const Symbol* index = params ? params->first.Get() : nullptr; + params = params && params->rest ? params->rest.Get() : nullptr; + const Symbol* value = params ? params->first.Get() : nullptr; + + if (!index) { + frame->Error("array-foreach: should be at least 1 symbol parameter"); + return; + } + if (!value) { + // Only 1 parameter -- treat it as value. + std::swap(index, value); + } + + ScriptEnv* env = frame->GetEnv(); + + // Now iterate the array elements, evaluating body. + ScriptValue result; + for (size_t i = 0; i < array->Count(); ++i) { + if (index) { + env->SetValue(index->value, static_cast(i)); + } + env->SetValue(value->value, array->At(i)); + + const ScriptValue* iter = &node->rest; + while (auto* node = iter->Get()) { + result = env->Eval(*iter); + // Maybe implement continue/break here? + iter = &node->rest; + if (iter->IsNil()) { + break; + } + } + } + frame->Return(std::move(result)); +} +} // namespace script_functions +} // namespace redux + +#endif // REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_ARRAY_H_ diff --git a/redux/redux/engines/script/redux/functions/array_tests.cc b/redux/redux/engines/script/redux/functions/array_tests.cc new file mode 100644 index 0000000..a152eac --- /dev/null +++ b/redux/redux/engines/script/redux/functions/array_tests.cc @@ -0,0 +1,152 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/engines/script/redux/script_env.h" +#include "redux/engines/script/redux/testing.h" + +namespace redux { +namespace { + +using ::testing::Eq; +using ::testing::IsNull; +using ::testing::Not; + +TEST(ScriptArrayTest, Array) { + ScriptEnv env; + ScriptValue res; + VarArray* arr = nullptr; + + res = env.Exec("(make-array 1 2 3 4)"); + arr = res.Get(); + EXPECT_THAT(arr, Not(IsNull())); + EXPECT_THAT(arr->Count(), Eq(4)); + + res = env.Exec("[1 2 3 4]"); + arr = res.Get(); + EXPECT_THAT(arr, Not(IsNull())); + EXPECT_THAT(arr->Count(), Eq(4)); +} + +TEST(ScriptArrayTest, ArraySize) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(array-size [])", 0); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-size [1])", 1); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-size [1 2 3])", 3); +} + +TEST(ScriptArrayTest, ArrayEmpty) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(array-empty [])", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-empty [1])", false); +} + +TEST(ScriptArrayTest, ArrayAt) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(array-at [1 2 3] 0)", 1); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-at [1 2 3] 1)", 2); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-at [1 2 3] 2)", 3); +} + +TEST(ScriptArrayTest, ArrayPush) { + ScriptEnv env; + + env.Exec("(= arr [])"); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-size arr)", 0); + + env.Exec("(array-push arr 1)"); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-size arr)", 1); + + env.Exec("(array-push arr 2)"); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-size arr)", 2); +} + +TEST(ScriptArrayTest, ArrayPop) { + ScriptEnv env; + + env.Exec("(= arr [1 2])"); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-size arr)", 2); + + env.Exec("(array-pop arr)"); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-size arr)", 1); + + env.Exec("(array-pop arr)"); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-size arr)", 0); +} + +TEST(ScriptArrayTest, ArrayInsert) { + ScriptEnv env; + + env.Exec("(= arr [1 2])"); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-size arr)", 2); + + env.Exec("(array-insert arr 1 3)"); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-size arr)", 3); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-at arr 0)", 1); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-at arr 1)", 3); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-at arr 2)", 2); +} + +TEST(ScriptArrayTest, ArrayErase) { + ScriptEnv env; + + env.Exec("(= arr [1 2 3])"); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-size arr)", 3); + + env.Exec("(array-erase arr 1)"); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-size arr)", 2); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-at arr 0)", 1); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-at arr 1)", 3); +} + +TEST(ScriptArrayTest, ArraySet) { + ScriptEnv env; + + env.Exec("(= arr [1 2 3])"); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-at arr 1)", 2); + + env.Exec("(array-set arr 1 5)"); + REDUX_CHECK_SCRIPT_RESULT(env, "(array-at arr 1)", 5); +} + +TEST(ScriptArrayTest, ArrayForEach) { + ScriptEnv env; + ScriptValue res; + + env.Exec("(= arr [1 3 5])"); + res = env.Exec( + "(do" + " (= sum 0)" + " (array-foreach arr (val) (= sum (+ sum val)))" + ")"); + REDUX_CHECK_SCRIPT_RESULT(env, "(do sum)", 9); + + env.Exec( + "(do" + " (= isum 0)" + " (= vsum 0)" + " (array-foreach arr (idx val) (do" + " (= isum (+ isum idx))" + " (= vsum (+ vsum val))" + " ))" + ")"); + REDUX_CHECK_SCRIPT_RESULT(env, "(do vsum)", 9); + REDUX_CHECK_SCRIPT_RESULT(env, "(do isum)", 3); +} + +} // namespace +} // namespace redux diff --git a/redux/redux/engines/script/redux/functions/cond.h b/redux/redux/engines/script/redux/functions/cond.h new file mode 100644 index 0000000..03949ce --- /dev/null +++ b/redux/redux/engines/script/redux/functions/cond.h @@ -0,0 +1,87 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_COND_H_ +#define REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_COND_H_ + +#include "redux/engines/script/redux/script_env.h" + +// This file implements the following script functions: +// +// (cond ([condition] [statements]...) +// ([condition] [statements]...) +// ... +// ) +// Executes the statements associated with the first condition that is true. +// +// (if [condition] [true-statement] [false-statement]) +// Conditionally evaluates one of two statements based on a boolean condition. +namespace redux { +namespace script_functions { + +inline void CondFn(ScriptFrame* frame) { + while (frame->HasNext()) { + const ScriptValue& arg = frame->Next(); + const AstNode* node = arg.Get(); + if (!node) { + frame->Error("Expected AST Node."); + return; + } + + ScriptFrame inner(frame->GetEnv(), node->first); + ScriptValue cond_value = inner.EvalNext(); + + const bool* cond = cond_value.Get(); + if (cond && *cond) { + while (inner.HasNext()) { + frame->Return(inner.EvalNext()); + } + return; + } + } +} + +inline void IfFn(ScriptFrame* frame) { + ScriptValue value = frame->EvalNext(); + const bool* condition = value.Get(); + if (condition && *condition) { + // Evaluate the first path. + if (frame->HasNext()) { + frame->Return(frame->EvalNext()); + } + // Skip the other path. + if (frame->HasNext()) { + frame->Next(); + } + } else { + // Skip the first path. + if (frame->HasNext()) { + frame->Next(); + } + // Evaluate the second path. + if (frame->HasNext()) { + frame->Return(frame->EvalNext()); + } + } + + if (frame->HasNext()) { + frame->Error("if: should only have two paths."); + } +} +} // namespace script_functions +} // namespace redux + +#endif // REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_COND_H_ diff --git a/redux/redux/engines/script/redux/functions/cond_tests.cc b/redux/redux/engines/script/redux/functions/cond_tests.cc new file mode 100644 index 0000000..01ecdd7 --- /dev/null +++ b/redux/redux/engines/script/redux/functions/cond_tests.cc @@ -0,0 +1,38 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/engines/script/redux/script_env.h" +#include "redux/engines/script/redux/testing.h" + +namespace redux { +namespace { + +TEST(ScriptCondTest, Cond) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(cond (true 1) (false 2))", 1); + REDUX_CHECK_SCRIPT_RESULT(env, "(cond (false 1) (true 2))", 2); +} + +TEST(ScriptCondTest, If) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(if true 1 2)", 1); + REDUX_CHECK_SCRIPT_RESULT(env, "(if false 1 2)", 2); +} + +} // namespace +} // namespace redux diff --git a/redux/redux/engines/script/redux/functions/hash.h b/redux/redux/engines/script/redux/functions/hash.h new file mode 100644 index 0000000..9ee950d --- /dev/null +++ b/redux/redux/engines/script/redux/functions/hash.h @@ -0,0 +1,48 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_HASH_H_ +#define REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_HASH_H_ + +#include "redux/engines/script/redux/script_env.h" + +// This file implements the following script functions: +// +// (hash [string]) +// Returns the hash value for the given string. +// +// Note that redux script parsing treats any symbol that is prefixed with a +// colon as a short-cut for hash. In other words, the following two are +// equivalent: +// (:hello) +// (hash `hello`) + +namespace redux { +namespace script_functions { + +inline void HashFn(ScriptFrame* frame) { + ScriptValue arg = frame->EvalNext(); + if (const std::string* str = arg.Get()) { + frame->Return(redux::Hash(*str)); + } else { + frame->Return(HashValue(0)); + } +} + +} // namespace script_functions +} // namespace redux + +#endif // REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_HASH_H_ diff --git a/redux/redux/engines/script/redux/functions/map.h b/redux/redux/engines/script/redux/functions/map.h new file mode 100644 index 0000000..c70c950 --- /dev/null +++ b/redux/redux/engines/script/redux/functions/map.h @@ -0,0 +1,167 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_MAP_H_ +#define REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_MAP_H_ + +#include "redux/engines/script/redux/script_env.h" + +// This file implements the following script functions: +// +// (make-map [(key value)] [(key value)] ... ) +// Creates a map/dictionary with the optional list of key/value pairs. Each +// pair must be specified as a tuple (ie. within parentheses). The keys +// must be integer or hashvalue types. +// +// (map-size [map]) +// Returns the number of elements in the map. +// +// (map-empty [map]) +// Returns true if the map is empty (ie. contains no elements). +// +// (map-insert [map] [key] [value]) +// Inserts a value into the map at the specified key. The key must be an +// integer or hashvalue type. +// +// (map-erase [map] [key]) +// Removes the element in the map specified by the key. The key must be an +// integer or hashvalue type. +// +// (map-get [map] [key]) +// Returns the value associated with the key in the map. The key must be an +// integer or hashvalue type. +// +// (map-set [map] [key] [value]) +// Sets the value associated with the key in the map. The key must be an +// integer or hashvalue type. +// +// (map-foreach [map] ([key-name] [value-name]) [expressions...]) +// Passes each element of the map to expressions with the key bound to +// [key-name] and the value bound to [value-name]. +// +// Note that redux script parsing uses { and } as a short-cut for make-map. +// In other words, the following are equivalent: +// (make-map (:city 'new york') (:country 'usa')) +// {(:city 'new york') (:country 'usa')} + +#include "redux/modules/var/var_table.h" + +namespace redux { +namespace script_functions { + +inline void MapCreateFn(ScriptFrame* frame) { + VarTable map; + while (frame->HasNext()) { + const ScriptValue& arg = frame->Next(); + const AstNode* node = arg.Get(); + if (!node) { + frame->Error("map: expected tuple as map arguments"); + break; + } + + ScriptFrame tuple(frame->GetEnv(), node->first); + ScriptValue key = tuple.EvalNext(); + ScriptValue value = tuple.EvalNext(); + if (key) { + uint32_t id = 0; + key.GetAs(&id); + map.Insert(HashValue(id), *value.Get()); + } else { + CHECK(false); + } + } + frame->Return(map); +} + +inline int MapSizeFn(const VarTable* map) { return map->Count(); } + +inline bool MapEmptyFn(const VarTable* map) { return map->Count() == 0; } + +inline void MapInsertFn(VarTable* map, HashValue key, const Var* value) { + map->Insert(key, *value); +} + +inline void MapEraseFn(VarTable* map, HashValue key) { map->Erase(key); } + +inline Var MapGetOrFn(const VarTable* map, HashValue key, const Var* default_value) { + const Var* var = map->TryFind(key); + if (var) { + return *var; + } + return *default_value; +} + +inline Var MapGetFn(const VarTable* map, HashValue key) { + Var v; + return MapGetOrFn(map, key, &v); +} + +inline void MapSetFn(VarTable* map, HashValue key, const Var* value) { + (*map)[key] = *value; +} + +inline void MapForEachFn(ScriptFrame* frame) { + if (!frame->HasNext()) { + frame->Error("map-foreach: expect [map] ([args]) [body]."); + return; + } + ScriptValue map_arg = frame->EvalNext(); + if (!map_arg.Is()) { + frame->Error("map-foreach: first argument should be a map."); + return; + } + const VarTable* map = map_arg.Get(); + + const AstNode* node = frame->GetArgs().Get(); + if (!node) { + frame->Error("map-foreach: expected parameters after map."); + return; + } + + const AstNode* params = node->first.Get(); + const Symbol* key = params ? params->first.Get() : nullptr; + params = params && params->rest ? params->rest.Get() : nullptr; + const Symbol* value = params ? params->first.Get() : nullptr; + + if (!key || !value) { + frame->Error("map-foreach: should be at least 2 symbol parameters"); + return; + } + + ScriptEnv* env = frame->GetEnv(); + + // Now iterate the map elements, evaluating body. + ScriptValue result; + for (const auto& kv : *map) { + env->SetValue(key->value, kv.first); + env->SetValue(value->value, kv.second); + + const ScriptValue* iter = &node->rest; + while (auto* node = iter->Get()) { + result = env->Eval(*iter); + // Maybe implement continue/break here? + iter = &node->rest; + if (iter->IsNil()) { + break; + } + } + } + frame->Return(result); +} +} // namespace script_functions +} // namespace redux + +#endif // REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_MAP_H_ diff --git a/redux/redux/engines/script/redux/functions/map_tests.cc b/redux/redux/engines/script/redux/functions/map_tests.cc new file mode 100644 index 0000000..754cf96 --- /dev/null +++ b/redux/redux/engines/script/redux/functions/map_tests.cc @@ -0,0 +1,125 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/engines/script/redux/script_env.h" +#include "redux/engines/script/redux/testing.h" + +namespace redux { +namespace { + +using ::testing::Eq; +using ::testing::IsNull; +using ::testing::Not; + +TEST(ScriptMapTest, Map) { + ScriptEnv env; + ScriptValue res; + VarTable* map = nullptr; + + res = env.Exec("(make-map (:a 'a') (:b 123) (:d 'd'))"); + map = res.Get(); + EXPECT_THAT(map, Not(IsNull())); + EXPECT_THAT(map->Count(), Eq(3)); + EXPECT_TRUE(map->Contains(ConstHash("a"))); + EXPECT_TRUE(map->Contains(ConstHash("b"))); + EXPECT_FALSE(map->Contains(ConstHash("c"))); + EXPECT_TRUE(map->Contains(ConstHash("d"))); + + res = env.Exec("{(:a 'a') (:b 123) (:d 'd')}"); + map = res.Get(); + EXPECT_THAT(map, Not(IsNull())); + EXPECT_THAT(map->Count(), Eq(3)); + EXPECT_TRUE(map->Contains(ConstHash("a"))); + EXPECT_TRUE(map->Contains(ConstHash("b"))); + EXPECT_FALSE(map->Contains(ConstHash("c"))); + EXPECT_TRUE(map->Contains(ConstHash("d"))); +} + +TEST(ScriptMapTest, MapSize) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(map-size {})", 0); + REDUX_CHECK_SCRIPT_RESULT(env, "(map-size {(:a 'a')})", 1); + REDUX_CHECK_SCRIPT_RESULT(env, "(map-size {(:a 'a') (:b 123) (:d 'd')})", 3); +} + +TEST(ScriptMapTest, MapEmpty) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(map-empty {})", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(map-empty {(:a 'a')})", false); +} + +TEST(ScriptMapTest, MapGet) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(map-get {(:a 'a') (:b 123)} :a)", + std::string("a")); + REDUX_CHECK_SCRIPT_RESULT(env, "(map-get {(:a 'a') (:b 123)} :b)", 123); + REDUX_CHECK_SCRIPT_RESULT_NIL(env, "(map-get {(:a 'a') (:b 123)} :c)"); +} + +TEST(ScriptMapTest, MapGetOr) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(map-get-or {(:a 'a') (:b 123)} :a 1)", + std::string("a")); + REDUX_CHECK_SCRIPT_RESULT(env, "(map-get-or {(:a 'a') (:b 123)} :b 1)", 123); + REDUX_CHECK_SCRIPT_RESULT(env, "(map-get-or {(:a 'a') (:b 123)} :c 1)", 1); +} + +TEST(ScriptMapTest, MapInsert) { + ScriptEnv env; + + env.Exec("(= m {(:a 'a') (:b 123) (:d 'd')})"); + REDUX_CHECK_SCRIPT_RESULT(env, "(map-size m)", 3); + // EXPECT_FALSE(map->Contains(ConstHash("c"))); + REDUX_CHECK_SCRIPT_RESULT_NIL(env, "(map-get m :c)"); + + env.Exec("(map-insert m :c 456.789f)"); + REDUX_CHECK_SCRIPT_RESULT(env, "(map-size m)", 4); + // EXPECT_TRUE(map->Contains(ConstHash("c"))); + REDUX_CHECK_SCRIPT_RESULT(env, "(map-get m :c)", 456.789f); +} + +TEST(ScriptMapTest, MapErase) { + ScriptEnv env; + Var res; + + env.Exec("(= m {(:a 'a') (:b 123) (:d 'd')})"); + REDUX_CHECK_SCRIPT_RESULT(env, "(map-size m)", 3); + // EXPECT_TRUE(map->Contains(ConstHash("b"))); + REDUX_CHECK_SCRIPT_RESULT(env, "(map-get m :b)", 123); + + env.Exec("(map-erase m :b)"); + REDUX_CHECK_SCRIPT_RESULT(env, "(map-size m)", 2); + // EXPECT_TRUE(map->Contains(ConstHash("b"))); + REDUX_CHECK_SCRIPT_RESULT_NIL(env, "(map-get m :b)"); +} + +TEST(ScriptMapTest, MapForEach) { + ScriptEnv env; + Var res; + + env.Exec("(= map {(:a 1) (:b 3) (:d 5)})"); + res = env.Exec( + "(do" + " (= vsum 0)" + " (map-foreach map (key val) (= vsum (+ vsum val)))" + ")"); + REDUX_CHECK_SCRIPT_RESULT(env, "(do vsum)", 9); +} + +} // namespace +} // namespace redux diff --git a/redux/redux/engines/script/redux/functions/math.h b/redux/redux/engines/script/redux/functions/math.h new file mode 100644 index 0000000..3a7ace0 --- /dev/null +++ b/redux/redux/engines/script/redux/functions/math.h @@ -0,0 +1,148 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_MATH_H_ +#define REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_MATH_H_ + +#include "redux/engines/script/redux/script_env.h" +#include "redux/modules/math/quaternion.h" +#include "redux/modules/math/vector.h" + +// This file implements the following script functions: +// +// (get-x [mathfu-value]) +// Returns the x-component of the specified vector/quat. +// +// (get-y [mathfu-value]) +// Returns the y-component of the specified vector/quat. +// +// (get-z [mathfu-value]) +// Returns the z-component of the specified vector/quat. +// +// (get-w [mathfu-value]) +// Returns the w-component of the specified vector/quat. +// +// (set-x [mathfu-value] [value]) +// Sets the x-component of the specified vector/quat to the given value. +// +// (set-y [mathfu-value] [value]) +// Sets the y-component of the specified vector/quat to the given value. +// +// (set-z [mathfu-value] [value]) +// Sets the z-component of the specified vector/quat to the given value. +// +// (set-w [mathfu-value] [value]) +// Sets the w-component of the specified vector/quat to the given value. + +namespace redux { +namespace script_functions { + +template +void GetElement(ScriptFrame* frame, const T* obj) { + frame->Return((*obj)[N]); +} + +template +void SetElement(T* obj, const ScriptValue& value) { + using U = typename std::decay::type; + + U scalar{}; + if (!value.GetAs(&scalar)) { + CHECK(false) << "Unable to set element " << N; + } + (*obj)[N] = scalar; +} + +template +void GetElementFn(ScriptFrame* frame) { + ScriptValue v0 = frame->EvalNext(); + if (auto x = v0.Get()) { + GetElement(frame, x); + return; + } + if (auto x = v0.Get()) { + GetElement(frame, x); + return; + } + if (auto x = v0.Get()) { + GetElement(frame, x); + return; + } + if constexpr (N < 3) { + if (auto x = v0.Get()) { + GetElement(frame, x); + return; + } + if (auto x = v0.Get()) { + GetElement(frame, x); + return; + } + } + if constexpr (N < 2) { + if (auto x = v0.Get()) { + GetElement(frame, x); + return; + } + if (auto x = v0.Get()) { + GetElement(frame, x); + return; + } + } +} + +template +void SetElementFn(ScriptFrame* frame) { + ScriptValue v0 = frame->EvalNext(); + ScriptValue v1 = frame->EvalNext(); + + if (auto x = v0.Get()) { + SetElement(x, v1); + return; + } + if (auto x = v0.Get()) { + SetElement(x, v1); + return; + } + if (auto x = v0.Get()) { + SetElement(x, v1); + return; + } + if constexpr (N < 3) { + if (auto x = v0.Get()) { + SetElement(x, v1); + return; + } + if (auto x = v0.Get()) { + SetElement(x, v1); + return; + } + } + if constexpr (N < 2) { + if (auto x = v0.Get()) { + SetElement(x, v1); + return; + } + if (auto x = v0.Get()) { + SetElement(x, v1); + return; + } + } +} + +} // namespace script_functions +} // namespace redux + +#endif // REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_MATH_H_ diff --git a/redux/redux/engines/script/redux/functions/math_tests.cc b/redux/redux/engines/script/redux/functions/math_tests.cc new file mode 100644 index 0000000..bfca048 --- /dev/null +++ b/redux/redux/engines/script/redux/functions/math_tests.cc @@ -0,0 +1,196 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/engines/script/redux/script_env.h" +#include "redux/engines/script/redux/testing.h" +#include "redux/modules/math/quaternion.h" +#include "redux/modules/math/vector.h" + +namespace redux { +namespace { + +using ::testing::Eq; + +TEST(ScriptMathTest, MathTypes) { + ScriptEnv env; + ScriptValue res; + + res = env.Exec("(vec2i 1 2)"); + EXPECT_THAT(res.Is(), Eq(true)); + EXPECT_THAT(res.Get()->x, Eq(1)); + EXPECT_THAT(res.Get()->y, Eq(2)); + + res = env.Exec("(vec3i 3 4 5)"); + EXPECT_THAT(res.Is(), Eq(true)); + EXPECT_THAT(res.Get()->x, Eq(3)); + EXPECT_THAT(res.Get()->y, Eq(4)); + EXPECT_THAT(res.Get()->z, Eq(5)); + + res = env.Exec("(vec4i 6 7 8 9)"); + EXPECT_THAT(res.Is(), Eq(true)); + EXPECT_THAT(res.Get()->x, Eq(6)); + EXPECT_THAT(res.Get()->y, Eq(7)); + EXPECT_THAT(res.Get()->z, Eq(8)); + EXPECT_THAT(res.Get()->w, Eq(9)); + + res = env.Exec("(vec2 1.0f 2.0f)"); + EXPECT_THAT(res.Is(), Eq(true)); + EXPECT_THAT(res.Get()->x, Eq(1.f)); + EXPECT_THAT(res.Get()->y, Eq(2.f)); + + res = env.Exec("(vec3 3.0f 4.0f 5.0f)"); + EXPECT_THAT(res.Is(), Eq(true)); + EXPECT_THAT(res.Get()->x, Eq(3.f)); + EXPECT_THAT(res.Get()->y, Eq(4.f)); + EXPECT_THAT(res.Get()->z, Eq(5.f)); + + res = env.Exec("(vec4 6.0f 7.0f 8.0f 9.0f)"); + EXPECT_THAT(res.Is(), Eq(true)); + EXPECT_THAT(res.Get()->x, Eq(6.f)); + EXPECT_THAT(res.Get()->y, Eq(7.f)); + EXPECT_THAT(res.Get()->z, Eq(8.f)); + EXPECT_THAT(res.Get()->w, Eq(9.f)); + + res = env.Exec("(quat 0.1f 0.2f 0.3f 0.4f)"); + EXPECT_THAT(res.Is(), Eq(true)); + EXPECT_THAT(res.Get()->x, Eq(0.1f)); + EXPECT_THAT(res.Get()->y, Eq(0.2f)); + EXPECT_THAT(res.Get()->z, Eq(0.3f)); + EXPECT_THAT(res.Get()->w, Eq(0.4f)); +} + +TEST(ScriptMathTest, MathGetters) { + ScriptEnv env; + ScriptValue res; + + res = env.Exec("(= v2i (vec2i 1 2))"); + res = env.Exec("(= v3i (vec3i 3 4 5))"); + res = env.Exec("(= v4i (vec4i 6 7 8 9))"); + res = env.Exec("(= v2f (vec2 1.0f 2.0f))"); + res = env.Exec("(= v3f (vec3 3.0f 4.0f 5.0f))"); + res = env.Exec("(= v4f (vec4 6.0f 7.0f 8.0f 9.0f))"); + res = env.Exec("(= qtf (quat 0.1f 0.2f 0.3f 0.4f))"); + + REDUX_CHECK_SCRIPT_RESULT(env, "(get-x v2i)", 1); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-x v3i)", 3); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-x v4i)", 6); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-x v2f)", 1.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-x v3f)", 3.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-x v4f)", 6.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-x qtf)", 0.1f); + + REDUX_CHECK_SCRIPT_RESULT(env, "(get-y v2i)", 2); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-y v3i)", 4); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-y v4i)", 7); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-y v2f)", 2.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-y v3f)", 4.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-y v4f)", 7.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-y qtf)", 0.2f); + + REDUX_CHECK_SCRIPT_RESULT(env, "(get-z v3i)", 5); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-z v4i)", 8); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-z v3f)", 5.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-z v4f)", 8.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-z qtf)", 0.3f); + + REDUX_CHECK_SCRIPT_RESULT(env, "(get-w v4i)", 9); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-w v4f)", 9.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-w qtf)", 0.4f); +} + +TEST(ScriptMathTest, MathSetters) { + ScriptEnv env; + ScriptValue res; + + res = env.Exec("(= v2i (vec2i 1 2))"); + res = env.Exec("(= v3i (vec3i 3 4 5))"); + res = env.Exec("(= v4i (vec4i 6 7 8 9))"); + res = env.Exec("(= v2f (vec2 1.0f 2.0f))"); + res = env.Exec("(= v3f (vec3 3.0f 4.0f 5.0f))"); + res = env.Exec("(= v4f (vec4 6.0f 7.0f 8.0f 9.0f))"); + res = env.Exec("(= qtf (quat 0.1f 0.2f 0.3f 0.4f))"); + + env.Exec("(set-x v2i 0)"); + env.Exec("(set-x v3i 0)"); + env.Exec("(set-x v4i 0)"); + env.Exec("(set-x v2f 0.0f)"); + env.Exec("(set-x v3f 0.0f)"); + env.Exec("(set-x v4f 0.0f)"); + env.Exec("(set-x qtf 0.0f)"); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-x v2i)", 0); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-x v3i)", 0); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-x v4i)", 0); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-x v2f)", 0.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-x v3f)", 0.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-x v4f)", 0.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-x qtf)", 0.0f); + + env.Exec("(set-y v2i 0)"); + env.Exec("(set-y v3i 0)"); + env.Exec("(set-y v4i 0)"); + env.Exec("(set-y v2f 0.0f)"); + env.Exec("(set-y v3f 0.0f)"); + env.Exec("(set-y v4f 0.0f)"); + env.Exec("(set-y qtf 0.0f)"); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-y v2i)", 0); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-y v3i)", 0); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-y v4i)", 0); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-y v2f)", 0.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-y v3f)", 0.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-y v4f)", 0.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-y qtf)", 0.0f); + + env.Exec("(set-z v3i 0)"); + env.Exec("(set-z v4i 0)"); + env.Exec("(set-z v3f 0.0f)"); + env.Exec("(set-z v4f 0.0f)"); + env.Exec("(set-z qtf 0.0f)"); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-z v3i)", 0); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-z v4i)", 0); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-z v3f)", 0.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-z v4f)", 0.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-z qtf)", 0.0f); + + env.Exec("(set-w v4i 0)"); + env.Exec("(set-w v4f 0.0f)"); + env.Exec("(set-w qtf 0.0f)"); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-w v4i)", 0); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-w v4f)", 0.0f); + REDUX_CHECK_SCRIPT_RESULT(env, "(get-w qtf)", 0.0f); +} + +TEST(ScriptMathTest, MathOperators) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(+ (vec2i 1 2) (vec2i 3 4))", vec2i(4, 6)); + REDUX_CHECK_SCRIPT_RESULT(env, "(- (vec2i 3 4) (vec2i 1 2))", vec2i(2, 2)); + REDUX_CHECK_SCRIPT_RESULT(env, "(* (vec2 1.f 2.f) 2.f)", vec2(2.f, 4.f)); + REDUX_CHECK_SCRIPT_RESULT(env, "(* (vec3 1.f 2.f 3.f) 2.f)", + vec3(2.f, 4.f, 6.f)); + REDUX_CHECK_SCRIPT_RESULT(env, "(* 2.f (vec2 1.f 2.f))", vec2(2.f, 4.f)); + REDUX_CHECK_SCRIPT_RESULT(env, "(* 2.f (vec3 1.f 2.f 3.f))", + vec3(2.f, 4.f, 6.f)); + REDUX_CHECK_SCRIPT_RESULT(env, + "(* (quat 1.f 0.f 0.f 0.f) (vec3 1.f 2.f 3.f))", + vec3(1.f, -2.f, -3.f)); + REDUX_CHECK_SCRIPT_RESULT(env, "(== (vec2i 1 2) (vec2i 1 2))", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(== (vec2i 1 2) (vec2i 2 1))", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(!= (vec2i 1 2) (vec2i 1 2))", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(!= (vec2i 1 2) (vec2i 2 1))", true); +} +} // namespace +} // namespace redux diff --git a/redux/redux/engines/script/redux/functions/message.h b/redux/redux/engines/script/redux/functions/message.h new file mode 100644 index 0000000..84c0570 --- /dev/null +++ b/redux/redux/engines/script/redux/functions/message.h @@ -0,0 +1,89 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_MESSAGE_H_ +#define REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_MESSAGE_H_ + +#include "redux/engines/script/redux/script_env.h" + +// This file implements the following script functions: +// +// (make-event [type] [(map ...)] ) +// Creates an event wrapper with the optional map of values. The type +// must have integer or hashvalue type. +// +// (event-type [event]) +// Returns the name of the event. +// +// (event-size [event]) +// Returns the number of elements in the event. +// +// (event-get [event] [key]) +// Returns the value associated with the key in the event. The key must be an +// integer or hashvalue type. + +#include "redux/modules/dispatcher/message.h" + +namespace redux { +namespace script_functions { + +inline void MessageCreateFn(ScriptFrame* frame) { + TypeId type(0); + if (frame->HasNext()) { + ScriptValue type_arg = frame->EvalNext(); + uint32_t id = 0; + type_arg.GetAs(&id); + type = TypeId(id); + } + + if (type == TypeId(0)) { + frame->Error("msg: type not provided"); + return; + } + + if (frame->HasNext()) { + ScriptValue values_arg = frame->EvalNext(); + const VarTable* values = values_arg.Get(); + CHECK(values); + frame->Return(Message(type, *values)); + } else { + frame->Return(Message(type)); + } +} + +inline TypeId MessageTypeFn(const Message* msg) { return msg->GetTypeId(); } + +inline Var MessageGetOrFn(const Message* msg, HashValue key, + const Var* default_value) { + const auto* values = msg->Get(); + if (!values) { + return *default_value; + } + const Var* var = values->TryFind(key); + if (var) { + return *var; + } + return *default_value; +} + +inline Var MessageGetFn(const Message* msg, HashValue key) { + Var nil; + return MessageGetOrFn(msg, key, &nil); +} +} // namespace script_functions +} // namespace redux + +#endif // REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_MESSAGE_H_ diff --git a/redux/redux/engines/script/redux/functions/message_tests.cc b/redux/redux/engines/script/redux/functions/message_tests.cc new file mode 100644 index 0000000..e42d266 --- /dev/null +++ b/redux/redux/engines/script/redux/functions/message_tests.cc @@ -0,0 +1,51 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/engines/script/redux/script_env.h" +#include "redux/engines/script/redux/testing.h" +#include "redux/modules/dispatcher/message.h" + +namespace redux { +namespace { + +using ::testing::Eq; +using ::testing::Ne; + +TEST(ScriptMessageTest, MessageType) { + ScriptEnv env; + env.Exec("(= foo (make-msg :foo))"); + env.Exec("(= bar (make-msg :bar {(:a 'a')}))"); + env.Exec("(= baz (make-msg :baz {(:a 'a') (:b 123) (:d 'd')}))"); + + REDUX_CHECK_SCRIPT_RESULT(env, "(msg-type foo)", ConstHash("foo").get()); + REDUX_CHECK_SCRIPT_RESULT(env, "(msg-type bar)", ConstHash("bar").get()); + REDUX_CHECK_SCRIPT_RESULT(env, "(msg-type baz)", ConstHash("baz").get()); +} + +TEST(ScriptMessageTest, MessageGet) { + ScriptEnv env; + env.Exec("(= foo (make-msg :foo))"); + env.Exec("(= bar (make-msg :bar {(:a 'a')}))"); + env.Exec("(= baz (make-msg :baz {(:a 'a') (:b 123) (:d 'd')}))"); + + REDUX_CHECK_SCRIPT_RESULT(env, "(msg-get foo :b)", nullptr); + REDUX_CHECK_SCRIPT_RESULT(env, "(msg-get baz :b)", 123); + REDUX_CHECK_SCRIPT_RESULT(env, "(msg-get-or bar :b 345)", 345); +} +} // namespace +} // namespace redux diff --git a/redux/redux/engines/script/redux/functions/operators.h b/redux/redux/engines/script/redux/functions/operators.h new file mode 100644 index 0000000..2815402 --- /dev/null +++ b/redux/redux/engines/script/redux/functions/operators.h @@ -0,0 +1,416 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_OPERATORS_H_ +#define REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_OPERATORS_H_ + +#include "redux/engines/script/redux/script_env.h" +#include "redux/modules/ecs/entity.h" +#include "redux/modules/math/quaternion.h" +#include "redux/modules/math/vector.h" + +// This file implements the following script functions: +// +// (== [lhs] [rhs]) +// Returns true if two arguments have the same value. Valid for: integers, +// floating-point numbers, Clock::duration, and mathfu types. +// +// (!= [lhs] [rhs]) +// Returns true if two arguments have different values. Valid for: integers, +// floating-point numbers, Clock::duration, and mathfu types. +// +// (< [lhs] [rhs]) +// Returns true if first argument's value is less than the second argument's +// value. Valid for: integers, floating-point numbers, and Clock::duration. +// +// (> [lhs] [rhs]) +// Returns true if first argument's value is greater than the second +// argument's value. Valid for: integers, floating-point numbers, and +// Clock::duration. +// +// (<= [lhs] [rhs]) +// Returns true if first argument's value is less than or equal to the second +// argument's value. Valid for: integers, floating-point numbers, and +// Clock::duration. +// +// (>= [lhs] [rhs]) +// Returns true if first argument's value is greater than or equal to the +// second argument's value. Valid for: integers, floating-point numbers, and +// Clock::duration. +// +// (+ [value] [value]) +// Returns the sum of the arguments. +// +// (- [value] [value]) +// Returns the difference of the arguments. +// +// (* [value] [value]) +// Returns the multiplicative product of the arguments. +// +// (/ [value] [value]) +// Returns the divided quotient of the arguments. +// +// (% [value] [value]) +// Returns the modulo of the arguments. +// +// (and [args...]) +// Returns false if any of the arguments is false, true otherwise. +// +// (or [args...]) +// Returns true if any of the arguments is true, false otherwise. +// +// (not [arg]) +// Returns false if arg is true or true if arg is false. + +namespace redux { +namespace script_functions { + +// For each operator, we want: +// +// - A template function for the operator that returns an std::false_type. This +// declaration is used as a fallback if an actual operator isn't defined that +// operates on the two types. +// +// - A Traits class that has a single "kEnabled" constexpr bool value that we +// use to decide if we actually want to support this operator for the given +// types. The default implementation for this class uses the return type of +// the result of applying the operator to both types. If the return type is +// std::false_type, it means we could only "find" the template function we +// defined earlier. If we get a different return type, it means that an actual +// operator is defined somewhere for these two arguments. +// +// - A ScriptOp class with an operator() that applies the operator to the args +// and stores it into the ScriptFrame. By "default", this will be a no-op +// (with a CHECK-fail). However, if our Traits class above declares that +// the function is valid for the given types, then we'll call the operator +// directly and store the result in the ScriptFrame. + +#define SCRIPT_OP(NAME__, OP__) \ + template \ + std::false_type operator OP__(T, U); \ + \ + template \ + struct NAME__##Traits { \ + using Res = decltype(std::declval() OP__ std::declval()); \ + static constexpr bool kEnabled = !std::is_same_v; \ + }; \ + \ + struct ScriptOp##NAME__ { \ + void operator()(ScriptFrame*, const void*, const void*) const { \ + CHECK(false); \ + } \ + \ + template \ + typename std::enable_if::kEnabled, void>::type \ + operator()(ScriptFrame* frame, const T* lhs, const U* rhs) const { \ + frame->Return(*lhs OP__ *rhs); \ + } \ + }; + +// Define the above templates for all the binary operators we want to support. +SCRIPT_OP(Eq, ==); +SCRIPT_OP(Ne, !=); +SCRIPT_OP(Lt, <); +SCRIPT_OP(Le, <=); +SCRIPT_OP(Gt, >); +SCRIPT_OP(Ge, >=); +SCRIPT_OP(Add, +); +SCRIPT_OP(Sub, -); +SCRIPT_OP(Mul, *); +SCRIPT_OP(Div, /); +SCRIPT_OP(Mod, %); + +// There are cases (mostly dealing with templates) where we have a declaration +// that "matches" what we're looking for, but we can't actually call that +// operator. In such cases, we can explicitly "disable" the lookup. +struct DisableOp { + static constexpr bool kEnabled = false; +}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; + +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; +template <> +struct MulTraits : DisableOp {}; + +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; +template <> +struct DivTraits : DisableOp {}; + +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; + +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; + +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; +template <> +struct ModTraits : DisableOp {}; + +// All the different types against which we want to be able to apply a binary +// operator. +#define EXPAND_TYPES \ + TYPE(int) \ + TYPE(unsigned int) \ + TYPE(float) \ + TYPE(double) \ + TYPE(int8_t) \ + TYPE(uint8_t) \ + TYPE(int16_t) \ + TYPE(uint16_t) \ + TYPE(int64_t) \ + TYPE(uint64_t) \ + TYPE(vec2i) \ + TYPE(vec3i) \ + TYPE(vec4i) \ + TYPE(vec2) \ + TYPE(vec3) \ + TYPE(vec4) \ + TYPE(quat) \ + TYPE(Entity) \ + TYPE(absl::Duration) + +template +void ExpandRhs(ScriptFrame* frame, const T* lhs, const ScriptValue& rhs) { +#define TYPE(T__) \ + if (auto x = rhs.Get()) { \ + Op{}(frame, lhs, x); \ + return; \ + } + EXPAND_TYPES +#undef TYPE + CHECK(false); +} + +template +void ExpandLhs(ScriptFrame* frame, const ScriptValue& lhs, + const ScriptValue& rhs) { +#define TYPE(T__) \ + if (auto x = lhs.Get()) { \ + ExpandRhs(frame, x, rhs); \ + return; \ + } + EXPAND_TYPES +#undef TYPE + CHECK(false) << lhs.GetTypeId() << " " << rhs.GetTypeId(); +} + +template +void ApplyOperator(ScriptFrame* frame) { + const ScriptValue a1 = frame->EvalNext(); + const ScriptValue a2 = frame->EvalNext(); + ExpandLhs(frame, a1, a2); +} + +inline void EqFn(ScriptFrame* frame) { ApplyOperator(frame); } + +inline void NeFn(ScriptFrame* frame) { ApplyOperator(frame); } + +inline void LtFn(ScriptFrame* frame) { ApplyOperator(frame); } + +inline void LeFn(ScriptFrame* frame) { ApplyOperator(frame); } + +inline void GtFn(ScriptFrame* frame) { ApplyOperator(frame); } + +inline void GeFn(ScriptFrame* frame) { ApplyOperator(frame); } + +inline void AddFn(ScriptFrame* frame) { ApplyOperator(frame); } + +inline void SubFn(ScriptFrame* frame) { ApplyOperator(frame); } + +inline void MulFn(ScriptFrame* frame) { ApplyOperator(frame); } + +inline void DivFn(ScriptFrame* frame) { ApplyOperator(frame); } + +inline void ModFn(ScriptFrame* frame) { ApplyOperator(frame); } + +inline void AndFn(ScriptFrame* frame) { + if (!frame->HasNext()) { + frame->Return(false); + return; + } + + bool result = true; + while (result && frame->HasNext()) { + ScriptValue arg = frame->EvalNext(); + if (!arg.Is()) { + frame->Error("and: argument should have type bool."); + return; + } + result &= *arg.Get(); + } + frame->Return(result); +} + +inline void OrFn(ScriptFrame* frame) { + if (!frame->HasNext()) { + frame->Return(false); + return; + } + + bool result = false; + while (!result && frame->HasNext()) { + ScriptValue arg = frame->EvalNext(); + if (!arg.Is()) { + frame->Error("or: argument should have type bool."); + return; + } + result |= *arg.Get(); + } + frame->Return(result); +} + +inline bool NotFn(bool arg) { return !arg; } + +} // namespace script_functions +} // namespace redux + +#endif // REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_OPERATORS_H_ diff --git a/redux/redux/engines/script/redux/functions/operators_tests.cc b/redux/redux/engines/script/redux/functions/operators_tests.cc new file mode 100644 index 0000000..03c7085 --- /dev/null +++ b/redux/redux/engines/script/redux/functions/operators_tests.cc @@ -0,0 +1,179 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/engines/script/redux/script_env.h" +#include "redux/engines/script/redux/testing.h" + +namespace redux { +namespace { + +TEST(ScriptOperatorTest, BasicMath) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(+ 1 1)", 2); + REDUX_CHECK_SCRIPT_RESULT(env, "(- 4 1)", 3); + REDUX_CHECK_SCRIPT_RESULT(env, "(* 2 3)", 6); + REDUX_CHECK_SCRIPT_RESULT(env, "(/ 8 2)", 4); + REDUX_CHECK_SCRIPT_RESULT(env, "(+ 1u 1u)", 2u); + REDUX_CHECK_SCRIPT_RESULT(env, "(- 4u 1u)", 3u); + REDUX_CHECK_SCRIPT_RESULT(env, "(* 2u 3u)", 6u); + REDUX_CHECK_SCRIPT_RESULT(env, "(/ 8u 2u)", 4u); + REDUX_CHECK_SCRIPT_RESULT(env, "(+ 1.f 1.f)", 2.f); + REDUX_CHECK_SCRIPT_RESULT(env, "(- 4.f 1.f)", 3.f); + REDUX_CHECK_SCRIPT_RESULT(env, "(* 2.f 3.f)", 6.f); + REDUX_CHECK_SCRIPT_RESULT(env, "(/ 8.f 2.f)", 4.f); + REDUX_CHECK_SCRIPT_RESULT(env, "(+ 1.0 1.0)", 2.0); + REDUX_CHECK_SCRIPT_RESULT(env, "(- 4.0 1.0)", 3.0); + REDUX_CHECK_SCRIPT_RESULT(env, "(* 2.0 3.0)", 6.0); + REDUX_CHECK_SCRIPT_RESULT(env, "(/ 8.0 2.0)", 4.0); +} + +TEST(ScriptOperatorTest, BasicComparisons) { + ScriptEnv env; + + REDUX_CHECK_SCRIPT_RESULT(env, "(== 1 1)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(== 1 2)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(!= 1 1)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(!= 1 2)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "( < 1 2)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "( < 1 1)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( < 2 1)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(<= 1 2)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(<= 1 1)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(<= 2 1)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( > 1 2)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( > 1 1)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( > 2 1)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(>= 1 2)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(>= 1 1)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(>= 2 1)", true); + + REDUX_CHECK_SCRIPT_RESULT(env, "(== 1u 1u)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(== 1u 2u)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(!= 1u 1u)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(!= 1u 2u)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "( < 1u 2u)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "( < 1u 1u)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( < 2u 1u)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(<= 1u 2u)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(<= 1u 1u)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(<= 2u 1u)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( > 1u 2u)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( > 1u 1u)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( > 2u 1u)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(>= 1u 2u)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(>= 1u 1u)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(>= 2u 1u)", true); + + REDUX_CHECK_SCRIPT_RESULT(env, "(== 1.f 1.f)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(== 1.f 2.f)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(!= 1.f 1.f)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(!= 1.f 2.f)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "( < 1.f 2.f)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "( < 1.f 1.f)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( < 2.f 1.f)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(<= 1.f 2.f)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(<= 1.f 1.f)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(<= 2.f 1.f)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( > 1.f 2.f)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( > 1.f 1.f)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( > 2.f 1.f)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(>= 1.f 2.f)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(>= 1.f 1.f)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(>= 2.f 1.f)", true); + + REDUX_CHECK_SCRIPT_RESULT(env, "(== 1.0 1.0)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(== 1.0 2.0)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(!= 1.0 1.0)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(!= 1.0 2.0)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "( < 1.0 2.0)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "( < 1.0 1.0)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( < 2.0 1.0)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(<= 1.0 2.0)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(<= 1.0 1.0)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(<= 2.0 1.0)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( > 1.0 2.0)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( > 1.0 1.0)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( > 2.0 1.0)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(>= 1.0 2.0)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(>= 1.0 1.0)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(>= 2.0 1.0)", true); +} + +TEST(ScriptOperatorTest, And) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(and)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(and true)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(and false)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(and true true)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(and true false)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(and false true)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(and true true true)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(and true false true)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(and false false false)", false); +} + +TEST(ScriptOperatorTest, Or) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(or)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(or true)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(or false)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(or true true)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(or true false)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(or false true)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(or true true true)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(or true false true)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(or false false false)", false); +} + +TEST(ScriptOperatorTest, Not) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(not true)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(not false)", true); +} + +TEST(ScriptOperatorTest, Time) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(== (seconds 1.f) (seconds 1.f))", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(== (seconds 1.f) (seconds 2.f))", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(!= (seconds 1.f) (seconds 1.f))", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(!= (seconds 1.f) (seconds 2.f))", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(+ (seconds 1.0f) (seconds 2.0f))", + absl::Seconds(3.f)); +} + +TEST(ScriptOperatorTest, Entity) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(== (entity 1) (entity 1))", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(== (entity 1) (entity 2))", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(!= (entity 1) (entity 1))", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(!= (entity 1) (entity 2))", true); + REDUX_CHECK_SCRIPT_RESULT(env, "( < (entity 1) (entity 2))", true); + REDUX_CHECK_SCRIPT_RESULT(env, "( < (entity 1) (entity 1))", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( < (entity 2) (entity 1))", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( > (entity 1) (entity 2))", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( > (entity 1) (entity 1))", false); + REDUX_CHECK_SCRIPT_RESULT(env, "( > (entity 2) (entity 1))", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(<= (entity 1) (entity 2))", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(<= (entity 1) (entity 1))", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(<= (entity 2) (entity 1))", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(>= (entity 1) (entity 2))", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(>= (entity 1) (entity 1))", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(>= (entity 2) (entity 1))", true); +} +} // namespace +} // namespace redux diff --git a/redux/redux/engines/script/redux/functions/typeof.h b/redux/redux/engines/script/redux/functions/typeof.h new file mode 100644 index 0000000..8725477 --- /dev/null +++ b/redux/redux/engines/script/redux/functions/typeof.h @@ -0,0 +1,62 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_TYPEOF_H_ +#define REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_TYPEOF_H_ + +#include "redux/engines/script/redux/script_env.h" + +// This file implements the following script functions: +// +// (nil? [value]) +// Returns true if the variant is empty. +// +// (typeof [value]) +// Returns the TypeId of the value. +// +// (is [value] [symbol]) +// Returns true if the specified value is of the same type as identified by +// the symbol. For example, '(is 1.0 float)' will return true. + +namespace redux { +namespace script_functions { + +inline void IsNilFn(ScriptFrame* frame) { + const ScriptValue value = frame->EvalNext(); + frame->Return(value.IsNil()); +} + +inline void TypeOfFn(ScriptFrame* frame) { + const ScriptValue value = frame->EvalNext(); + frame->Return(value.GetTypeId()); +} + +inline void IsFn(ScriptFrame* frame) { + bool match = false; + const TypeId lhs = frame->EvalNext().GetTypeId(); + + const ScriptValue& type_value = frame->Next(); + if (const AstNode* node = type_value.Get()) { + if (const Symbol* symbol = node->first.Get()) { + match = (lhs == symbol->value.get()); + } + } + frame->Return(match); +} +} // namespace script_functions +} // namespace redux + +#endif // REDUX_ENGINES_SCRIPT_REDUX_FUNCTIONS_TYPEOF_H_ diff --git a/redux/redux/engines/script/redux/functions/typeof_tests.cc b/redux/redux/engines/script/redux/functions/typeof_tests.cc new file mode 100644 index 0000000..a4e6615 --- /dev/null +++ b/redux/redux/engines/script/redux/functions/typeof_tests.cc @@ -0,0 +1,50 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/engines/script/redux/script_env.h" +#include "redux/engines/script/redux/testing.h" + +namespace redux { +namespace { + +TEST(ScriptTypeOfTest, IsNil) { + ScriptEnv env; + env.SetValue(ConstHash("nillie"), Var()); + + REDUX_CHECK_SCRIPT_RESULT(env, "(nil? 1)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(nil? 2.0f)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(nil? 'hello')", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(nil? nillie)", true); +} + +TEST(ScriptTypeOfTest, TypeOf) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(typeof 1)", GetTypeId()); + REDUX_CHECK_SCRIPT_RESULT(env, "(typeof 2.0f)", GetTypeId()); + REDUX_CHECK_SCRIPT_RESULT(env, "(typeof 'hi')", GetTypeId()); +} + +TEST(ScriptTypeOfTest, Is) { + ScriptEnv env; + REDUX_CHECK_SCRIPT_RESULT(env, "(is? 1.0f int)", false); + REDUX_CHECK_SCRIPT_RESULT(env, "(is? 1.0f float)", true); + REDUX_CHECK_SCRIPT_RESULT(env, "(is? 'hello' std::string)", true); +} + +} // namespace +} // namespace redux diff --git a/redux/redux/engines/script/redux/script_ast_builder.cc b/redux/redux/engines/script/redux/script_ast_builder.cc new file mode 100644 index 0000000..82cc988 --- /dev/null +++ b/redux/redux/engines/script/redux/script_ast_builder.cc @@ -0,0 +1,184 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/script/redux/script_ast_builder.h" + +#include "redux/modules/base/logging.h" + +namespace redux { + +ScriptAstBuilder::ScriptAstBuilder() { Push(); } + +void ScriptAstBuilder::Process(TokenType type, const void* ptr, + std::string_view token) { + switch (type) { + case kPush: { + Push(); + break; + } + case kPop: { + Pop(); + break; + } + case kPushArray: { + Push(); + Append(Symbol(ConstHash("make-array")), "make-array"); + break; + } + case kPopArray: { + Pop(); + break; + } + case kPushMap: { + Push(); + Append(Symbol(ConstHash("make-map")), "make-map"); + break; + } + case kPopMap: { + Pop(); + break; + } + case kBool: { + const auto value = *reinterpret_cast(ptr); + Append(value, token); + break; + } + case kInt8: { + const auto value = *reinterpret_cast(ptr); + Append(value, token); + break; + } + case kUint8: { + const auto value = *reinterpret_cast(ptr); + Append(value, token); + break; + } + case kInt16: { + const auto value = *reinterpret_cast(ptr); + Append(value, token); + break; + } + case kUint16: { + const auto value = *reinterpret_cast(ptr); + Append(value, token); + break; + } + case kInt32: { + const auto value = *reinterpret_cast(ptr); + Append(value, token); + break; + } + case kUint32: { + const auto value = *reinterpret_cast(ptr); + Append(value, token); + break; + } + case kInt64: { + const auto value = *reinterpret_cast(ptr); + Append(value, token); + break; + } + case kUint64: { + const auto value = *reinterpret_cast(ptr); + Append(value, token); + break; + } + case kFloat: { + const auto value = *reinterpret_cast(ptr); + Append(value, token); + break; + } + case kDouble: { + const auto value = *reinterpret_cast(ptr); + Append(value, token); + break; + } + case kHashValue: { + const auto value = *reinterpret_cast(ptr); + Append(value, token); + break; + } + case kNull: { + Append(Var(), token); + break; + } + case kSymbol: { + const auto value = *reinterpret_cast(ptr); + Append(value, token); + break; + } + case kString: { + const std::string value(*reinterpret_cast(ptr)); + Append(value, token); + break; + } + case kEof: { + break; + } + } +} + +void ScriptAstBuilder::Append(Var value, std::string_view token) { + ScriptValue first(std::move(value)); + ScriptValue node(AstNode(std::move(first), ScriptValue())); + + List& list = stack_.back(); + if (list.head.IsNil()) { + // The current list being created is empty, so both the head and tail should + // point to the newly created value. + list.head = node; + list.tail = node; + } else { + // Set the tail's sibling to the newly created value. + list.tail.Get()->rest = node; + // Update the tail to point to the newly created value. + list.tail = node; + } +} + +void ScriptAstBuilder::Push() { stack_.emplace_back(); } + +void ScriptAstBuilder::Pop() { + List list = std::move(stack_.back()); + stack_.pop_back(); + + if (Var* var = list.head.Get()) { + Append(std::move(*var), ""); + } else { + LOG(ERROR) << "There were errors trying to build the AST."; + } +} + +const AstNode* ScriptAstBuilder::GetRoot() const { + if (error_) { + return nullptr; + } + if (stack_.empty()) { + return nullptr; + } + const List& list = stack_.back(); + if (list.head.IsNil()) { + return nullptr; + } + return list.head.Get(); +} + +void ScriptAstBuilder::Error(std::string_view token, std::string_view message) { + LOG(WARNING) << "Error parsing " << token << ": " << message; + error_ = true; +} + +} // namespace redux diff --git a/redux/redux/engines/script/redux/script_ast_builder.h b/redux/redux/engines/script/redux/script_ast_builder.h new file mode 100644 index 0000000..11de921 --- /dev/null +++ b/redux/redux/engines/script/redux/script_ast_builder.h @@ -0,0 +1,58 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_SCRIPT_REDUX_SCRIPT_AST_BUILDER_H_ +#define REDUX_ENGINES_SCRIPT_REDUX_SCRIPT_AST_BUILDER_H_ + +#include + +#include "redux/engines/script/redux/script_parser.h" +#include "redux/engines/script/redux/script_types.h" + +namespace redux { + +// ParserCallback that generates the abstract syntax tree (AST) representation. +class ScriptAstBuilder : public ParserCallbacks { + public: + ScriptAstBuilder(); + + // Creates an AstNode from the given |type| and associated data. + void Process(TokenType type, const void* ptr, + std::string_view token) override; + + // Returns the root of the AST from the processed data. + const AstNode* GetRoot() const; + + // Sets the internal state to an error state. + void Error(std::string_view token, std::string_view message) override; + + private: + struct List { + ScriptValue head; + ScriptValue tail; + }; + + void Append(Var value, std::string_view token); + void Push(); + void Pop(); + + std::vector stack_; + bool error_ = false; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_SCRIPT_REDUX_SCRIPT_AST_BUILDER_H_ diff --git a/redux/redux/engines/script/redux/script_compiler.cc b/redux/redux/engines/script/redux/script_compiler.cc new file mode 100644 index 0000000..a43a7a2 --- /dev/null +++ b/redux/redux/engines/script/redux/script_compiler.cc @@ -0,0 +1,263 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/script/redux/script_compiler.h" + +#include "redux/engines/script/redux/script_env.h" +#include "redux/engines/script/redux/script_types.h" + +namespace redux { + +static const uint8_t kByteCodeMarker[] = {')', ')', '(', '('}; + +class ByteCodeWriter { + public: + explicit ByteCodeWriter(ScriptCompiler::CodeBuffer* code) : code_(code) { + if (code_->empty()) { + code_->emplace_back(kByteCodeMarker[0]); + code_->emplace_back(kByteCodeMarker[1]); + code_->emplace_back(kByteCodeMarker[2]); + code_->emplace_back(kByteCodeMarker[3]); + } + } + + template + void operator()(const T& value) { + const uint8_t* bytes = reinterpret_cast(&value); + code_->insert(code_->end(), bytes, bytes + sizeof(T)); + } + + void operator()(std::string_view str) { + (*this)(str.size()); + if (!str.empty()) { + code_->insert(code_->end(), str.begin(), str.end()); + } + } + + private: + ScriptCompiler::CodeBuffer* code_ = nullptr; +}; + +class ByteCodeReader { + public: + explicit ByteCodeReader(ScriptCompiler::CodeBuffer* code, + ParserCallbacks* builder) + : code_(code), builder_(builder) { + if (code_->size() <= 4) { + LOG(ERROR) << "Bytecode is empty."; + return; + } + CHECK((*code_)[0] == kByteCodeMarker[0]); + CHECK((*code_)[1] == kByteCodeMarker[1]); + CHECK((*code_)[2] == kByteCodeMarker[2]); + CHECK((*code_)[3] == kByteCodeMarker[3]); + read_head_ = 4; + } + + template + T Process(ParserCallbacks::TokenType type) { + if constexpr (std::is_same_v) { + builder_->Process(type, nullptr, ""); + return nullptr; + } else if constexpr (std::is_same_v) { + T value = HashValue(Read()); + builder_->Process(type, &value, ""); + return value; + } else if constexpr (std::is_same_v) { + T value = Symbol(HashValue(Read())); + builder_->Process(type, &value, ""); + return value; + } else if constexpr (std::is_same_v) { + std::string_view view; + const size_t size = Read(); + if (size > 0) { + const char* str = reinterpret_cast(&(*code_)[read_head_]); + view = std::string_view(str, size); + read_head_ += size; + } + builder_->Process(type, &view, ""); + return view; + } else { + T value = Read(); + builder_->Process(type, &value, ""); + return value; + } + } + + template + T Read() { + const size_t size = sizeof(T); + const T* ptr = reinterpret_cast(&(*code_)[read_head_]); + read_head_ += size; + return *ptr; + } + + private: + ScriptCompiler::CodeBuffer* code_ = nullptr; + ParserCallbacks* builder_ = nullptr; + size_t read_head_ = 0; +}; + +ScriptCompiler::ScriptCompiler(CodeBuffer* code) : code_(code) {} + +void ScriptCompiler::Process(TokenType type, const void* ptr, + std::string_view token) { + if (error_) { + return; + } + + ByteCodeWriter writer(code_); + + const int code_value = type; + writer(code_value); + + switch (type) { + case kBool: + writer(*reinterpret_cast(ptr)); + break; + case kInt8: + writer(*reinterpret_cast(ptr)); + break; + case kUint8: + writer(*reinterpret_cast(ptr)); + break; + case kInt16: + writer(*reinterpret_cast(ptr)); + break; + case kUint16: + writer(*reinterpret_cast(ptr)); + break; + case kInt32: + writer(*reinterpret_cast(ptr)); + break; + case kUint32: + writer(*reinterpret_cast(ptr)); + break; + case kInt64: + writer(*reinterpret_cast(ptr)); + break; + case kUint64: + writer(*reinterpret_cast(ptr)); + break; + case kFloat: + writer(*reinterpret_cast(ptr)); + break; + case kDouble: + writer(*reinterpret_cast(ptr)); + break; + case kHashValue: + writer(reinterpret_cast(ptr)->get()); + break; + case kSymbol: + writer(reinterpret_cast(ptr)->value.get()); + break; + case kString: + writer(*reinterpret_cast(ptr)); + break; + case kNull: + case kEof: + case kPush: + case kPop: + case kPushArray: + case kPopArray: + case kPushMap: + case kPopMap: + break; + } +} + +void ScriptCompiler::Build(ParserCallbacks* builder) { + ByteCodeReader reader(code_, builder); + + bool done = false; + while (!done) { + int code = reader.Read(); + + const TokenType type = static_cast(code); + switch (type) { + case kBool: + reader.Process(type); + break; + case kInt8: + reader.Process(type); + break; + case kUint8: + reader.Process(type); + break; + case kInt16: + reader.Process(type); + break; + case kUint16: + reader.Process(type); + break; + case kInt32: + reader.Process(type); + break; + case kUint32: + reader.Process(type); + break; + case kInt64: + reader.Process(type); + break; + case kUint64: + reader.Process(type); + break; + case kFloat: + reader.Process(type); + break; + case kDouble: + reader.Process(type); + break; + case kHashValue: + reader.Process(type); + break; + case kSymbol: + reader.Process(type); + break; + case kString: + reader.Process(type); + break; + case kNull: + case kPush: + case kPop: + case kPushArray: + case kPopArray: + case kPushMap: + case kPopMap: + case kEof: + reader.Process(type); + break; + } + + if (type == kEof) { + done = true; + } + } +} + +void ScriptCompiler::Error(std::string_view token, std::string_view message) { + LOG(WARNING) << "Error parsing " << token << ": " << message; + code_->clear(); + error_ = true; +} + +bool ScriptCompiler::IsByteCode(absl::Span bytes) { + return bytes.size() > 4 && bytes[0] == kByteCodeMarker[0] && + bytes[1] == kByteCodeMarker[1] && bytes[2] == kByteCodeMarker[2] && + bytes[3] == kByteCodeMarker[3]; +} + +} // namespace redux diff --git a/redux/redux/engines/script/redux/script_compiler.h b/redux/redux/engines/script/redux/script_compiler.h new file mode 100644 index 0000000..8239094 --- /dev/null +++ b/redux/redux/engines/script/redux/script_compiler.h @@ -0,0 +1,63 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef REDUX_ENGINES_SCRIPT_REDUX_SCRIPT_COMPILER_H_ +#define REDUX_ENGINES_SCRIPT_REDUX_SCRIPT_COMPILER_H_ + +#include + +#include "redux/engines/script/redux/script_parser.h" +#include "redux/engines/script/redux/script_types.h" + +namespace redux { + +class ScriptEnv; + +// ParserCallback that generates a binary block of data that can then saved to +// disk or built into the AST. +// +// Specifically, source code can be compiled into a byte array by passing a +// ScriptCompiler to the ParseScript function as part of the build process. +// The byte array can then be converted (again using the ScriptCompiler) to +// the appropriate runtime structure by calling ScriptCompiler::Build and +// passing it another set of ParserCallbacks. +class ScriptCompiler : public ParserCallbacks { + public: + using CodeBuffer = std::vector; + explicit ScriptCompiler(CodeBuffer* code); + + // Stores the |type| and associated data into the byte array buffer. + void Process(TokenType type, const void* ptr, + std::string_view token) override; + + // Processes the stored byte array buffer into another sequence of + // ParserCallbacks. + void Build(ParserCallbacks* builder); + + // Sets the internal state to an error state. + void Error(std::string_view token, std::string_view message) override; + + // Determines if the specified array of bytes is actually ScriptByteCode. + static bool IsByteCode(absl::Span bytes); + + private: + CodeBuffer* code_ = nullptr; + bool error_ = false; +}; + +} // namespace redux + +#endif // REDUX_ENGINES_SCRIPT_REDUX_SCRIPT_COMPILER_H_ diff --git a/redux/redux/engines/script/redux/script_compiler_tests.cc b/redux/redux/engines/script/redux/script_compiler_tests.cc new file mode 100644 index 0000000..1455f16 --- /dev/null +++ b/redux/redux/engines/script/redux/script_compiler_tests.cc @@ -0,0 +1,204 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "redux/engines/script/redux/script_compiler.h" + +namespace redux { +namespace { + +using ::testing::Eq; + +struct Entry { + ParserCallbacks::TokenType type; + Var value; +}; + +template +bool Compare(const Var& lhs, const Var& rhs) { + const T* a = lhs.Get(); + const T* b = rhs.Get(); + const bool result = a && b && *a == *b; + return result; +} + +bool operator==(const Entry& lhs, const Entry& rhs) { + if (lhs.type != rhs.type) { + return false; + } else if (lhs.value.GetTypeId() != rhs.value.GetTypeId()) { + return false; + } + switch (lhs.type) { + case ParserCallbacks::kEof: + return true; + case ParserCallbacks::kPush: + return true; + case ParserCallbacks::kPop: + return true; + case ParserCallbacks::kPushArray: + return true; + case ParserCallbacks::kPopArray: + return true; + case ParserCallbacks::kPushMap: + return true; + case ParserCallbacks::kPopMap: + return true; + case ParserCallbacks::kNull: + return lhs.value.Empty() && rhs.value.Empty(); + case ParserCallbacks::kBool: + return Compare(lhs.value, rhs.value); + case ParserCallbacks::kInt8: + return Compare(lhs.value, rhs.value); + case ParserCallbacks::kUint8: + return Compare(lhs.value, rhs.value); + case ParserCallbacks::kInt16: + return Compare(lhs.value, rhs.value); + case ParserCallbacks::kUint16: + return Compare(lhs.value, rhs.value); + case ParserCallbacks::kInt32: + return Compare(lhs.value, rhs.value); + case ParserCallbacks::kUint32: + return Compare(lhs.value, rhs.value); + case ParserCallbacks::kInt64: + return Compare(lhs.value, rhs.value); + case ParserCallbacks::kUint64: + return Compare(lhs.value, rhs.value); + case ParserCallbacks::kFloat: + return Compare(lhs.value, rhs.value); + case ParserCallbacks::kDouble: + return Compare(lhs.value, rhs.value); + case ParserCallbacks::kHashValue: + return Compare(lhs.value, rhs.value); + case ParserCallbacks::kSymbol: + return Compare(lhs.value, rhs.value); + case ParserCallbacks::kString: + return Compare(lhs.value, rhs.value); + } +} + +struct TestParserCallbacks : ParserCallbacks { + void Expect(TokenType type, Var var = Var()) { + expected.push_back({type, std::move(var)}); + } + + void Process(TokenType type, const void* ptr, + std::string_view token) override { + Var var; + switch (type) { + case kEof: + break; + case kPush: + break; + case kPop: + break; + case kPushArray: + break; + case kPopArray: + break; + case kPushMap: + break; + case kPopMap: + break; + case kNull: + break; + case kBool: + var = *reinterpret_cast(ptr); + break; + case kInt8: + var = *reinterpret_cast(ptr); + break; + case kUint8: + var = *reinterpret_cast(ptr); + break; + case kInt16: + var = *reinterpret_cast(ptr); + break; + case kUint16: + var = *reinterpret_cast(ptr); + break; + case kInt32: + var = *reinterpret_cast(ptr); + break; + case kUint32: + var = *reinterpret_cast(ptr); + break; + case kInt64: + var = *reinterpret_cast(ptr); + break; + case kUint64: + var = *reinterpret_cast(ptr); + break; + case kFloat: + var = *reinterpret_cast(ptr); + break; + case kDouble: + var = *reinterpret_cast(ptr); + break; + case kSymbol: + var = *reinterpret_cast(ptr); + break; + case kHashValue: + var = *reinterpret_cast(ptr); + break; + case kString: + var = std::string(*reinterpret_cast(ptr)); + break; + } + parsed.push_back({type, var}); + } + + void Error(std::string_view token, std::string_view message) override { + errors.emplace_back(token); + } + + std::vector parsed; + std::vector expected; + std::vector errors; +}; + +TEST(ScriptCompilerTest, CompileAndBuild) { + std::vector buffer; + + ScriptCompiler saver(&buffer); + ParseScript("(1 (2.0f (true (false) null 'hello') world))", &saver); + + ScriptCompiler loader(&buffer); + + TestParserCallbacks callbacks; + loader.Build(&callbacks); + + callbacks.Expect(ParserCallbacks::kPush, Var()); + callbacks.Expect(ParserCallbacks::kInt32, 1); + callbacks.Expect(ParserCallbacks::kPush); + callbacks.Expect(ParserCallbacks::kFloat, 2.f); + callbacks.Expect(ParserCallbacks::kPush); + callbacks.Expect(ParserCallbacks::kBool, true); + callbacks.Expect(ParserCallbacks::kPush); + callbacks.Expect(ParserCallbacks::kBool, false); + callbacks.Expect(ParserCallbacks::kPop); + callbacks.Expect(ParserCallbacks::kNull); + callbacks.Expect(ParserCallbacks::kString, std::string("hello")); + callbacks.Expect(ParserCallbacks::kPop); + callbacks.Expect(ParserCallbacks::kSymbol, Symbol(ConstHash("world"))); + callbacks.Expect(ParserCallbacks::kPop); + callbacks.Expect(ParserCallbacks::kPop); + callbacks.Expect(ParserCallbacks::kEof); + + EXPECT_THAT(callbacks.parsed, Eq(callbacks.expected)); +} +} // namespace +} // namespace redux diff --git a/redux/redux/engines/script/redux/script_engine.cc b/redux/redux/engines/script/redux/script_engine.cc new file mode 100644 index 0000000..90199a5 --- /dev/null +++ b/redux/redux/engines/script/redux/script_engine.cc @@ -0,0 +1,167 @@ +/* +Copyright 2017-2022 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS-IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "redux/engines/script/script_engine.h" + +#include +#include + +#include "redux/engines/script/redux/script_env.h" +#include "redux/engines/script/redux/script_stack.h" +#include "redux/engines/script/redux/script_value.h" +#include "redux/modules/base/asset_loader.h" +#include "redux/modules/base/static_registry.h" +#include "redux/modules/ecs/entity.h" +#include "redux/modules/math/matrix.h" +#include "redux/modules/math/quaternion.h" +#include "redux/modules/math/vector.h" +#include "redux/modules/var/var.h" +#include "redux/modules/var/var_array.h" +#include "redux/modules/var/var_table.h" + +namespace redux { + +class ScriptCallContextImpl : public ScriptCallContext { + public: + explicit ScriptCallContextImpl(ScriptFrameContext* context) + : context_(context) {} + ScriptFrameContext* context_; +}; + +class ScriptImpl : public Script { + public: + ScriptImpl(std::unique_ptr env, ScriptValue script) + : env_(std::move(env)), script_(std::move(script)) {} + + std::unique_ptr env_; + ScriptValue script_; +}; + +class ScriptEngineImpl : public ScriptEngine { + public: + explicit ScriptEngineImpl(Registry* registry) : ScriptEngine(registry) {} + + ScriptStack* Globals() { return &globals_; } + + void RegisterFunctionImpl(std::string_view name, ScriptableFn fn); + void UnregisterFunctionImpl(std::string_view name); + void SetEnumValueImpl(std::string_view name, Var value); + + private: + ScriptStack globals_; +}; + +void ScriptEngineImpl::RegisterFunctionImpl(std::string_view name, + ScriptableFn fn) { + auto wrapped_fn = [=](ScriptFrame* frame) mutable { + ScriptFrameContext context(frame); + ScriptCallContextImpl impl(&context); + fn(&impl); + }; + + const HashValue key = Hash(name); + globals_.SetValue(key, ScriptValue(NativeFunction(wrapped_fn))); +} + +void ScriptEngineImpl::UnregisterFunctionImpl(std::string_view name) { + const HashValue key = Hash(name); + globals_.SetValue(key, ScriptValue()); +} + +void ScriptEngineImpl::SetEnumValueImpl(std::string_view name, Var value) { + const HashValue key = Hash(name); + globals_.SetValue(key, ScriptValue(value)); +} + +ScriptEngine::ScriptEngine(Registry* registry) : registry_(registry) {} + +void ScriptEngine::Create(Registry* registry) { + auto ptr = new ScriptEngineImpl(registry); + registry->Register(std::unique_ptr(ptr)); +} + +std::unique_ptr