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,6 +4,8 @@ module WGPU
4
4
  class Instance
5
5
  attr_reader :handle
6
6
 
7
+ # Creates a WebGPU instance.
8
+ # @raise [InitializationError] if native instance creation fails
7
9
  def initialize
8
10
  desc = Native::InstanceDescriptor.new
9
11
  desc[:next_in_chain] = nil
@@ -15,29 +17,40 @@ module WGPU
15
17
  raise InitializationError, "Failed to create WebGPU instance" if @handle.null?
16
18
  end
17
19
 
18
- def request_adapter(power_preference: :high_performance, backend: nil, feature_level: :core, force_fallback_adapter: false, compatible_surface: nil)
20
+ # Requests an adapter matching the supplied preferences.
21
+ # @return [Adapter]
22
+ def request_adapter(power_preference: :high_performance, backend: nil, feature_level: :core,
23
+ force_fallback_adapter: false, compatible_surface: nil, timeout: nil)
19
24
  Adapter.request(
20
25
  self,
21
26
  power_preference: power_preference,
22
27
  backend: backend,
23
28
  feature_level: feature_level,
24
29
  force_fallback_adapter: force_fallback_adapter,
25
- compatible_surface: compatible_surface
30
+ compatible_surface: compatible_surface,
31
+ timeout: timeout
26
32
  )
27
33
  end
28
34
 
29
- def request_adapter_async(power_preference: :high_performance, backend: nil, feature_level: :core, force_fallback_adapter: false, compatible_surface: nil)
35
+ # Requests an adapter on a background task.
36
+ # @return [AsyncTask] task yielding an {Adapter}
37
+ def request_adapter_async(power_preference: :high_performance, backend: nil, feature_level: :core,
38
+ force_fallback_adapter: false, compatible_surface: nil, timeout: nil)
30
39
  AsyncTask.new do
31
40
  request_adapter(
32
41
  power_preference: power_preference,
33
42
  backend: backend,
34
43
  feature_level: feature_level,
35
44
  force_fallback_adapter: force_fallback_adapter,
36
- compatible_surface: compatible_surface
45
+ compatible_surface: compatible_surface,
46
+ timeout: timeout
37
47
  )
38
48
  end
39
49
  end
40
50
 
51
+ # Lists adapters exposed by the instance.
52
+ # @param backends [Integer, nil] backend bit mask
53
+ # @return [Array<Adapter>]
41
54
  def enumerate_adapters(backends: nil)
42
55
  options = nil
43
56
  if backends
@@ -57,20 +70,31 @@ module WGPU
57
70
  end
58
71
  end
59
72
 
73
+ # Enumerates adapters on a background task.
74
+ # @return [AsyncTask] task yielding adapter objects
60
75
  def enumerate_adapters_async(backends: nil)
61
76
  AsyncTask.new do
62
77
  enumerate_adapters(backends: backends)
63
78
  end
64
79
  end
65
80
 
81
+ # Creates a canvas context from platform presentation information.
82
+ # @param present_info [Hash] platform surface information
83
+ # @return [CanvasContext]
66
84
  def get_canvas_context(present_info)
67
85
  CanvasContext.new(self, present_info)
68
86
  end
69
87
 
88
+ # Processes pending instance callbacks and events.
89
+ # @return [void]
70
90
  def process_events
71
91
  Native.wgpuInstanceProcessEvents(@handle)
72
92
  end
73
93
 
94
+ # Releases the native instance handle.
95
+ #
96
+ # Calling this method more than once has no effect.
97
+ # @return [void]
74
98
  def release
75
99
  return if @handle.null?
76
100
  Native.wgpuInstanceRelease(@handle)
@@ -4,24 +4,45 @@ module WGPU
4
4
  class Queue
5
5
  attr_reader :handle
6
6
 
7
+ # Wraps a device's native submission queue.
8
+ # @param handle [FFI::Pointer] native queue handle
9
+ # @param device [Device, nil] owning device
7
10
  def initialize(handle, device: nil)
8
11
  @handle = handle
9
12
  @device = device
10
13
  end
11
14
 
15
+ # Submits command buffers exactly once in the supplied order.
16
+ # @param command_buffers [CommandBuffer, Array<CommandBuffer>] buffers to submit
17
+ # @return [void]
18
+ # @raise [CommandError] for duplicate or previously submitted buffers
12
19
  def submit(command_buffers)
13
20
  buffers = Array(command_buffers)
14
21
  return if buffers.empty?
15
22
 
23
+ if buffers.map(&:object_id).uniq.length != buffers.length
24
+ raise CommandError, "The same command buffer cannot appear twice in one submission"
25
+ end
26
+ buffers.each do |buffer|
27
+ raise CommandError, "Command buffer has already been submitted" if buffer.submitted?
28
+ end
29
+
16
30
  handles = buffers.map(&:handle)
17
31
  ptr = FFI::MemoryPointer.new(:pointer, handles.size)
18
32
  ptr.write_array_of_pointer(handles)
19
33
 
20
34
  Native.wgpuQueueSubmit(@handle, handles.size, ptr)
35
+ buffers.each(&:mark_submitted!)
21
36
  end
22
37
 
23
- def write_buffer(buffer, buffer_offset, data, data_offset: 0, size: nil)
24
- data_ptr, byte_size = data_to_pointer(data)
38
+ # Writes typed data into a GPU buffer.
39
+ # @param buffer [Buffer] destination buffer
40
+ # @param buffer_offset [Integer] destination byte offset
41
+ # @param data [Array, String, FFI::Pointer] source data
42
+ # @return [void]
43
+ def write_buffer(buffer, buffer_offset, data, data_offset: 0, size: nil, type: :f32)
44
+ data_ptr, byte_size = DataTypes.to_pointer(data, type:)
45
+ DataTypes.validate_alignment!(buffer_offset, 4, name: "buffer_offset")
25
46
  data_offset = Integer(data_offset)
26
47
  raise ArgumentError, "data_offset must be non-negative" if data_offset.negative?
27
48
  raise ArgumentError, "data_offset out of range" if data_offset > byte_size
@@ -29,6 +50,7 @@ module WGPU
29
50
  write_size = size.nil? ? (byte_size - data_offset) : Integer(size)
30
51
  raise ArgumentError, "size must be non-negative" if write_size.negative?
31
52
  raise ArgumentError, "data_offset + size out of range" if data_offset + write_size > byte_size
53
+ DataTypes.validate_alignment!(write_size, 4, name: "size")
32
54
 
33
55
  Native.wgpuQueueWriteBuffer(
34
56
  @handle,
@@ -39,8 +61,14 @@ module WGPU
39
61
  )
40
62
  end
41
63
 
42
- def write_texture(destination:, data:, data_layout:, size:)
43
- data_ptr, byte_size = data_to_pointer(data)
64
+ # Writes typed data into a texture region.
65
+ # @param destination [Hash] destination texture and origin
66
+ # @param data [Array, String, FFI::Pointer] source data
67
+ # @param data_layout [Hash] source byte layout
68
+ # @param size [Hash, Array] extent to write
69
+ # @return [void]
70
+ def write_texture(destination:, data:, data_layout:, size:, type: :f32)
71
+ data_ptr, byte_size = DataTypes.to_pointer(data, type:)
44
72
 
45
73
  dst = Native::ImageCopyTexture.new
46
74
  dst[:texture] = destination[:texture].handle
@@ -48,7 +76,11 @@ module WGPU
48
76
  dst[:origin][:x] = destination.dig(:origin, :x) || 0
49
77
  dst[:origin][:y] = destination.dig(:origin, :y) || 0
50
78
  dst[:origin][:z] = destination.dig(:origin, :z) || 0
51
- dst[:aspect] = destination[:aspect] || :all
79
+ dst[:aspect] = Native::EnumHelper.coerce(
80
+ Native::TextureAspect,
81
+ destination[:aspect] || :all,
82
+ name: "texture aspect"
83
+ )
52
84
 
53
85
  extent = Native::Extent3D.new
54
86
  if size.is_a?(Array)
@@ -69,14 +101,18 @@ module WGPU
69
101
  Native.wgpuQueueWriteTexture(@handle, dst, data_ptr, byte_size, layout, extent)
70
102
  end
71
103
 
72
- def read_buffer(buffer, offset: 0, size: nil, device:)
73
- size ||= buffer.size - offset
74
-
75
- staging = Buffer.new(device,
76
- size: size,
77
- usage: [:map_read, :copy_dst]
78
- )
104
+ # Copies a GPU buffer to mapped staging memory and returns its bytes.
105
+ # @param buffer [Buffer] source buffer
106
+ # @param staging [Buffer, nil] reusable map-read staging buffer
107
+ # @return [String] copied bytes
108
+ def read_buffer(buffer, offset: 0, size: nil, device: nil, staging: nil)
109
+ device ||= @device
110
+ raise ArgumentError, "device is required when the queue has no owning device" unless device
79
111
 
112
+ size ||= buffer.size - offset
113
+ owns_staging = staging.nil?
114
+ staging ||= Buffer.new(device, size: size, usage: [:map_read, :copy_dst])
115
+ validate_readback_staging!(staging, size)
80
116
  encoder = CommandEncoder.new(device)
81
117
  encoder.copy_buffer_to_buffer(
82
118
  source: buffer,
@@ -88,27 +124,55 @@ module WGPU
88
124
  command_buffer = encoder.finish
89
125
  submit([command_buffer])
90
126
 
127
+ map_requested = true
91
128
  staging.map_sync(:read)
92
- data = staging.read_mapped_data
93
- staging.unmap
94
- staging.release
95
-
96
- data
129
+ staging.read_mapped_data(size:)
130
+ ensure
131
+ cleanup_readback_resources(
132
+ staging:,
133
+ unmap_staging: map_requested,
134
+ owns_staging:,
135
+ command_buffer:,
136
+ encoder:,
137
+ active_error: $!
138
+ )
97
139
  end
98
140
 
99
- def read_texture(source:, data_layout:, size:, device:)
100
- width = size[:width] || size[0]
101
- height = size[:height] || size[1] || 1
102
- depth = size[:depth_or_array_layers] || size[2] || 1
141
+ # Copies a texture region to mapped staging memory and returns its padded bytes.
142
+ # @param source [Hash] source texture, origin, aspect, and optional format
143
+ # @param data_layout [Hash] destination byte layout
144
+ # @param size [Hash, Array] extent to read
145
+ # @param staging [Buffer, nil] reusable map-read staging buffer
146
+ # @return [String] copied bytes
147
+ def read_texture(source:, data_layout:, size:, device: nil, staging: nil)
148
+ device ||= @device
149
+ raise ArgumentError, "device is required when the queue has no owning device" unless device
150
+
151
+ width, height, depth = texture_extent(size)
103
152
  bytes_per_row = data_layout[:bytes_per_row]
104
- rows_per_image = data_layout[:rows_per_image] || height
105
- buffer_size = bytes_per_row * rows_per_image * depth
153
+ raise ArgumentError, "data_layout[:bytes_per_row] is required" unless bytes_per_row
106
154
 
107
- staging = Buffer.new(device,
108
- size: buffer_size,
109
- usage: [:map_read, :copy_dst]
155
+ DataTypes.validate_alignment!(
156
+ bytes_per_row,
157
+ TextureFormat::COPY_ALIGNMENT,
158
+ name: "bytes_per_row"
110
159
  )
160
+ format = source[:format] || source[:texture].format
161
+ aspect = source[:aspect] || :all
162
+ tight_bytes_per_row = TextureFormat.bytes_per_row(width, format, aspect:)
163
+ minimum_bytes_per_row = TextureFormat.aligned_bytes_per_row(width, format, aspect:)
164
+ if bytes_per_row < minimum_bytes_per_row
165
+ raise ArgumentError,
166
+ "bytes_per_row must be at least #{minimum_bytes_per_row} for width #{width} and #{format.inspect} " \
167
+ "(tight row is #{tight_bytes_per_row} bytes)"
168
+ end
111
169
 
170
+ rows_per_image = data_layout[:rows_per_image] || height
171
+ buffer_size = bytes_per_row * rows_per_image * depth
172
+
173
+ owns_staging = staging.nil?
174
+ staging ||= Buffer.new(device, size: buffer_size, usage: [:map_read, :copy_dst])
175
+ validate_readback_staging!(staging, buffer_size)
112
176
  encoder = CommandEncoder.new(device)
113
177
  encoder.copy_texture_to_buffer(
114
178
  source: source,
@@ -123,24 +187,41 @@ module WGPU
123
187
  command_buffer = encoder.finish
124
188
  submit([command_buffer])
125
189
 
190
+ map_requested = true
126
191
  staging.map_sync(:read)
127
- data = staging.read_mapped_data
128
- staging.unmap
129
- staging.release
130
-
131
- data
192
+ staging.read_mapped_data(size: buffer_size)
193
+ ensure
194
+ cleanup_readback_resources(
195
+ staging:,
196
+ unmap_staging: map_requested,
197
+ owns_staging:,
198
+ command_buffer:,
199
+ encoder:,
200
+ active_error: $!
201
+ )
132
202
  end
133
203
 
134
- def on_submitted_work_done(device: nil)
204
+ # Waits until all previously submitted queue work completes.
205
+ # @param device [Device, nil] device used to drive callback progress
206
+ # @param timeout [Numeric, nil] maximum wait time in seconds
207
+ # @return [Symbol] native completion status
208
+ def on_submitted_work_done(device: nil, timeout: nil)
135
209
  device ||= @device
136
210
  instance = device&.adapter&.instance
137
211
  status_holder = { done: false, status: nil }
138
212
 
213
+ callback_lifetime_release = device_callback_lifetime_lease
214
+ callback_token = nil
139
215
  callback = FFI::Function.new(
140
216
  :void, [:uint32, :pointer, :pointer]
141
217
  ) do |status, _userdata1, _userdata2|
142
- status_holder[:done] = true
143
- status_holder[:status] = Native::QueueWorkDoneStatus[status]
218
+ begin
219
+ status_holder[:status] = Native::QueueWorkDoneStatus[status]
220
+ status_holder[:done] = true
221
+ ensure
222
+ CallbackKeepalive.release(self, callback_token)
223
+ callback_lifetime_release.call
224
+ end
144
225
  end
145
226
 
146
227
  callback_info = Native::QueueWorkDoneCallbackInfo.new
@@ -150,18 +231,38 @@ module WGPU
150
231
  callback_info[:userdata1] = nil
151
232
  callback_info[:userdata2] = nil
152
233
 
153
- future = Native.wgpuQueueOnSubmittedWorkDone(@handle, callback_info)
154
- AsyncWaiter.wait(status_holder: status_holder, instance: instance, device: device, future: future)
234
+ callback_token = CallbackKeepalive.retain(self, callback)
235
+ future =
236
+ begin
237
+ Native.wgpuQueueOnSubmittedWorkDone(@handle, callback_info)
238
+ rescue StandardError
239
+ CallbackKeepalive.release(self, callback_token)
240
+ callback_lifetime_release.call
241
+ raise
242
+ end
243
+ AsyncWaiter.wait(
244
+ status_holder: status_holder,
245
+ instance: instance,
246
+ device: device,
247
+ future: future,
248
+ timeout: timeout
249
+ )
155
250
 
156
251
  status_holder[:status]
157
252
  end
158
253
 
159
- def on_submitted_work_done_async(device: nil)
254
+ # Waits for prior queue work on a background task.
255
+ # @return [AsyncTask] task yielding the native completion status
256
+ def on_submitted_work_done_async(device: nil, timeout: nil)
160
257
  AsyncTask.new do
161
- on_submitted_work_done(device: device)
258
+ on_submitted_work_done(device: device, timeout: timeout)
162
259
  end
163
260
  end
164
261
 
262
+ # Releases the native queue handle.
263
+ #
264
+ # Calling this method more than once has no effect.
265
+ # @return [void]
165
266
  def release
166
267
  return if @handle.null?
167
268
  Native.wgpuQueueRelease(@handle)
@@ -170,21 +271,66 @@ module WGPU
170
271
 
171
272
  private
172
273
 
173
- def data_to_pointer(data)
174
- case data
175
- when String
176
- ptr = FFI::MemoryPointer.new(:char, data.bytesize)
177
- ptr.put_bytes(0, data)
178
- [ptr, data.bytesize]
179
- when Array
180
- ptr = FFI::MemoryPointer.new(:float, data.size)
181
- ptr.write_array_of_float(data)
182
- [ptr, data.size * 4]
183
- when FFI::Pointer
184
- [data, data.size]
274
+ def texture_extent(size)
275
+ if size.is_a?(Array)
276
+ [size.fetch(0), size[1] || 1, size[2] || 1]
185
277
  else
186
- raise ArgumentError, "Unsupported data type: #{data.class}"
278
+ [size.fetch(:width), size[:height] || 1, size[:depth_or_array_layers] || 1]
187
279
  end
188
280
  end
281
+
282
+ def validate_readback_staging!(staging, required_size)
283
+ if staging.size < required_size
284
+ raise ArgumentError,
285
+ "staging buffer size must be at least #{required_size} bytes (got #{staging.size})"
286
+ end
287
+
288
+ required_usage = Native::BufferUsage.fetch(:map_read) | Native::BufferUsage.fetch(:copy_dst)
289
+ unless (staging.usage & required_usage) == required_usage
290
+ raise ArgumentError, "staging buffer usage must include :map_read and :copy_dst"
291
+ end
292
+
293
+ return if staging.map_state == :unmapped
294
+
295
+ raise ArgumentError, "staging buffer must be unmapped before readback"
296
+ end
297
+
298
+ def cleanup_readback_resources(
299
+ staging:,
300
+ unmap_staging:,
301
+ owns_staging:,
302
+ command_buffer:,
303
+ encoder:,
304
+ active_error:
305
+ )
306
+ cleanup_error = nil
307
+
308
+ begin
309
+ staging&.unmap if unmap_staging && staging && staging.map_state == :mapped
310
+ rescue StandardError => e
311
+ cleanup_error ||= e
312
+ ensure
313
+ begin
314
+ command_buffer&.release
315
+ rescue StandardError => e
316
+ cleanup_error ||= e
317
+ ensure
318
+ begin
319
+ encoder&.release
320
+ rescue StandardError => e
321
+ cleanup_error ||= e
322
+ ensure
323
+ begin
324
+ staging&.release if owns_staging
325
+ rescue StandardError => e
326
+ cleanup_error ||= e
327
+ end
328
+ end
329
+ end
330
+ end
331
+
332
+ raise cleanup_error if cleanup_error && active_error.nil?
333
+ end
334
+
189
335
  end
190
336
  end
@@ -4,6 +4,9 @@ module WGPU
4
4
  class Surface
5
5
  attr_reader :handle
6
6
 
7
+ # Creates a surface backed by a Core Animation Metal layer.
8
+ # @return [Surface]
9
+ # @raise [SurfaceError] if native surface creation fails
7
10
  def self.from_metal_layer(instance, layer)
8
11
  source = Native::SurfaceSourceMetalLayer.new
9
12
  source[:chain][:next] = nil
@@ -21,6 +24,9 @@ module WGPU
21
24
  new(handle, instance)
22
25
  end
23
26
 
27
+ # Creates a surface backed by a Windows window handle.
28
+ # @return [Surface]
29
+ # @raise [SurfaceError] if native surface creation fails
24
30
  def self.from_windows_hwnd(instance, hinstance, hwnd)
25
31
  source = Native::SurfaceSourceWindowsHWND.new
26
32
  source[:chain][:next] = nil
@@ -39,6 +45,9 @@ module WGPU
39
45
  new(handle, instance)
40
46
  end
41
47
 
48
+ # Creates a surface backed by an Xlib window.
49
+ # @return [Surface]
50
+ # @raise [SurfaceError] if native surface creation fails
42
51
  def self.from_xlib_window(instance, display, window)
43
52
  source = Native::SurfaceSourceXlibWindow.new
44
53
  source[:chain][:next] = nil
@@ -57,6 +66,9 @@ module WGPU
57
66
  new(handle, instance)
58
67
  end
59
68
 
69
+ # Creates a surface backed by a Wayland surface.
70
+ # @return [Surface]
71
+ # @raise [SurfaceError] if native surface creation fails
60
72
  def self.from_wayland_surface(instance, display, surface)
61
73
  source = Native::SurfaceSourceWaylandSurface.new
62
74
  source[:chain][:next] = nil
@@ -75,6 +87,9 @@ module WGPU
75
87
  new(handle, instance)
76
88
  end
77
89
 
90
+ # Wraps a native presentation surface.
91
+ # @param handle [FFI::Pointer] native surface handle
92
+ # @param instance [Instance] owning instance
78
93
  def initialize(handle, instance)
79
94
  @handle = handle
80
95
  @instance = instance
@@ -82,11 +97,13 @@ module WGPU
82
97
  @config = nil
83
98
  end
84
99
 
100
+ # Configures this surface for presentation by a device.
101
+ # @return [Hash] stored configuration
85
102
  def configure(device:, format:, usage: :render_attachment, width:, height:, present_mode: :fifo, alpha_mode: :auto, view_formats: [])
86
103
  config = Native::SurfaceConfiguration.new
87
104
  config[:next_in_chain] = nil
88
105
  config[:device] = device.handle
89
- config[:format] = format
106
+ config[:format] = Native::EnumHelper.coerce(Native::TextureFormat, format, name: "surface format")
90
107
  config[:usage] = normalize_usage(usage)
91
108
  config[:width] = width
92
109
  config[:height] = height
@@ -95,19 +112,29 @@ module WGPU
95
112
  @view_formats_ptr = nil
96
113
  config[:view_formats] = nil
97
114
  else
98
- format_values = view_formats.map do |vf|
99
- vf.is_a?(Integer) ? vf : Native::TextureFormat[vf]
115
+ format_values = view_formats.map do |view_format|
116
+ Native::EnumHelper.coerce(Native::TextureFormat, view_format, name: "view format")
100
117
  end
101
118
  @view_formats_ptr = FFI::MemoryPointer.new(:uint32, format_values.size)
102
119
  @view_formats_ptr.write_array_of_uint32(format_values)
103
120
  config[:view_formats] = @view_formats_ptr
104
121
  end
105
- config[:alpha_mode] = Native::CompositeAlphaMode[alpha_mode]
106
- config[:present_mode] = Native::PresentMode[present_mode]
122
+ config[:alpha_mode] = Native::EnumHelper.coerce(
123
+ Native::CompositeAlphaMode,
124
+ alpha_mode,
125
+ name: "alpha mode"
126
+ )
127
+ config[:present_mode] = Native::EnumHelper.coerce(
128
+ Native::PresentMode,
129
+ present_mode,
130
+ name: "present mode"
131
+ )
107
132
 
108
133
  Native.wgpuSurfaceConfigure(@handle, config)
134
+ release_device_callback_lifetime
109
135
  @configured = true
110
136
  @device = device
137
+ attach_device_callback_lifetime(device)
111
138
  @config = {
112
139
  device: device,
113
140
  format: format,
@@ -120,12 +147,20 @@ module WGPU
120
147
  }
121
148
  end
122
149
 
150
+ # Removes the current surface configuration.
151
+ # @return [void]
123
152
  def unconfigure
124
153
  Native.wgpuSurfaceUnconfigure(@handle)
154
+ release_device_callback_lifetime
125
155
  @configured = false
156
+ @device = nil
126
157
  @config = nil
127
158
  end
128
159
 
160
+ # Acquires the current surface texture.
161
+ # @return [Texture]
162
+ # @raise [SurfaceError] if unconfigured or no texture is returned
163
+ # @raise [SurfaceAcquisitionError] if the surface status is unsuccessful
129
164
  def current_texture
130
165
  raise SurfaceError, "Surface is not configured" unless @configured
131
166
 
@@ -134,7 +169,7 @@ module WGPU
134
169
 
135
170
  status = Native::SurfaceGetCurrentTextureStatus[surface_texture[:status]]
136
171
  unless status == :success_optimal || status == :success_suboptimal
137
- raise SurfaceError, "Failed to get current texture: #{status}"
172
+ raise SurfaceAcquisitionError.new(status)
138
173
  end
139
174
 
140
175
  texture_ptr = surface_texture[:texture]
@@ -142,26 +177,43 @@ module WGPU
142
177
  raise SurfaceError, "Surface returned null texture"
143
178
  end
144
179
 
145
- Texture.from_handle(texture_ptr)
180
+ Texture.from_handle(texture_ptr, surface_status: status, device: @device)
146
181
  end
147
182
 
183
+ # Acquires the texture for the current presentation frame.
184
+ #
185
+ # @return [Texture] acquired surface texture
186
+ # @raise [SurfaceError] if the surface is not configured
187
+ # @raise [SurfaceAcquisitionError] if acquisition fails
148
188
  def get_current_texture
149
189
  current_texture
150
190
  end
151
191
 
192
+ # Presents the current surface texture.
193
+ # @return [void]
152
194
  def present
153
195
  Native.wgpuSurfacePresent(@handle)
154
196
  end
155
197
 
198
+ # Returns the most recent surface configuration.
199
+ #
200
+ # @return [Hash, nil] configuration options, or +nil+ when unconfigured
156
201
  def get_configuration
157
202
  @config
158
203
  end
159
204
 
205
+ # Returns the first surface format supported by an adapter.
206
+ #
207
+ # @param adapter [Adapter] adapter used to query surface capabilities
208
+ # @return [Symbol] preferred texture format
160
209
  def get_preferred_format(adapter)
161
210
  caps = capabilities(adapter)
162
211
  caps[:formats].first || :bgra8_unorm
163
212
  end
164
213
 
214
+ # Returns the formats, presentation modes, alpha modes, and usages supported by an adapter.
215
+ # @param adapter [Adapter] adapter to query
216
+ # @return [Hash]
165
217
  def capabilities(adapter)
166
218
  caps = Native::SurfaceCapabilities.new
167
219
  Native.wgpuSurfaceGetCapabilities(@handle, adapter.handle, caps)
@@ -197,6 +249,10 @@ module WGPU
197
249
  Native.wgpuSurfaceCapabilitiesFreeMembers(caps) if caps
198
250
  end
199
251
 
252
+ # Releases the native surface handle.
253
+ #
254
+ # Calling this method more than once has no effect.
255
+ # @return [void]
200
256
  def release
201
257
  return if @handle.null?
202
258
  Native.wgpuSurfaceRelease(@handle)
@@ -206,16 +262,7 @@ module WGPU
206
262
  private
207
263
 
208
264
  def normalize_usage(usage)
209
- case usage
210
- when Integer
211
- usage
212
- when Symbol
213
- Native::TextureUsage[usage]
214
- when Array
215
- usage.reduce(0) { |acc, u| acc | Native::TextureUsage[u] }
216
- else
217
- raise ArgumentError, "Invalid usage: #{usage}"
218
- end
265
+ Native::EnumHelper.coerce_flags(Native::TextureUsage, usage, name: "surface usage")
219
266
  end
220
267
  end
221
268
  end