jolt-ruby 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitmodules +3 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +228 -0
  5. data/Rakefile +180 -0
  6. data/examples/character_virtual.rb +35 -0
  7. data/examples/falling_sphere.rb +25 -0
  8. data/examples/stagecraft_binding.rb +29 -0
  9. data/ext/jolt_ruby/CMakeLists.txt +42 -0
  10. data/ext/jolt_ruby/character_helper.cpp +44 -0
  11. data/ext/jolt_ruby/constraint_helper.cpp +154 -0
  12. data/ext/jolt_ruby/extconf.rb +56 -0
  13. data/ext/jolt_ruby/helper.cpp +225 -0
  14. data/ext/jolt_ruby/helper.h +103 -0
  15. data/ext/joltc/.github/FUNDING.yml +1 -0
  16. data/ext/joltc/.github/workflows/build.yml +298 -0
  17. data/ext/joltc/.gitignore +272 -0
  18. data/ext/joltc/CMakeLists.txt +431 -0
  19. data/ext/joltc/LICENSE +21 -0
  20. data/ext/joltc/README.md +10 -0
  21. data/ext/joltc/build/cmake_vs2026_arm64.bat +3 -0
  22. data/ext/joltc/build/cmake_vs2026_clang.bat +10 -0
  23. data/ext/joltc/build/cmake_vs2026_x64.bat +3 -0
  24. data/ext/joltc/build/cmake_vs2026_x64_double.bat +3 -0
  25. data/ext/joltc/include/joltc.h +3166 -0
  26. data/ext/joltc/samples/01_HelloWorld/main.cpp +170 -0
  27. data/ext/joltc/samples/CMakeLists.txt +35 -0
  28. data/ext/joltc/src/joltc.c +4 -0
  29. data/ext/joltc/src/joltc.cpp +11812 -0
  30. data/ext/joltc/src/joltc_assert.cpp +271 -0
  31. data/ext/joltc/tests/CMakeLists.txt +77 -0
  32. data/ext/joltc/tests/test_character.cpp +149 -0
  33. data/ext/joltc/tests/test_collision.cpp +146 -0
  34. data/ext/joltc/tests/test_constraints.cpp +353 -0
  35. data/ext/joltc/tests/test_core.cpp +94 -0
  36. data/ext/joltc/tests/test_math.cpp +585 -0
  37. data/ext/joltc/tests/test_physics_system.cpp +465 -0
  38. data/ext/joltc/tests/test_shapes.cpp +789 -0
  39. data/ext/joltc/tests/test_skeleton.cpp +370 -0
  40. data/ext/joltc/tests/test_vehicle.cpp +319 -0
  41. data/generator/generate.rb +237 -0
  42. data/generator/layout_probe.cpp +489 -0
  43. data/generator/verify_layout.rb +32 -0
  44. data/lib/jolt/body.rb +147 -0
  45. data/lib/jolt/body_collection.rb +115 -0
  46. data/lib/jolt/body_dynamics.rb +93 -0
  47. data/lib/jolt/character_virtual.rb +162 -0
  48. data/lib/jolt/constraint.rb +136 -0
  49. data/lib/jolt/constraint_collection.rb +123 -0
  50. data/lib/jolt/contact_events.rb +19 -0
  51. data/lib/jolt/conversions.rb +76 -0
  52. data/lib/jolt/errors.rb +12 -0
  53. data/lib/jolt/fixed_stepper.rb +37 -0
  54. data/lib/jolt/hit.rb +5 -0
  55. data/lib/jolt/layers.rb +182 -0
  56. data/lib/jolt/native/character_functions.rb +16 -0
  57. data/lib/jolt/native/constraint_functions.rb +27 -0
  58. data/lib/jolt/native/core_functions.rb +17 -0
  59. data/lib/jolt/native/generated.rb +1995 -0
  60. data/lib/jolt/native/platform.rb +51 -0
  61. data/lib/jolt/native/query_functions.rb +43 -0
  62. data/lib/jolt/native/types.rb +28 -0
  63. data/lib/jolt/native.rb +94 -0
  64. data/lib/jolt/shape.rb +90 -0
  65. data/lib/jolt/shape_builders.rb +155 -0
  66. data/lib/jolt/system.rb +238 -0
  67. data/lib/jolt/system_characters.rb +25 -0
  68. data/lib/jolt/system_constraints.rb +36 -0
  69. data/lib/jolt/system_contacts.rb +57 -0
  70. data/lib/jolt/system_queries.rb +111 -0
  71. data/lib/jolt/transform.rb +5 -0
  72. data/lib/jolt/version.rb +5 -0
  73. data/lib/jolt.rb +83 -0
  74. data/script/smoke_gem.rb +29 -0
  75. data/script/verify_release.rb +36 -0
  76. metadata +147 -0
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module Jolt
6
+ module Native
7
+ module Platform
8
+ module_function
9
+
10
+ def tag(cpu: RbConfig::CONFIG.fetch("host_cpu"), os: RbConfig::CONFIG.fetch("host_os"))
11
+ case os
12
+ when /darwin/
13
+ "#{darwin_cpu(cpu)}-darwin"
14
+ when /linux/
15
+ "#{linux_cpu(cpu)}-linux"
16
+ when /mswin|mingw/
17
+ "#{windows_cpu(cpu)}-mingw-ucrt"
18
+ else
19
+ "#{cpu}-#{os}".downcase.gsub(/[^a-z0-9._-]+/, "-")
20
+ end
21
+ end
22
+
23
+ def darwin_cpu(cpu)
24
+ case cpu
25
+ when "arm64", "aarch64" then "arm64"
26
+ when "x86_64", "amd64" then "x86_64"
27
+ else cpu
28
+ end
29
+ end
30
+ private_class_method :darwin_cpu
31
+
32
+ def linux_cpu(cpu)
33
+ case cpu
34
+ when "arm64", "aarch64" then "aarch64"
35
+ when "x64", "amd64", "x86_64" then "x86_64"
36
+ else cpu
37
+ end
38
+ end
39
+ private_class_method :linux_cpu
40
+
41
+ def windows_cpu(cpu)
42
+ case cpu
43
+ when "x64", "amd64", "x86_64" then "x64"
44
+ when "arm64", "aarch64" then "arm64"
45
+ else cpu
46
+ end
47
+ end
48
+ private_class_method :windows_cpu
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jolt
4
+ module Native
5
+ module QueryFunctions
6
+ module_function
7
+
8
+ def attach(native)
9
+ install_object_layer_filter(native)
10
+ end
11
+
12
+ def object_layer_filter(native, layer_ids, layer_count)
13
+ context = FFI::MemoryPointer.new(:uint8, layer_count + 4, true)
14
+ context.write_uint32(layer_count)
15
+ layer_ids.each { |id| context.put_uint8(id + 4, 1) }
16
+ filter = native.JPH_ObjectLayerFilter_Create(context)
17
+ raise InitializationError, "failed to create object layer filter" if filter.null?
18
+
19
+ [filter, context]
20
+ end
21
+
22
+ def install_object_layer_filter(native)
23
+ @object_layer_callback ||= FFI::Function.new(:bool, %i[pointer uint32]) do |context, layer|
24
+ count = context.read_uint32
25
+ layer < count && context.get_uint8(layer + 4) == 1
26
+ end
27
+ @object_layer_procs ||= ObjectLayerFilterProcs.new.tap do |procs|
28
+ procs[:should_collide] = @object_layer_callback
29
+ end
30
+ native.JPH_ObjectLayerFilter_SetProcs(@object_layer_procs.pointer)
31
+ end
32
+
33
+ def body_collector
34
+ hits = []
35
+ callback = FFI::Function.new(:float, %i[pointer uint32]) do |_context, body_id|
36
+ hits << body_id
37
+ 1.0
38
+ end
39
+ [callback, hits]
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "generated"
4
+
5
+ module Jolt
6
+ module Native
7
+ Vec3 = Generated::Vec3
8
+ Vec4 = Generated::Vec4
9
+ Quat = Generated::Quat
10
+ Mat4 = Generated::Mat4
11
+ MassProperties = Generated::MassProperties
12
+ PhysicsSystemSettings = Generated::PhysicsSystemSettings
13
+ RayCastResult = Generated::RayCastResult
14
+ IndexedTriangle = Generated::IndexedTriangle
15
+ ObjectLayerFilterProcs = Generated::ObjectLayerFilter_Procs
16
+
17
+ class ContactEvent < FFI::Struct
18
+ layout :type, :uint32,
19
+ :body_a, :uint32,
20
+ :body_b, :uint32,
21
+ :sub_shape_a, :uint32,
22
+ :sub_shape_b, :uint32,
23
+ :point, [:float, 3],
24
+ :normal, [:float, 3],
25
+ :penetration, :float
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+ require "rbconfig"
5
+ require_relative "native/platform"
6
+ require_relative "native/types"
7
+ require_relative "native/core_functions"
8
+ require_relative "native/query_functions"
9
+ require_relative "native/constraint_functions"
10
+ require_relative "native/character_functions"
11
+
12
+ module Jolt
13
+ module Native
14
+ extend FFI::Library
15
+
16
+ class << self
17
+ def load!
18
+ return self if @loaded
19
+
20
+ ffi_lib(*Loader.library_paths)
21
+ Generated.attach(self)
22
+ [
23
+ CoreFunctions,
24
+ QueryFunctions,
25
+ ConstraintFunctions,
26
+ CharacterFunctions
27
+ ].each { |functions| functions.attach(self) }
28
+ @loaded = true
29
+ self
30
+ rescue LoadError => error
31
+ raise NativeLoadError, error.message
32
+ end
33
+
34
+ def loaded?
35
+ @loaded == true
36
+ end
37
+
38
+ end
39
+
40
+ module Loader
41
+ module_function
42
+
43
+ def library_paths
44
+ directory = candidate_directories.find { |path| libraries_exist?(path) }
45
+ return library_names unless directory
46
+
47
+ library_names.map { |name| File.join(directory, name) }
48
+ end
49
+
50
+ def candidate_directories
51
+ [
52
+ ENV["JOLT_RUBY_NATIVE_DIR"],
53
+ File.join(__dir__, "native", platform),
54
+ installed_extension_directory,
55
+ File.join(project_root, "tmp", "native", platform, "lib"),
56
+ File.join(project_root, "tmp", "native", platform, "lib", "Release"),
57
+ File.join(project_root, "tmp", "native", platform, "Release")
58
+ ].compact
59
+ end
60
+
61
+ def installed_extension_directory
62
+ return unless defined?(Gem)
63
+
64
+ specification = Gem.loaded_specs["jolt-ruby"]
65
+ return unless specification
66
+
67
+ File.join(specification.extension_dir, "jolt", "native", platform)
68
+ end
69
+
70
+ def platform
71
+ Platform.tag
72
+ end
73
+
74
+ def project_root
75
+ File.expand_path("../..", __dir__)
76
+ end
77
+
78
+ def library_names
79
+ case RbConfig::CONFIG.fetch("host_os")
80
+ when /darwin/
81
+ %w[libjoltc.dylib libjolt_ruby_helper.dylib]
82
+ when /mswin|mingw/
83
+ %w[joltc.dll jolt_ruby_helper.dll]
84
+ else
85
+ %w[libjoltc.so libjolt_ruby_helper.so]
86
+ end
87
+ end
88
+
89
+ def libraries_exist?(directory)
90
+ library_names.all? { |name| File.file?(File.join(directory, name)) }
91
+ end
92
+ end
93
+ end
94
+ end
data/lib/jolt/shape.rb ADDED
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jolt
4
+ class Shape
5
+ extend ShapeBuilders
6
+
7
+ DEFAULT_CONVEX_RADIUS = 0.05
8
+
9
+ class << self
10
+ def box(half_extents, convex_radius: DEFAULT_CONVEX_RADIUS)
11
+ Jolt.init
12
+ extents = Conversions.native_vec3(half_extents, name: "half_extents")
13
+ %i[x y z].each do |component|
14
+ raise InvalidArgumentError, "half_extents must be positive" unless extents[component].positive?
15
+ end
16
+ radius = Conversions.non_negative_float(convex_radius, "convex_radius")
17
+ build(Native.JPH_BoxShape_Create(extents.pointer, radius), :box)
18
+ end
19
+
20
+ def sphere(radius)
21
+ Jolt.init
22
+ build(Native.JPH_SphereShape_Create(Conversions.positive_float(radius, "radius")), :sphere)
23
+ end
24
+
25
+ def capsule(half_height:, radius:)
26
+ Jolt.init
27
+ height = Conversions.non_negative_float(half_height, "half_height")
28
+ radius = Conversions.positive_float(radius, "radius")
29
+ build(Native.JPH_CapsuleShape_Create(height, radius), :capsule)
30
+ end
31
+
32
+ def cylinder(half_height:, radius:)
33
+ Jolt.init
34
+ height = Conversions.positive_float(half_height, "half_height")
35
+ radius = Conversions.positive_float(radius, "radius")
36
+ build(Native.JPH_CylinderShape_Create(height, radius), :cylinder)
37
+ end
38
+
39
+ private
40
+
41
+ def build(pointer, kind)
42
+ raise ShapeError, "failed to create #{kind} shape" if pointer.null?
43
+
44
+ new(pointer, kind)
45
+ end
46
+ end
47
+
48
+ attr_reader :kind
49
+
50
+ def initialize(pointer, kind)
51
+ @pointer = pointer
52
+ @kind = kind
53
+ @released = false
54
+ end
55
+
56
+ def release
57
+ return if @released
58
+
59
+ Native.JPH_Shape_Destroy(@pointer)
60
+ @released = true
61
+ @pointer = nil
62
+ nil
63
+ end
64
+
65
+ def released?
66
+ @released
67
+ end
68
+
69
+ def volume
70
+ check_alive!
71
+ Native.JPH_Shape_GetVolume(@pointer)
72
+ end
73
+
74
+ def must_be_static?
75
+ check_alive!
76
+ Native.JPH_Shape_MustBeStatic(@pointer)
77
+ end
78
+
79
+ def native_pointer
80
+ check_alive!
81
+ @pointer
82
+ end
83
+
84
+ private
85
+
86
+ def check_alive!
87
+ raise UseAfterDestroyError, "shape has been released" if @released
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jolt
4
+ module ShapeBuilders
5
+ def convex_hull(points, max_convex_radius: Shape::DEFAULT_CONVEX_RADIUS)
6
+ Jolt.init
7
+ coordinates = packed_floats(points, 3, "points")
8
+ raise InvalidArgumentError, "convex hull requires at least 4 points" if coordinates.length < 12
9
+
10
+ point_memory = float_memory(coordinates)
11
+ settings = Native.JPH_ConvexHullShapeSettings_Create(
12
+ point_memory, coordinates.length / 3,
13
+ Conversions.non_negative_float(max_convex_radius, "max_convex_radius")
14
+ )
15
+ create_from_settings(settings, :convex_hull, :JPH_ConvexHullShapeSettings_CreateShape)
16
+ end
17
+
18
+ def mesh(vertices:, indices:)
19
+ Jolt.init
20
+ coordinates = packed_floats(vertices, 3, "vertices")
21
+ triangles = packed_indices(indices)
22
+ vertex_count = coordinates.length / 3
23
+ raise InvalidArgumentError, "mesh requires at least 3 vertices" if vertex_count < 3
24
+ raise InvalidArgumentError, "mesh requires at least 1 triangle" if triangles.empty?
25
+ raise InvalidArgumentError, "mesh index is out of bounds" if triangles.any? { |index| index >= vertex_count }
26
+
27
+ vertex_memory = float_memory(coordinates)
28
+ triangle_memory = indexed_triangle_memory(triangles)
29
+ settings = Native.JPH_MeshShapeSettings_Create2(
30
+ vertex_memory, vertex_count, triangle_memory, triangles.length / 3
31
+ )
32
+ create_from_settings(settings, :mesh, :JPH_MeshShapeSettings_CreateShape)
33
+ end
34
+
35
+ def heightfield(samples:, size:, offset: [0, 0, 0], scale: [1, 1, 1])
36
+ Jolt.init
37
+ unless size.is_a?(Integer) && size >= 4
38
+ raise InvalidArgumentError, "size must be an integer greater than or equal to 4"
39
+ end
40
+ values = packed_floats(samples, 1, "samples")
41
+ unless values.length == size * size
42
+ raise InvalidArgumentError, "samples must contain size * size values"
43
+ end
44
+
45
+ sample_memory = float_memory(values)
46
+ offset = Conversions.native_vec3(offset, name: "offset")
47
+ scale = Conversions.native_vec3(scale, name: "scale")
48
+ settings = Native.JPH_HeightFieldShapeSettings_Create(
49
+ sample_memory, offset.pointer, scale.pointer, size, nil
50
+ )
51
+ create_from_settings(settings, :heightfield, :JPH_HeightFieldShapeSettings_CreateShape)
52
+ end
53
+
54
+ def compound(children)
55
+ Jolt.init
56
+ children = children.to_a
57
+ raise InvalidArgumentError, "compound requires at least one child" if children.empty?
58
+
59
+ settings = Native.JPH_StaticCompoundShapeSettings_Create
60
+ raise ShapeError, "failed to create compound settings" if settings.null?
61
+
62
+ children.each_with_index do |child, index|
63
+ shape, position, rotation = child
64
+ raise InvalidArgumentError, "child #{index} shape must be a Jolt::Shape" unless shape.is_a?(Shape)
65
+
66
+ position = Conversions.native_vec3(position || [0, 0, 0], name: "child position")
67
+ rotation = Conversions.native_quat(rotation || [0, 0, 0, 1], name: "child rotation")
68
+ Native.JPH_CompoundShapeSettings_AddShape2(
69
+ settings, position.pointer, rotation.pointer, shape.native_pointer, index
70
+ )
71
+ end
72
+ build(Native.JPH_StaticCompoundShape_Create(settings), :compound)
73
+ ensure
74
+ Native.JPH_ShapeSettings_Destroy(settings) if settings && !settings.null?
75
+ end
76
+
77
+ def scaled(shape, scale)
78
+ validate_shape(shape)
79
+ scale = Conversions.native_vec3(scale, name: "scale")
80
+ build(Native.JPH_ScaledShape_Create(shape.native_pointer, scale.pointer), :scaled)
81
+ end
82
+
83
+ def offset(shape, position: [0, 0, 0], rotation: [0, 0, 0, 1])
84
+ validate_shape(shape)
85
+ position = Conversions.native_vec3(position, name: "position")
86
+ rotation = Conversions.native_quat(rotation)
87
+ pointer = Native.JPH_RotatedTranslatedShape_Create(
88
+ position.pointer, rotation.pointer, shape.native_pointer
89
+ )
90
+ build(pointer, :offset)
91
+ end
92
+
93
+ private
94
+
95
+ def create_from_settings(settings, kind, function)
96
+ raise ShapeError, "failed to create #{kind} settings" if settings.null?
97
+
98
+ build(Native.public_send(function, settings), kind)
99
+ ensure
100
+ Native.JPH_ShapeSettings_Destroy(settings) if settings && !settings.null?
101
+ end
102
+
103
+ def packed_floats(value, tuple_size, name)
104
+ if value.is_a?(String) && !(value.bytesize % FFI.type_size(:float)).zero?
105
+ raise InvalidArgumentError, "#{name} packed f32 buffer has trailing bytes"
106
+ end
107
+ values = value.is_a?(String) ? value.unpack("e*") : value.to_a.flatten
108
+ unless (values.length % tuple_size).zero?
109
+ raise InvalidArgumentError, "#{name} has an incomplete tuple"
110
+ end
111
+
112
+ values.map.with_index { |item, index| Conversions.finite_float(item, "#{name}[#{index}]") }
113
+ rescue NoMethodError
114
+ raise InvalidArgumentError, "#{name} must be an array or packed f32 String"
115
+ end
116
+
117
+ def packed_indices(value)
118
+ if value.is_a?(String) && !(value.bytesize % FFI.type_size(:uint32)).zero?
119
+ raise InvalidArgumentError, "indices packed u32 buffer has trailing bytes"
120
+ end
121
+ values = value.is_a?(String) ? value.unpack("V*") : value.to_a.flatten
122
+ raise InvalidArgumentError, "indices has an incomplete triangle" unless (values.length % 3).zero?
123
+ unless values.all? { |index| index.is_a?(Integer) && index >= 0 }
124
+ raise InvalidArgumentError, "indices must contain non-negative integers"
125
+ end
126
+
127
+ values
128
+ rescue NoMethodError
129
+ raise InvalidArgumentError, "indices must be an array or packed u32 String"
130
+ end
131
+
132
+ def float_memory(values)
133
+ FFI::MemoryPointer.new(:float, values.length).tap { |memory| memory.write_array_of_float(values) }
134
+ end
135
+
136
+ def indexed_triangle_memory(indices)
137
+ count = indices.length / 3
138
+ FFI::MemoryPointer.new(Native::IndexedTriangle, count).tap do |memory|
139
+ indices.each_slice(3).with_index do |(i1, i2, i3), index|
140
+ triangle = Native::IndexedTriangle.new(memory + index * Native::IndexedTriangle.size)
141
+ triangle[:i1], triangle[:i2], triangle[:i3] = i1, i2, i3
142
+ triangle[:material_index] = 0
143
+ triangle[:user_data] = 0
144
+ end
145
+ end
146
+ end
147
+
148
+ def validate_shape(shape)
149
+ raise InvalidArgumentError, "shape must be a Jolt::Shape" unless shape.is_a?(Shape)
150
+
151
+ shape.native_pointer
152
+ Jolt.init
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jolt
4
+ class System
5
+ include SystemContacts
6
+ include SystemQueries
7
+ include SystemConstraints
8
+ include SystemCharacters
9
+
10
+ DEFAULT_LIMITS = {
11
+ max_bodies: 10_240,
12
+ max_body_pairs: 65_536,
13
+ max_contact_constraints: 10_240
14
+ }.freeze
15
+ MAX_UINT32 = 0xffff_ffff
16
+ MAX_INT32 = 0x7fff_ffff
17
+
18
+ attr_reader :layers, :bodies, :constraints, :contact_events
19
+
20
+ def initialize(max_bodies: DEFAULT_LIMITS[:max_bodies],
21
+ max_body_pairs: DEFAULT_LIMITS[:max_body_pairs],
22
+ max_contact_constraints: DEFAULT_LIMITS[:max_contact_constraints],
23
+ gravity: [0, -9.81, 0], layers: Layers.default,
24
+ contact_queue_capacity: 65_536)
25
+ Jolt.init
26
+ @destroyed = false
27
+ @active_thread = nil
28
+ @body_registry = {}
29
+ @constraint_registry = []
30
+ @character_registry = []
31
+ @user_data = {}
32
+ @shapes = {}
33
+ @layers = validate_layers(layers)
34
+ create_native_system(
35
+ max_bodies, max_body_pairs, max_contact_constraints, contact_queue_capacity
36
+ )
37
+ self.gravity = gravity
38
+ @bodies = BodyCollection.new(self)
39
+ @constraints = ConstraintCollection.new(self)
40
+ Jolt.__register_system(self)
41
+ rescue StandardError
42
+ destroy_native_resources
43
+ raise
44
+ end
45
+
46
+ def update(delta_time, collision_steps: 1)
47
+ __check_alive!
48
+ delta_time = Conversions.positive_float(delta_time, "delta_time")
49
+ unless collision_steps.is_a?(Integer) && collision_steps.between?(1, MAX_INT32)
50
+ raise InvalidArgumentError, "collision_steps must be a positive 32-bit integer"
51
+ end
52
+
53
+ __with_native_operation("recursive System#update is not allowed") do
54
+ @body_registry.each_value(&:__capture_before_step)
55
+ error = Native.JPH_PhysicsSystem_Update(@pointer, delta_time, collision_steps, @job_system)
56
+ @body_registry.each_value(&:__capture_after_step)
57
+ __drain_contact_events
58
+ raise_update_error!(error) unless error.zero?
59
+ self
60
+ end
61
+ end
62
+
63
+ def gravity
64
+ __check_alive!
65
+ native = Native::Vec3.new
66
+ Native.JPH_PhysicsSystem_GetGravity(@pointer, native.pointer)
67
+ Conversions.vec3(native)
68
+ end
69
+
70
+ def gravity=(value)
71
+ __check_alive!
72
+ native = Conversions.native_vec3(value, name: "gravity")
73
+ Native.JPH_PhysicsSystem_SetGravity(@pointer, native.pointer)
74
+ value
75
+ end
76
+
77
+ def optimize_broad_phase
78
+ __check_alive!
79
+ Native.JPH_PhysicsSystem_OptimizeBroadPhase(@pointer)
80
+ self
81
+ end
82
+
83
+ def destroy
84
+ return if @destroyed
85
+
86
+ __check_alive!
87
+ if @active_thread
88
+ raise ConcurrentAccessError, "cannot destroy a system during a native operation"
89
+ end
90
+
91
+ __destroy_all_constraints
92
+ __destroy_all_characters
93
+ @body_registry.each_value(&:__mark_destroyed)
94
+ @body_registry.clear
95
+ @user_data.clear
96
+ destroy_native_resources
97
+ @shapes.each_value { |shape| shape.release unless shape.released? }
98
+ @shapes.clear
99
+ @destroyed = true
100
+ Jolt.__unregister_system(self)
101
+ nil
102
+ end
103
+
104
+ def destroyed?
105
+ @destroyed
106
+ end
107
+
108
+ def __body_interface
109
+ __check_alive!
110
+ @body_interface
111
+ end
112
+
113
+ def __register_body(body, shape)
114
+ @body_registry[body.id] = body
115
+ @shapes[shape.object_id] = shape
116
+ end
117
+
118
+ def __destroy_body(body)
119
+ return if body.destroyed?
120
+
121
+ __check_alive!
122
+ registered = @body_registry.delete(body.id)
123
+ raise InvalidArgumentError, "body does not belong to this system" unless registered.equal?(body)
124
+
125
+ __destroy_constraints_for(body)
126
+ Native.JPH_BodyInterface_RemoveAndDestroyBody(@body_interface, body.id)
127
+ @user_data.delete(body.id)
128
+ body.__mark_destroyed
129
+ nil
130
+ end
131
+
132
+ def __body(id)
133
+ __check_alive!
134
+ @body_registry[id]
135
+ end
136
+
137
+ def __native_pointer
138
+ __check_alive!
139
+ @pointer
140
+ end
141
+
142
+ def __bodies_snapshot
143
+ __check_alive!
144
+ @body_registry.values
145
+ end
146
+
147
+ def __user_data(id)
148
+ __check_alive!
149
+ @user_data[id]
150
+ end
151
+
152
+ def __set_user_data(id, value)
153
+ __check_alive!
154
+ @user_data[id] = value
155
+ end
156
+
157
+ def __check_alive!
158
+ raise UseAfterDestroyError, "system has been destroyed" if @destroyed
159
+ return unless @active_thread && !@active_thread.equal?(Thread.current)
160
+
161
+ raise ConcurrentAccessError, "system is in use by another thread"
162
+ end
163
+
164
+ def __with_native_operation(recursive_message = "recursive native operation is not allowed")
165
+ acquired = false
166
+ __check_alive!
167
+ raise Error, recursive_message if @active_thread
168
+
169
+ @active_thread = Thread.current
170
+ acquired = true
171
+ yield
172
+ ensure
173
+ @active_thread = nil if acquired
174
+ end
175
+
176
+ private
177
+
178
+ def validate_layers(layers)
179
+ raise InvalidArgumentError, "layers must be a Jolt::Layers" unless layers.is_a?(Layers)
180
+
181
+ layers
182
+ end
183
+
184
+ def create_native_system(max_bodies, max_body_pairs, max_contact_constraints, contact_queue_capacity)
185
+ limits = [max_bodies, max_body_pairs, max_contact_constraints]
186
+ unless limits.all? { |value| value.is_a?(Integer) && value.between?(1, MAX_UINT32) }
187
+ raise InvalidArgumentError, "system limits must be positive 32-bit integers"
188
+ end
189
+
190
+ @layer_resources = @layers.native_resources
191
+ @job_system = Native.JPH_JobSystemThreadPool_Create(nil)
192
+ raise InitializationError, "failed to create Jolt job system" if @job_system.null?
193
+
194
+ settings = Native::PhysicsSystemSettings.new
195
+ settings[:max_bodies], settings[:max_body_pairs], settings[:max_contact_constraints] = limits
196
+ settings[:num_body_mutexes] = 0
197
+ settings[:padding] = 0
198
+ settings[:broad_phase_layer_interface] = @layer_resources.broad_phase_interface
199
+ settings[:object_layer_pair_filter] = @layer_resources.object_pair_filter
200
+ settings[:object_vs_broad_phase_layer_filter] = @layer_resources.object_vs_broad_phase_filter
201
+ @pointer = Native.JPH_PhysicsSystem_Create(settings.pointer)
202
+ raise InitializationError, "failed to create Jolt physics system" if @pointer.null?
203
+
204
+ @layer_resources.transfer_to_system!
205
+ @body_interface = Native.JPH_PhysicsSystem_GetBodyInterface(@pointer)
206
+ raise InitializationError, "failed to get Jolt body interface" if @body_interface.null?
207
+
208
+ @narrow_phase_query = Native.JPH_PhysicsSystem_GetNarrowPhaseQuery(@pointer)
209
+ raise InitializationError, "failed to get Jolt narrow-phase query" if @narrow_phase_query.null?
210
+
211
+ @broad_phase_query = Native.JPH_PhysicsSystem_GetBroadPhaseQuery(@pointer)
212
+ raise InitializationError, "failed to get Jolt broad-phase query" if @broad_phase_query.null?
213
+
214
+ __create_contact_queue(contact_queue_capacity)
215
+ end
216
+
217
+ def destroy_native_resources
218
+ __destroy_contact_queue
219
+ Native.JPH_PhysicsSystem_Destroy(@pointer) if @pointer && !@pointer.null?
220
+ Native.JPH_JobSystem_Destroy(@job_system) if @job_system && !@job_system.null?
221
+ @layer_resources&.destroy
222
+ @pointer = nil
223
+ @job_system = nil
224
+ @body_interface = nil
225
+ @broad_phase_query = nil
226
+ @narrow_phase_query = nil
227
+ @layer_resources = nil
228
+ end
229
+
230
+ def raise_update_error!(error)
231
+ names = []
232
+ names << "manifold cache full" unless (error & 1).zero?
233
+ names << "body pair cache full" unless (error & 2).zero?
234
+ names << "contact constraints full" unless (error & 4).zero?
235
+ raise PhysicsUpdateError, "physics update failed: #{names.join(", ")}"
236
+ end
237
+ end
238
+ end