wgpu 1.1.0 → 1.2.1

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +88 -0
  3. data/README.md +44 -5
  4. data/docs/README.md +22 -0
  5. data/docs/api_coverage.md +140 -0
  6. data/docs/async.md +41 -0
  7. data/docs/bind_groups.md +25 -0
  8. data/docs/buffer_data.md +37 -0
  9. data/docs/command_encoding.md +49 -0
  10. data/docs/errors.md +40 -0
  11. data/docs/getting_started_compute.md +62 -0
  12. data/docs/getting_started_rendering.md +62 -0
  13. data/docs/installation.md +94 -0
  14. data/docs/pipeline_descriptors.md +43 -0
  15. data/docs/releasing.md +32 -0
  16. data/docs/resource_lifetime.md +108 -0
  17. data/docs/shaders.md +32 -0
  18. data/docs/texture_readback.md +46 -0
  19. data/docs/troubleshooting.md +53 -0
  20. data/docs/upgrading_wgpu_native.md +37 -0
  21. data/ext/wgpu/extconf.rb +10 -142
  22. data/lib/wgpu/async_task.rb +19 -0
  23. data/lib/wgpu/commands/command_buffer.rb +25 -1
  24. data/lib/wgpu/commands/command_encoder.rb +123 -9
  25. data/lib/wgpu/commands/compute_pass.rb +53 -0
  26. data/lib/wgpu/commands/render_bundle.rb +9 -1
  27. data/lib/wgpu/commands/render_bundle_encoder.rb +65 -4
  28. data/lib/wgpu/commands/render_pass.rb +136 -8
  29. data/lib/wgpu/core/adapter.rb +123 -19
  30. data/lib/wgpu/core/async_waiter.rb +47 -4
  31. data/lib/wgpu/core/canvas_context.rb +32 -2
  32. data/lib/wgpu/core/device.rb +399 -53
  33. data/lib/wgpu/core/instance.rb +28 -4
  34. data/lib/wgpu/core/queue.rb +197 -51
  35. data/lib/wgpu/core/surface.rb +64 -17
  36. data/lib/wgpu/data_types.rb +83 -0
  37. data/lib/wgpu/descriptor_helpers.rb +104 -0
  38. data/lib/wgpu/error.rb +70 -0
  39. data/lib/wgpu/logging.rb +63 -0
  40. data/lib/wgpu/native/abi_verifier.rb +143 -0
  41. data/lib/wgpu/native/callbacks.rb +10 -1
  42. data/lib/wgpu/native/capabilities.rb +32 -2
  43. data/lib/wgpu/native/distribution.rb +176 -0
  44. data/lib/wgpu/native/enum_helper.rb +80 -0
  45. data/lib/wgpu/native/enums.rb +68 -8
  46. data/lib/wgpu/native/fixtures/webgpu-v27.0.4.0-enums.h +848 -0
  47. data/lib/wgpu/native/functions.rb +21 -10
  48. data/lib/wgpu/native/installer.rb +223 -0
  49. data/lib/wgpu/native/loader.rb +61 -21
  50. data/lib/wgpu/native/structs.rb +18 -1
  51. data/lib/wgpu/native_resource.rb +342 -0
  52. data/lib/wgpu/pipeline/bind_group.rb +21 -0
  53. data/lib/wgpu/pipeline/bind_group_layout.rb +108 -41
  54. data/lib/wgpu/pipeline/compute_pipeline.rb +40 -50
  55. data/lib/wgpu/pipeline/pipeline_layout.rb +8 -0
  56. data/lib/wgpu/pipeline/render_pipeline.rb +207 -110
  57. data/lib/wgpu/pipeline/shader_module.rb +95 -28
  58. data/lib/wgpu/resources/buffer.rb +387 -85
  59. data/lib/wgpu/resources/query_set.rb +26 -12
  60. data/lib/wgpu/resources/sampler.rb +43 -20
  61. data/lib/wgpu/resources/texture.rb +89 -48
  62. data/lib/wgpu/resources/texture_view.rb +35 -7
  63. data/lib/wgpu/texture_format.rb +96 -0
  64. data/lib/wgpu/version.rb +1 -1
  65. data/lib/wgpu/window.rb +34 -1
  66. data/lib/wgpu.rb +32 -0
  67. data/sig/wgpu.rbs +460 -0
  68. metadata +33 -16
@@ -4,10 +4,20 @@ module WGPU
4
4
  class RenderPass
5
5
  attr_reader :handle
6
6
 
7
+ # Begins a render pass owned by the command encoder.
8
+ # @param encoder [CommandEncoder] owning encoder
9
+ # @param label [String, nil] optional debug label
10
+ # @param color_attachments [Array<Hash>] color attachment descriptors
11
+ # @param depth_stencil_attachment [Hash, nil] depth/stencil attachment descriptor
12
+ # @param occlusion_query_set [QuerySet, nil] query set for occlusion queries
13
+ # @param timestamp_writes [Hash, nil] beginning and ending timestamp query settings
14
+ # @param max_draw_count [Integer, nil] implementation hint for the maximum draw count
15
+ # @raise [CommandError] if the native pass cannot be created
7
16
  def initialize(encoder, label: nil, color_attachments:, depth_stencil_attachment: nil, occlusion_query_set: nil, timestamp_writes: nil, max_draw_count: nil)
8
17
  @encoder = encoder
9
18
  @pointers = []
10
19
  @max_draw_count = max_draw_count
20
+ @ended = false
11
21
 
12
22
  desc = Native::RenderPassDescriptor.new
13
23
  desc[:next_in_chain] = nil
@@ -40,10 +50,18 @@ module WGPU
40
50
  raise CommandError, "Failed to begin render pass" if @handle.null?
41
51
  end
42
52
 
53
+ # Selects the render pipeline used by subsequent draws.
54
+ # @param pipeline [RenderPipeline] pipeline to bind
55
+ # @return [void]
43
56
  def set_pipeline(pipeline)
44
57
  Native.wgpuRenderPassEncoderSetPipeline(@handle, pipeline.handle)
45
58
  end
46
59
 
60
+ # Binds a resource group for subsequent draws.
61
+ # @param index [Integer] bind group index
62
+ # @param bind_group [BindGroup] group to bind
63
+ # @param dynamic_offsets [Array<Integer>] dynamic buffer offsets
64
+ # @return [void]
47
65
  def set_bind_group(index, bind_group, dynamic_offsets: [])
48
66
  if dynamic_offsets.empty?
49
67
  Native.wgpuRenderPassEncoderSetBindGroup(@handle, index, bind_group.handle, 0, nil)
@@ -54,32 +72,72 @@ module WGPU
54
72
  end
55
73
  end
56
74
 
75
+ # Binds a vertex buffer to a slot.
76
+ # @param slot [Integer] vertex buffer slot
77
+ # @param buffer [Buffer] vertex data buffer
78
+ # @param offset [Integer] first byte to bind
79
+ # @param size [Integer, nil] number of bytes to bind
80
+ # @return [void]
57
81
  def set_vertex_buffer(slot, buffer, offset: 0, size: nil)
58
82
  size ||= buffer.size - offset
59
83
  Native.wgpuRenderPassEncoderSetVertexBuffer(@handle, slot, buffer.handle, offset, size)
60
84
  end
61
85
 
86
+ # Binds an index buffer for indexed draws.
87
+ # @param buffer [Buffer] index data buffer
88
+ # @param format [Symbol, Integer] index element format
89
+ # @param offset [Integer] first byte to bind
90
+ # @param size [Integer, nil] number of bytes to bind
91
+ # @return [void]
62
92
  def set_index_buffer(buffer, format, offset: 0, size: nil)
63
93
  size ||= buffer.size - offset
64
- Native.wgpuRenderPassEncoderSetIndexBuffer(@handle, buffer.handle, format, offset, size)
94
+ format_value = Native::EnumHelper.coerce(Native::IndexFormat, format, name: "index format")
95
+ Native.wgpuRenderPassEncoderSetIndexBuffer(@handle, buffer.handle, format_value, offset, size)
65
96
  end
66
97
 
98
+ # Records a non-indexed draw.
99
+ # @return [void]
67
100
  def draw(vertex_count, instance_count: 1, first_vertex: 0, first_instance: 0)
68
101
  Native.wgpuRenderPassEncoderDraw(@handle, vertex_count, instance_count, first_vertex, first_instance)
69
102
  end
70
103
 
104
+ # Records an indexed draw.
105
+ # @return [void]
71
106
  def draw_indexed(index_count, instance_count: 1, first_index: 0, base_vertex: 0, first_instance: 0)
72
107
  Native.wgpuRenderPassEncoderDrawIndexed(@handle, index_count, instance_count, first_index, base_vertex, first_instance)
73
108
  end
74
109
 
110
+ # Sets the viewport used by subsequent draw calls.
111
+ #
112
+ # @param x [Numeric] left coordinate in pixels
113
+ # @param y [Numeric] top coordinate in pixels
114
+ # @param width [Numeric] viewport width in pixels
115
+ # @param height [Numeric] viewport height in pixels
116
+ # @param min_depth [Numeric] minimum depth value
117
+ # @param max_depth [Numeric] maximum depth value
118
+ # @return [void]
75
119
  def set_viewport(x, y, width, height, min_depth: 0.0, max_depth: 1.0)
76
120
  Native.wgpuRenderPassEncoderSetViewport(@handle, x, y, width, height, min_depth, max_depth)
77
121
  end
78
122
 
123
+ # Restricts rasterization to a rectangular region.
124
+ #
125
+ # @param x [Integer] left coordinate in pixels
126
+ # @param y [Integer] top coordinate in pixels
127
+ # @param width [Integer] rectangle width in pixels
128
+ # @param height [Integer] rectangle height in pixels
129
+ # @return [void]
79
130
  def set_scissor_rect(x, y, width, height)
80
131
  Native.wgpuRenderPassEncoderSetScissorRect(@handle, x, y, width, height)
81
132
  end
82
133
 
134
+ # Sets the constant color used by blend factors.
135
+ #
136
+ # @param r [Numeric] red component
137
+ # @param g [Numeric] green component
138
+ # @param b [Numeric] blue component
139
+ # @param a [Numeric] alpha component
140
+ # @return [void]
83
141
  def set_blend_constant(r: 0.0, g: 0.0, b: 0.0, a: 1.0)
84
142
  color = Native::Color.new
85
143
  color[:r] = r
@@ -89,18 +147,36 @@ module WGPU
89
147
  Native.wgpuRenderPassEncoderSetBlendConstant(@handle, color.to_ptr)
90
148
  end
91
149
 
150
+ # Sets the stencil reference used by subsequent draw calls.
151
+ #
152
+ # @param reference [Integer] unsigned stencil reference
153
+ # @return [void]
92
154
  def set_stencil_reference(reference)
93
155
  Native.wgpuRenderPassEncoderSetStencilReference(@handle, reference)
94
156
  end
95
157
 
158
+ # Draws using non-indexed arguments stored in a buffer.
159
+ #
160
+ # @param buffer [Buffer] buffer containing the indirect arguments
161
+ # @param offset [Integer] byte offset of the arguments
162
+ # @return [void]
96
163
  def draw_indirect(buffer, offset: 0)
97
164
  Native.wgpuRenderPassEncoderDrawIndirect(@handle, buffer.handle, offset)
98
165
  end
99
166
 
167
+ # Draws using indexed arguments stored in a buffer.
168
+ #
169
+ # @param buffer [Buffer] buffer containing the indirect arguments
170
+ # @param offset [Integer] byte offset of the arguments
171
+ # @return [void]
100
172
  def draw_indexed_indirect(buffer, offset: 0)
101
173
  Native.wgpuRenderPassEncoderDrawIndexedIndirect(@handle, buffer.handle, offset)
102
174
  end
103
175
 
176
+ # Executes pre-recorded render bundles.
177
+ #
178
+ # @param bundles [Array<RenderBundle>] bundles to execute in order
179
+ # @return [void]
104
180
  def execute_bundles(bundles)
105
181
  bundle_handles = bundles.map(&:handle)
106
182
  bundles_ptr = FFI::MemoryPointer.new(:pointer, bundle_handles.size)
@@ -108,14 +184,25 @@ module WGPU
108
184
  Native.wgpuRenderPassEncoderExecuteBundles(@handle, bundle_handles.size, bundles_ptr)
109
185
  end
110
186
 
187
+ # Begins an occlusion query for subsequent draw calls.
188
+ #
189
+ # @param query_index [Integer] destination index in the pass query set
190
+ # @return [void]
111
191
  def begin_occlusion_query(query_index)
112
192
  Native.wgpuRenderPassEncoderBeginOcclusionQuery(@handle, query_index)
113
193
  end
114
194
 
195
+ # Ends the active occlusion query.
196
+ #
197
+ # @return [void]
115
198
  def end_occlusion_query
116
199
  Native.wgpuRenderPassEncoderEndOcclusionQuery(@handle)
117
200
  end
118
201
 
202
+ # Starts a labeled group in GPU debugging tools.
203
+ #
204
+ # @param label [String] group label
205
+ # @return [void]
119
206
  def push_debug_group(label)
120
207
  label_view = Native::StringView.new
121
208
  label_ptr = FFI::MemoryPointer.from_string(label)
@@ -124,10 +211,17 @@ module WGPU
124
211
  Native.wgpuRenderPassEncoderPushDebugGroup(@handle, label_view)
125
212
  end
126
213
 
214
+ # Ends the most recently pushed debug group.
215
+ #
216
+ # @return [void]
127
217
  def pop_debug_group
128
218
  Native.wgpuRenderPassEncoderPopDebugGroup(@handle)
129
219
  end
130
220
 
221
+ # Inserts a labeled point in GPU debugging tools.
222
+ #
223
+ # @param label [String] marker label
224
+ # @return [void]
131
225
  def insert_debug_marker(label)
132
226
  label_view = Native::StringView.new
133
227
  label_ptr = FFI::MemoryPointer.from_string(label)
@@ -136,14 +230,32 @@ module WGPU
136
230
  Native.wgpuRenderPassEncoderInsertDebugMarker(@handle, label_view)
137
231
  end
138
232
 
233
+ # Ends the pass if it has not already ended.
234
+ # @return [void]
139
235
  def end_pass
236
+ return if @ended
237
+
140
238
  Native.wgpuRenderPassEncoderEnd(@handle)
239
+ @ended = true
240
+ @encoder.send(:pass_ended, self)
141
241
  end
142
242
 
243
+ # Ends the pass.
244
+ # @return [void]
143
245
  def end
144
246
  end_pass
145
247
  end
146
248
 
249
+ # Reports whether the pass has ended.
250
+ # @return [Boolean]
251
+ def ended?
252
+ @ended
253
+ end
254
+
255
+ # Releases the native render pass encoder handle.
256
+ #
257
+ # Calling this method more than once has no effect.
258
+ # @return [void]
147
259
  def release
148
260
  return if @handle.null?
149
261
  Native.wgpuRenderPassEncoderRelease(@handle)
@@ -169,13 +281,13 @@ module WGPU
169
281
  @pointers << ptr
170
282
 
171
283
  attachments.each_with_index do |att, i|
172
- ca = Native::RenderPassColorAttachment.new(ptr + i * Native::RenderPassColorAttachment.size)
284
+ ca = Native::RenderPassColorAttachment.new(ptr + (i * Native::RenderPassColorAttachment.size))
173
285
  ca[:next_in_chain] = nil
174
286
  ca[:view] = att[:view].handle
175
287
  ca[:depth_slice] = att[:depth_slice] || 0xFFFFFFFF
176
288
  ca[:resolve_target] = att[:resolve_target]&.handle
177
- ca[:load_op] = att[:load_op] || :clear
178
- ca[:store_op] = att[:store_op] || :store
289
+ ca[:load_op] = Native::EnumHelper.coerce(Native::LoadOp, att[:load_op] || :clear, name: "load op")
290
+ ca[:store_op] = Native::EnumHelper.coerce(Native::StoreOp, att[:store_op] || :store, name: "store op")
179
291
 
180
292
  clear = att[:clear_value] || { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }
181
293
  ca[:clear_value][:r] = clear[:r] || 0.0
@@ -192,12 +304,28 @@ module WGPU
192
304
  @pointers << ds
193
305
 
194
306
  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
307
+ ds[:depth_load_op] = Native::EnumHelper.coerce(
308
+ Native::LoadOp,
309
+ att[:depth_load_op] || :clear,
310
+ name: "depth load op"
311
+ )
312
+ ds[:depth_store_op] = Native::EnumHelper.coerce(
313
+ Native::StoreOp,
314
+ att[:depth_store_op] || :store,
315
+ name: "depth store op"
316
+ )
197
317
  ds[:depth_clear_value] = att[:depth_clear_value] || 1.0
198
318
  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
319
+ ds[:stencil_load_op] = Native::EnumHelper.coerce(
320
+ Native::LoadOp,
321
+ att[:stencil_load_op] || :clear,
322
+ name: "stencil load op"
323
+ )
324
+ ds[:stencil_store_op] = Native::EnumHelper.coerce(
325
+ Native::StoreOp,
326
+ att[:stencil_store_op] || :store,
327
+ name: "stencil store op"
328
+ )
201
329
  ds[:stencil_clear_value] = att[:stencil_clear_value] || 0
202
330
  ds[:stencil_read_only] = att[:stencil_read_only] ? 1 : 0
203
331
 
@@ -4,34 +4,82 @@ module WGPU
4
4
  class Adapter
5
5
  attr_reader :handle, :instance
6
6
 
7
+ # Wraps an existing native adapter handle.
8
+ # @param handle [FFI::Pointer] native adapter handle
9
+ # @param instance [Instance, nil] instance that owns the adapter
10
+ # @return [Adapter]
7
11
  def self.from_handle(handle, instance: nil)
8
- adapter = allocate
9
- adapter.instance_variable_set(:@handle, handle)
12
+ adapter = adopt_native_handle(handle)
10
13
  adapter.instance_variable_set(:@instance, instance)
11
14
  adapter
12
15
  end
13
16
 
14
- def self.request(instance, power_preference: :high_performance, backend: nil, feature_level: :core, force_fallback_adapter: false, compatible_surface: nil)
17
+ # Requests an adapter and waits for the native callback.
18
+ #
19
+ # @param instance [Instance] instance used for discovery and callback progress
20
+ # @param power_preference [Symbol, Integer] preferred power profile
21
+ # @param backend [Symbol, Integer, nil] backend to restrict discovery to
22
+ # @param feature_level [Symbol, Integer] requested WebGPU feature level
23
+ # @param force_fallback_adapter [Boolean] whether to require a fallback adapter
24
+ # @param compatible_surface [Surface, nil] surface the adapter must support
25
+ # @param timeout [Numeric, nil] maximum wait time in seconds
26
+ # @return [Adapter] requested adapter
27
+ # @raise [AdapterError] if no adapter can be acquired
28
+ # @raise [TimeoutError] if the request exceeds +timeout+
29
+ def self.request(instance, power_preference: :high_performance, backend: nil, feature_level: :core,
30
+ force_fallback_adapter: false, compatible_surface: nil, timeout: nil)
15
31
  adapter_ptr = FFI::MemoryPointer.new(:pointer)
16
- status_holder = { value: nil, message: nil }
32
+ status_holder = {
33
+ done: false,
34
+ value: nil,
35
+ message: nil,
36
+ abandoned: false,
37
+ cleanup_claimed: false,
38
+ mutex: Mutex.new
39
+ }
17
40
 
41
+ callback_token = nil
18
42
  callback = FFI::Function.new(
19
43
  :void, [:uint32, :pointer, Native::StringView.by_value, :pointer, :pointer]
20
44
  ) do |status, adapter, message, _userdata1, _userdata2|
21
- status_holder[:value] = Native::RequestAdapterStatus[status]
22
- if message[:data] && !message[:data].null? && message[:length] > 0
23
- status_holder[:message] = message[:data].read_string(message[:length])
45
+ abandoned_adapter = nil
46
+ begin
47
+ status_holder[:mutex].synchronize do
48
+ status_holder[:value] = Native::RequestAdapterStatus[status]
49
+ if message[:data] && !message[:data].null? && message[:length] > 0
50
+ status_holder[:message] = message[:data].read_string(message[:length])
51
+ end
52
+ adapter_ptr.write_pointer(adapter)
53
+ status_holder[:done] = true
54
+ if status_holder[:abandoned] && !status_holder[:cleanup_claimed]
55
+ status_holder[:cleanup_claimed] = true
56
+ abandoned_adapter = adapter
57
+ end
58
+ end
59
+ Native.wgpuAdapterRelease(abandoned_adapter) if abandoned_adapter && !abandoned_adapter.null?
60
+ ensure
61
+ CallbackKeepalive.release(instance, callback_token)
24
62
  end
25
- adapter_ptr.write_pointer(adapter)
26
- status_holder[:done] = true
27
63
  end
28
64
 
29
65
  options = Native::RequestAdapterOptions.new
30
66
  options[:next_in_chain] = nil
31
- options[:feature_level] = feature_level
32
- options[:power_preference] = power_preference
67
+ options[:feature_level] = Native::EnumHelper.coerce(
68
+ Native::FeatureLevel,
69
+ feature_level,
70
+ name: "feature level"
71
+ )
72
+ options[:power_preference] = Native::EnumHelper.coerce(
73
+ Native::PowerPreference,
74
+ power_preference,
75
+ name: "power preference"
76
+ )
33
77
  options[:force_fallback_adapter] = force_fallback_adapter ? 1 : 0
34
- options[:backend_type] = backend || :undefined
78
+ options[:backend_type] = Native::EnumHelper.coerce(
79
+ Native::BackendType,
80
+ backend || :undefined,
81
+ name: "backend type"
82
+ )
35
83
  options[:compatible_surface] = compatible_surface&.handle
36
84
 
37
85
  callback_info = Native::RequestAdapterCallbackInfo.new
@@ -41,9 +89,28 @@ module WGPU
41
89
  callback_info[:userdata1] = nil
42
90
  callback_info[:userdata2] = nil
43
91
 
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)
92
+ callback_token = CallbackKeepalive.retain(instance, callback)
93
+ future =
94
+ begin
95
+ Native.wgpuInstanceRequestAdapter(instance.handle, options, callback_info)
96
+ rescue StandardError
97
+ CallbackKeepalive.release(instance, callback_token)
98
+ raise
99
+ end
100
+
101
+ begin
102
+ AsyncWaiter.wait(status_holder: status_holder, instance: instance, future: future, timeout: timeout)
103
+ rescue TimeoutError
104
+ abandoned_adapter = status_holder[:mutex].synchronize do
105
+ status_holder[:abandoned] = true
106
+ next unless status_holder[:done] && !status_holder[:cleanup_claimed]
107
+
108
+ status_holder[:cleanup_claimed] = true
109
+ adapter_ptr.read_pointer
110
+ end
111
+ Native.wgpuAdapterRelease(abandoned_adapter) if abandoned_adapter && !abandoned_adapter.null?
112
+ raise
113
+ end
47
114
 
48
115
  handle = adapter_ptr.read_pointer
49
116
  if handle.null? || status_holder[:value] != :success
@@ -54,25 +121,41 @@ module WGPU
54
121
  new(handle, instance: instance)
55
122
  end
56
123
 
124
+ # Wraps a native adapter handle.
125
+ # @param handle [FFI::Pointer] native adapter handle
126
+ # @param instance [Instance, nil] instance that owns the adapter
57
127
  def initialize(handle, instance: nil)
58
128
  @handle = handle
59
129
  @instance = instance
60
130
  end
61
131
 
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)
132
+ # Requests a logical device from this adapter.
133
+ # @return [Device]
134
+ def request_device(label: nil, required_features: [], required_limits: nil, timeout: nil)
135
+ Device.request(
136
+ self,
137
+ label: label,
138
+ required_features: required_features,
139
+ required_limits: required_limits,
140
+ timeout: timeout
141
+ )
64
142
  end
65
143
 
66
- def request_device_async(label: nil, required_features: [], required_limits: nil)
144
+ # Requests a logical device on a background task.
145
+ # @return [AsyncTask] task yielding a {Device}
146
+ def request_device_async(label: nil, required_features: [], required_limits: nil, timeout: nil)
67
147
  AsyncTask.new do
68
148
  request_device(
69
149
  label: label,
70
150
  required_features: required_features,
71
- required_limits: required_limits
151
+ required_limits: required_limits,
152
+ timeout: timeout
72
153
  )
73
154
  end
74
155
  end
75
156
 
157
+ # Returns identifying and backend information for the adapter.
158
+ # @return [Hash]
76
159
  def info
77
160
  info_struct = Native::AdapterInfo.new
78
161
  Native.wgpuAdapterGetInfo(@handle, info_struct)
@@ -92,22 +175,32 @@ module WGPU
92
175
  result
93
176
  end
94
177
 
178
+ # Returns the adapter's device name.
179
+ # @return [String]
95
180
  def name
96
181
  info[:device]
97
182
  end
98
183
 
184
+ # Returns the adapter vendor name.
185
+ # @return [String]
99
186
  def vendor
100
187
  info[:vendor]
101
188
  end
102
189
 
190
+ # Returns the backend used by the adapter.
191
+ # @return [Symbol, Integer]
103
192
  def backend_type
104
193
  info[:backend_type]
105
194
  end
106
195
 
196
+ # Returns the adapter's device classification.
197
+ # @return [Symbol, Integer]
107
198
  def adapter_type
108
199
  info[:adapter_type]
109
200
  end
110
201
 
202
+ # Lists optional features supported by the adapter.
203
+ # @return [Array<Symbol>]
111
204
  def features
112
205
  supported = Native::SupportedFeatures.new
113
206
  Native.wgpuAdapterGetFeatures(@handle, supported)
@@ -121,10 +214,15 @@ module WGPU
121
214
  result
122
215
  end
123
216
 
217
+ # Reports whether the adapter supports a feature.
218
+ # @param feature [Symbol] feature name
219
+ # @return [Boolean]
124
220
  def has_feature?(feature)
125
221
  features.include?(feature)
126
222
  end
127
223
 
224
+ # Returns the adapter resource limits.
225
+ # @return [Hash{Symbol => Integer}]
128
226
  def limits
129
227
  supported = Native::SupportedLimits.new
130
228
  supported[:next_in_chain] = nil
@@ -132,11 +230,17 @@ module WGPU
132
230
  limits_to_hash(supported[:limits])
133
231
  end
134
232
 
233
+ # Returns a concise human-readable adapter description.
234
+ # @return [String]
135
235
  def summary
136
236
  info_hash = info
137
237
  "#{info_hash[:device]} (#{info_hash[:adapter_type]}) via #{info_hash[:backend_type]}"
138
238
  end
139
239
 
240
+ # Releases the native adapter handle.
241
+ #
242
+ # Calling this method more than once has no effect.
243
+ # @return [void]
140
244
  def release
141
245
  return if @handle.null?
142
246
  Native.wgpuAdapterRelease(@handle)
@@ -10,6 +10,27 @@ module WGPU
10
10
 
11
11
  module_function
12
12
 
13
+ # Returns the delay between callback progress polls.
14
+ # @return [Float] seconds
15
+ def poll_interval
16
+ @poll_interval ||= POLL_INTERVAL_SECONDS
17
+ end
18
+
19
+ # Sets the delay between callback progress polls.
20
+ # @param seconds [Numeric] positive delay in seconds
21
+ # @return [Float]
22
+ # @raise [ArgumentError] if the delay is not positive
23
+ def poll_interval=(seconds)
24
+ value = Float(seconds)
25
+ raise ArgumentError, "poll interval must be positive" unless value.positive?
26
+
27
+ @poll_interval = value
28
+ end
29
+
30
+ # Selects the native callback delivery mode for an operation.
31
+ #
32
+ # @param instance [Instance, nil] instance capable of processing events
33
+ # @return [Integer] native {Native::CallbackMode} value
13
34
  def callback_mode(instance:)
14
35
  if instance
15
36
  callback_mode_value(:allow_process_events)
@@ -18,18 +39,30 @@ module WGPU
18
39
  end
19
40
  end
20
41
 
21
- def wait(status_holder:, instance: nil, device: nil, future: nil)
42
+ # Drives native event processing until a callback completes.
43
+ # @param status_holder [Hash] state whose +:done+ flag is set by the callback
44
+ # @param timeout [Numeric, nil] maximum wait time in seconds
45
+ # @return [void]
46
+ # @raise [TimeoutError] if completion exceeds the timeout
47
+ def wait(status_holder:, instance: nil, device: nil, future: nil, timeout: nil)
48
+ timeout = Float(timeout) if timeout
49
+ raise ArgumentError, "timeout must be non-negative" if timeout&.negative?
50
+
51
+ deadline = monotonic_time + timeout if timeout
22
52
  wait_info = build_wait_info(future) if instance && Native.future_api?
23
53
 
24
54
  until status_holder[:done]
55
+ raise_timeout!(timeout) if deadline && monotonic_time >= deadline
56
+
57
+ waited = false
25
58
  if instance && wait_info
26
- wait_with_wait_any(instance, wait_info)
59
+ waited = wait_with_wait_any(instance, wait_info)
27
60
  elsif instance
28
61
  instance.process_events
29
62
  elsif device && Native.device_poll_available?
30
63
  Native.wgpuDevicePoll(device.handle, 0, nil)
31
64
  end
32
- sleep(POLL_INTERVAL_SECONDS) unless status_holder[:done]
65
+ sleep(poll_interval) unless status_holder[:done] || waited
33
66
  end
34
67
  end
35
68
 
@@ -55,10 +88,20 @@ module WGPU
55
88
 
56
89
  def wait_with_wait_any(instance, wait_info)
57
90
  status = Native.wgpuInstanceWaitAny(instance.handle, 1, wait_info.to_ptr, 0)
58
- return if [:success, :timed_out].include?(status)
91
+ return false if [:success, :timed_out].include?(status)
59
92
 
60
93
  raise Error, "wgpuInstanceWaitAny failed: #{status.inspect}"
61
94
  end
62
95
  private_class_method :wait_with_wait_any
96
+
97
+ def monotonic_time
98
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
99
+ end
100
+ private_class_method :monotonic_time
101
+
102
+ def raise_timeout!(timeout)
103
+ raise TimeoutError, "GPU operation timed out after #{timeout} seconds"
104
+ end
105
+ private_class_method :raise_timeout!
63
106
  end
64
107
  end
@@ -4,6 +4,9 @@ module WGPU
4
4
  class CanvasContext
5
5
  attr_reader :physical_size
6
6
 
7
+ # Creates a canvas context for platform presentation information.
8
+ # @param instance [Instance] owning WebGPU instance
9
+ # @param present_info [Hash] platform handles or an existing surface
7
10
  def initialize(instance, present_info = {})
8
11
  @instance = instance
9
12
  @present_info = present_info || {}
@@ -12,25 +15,40 @@ module WGPU
12
15
  @config = nil
13
16
  end
14
17
 
18
+ # Updates the drawable's physical pixel size.
19
+ # @param width [Integer] width in pixels
20
+ # @param height [Integer] height in pixels
21
+ # @return [Array<Integer>] stored width and height
22
+ # @raise [ArgumentError] if either dimension is negative
15
23
  def set_physical_size(width, height)
16
24
  raise ArgumentError, "width and height must be non-negative" if width.to_i.negative? || height.to_i.negative?
17
25
 
18
26
  @physical_size = [width.to_i, height.to_i]
19
27
  end
20
28
 
29
+ # Returns the surface format preferred by an adapter.
30
+ #
31
+ # @param adapter [Adapter] adapter used to query surface capabilities
32
+ # @return [Symbol] preferred texture format
21
33
  def get_preferred_format(adapter)
22
34
  ensure_surface
23
35
  @surface.get_preferred_format(adapter)
24
36
  end
25
37
 
38
+ # Returns the most recent canvas configuration.
39
+ #
40
+ # @return [Hash, nil] configuration options, or +nil+ when unconfigured
26
41
  def get_configuration
27
42
  @config
28
43
  end
29
44
 
45
+ # Configures the backing surface for presentation.
46
+ # @param device [Device] device used to render frames
47
+ # @param format [Symbol, nil] surface texture format
48
+ # @return [Hash] resolved canvas configuration
49
+ # @raise [SurfaceError] if no surface exists or dimensions are not positive
30
50
  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
51
  ensure_surface
32
- color_space # reserved for API parity
33
- tone_mapping # reserved for API parity
34
52
 
35
53
  width = width || @physical_size[0]
36
54
  height = height || @physical_size[1]
@@ -61,21 +79,33 @@ module WGPU
61
79
  }
62
80
  end
63
81
 
82
+ # Removes the current surface configuration.
83
+ # @return [void]
64
84
  def unconfigure
65
85
  @surface&.unconfigure
66
86
  @config = nil
67
87
  end
68
88
 
89
+ # Acquires the texture for the current presentation frame.
90
+ #
91
+ # @return [Texture] acquired surface texture
92
+ # @raise [SurfaceError] if the context has not been configured
93
+ # @raise [SurfaceAcquisitionError] if acquisition fails
69
94
  def get_current_texture
70
95
  raise SurfaceError, "Canvas context must be configured before get_current_texture" unless @config
71
96
 
72
97
  @surface.current_texture
73
98
  end
74
99
 
100
+ # Presents the current surface texture.
101
+ # @return [void]
75
102
  def present
76
103
  @surface&.present
77
104
  end
78
105
 
106
+ # Releases the backing surface and clears the configuration.
107
+ #
108
+ # @return [void]
79
109
  def release
80
110
  @surface&.release
81
111
  @surface = nil