wgpu 1.1.0 → 1.2.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/README.md +26 -3
  4. data/docs/README.md +22 -0
  5. data/docs/api_coverage.md +132 -0
  6. data/docs/async.md +31 -0
  7. data/docs/bind_groups.md +25 -0
  8. data/docs/buffer_data.md +37 -0
  9. data/docs/command_encoding.md +24 -0
  10. data/docs/errors.md +40 -0
  11. data/docs/getting_started_compute.md +61 -0
  12. data/docs/getting_started_rendering.md +62 -0
  13. data/docs/installation.md +94 -0
  14. data/docs/pipeline_descriptors.md +38 -0
  15. data/docs/releasing.md +22 -0
  16. data/docs/resource_lifetime.md +94 -0
  17. data/docs/shaders.md +32 -0
  18. data/docs/texture_readback.md +19 -0
  19. data/docs/troubleshooting.md +48 -0
  20. data/docs/upgrading_wgpu_native.md +26 -0
  21. data/ext/wgpu/extconf.rb +10 -142
  22. data/lib/wgpu/commands/command_buffer.rb +11 -0
  23. data/lib/wgpu/commands/command_encoder.rb +65 -8
  24. data/lib/wgpu/commands/compute_pass.rb +10 -0
  25. data/lib/wgpu/commands/render_bundle_encoder.rb +10 -3
  26. data/lib/wgpu/commands/render_pass.rb +34 -8
  27. data/lib/wgpu/core/adapter.rb +36 -11
  28. data/lib/wgpu/core/async_waiter.rb +32 -4
  29. data/lib/wgpu/core/canvas_context.rb +0 -2
  30. data/lib/wgpu/core/device.rb +138 -42
  31. data/lib/wgpu/core/instance.rb +8 -4
  32. data/lib/wgpu/core/queue.rb +80 -49
  33. data/lib/wgpu/core/surface.rb +16 -17
  34. data/lib/wgpu/data_types.rb +67 -0
  35. data/lib/wgpu/descriptor_helpers.rb +39 -0
  36. data/lib/wgpu/error.rb +55 -0
  37. data/lib/wgpu/logging.rb +63 -0
  38. data/lib/wgpu/native/abi_verifier.rb +109 -0
  39. data/lib/wgpu/native/callbacks.rb +4 -1
  40. data/lib/wgpu/native/capabilities.rb +18 -2
  41. data/lib/wgpu/native/distribution.rb +113 -0
  42. data/lib/wgpu/native/enum_helper.rb +63 -0
  43. data/lib/wgpu/native/enums.rb +60 -8
  44. data/lib/wgpu/native/functions.rb +10 -10
  45. data/lib/wgpu/native/installer.rb +192 -0
  46. data/lib/wgpu/native/loader.rb +39 -21
  47. data/lib/wgpu/native_resource.rb +171 -0
  48. data/lib/wgpu/pipeline/bind_group.rb +12 -0
  49. data/lib/wgpu/pipeline/bind_group_layout.rb +91 -37
  50. data/lib/wgpu/pipeline/compute_pipeline.rb +28 -26
  51. data/lib/wgpu/pipeline/render_pipeline.rb +180 -68
  52. data/lib/wgpu/pipeline/shader_module.rb +50 -8
  53. data/lib/wgpu/resources/buffer.rb +148 -73
  54. data/lib/wgpu/resources/query_set.rb +14 -12
  55. data/lib/wgpu/resources/sampler.rb +36 -20
  56. data/lib/wgpu/resources/texture.rb +44 -42
  57. data/lib/wgpu/resources/texture_view.rb +11 -3
  58. data/lib/wgpu/texture_format.rb +82 -0
  59. data/lib/wgpu/version.rb +1 -1
  60. data/lib/wgpu/window.rb +8 -1
  61. data/lib/wgpu.rb +32 -0
  62. data/sig/wgpu.rbs +381 -0
  63. metadata +31 -16
@@ -7,6 +7,7 @@ module WGPU
7
7
  def initialize(device, label: nil)
8
8
  @device = device
9
9
  @finished = false
10
+ @active_pass = nil
10
11
 
11
12
  desc = Native::CommandEncoderDescriptor.new
12
13
  desc[:next_in_chain] = nil
@@ -24,13 +25,26 @@ module WGPU
24
25
  end
25
26
 
26
27
  def begin_compute_pass(label: nil, timestamp_writes: nil)
27
- raise CommandError, "Encoder already finished" if @finished
28
- ComputePass.new(self, label: label, timestamp_writes: timestamp_writes)
28
+ ensure_can_begin_pass!
29
+ pass = ComputePass.new(self, label: label, timestamp_writes: timestamp_writes)
30
+ @active_pass = pass
31
+ return pass unless block_given?
32
+
33
+ begin
34
+ yield pass
35
+ ensure
36
+ begin
37
+ pass.end_pass unless pass.ended?
38
+ ensure
39
+ pass.release
40
+ pass_ended(pass)
41
+ end
42
+ end
29
43
  end
30
44
 
31
45
  def begin_render_pass(color_attachments:, depth_stencil_attachment: nil, occlusion_query_set: nil, timestamp_writes: nil, max_draw_count: nil, label: nil)
32
- raise CommandError, "Encoder already finished" if @finished
33
- RenderPass.new(self,
46
+ ensure_can_begin_pass!
47
+ pass = RenderPass.new(self,
34
48
  color_attachments: color_attachments,
35
49
  depth_stencil_attachment: depth_stencil_attachment,
36
50
  occlusion_query_set: occlusion_query_set,
@@ -38,6 +52,19 @@ module WGPU
38
52
  max_draw_count: max_draw_count,
39
53
  label: label
40
54
  )
55
+ @active_pass = pass
56
+ return pass unless block_given?
57
+
58
+ begin
59
+ yield pass
60
+ ensure
61
+ begin
62
+ pass.end_pass unless pass.ended?
63
+ ensure
64
+ pass.release
65
+ pass_ended(pass)
66
+ end
67
+ end
41
68
  end
42
69
 
43
70
  def copy_buffer_to_buffer(source:, source_offset: 0, destination:, destination_offset: 0, size:)
@@ -70,7 +97,11 @@ module WGPU
70
97
  dst[:origin][:x] = destination.dig(:origin, :x) || 0
71
98
  dst[:origin][:y] = destination.dig(:origin, :y) || 0
72
99
  dst[:origin][:z] = destination.dig(:origin, :z) || 0
73
- dst[:aspect] = destination[:aspect] || :all
100
+ dst[:aspect] = Native::EnumHelper.coerce(
101
+ Native::TextureAspect,
102
+ destination[:aspect] || :all,
103
+ name: "texture aspect"
104
+ )
74
105
 
75
106
  Native.wgpuCommandEncoderCopyBufferToTexture(@handle, src, dst, size)
76
107
  end
@@ -89,7 +120,11 @@ module WGPU
89
120
  src[:origin][:x] = source.dig(:origin, :x) || 0
90
121
  src[:origin][:y] = source.dig(:origin, :y) || 0
91
122
  src[:origin][:z] = source.dig(:origin, :z) || 0
92
- src[:aspect] = source[:aspect] || :all
123
+ src[:aspect] = Native::EnumHelper.coerce(
124
+ Native::TextureAspect,
125
+ source[:aspect] || :all,
126
+ name: "texture aspect"
127
+ )
93
128
 
94
129
  dst = Native::ImageCopyBuffer.new
95
130
  dst[:layout][:offset] = destination[:offset] || 0
@@ -109,7 +144,11 @@ module WGPU
109
144
  src[:origin][:x] = source.dig(:origin, :x) || 0
110
145
  src[:origin][:y] = source.dig(:origin, :y) || 0
111
146
  src[:origin][:z] = source.dig(:origin, :z) || 0
112
- src[:aspect] = source[:aspect] || :all
147
+ src[:aspect] = Native::EnumHelper.coerce(
148
+ Native::TextureAspect,
149
+ source[:aspect] || :all,
150
+ name: "source texture aspect"
151
+ )
113
152
 
114
153
  dst = Native::ImageCopyTexture.new
115
154
  dst[:texture] = destination[:texture].handle
@@ -117,7 +156,11 @@ module WGPU
117
156
  dst[:origin][:x] = destination.dig(:origin, :x) || 0
118
157
  dst[:origin][:y] = destination.dig(:origin, :y) || 0
119
158
  dst[:origin][:z] = destination.dig(:origin, :z) || 0
120
- dst[:aspect] = destination[:aspect] || :all
159
+ dst[:aspect] = Native::EnumHelper.coerce(
160
+ Native::TextureAspect,
161
+ destination[:aspect] || :all,
162
+ name: "destination texture aspect"
163
+ )
121
164
 
122
165
  size = Native::Extent3D.new
123
166
  size[:width] = copy_size[:width] || copy_size[0]
@@ -175,6 +218,9 @@ module WGPU
175
218
 
176
219
  def finish(label: nil)
177
220
  raise CommandError, "Encoder already finished" if @finished
221
+ if @active_pass && !@active_pass.ended?
222
+ raise CommandError, "Cannot finish command encoder while a pass is active"
223
+ end
178
224
  @finished = true
179
225
 
180
226
  desc = nil
@@ -197,5 +243,16 @@ module WGPU
197
243
  Native.wgpuCommandEncoderRelease(@handle)
198
244
  @handle = FFI::Pointer::NULL
199
245
  end
246
+
247
+ private
248
+
249
+ def ensure_can_begin_pass!
250
+ raise CommandError, "Encoder already finished" if @finished
251
+ raise CommandError, "A command pass is already active" if @active_pass && !@active_pass.ended?
252
+ end
253
+
254
+ def pass_ended(pass)
255
+ @active_pass = nil if @active_pass.equal?(pass)
256
+ end
200
257
  end
201
258
  end
@@ -5,6 +5,8 @@ module WGPU
5
5
  attr_reader :handle
6
6
 
7
7
  def initialize(encoder, label: nil, timestamp_writes: nil)
8
+ @encoder = encoder
9
+ @ended = false
8
10
  desc = Native::ComputePassDescriptor.new
9
11
  desc[:next_in_chain] = nil
10
12
  if label
@@ -73,13 +75,21 @@ module WGPU
73
75
  end
74
76
 
75
77
  def end_pass
78
+ return if @ended
79
+
76
80
  Native.wgpuComputePassEncoderEnd(@handle)
81
+ @ended = true
82
+ @encoder.send(:pass_ended, self)
77
83
  end
78
84
 
79
85
  def end
80
86
  end_pass
81
87
  end
82
88
 
89
+ def ended?
90
+ @ended
91
+ end
92
+
83
93
  def release
84
94
  return if @handle.null?
85
95
  Native.wgpuComputePassEncoderRelease(@handle)
@@ -21,13 +21,19 @@ module WGPU
21
21
  desc[:label][:length] = 0
22
22
  end
23
23
 
24
- formats = Array(color_formats).map { |f| Native::TextureFormat[f] }
24
+ formats = Array(color_formats).map do |format|
25
+ Native::EnumHelper.coerce(Native::TextureFormat, format, name: "color format")
26
+ end
25
27
  @formats_ptr = FFI::MemoryPointer.new(:uint32, formats.size)
26
28
  @formats_ptr.write_array_of_uint32(formats)
27
29
  desc[:color_format_count] = formats.size
28
30
  desc[:color_formats] = @formats_ptr
29
31
 
30
- desc[:depth_stencil_format] = depth_stencil_format || :undefined
32
+ desc[:depth_stencil_format] = Native::EnumHelper.coerce(
33
+ Native::TextureFormat,
34
+ depth_stencil_format || :undefined,
35
+ name: "depth stencil format"
36
+ )
31
37
  desc[:sample_count] = sample_count
32
38
  desc[:depth_read_only] = depth_read_only ? 1 : 0
33
39
  desc[:stencil_read_only] = stencil_read_only ? 1 : 0
@@ -65,7 +71,8 @@ module WGPU
65
71
  raise RenderBundleError, "Encoder already finished" if @finished
66
72
 
67
73
  size ||= buffer.size - offset
68
- Native.wgpuRenderBundleEncoderSetIndexBuffer(@handle, buffer.handle, format, offset, size)
74
+ format_value = Native::EnumHelper.coerce(Native::IndexFormat, format, name: "index format")
75
+ Native.wgpuRenderBundleEncoderSetIndexBuffer(@handle, buffer.handle, format_value, offset, size)
69
76
  end
70
77
 
71
78
  def draw(vertex_count, instance_count: 1, first_vertex: 0, first_instance: 0)
@@ -8,6 +8,7 @@ module WGPU
8
8
  @encoder = encoder
9
9
  @pointers = []
10
10
  @max_draw_count = max_draw_count
11
+ @ended = false
11
12
 
12
13
  desc = Native::RenderPassDescriptor.new
13
14
  desc[:next_in_chain] = nil
@@ -61,7 +62,8 @@ module WGPU
61
62
 
62
63
  def set_index_buffer(buffer, format, offset: 0, size: nil)
63
64
  size ||= buffer.size - offset
64
- Native.wgpuRenderPassEncoderSetIndexBuffer(@handle, buffer.handle, format, offset, size)
65
+ format_value = Native::EnumHelper.coerce(Native::IndexFormat, format, name: "index format")
66
+ Native.wgpuRenderPassEncoderSetIndexBuffer(@handle, buffer.handle, format_value, offset, size)
65
67
  end
66
68
 
67
69
  def draw(vertex_count, instance_count: 1, first_vertex: 0, first_instance: 0)
@@ -137,13 +139,21 @@ module WGPU
137
139
  end
138
140
 
139
141
  def end_pass
142
+ return if @ended
143
+
140
144
  Native.wgpuRenderPassEncoderEnd(@handle)
145
+ @ended = true
146
+ @encoder.send(:pass_ended, self)
141
147
  end
142
148
 
143
149
  def end
144
150
  end_pass
145
151
  end
146
152
 
153
+ def ended?
154
+ @ended
155
+ end
156
+
147
157
  def release
148
158
  return if @handle.null?
149
159
  Native.wgpuRenderPassEncoderRelease(@handle)
@@ -169,13 +179,13 @@ module WGPU
169
179
  @pointers << ptr
170
180
 
171
181
  attachments.each_with_index do |att, i|
172
- ca = Native::RenderPassColorAttachment.new(ptr + i * Native::RenderPassColorAttachment.size)
182
+ ca = Native::RenderPassColorAttachment.new(ptr + (i * Native::RenderPassColorAttachment.size))
173
183
  ca[:next_in_chain] = nil
174
184
  ca[:view] = att[:view].handle
175
185
  ca[:depth_slice] = att[:depth_slice] || 0xFFFFFFFF
176
186
  ca[:resolve_target] = att[:resolve_target]&.handle
177
- ca[:load_op] = att[:load_op] || :clear
178
- ca[:store_op] = att[:store_op] || :store
187
+ ca[:load_op] = Native::EnumHelper.coerce(Native::LoadOp, att[:load_op] || :clear, name: "load op")
188
+ ca[:store_op] = Native::EnumHelper.coerce(Native::StoreOp, att[:store_op] || :store, name: "store op")
179
189
 
180
190
  clear = att[:clear_value] || { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }
181
191
  ca[:clear_value][:r] = clear[:r] || 0.0
@@ -192,12 +202,28 @@ module WGPU
192
202
  @pointers << ds
193
203
 
194
204
  ds[:view] = att[:view].handle
195
- ds[:depth_load_op] = att[:depth_load_op] || :clear
196
- ds[:depth_store_op] = att[:depth_store_op] || :store
205
+ ds[:depth_load_op] = Native::EnumHelper.coerce(
206
+ Native::LoadOp,
207
+ att[:depth_load_op] || :clear,
208
+ name: "depth load op"
209
+ )
210
+ ds[:depth_store_op] = Native::EnumHelper.coerce(
211
+ Native::StoreOp,
212
+ att[:depth_store_op] || :store,
213
+ name: "depth store op"
214
+ )
197
215
  ds[:depth_clear_value] = att[:depth_clear_value] || 1.0
198
216
  ds[:depth_read_only] = att[:depth_read_only] ? 1 : 0
199
- ds[:stencil_load_op] = att[:stencil_load_op] || :clear
200
- ds[:stencil_store_op] = att[:stencil_store_op] || :store
217
+ ds[:stencil_load_op] = Native::EnumHelper.coerce(
218
+ Native::LoadOp,
219
+ att[:stencil_load_op] || :clear,
220
+ name: "stencil load op"
221
+ )
222
+ ds[:stencil_store_op] = Native::EnumHelper.coerce(
223
+ Native::StoreOp,
224
+ att[:stencil_store_op] || :store,
225
+ name: "stencil store op"
226
+ )
201
227
  ds[:stencil_clear_value] = att[:stencil_clear_value] || 0
202
228
  ds[:stencil_read_only] = att[:stencil_read_only] ? 1 : 0
203
229
 
@@ -11,7 +11,8 @@ module WGPU
11
11
  adapter
12
12
  end
13
13
 
14
- def self.request(instance, power_preference: :high_performance, backend: nil, feature_level: :core, force_fallback_adapter: false, compatible_surface: nil)
14
+ def self.request(instance, power_preference: :high_performance, backend: nil, feature_level: :core,
15
+ force_fallback_adapter: false, compatible_surface: nil, timeout: nil)
15
16
  adapter_ptr = FFI::MemoryPointer.new(:pointer)
16
17
  status_holder = { value: nil, message: nil }
17
18
 
@@ -28,10 +29,22 @@ module WGPU
28
29
 
29
30
  options = Native::RequestAdapterOptions.new
30
31
  options[:next_in_chain] = nil
31
- options[:feature_level] = feature_level
32
- options[:power_preference] = power_preference
32
+ options[:feature_level] = Native::EnumHelper.coerce(
33
+ Native::FeatureLevel,
34
+ feature_level,
35
+ name: "feature level"
36
+ )
37
+ options[:power_preference] = Native::EnumHelper.coerce(
38
+ Native::PowerPreference,
39
+ power_preference,
40
+ name: "power preference"
41
+ )
33
42
  options[:force_fallback_adapter] = force_fallback_adapter ? 1 : 0
34
- options[:backend_type] = backend || :undefined
43
+ options[:backend_type] = Native::EnumHelper.coerce(
44
+ Native::BackendType,
45
+ backend || :undefined,
46
+ name: "backend type"
47
+ )
35
48
  options[:compatible_surface] = compatible_surface&.handle
36
49
 
37
50
  callback_info = Native::RequestAdapterCallbackInfo.new
@@ -41,9 +54,14 @@ module WGPU
41
54
  callback_info[:userdata1] = nil
42
55
  callback_info[:userdata2] = nil
43
56
 
44
- status_holder[:done] = false
45
- future = Native.wgpuInstanceRequestAdapter(instance.handle, options, callback_info)
46
- AsyncWaiter.wait(status_holder: status_holder, instance: instance, future: future)
57
+ callback_token = CallbackKeepalive.retain(instance, callback)
58
+ begin
59
+ status_holder[:done] = false
60
+ future = Native.wgpuInstanceRequestAdapter(instance.handle, options, callback_info)
61
+ AsyncWaiter.wait(status_holder: status_holder, instance: instance, future: future, timeout: timeout)
62
+ ensure
63
+ CallbackKeepalive.release(instance, callback_token)
64
+ end
47
65
 
48
66
  handle = adapter_ptr.read_pointer
49
67
  if handle.null? || status_holder[:value] != :success
@@ -59,16 +77,23 @@ module WGPU
59
77
  @instance = instance
60
78
  end
61
79
 
62
- def request_device(label: nil, required_features: [], required_limits: nil)
63
- Device.request(self, label: label, required_features: required_features, required_limits: required_limits)
80
+ def request_device(label: nil, required_features: [], required_limits: nil, timeout: nil)
81
+ Device.request(
82
+ self,
83
+ label: label,
84
+ required_features: required_features,
85
+ required_limits: required_limits,
86
+ timeout: timeout
87
+ )
64
88
  end
65
89
 
66
- def request_device_async(label: nil, required_features: [], required_limits: nil)
90
+ def request_device_async(label: nil, required_features: [], required_limits: nil, timeout: nil)
67
91
  AsyncTask.new do
68
92
  request_device(
69
93
  label: label,
70
94
  required_features: required_features,
71
- required_limits: required_limits
95
+ required_limits: required_limits,
96
+ timeout: timeout
72
97
  )
73
98
  end
74
99
  end
@@ -10,6 +10,17 @@ module WGPU
10
10
 
11
11
  module_function
12
12
 
13
+ def poll_interval
14
+ @poll_interval ||= POLL_INTERVAL_SECONDS
15
+ end
16
+
17
+ def poll_interval=(seconds)
18
+ value = Float(seconds)
19
+ raise ArgumentError, "poll interval must be positive" unless value.positive?
20
+
21
+ @poll_interval = value
22
+ end
23
+
13
24
  def callback_mode(instance:)
14
25
  if instance
15
26
  callback_mode_value(:allow_process_events)
@@ -18,18 +29,25 @@ module WGPU
18
29
  end
19
30
  end
20
31
 
21
- def wait(status_holder:, instance: nil, device: nil, future: nil)
32
+ def wait(status_holder:, instance: nil, device: nil, future: nil, timeout: nil)
33
+ timeout = Float(timeout) if timeout
34
+ raise ArgumentError, "timeout must be non-negative" if timeout&.negative?
35
+
36
+ deadline = monotonic_time + timeout if timeout
22
37
  wait_info = build_wait_info(future) if instance && Native.future_api?
23
38
 
24
39
  until status_holder[:done]
40
+ raise_timeout!(timeout) if deadline && monotonic_time >= deadline
41
+
42
+ waited = false
25
43
  if instance && wait_info
26
- wait_with_wait_any(instance, wait_info)
44
+ waited = wait_with_wait_any(instance, wait_info)
27
45
  elsif instance
28
46
  instance.process_events
29
47
  elsif device && Native.device_poll_available?
30
48
  Native.wgpuDevicePoll(device.handle, 0, nil)
31
49
  end
32
- sleep(POLL_INTERVAL_SECONDS) unless status_holder[:done]
50
+ sleep(poll_interval) unless status_holder[:done] || waited
33
51
  end
34
52
  end
35
53
 
@@ -55,10 +73,20 @@ module WGPU
55
73
 
56
74
  def wait_with_wait_any(instance, wait_info)
57
75
  status = Native.wgpuInstanceWaitAny(instance.handle, 1, wait_info.to_ptr, 0)
58
- return if [:success, :timed_out].include?(status)
76
+ return false if [:success, :timed_out].include?(status)
59
77
 
60
78
  raise Error, "wgpuInstanceWaitAny failed: #{status.inspect}"
61
79
  end
62
80
  private_class_method :wait_with_wait_any
81
+
82
+ def monotonic_time
83
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
84
+ end
85
+ private_class_method :monotonic_time
86
+
87
+ def raise_timeout!(timeout)
88
+ raise TimeoutError, "GPU operation timed out after #{timeout} seconds"
89
+ end
90
+ private_class_method :raise_timeout!
63
91
  end
64
92
  end
@@ -29,8 +29,6 @@ module WGPU
29
29
 
30
30
  def configure(device:, format: nil, usage: :render_attachment, view_formats: [], color_space: "srgb", tone_mapping: nil, alpha_mode: :opaque, width: nil, height: nil, present_mode: :fifo)
31
31
  ensure_surface
32
- color_space # reserved for API parity
33
- tone_mapping # reserved for API parity
34
32
 
35
33
  width = width || @physical_size[0]
36
34
  height = height || @physical_size[1]