wgpu 1.2.0 → 1.2.2

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -0
  3. data/README.md +18 -2
  4. data/docs/README.md +2 -2
  5. data/docs/api_coverage.md +23 -15
  6. data/docs/async.md +13 -1
  7. data/docs/command_encoding.md +25 -0
  8. data/docs/errors.md +13 -0
  9. data/docs/getting_started_compute.md +1 -0
  10. data/docs/getting_started_rendering.md +5 -0
  11. data/docs/pipeline_descriptors.md +7 -2
  12. data/docs/releasing.md +11 -1
  13. data/docs/resource_lifetime.md +25 -2
  14. data/docs/texture_readback.md +27 -0
  15. data/docs/troubleshooting.md +5 -0
  16. data/docs/upgrading_wgpu_native.md +16 -5
  17. data/lib/wgpu/async_task.rb +19 -0
  18. data/lib/wgpu/commands/command_buffer.rb +14 -1
  19. data/lib/wgpu/commands/command_encoder.rb +82 -27
  20. data/lib/wgpu/commands/compute_pass.rb +49 -6
  21. data/lib/wgpu/commands/render_bundle.rb +9 -1
  22. data/lib/wgpu/commands/render_bundle_encoder.rb +63 -9
  23. data/lib/wgpu/commands/render_pass.rb +116 -14
  24. data/lib/wgpu/core/adapter.rb +95 -13
  25. data/lib/wgpu/core/async_waiter.rb +30 -4
  26. data/lib/wgpu/core/canvas_context.rb +37 -2
  27. data/lib/wgpu/core/device.rb +377 -70
  28. data/lib/wgpu/core/instance.rb +20 -0
  29. data/lib/wgpu/core/queue.rb +143 -43
  30. data/lib/wgpu/core/surface.rb +55 -7
  31. data/lib/wgpu/data_types.rb +16 -0
  32. data/lib/wgpu/descriptor_helpers.rb +87 -0
  33. data/lib/wgpu/error.rb +15 -0
  34. data/lib/wgpu/native/abi_verifier.rb +37 -3
  35. data/lib/wgpu/native/callbacks.rb +6 -0
  36. data/lib/wgpu/native/capabilities.rb +16 -2
  37. data/lib/wgpu/native/distribution.rb +63 -0
  38. data/lib/wgpu/native/enum_helper.rb +17 -0
  39. data/lib/wgpu/native/enums.rb +8 -0
  40. data/lib/wgpu/native/fixtures/webgpu-v27.0.4.0-enums.h +848 -0
  41. data/lib/wgpu/native/functions.rb +14 -0
  42. data/lib/wgpu/native/installer.rb +55 -18
  43. data/lib/wgpu/native/loader.rb +22 -0
  44. data/lib/wgpu/native/structs.rb +18 -1
  45. data/lib/wgpu/native_resource.rb +190 -3
  46. data/lib/wgpu/pipeline/bind_group.rb +16 -7
  47. data/lib/wgpu/pipeline/bind_group_layout.rb +20 -7
  48. data/lib/wgpu/pipeline/compute_pipeline.rb +25 -37
  49. data/lib/wgpu/pipeline/pipeline_layout.rb +12 -4
  50. data/lib/wgpu/pipeline/render_pipeline.rb +33 -48
  51. data/lib/wgpu/pipeline/shader_module.rb +57 -32
  52. data/lib/wgpu/resources/buffer.rb +330 -59
  53. data/lib/wgpu/resources/query_set.rb +13 -1
  54. data/lib/wgpu/resources/sampler.rb +10 -3
  55. data/lib/wgpu/resources/texture.rb +49 -12
  56. data/lib/wgpu/resources/texture_view.rb +25 -5
  57. data/lib/wgpu/texture_format.rb +14 -0
  58. data/lib/wgpu/version.rb +1 -1
  59. data/lib/wgpu/window.rb +26 -0
  60. data/sig/wgpu.rbs +85 -5
  61. metadata +12 -4
@@ -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,6 +17,8 @@ module WGPU
15
17
  raise InitializationError, "Failed to create WebGPU instance" if @handle.null?
16
18
  end
17
19
 
20
+ # Requests an adapter matching the supplied preferences.
21
+ # @return [Adapter]
18
22
  def request_adapter(power_preference: :high_performance, backend: nil, feature_level: :core,
19
23
  force_fallback_adapter: false, compatible_surface: nil, timeout: nil)
20
24
  Adapter.request(
@@ -28,6 +32,8 @@ module WGPU
28
32
  )
29
33
  end
30
34
 
35
+ # Requests an adapter on a background task.
36
+ # @return [AsyncTask] task yielding an {Adapter}
31
37
  def request_adapter_async(power_preference: :high_performance, backend: nil, feature_level: :core,
32
38
  force_fallback_adapter: false, compatible_surface: nil, timeout: nil)
33
39
  AsyncTask.new do
@@ -42,6 +48,9 @@ module WGPU
42
48
  end
43
49
  end
44
50
 
51
+ # Lists adapters exposed by the instance.
52
+ # @param backends [Integer, nil] backend bit mask
53
+ # @return [Array<Adapter>]
45
54
  def enumerate_adapters(backends: nil)
46
55
  options = nil
47
56
  if backends
@@ -61,20 +70,31 @@ module WGPU
61
70
  end
62
71
  end
63
72
 
73
+ # Enumerates adapters on a background task.
74
+ # @return [AsyncTask] task yielding adapter objects
64
75
  def enumerate_adapters_async(backends: nil)
65
76
  AsyncTask.new do
66
77
  enumerate_adapters(backends: backends)
67
78
  end
68
79
  end
69
80
 
81
+ # Creates a canvas context from platform presentation information.
82
+ # @param present_info [Hash] platform surface information
83
+ # @return [CanvasContext]
70
84
  def get_canvas_context(present_info)
71
85
  CanvasContext.new(self, present_info)
72
86
  end
73
87
 
88
+ # Processes pending instance callbacks and events.
89
+ # @return [void]
74
90
  def process_events
75
91
  Native.wgpuInstanceProcessEvents(@handle)
76
92
  end
77
93
 
94
+ # Releases the native instance handle.
95
+ #
96
+ # Calling this method more than once has no effect.
97
+ # @return [void]
78
98
  def release
79
99
  return if @handle.null?
80
100
  Native.wgpuInstanceRelease(@handle)
@@ -4,11 +4,18 @@ 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?
@@ -20,7 +27,7 @@ module WGPU
20
27
  raise CommandError, "Command buffer has already been submitted" if buffer.submitted?
21
28
  end
22
29
 
23
- handles = buffers.map(&:handle)
30
+ handles = buffers.map { |buffer| NativeResource.checked_handle(buffer, expected_class: CommandBuffer) }
24
31
  ptr = FFI::MemoryPointer.new(:pointer, handles.size)
25
32
  ptr.write_array_of_pointer(handles)
26
33
 
@@ -28,6 +35,11 @@ module WGPU
28
35
  buffers.each(&:mark_submitted!)
29
36
  end
30
37
 
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]
31
43
  def write_buffer(buffer, buffer_offset, data, data_offset: 0, size: nil, type: :f32)
32
44
  data_ptr, byte_size = DataTypes.to_pointer(data, type:)
33
45
  DataTypes.validate_alignment!(buffer_offset, 4, name: "buffer_offset")
@@ -42,18 +54,24 @@ module WGPU
42
54
 
43
55
  Native.wgpuQueueWriteBuffer(
44
56
  @handle,
45
- buffer.handle,
57
+ NativeResource.checked_handle(buffer, expected_class: Buffer),
46
58
  buffer_offset,
47
59
  data_ptr + data_offset,
48
60
  write_size
49
61
  )
50
62
  end
51
63
 
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]
52
70
  def write_texture(destination:, data:, data_layout:, size:, type: :f32)
53
71
  data_ptr, byte_size = DataTypes.to_pointer(data, type:)
54
72
 
55
73
  dst = Native::ImageCopyTexture.new
56
- dst[:texture] = destination[:texture].handle
74
+ dst[:texture] = NativeResource.checked_handle(destination[:texture], expected_class: Texture)
57
75
  dst[:mip_level] = destination[:mip_level] || 0
58
76
  dst[:origin][:x] = destination.dig(:origin, :x) || 0
59
77
  dst[:origin][:y] = destination.dig(:origin, :y) || 0
@@ -64,16 +82,7 @@ module WGPU
64
82
  name: "texture aspect"
65
83
  )
66
84
 
67
- extent = Native::Extent3D.new
68
- if size.is_a?(Array)
69
- extent[:width] = size[0]
70
- extent[:height] = size[1] || 1
71
- extent[:depth_or_array_layers] = size[2] || 1
72
- else
73
- extent[:width] = size[:width]
74
- extent[:height] = size[:height] || 1
75
- extent[:depth_or_array_layers] = size[:depth_or_array_layers] || 1
76
- end
85
+ extent = DescriptorHelpers.extent_3d(size)
77
86
 
78
87
  layout = Native::TextureDataLayout.new
79
88
  layout[:offset] = data_layout[:offset] || 0
@@ -83,6 +92,10 @@ module WGPU
83
92
  Native.wgpuQueueWriteTexture(@handle, dst, data_ptr, byte_size, layout, extent)
84
93
  end
85
94
 
95
+ # Copies a GPU buffer to mapped staging memory and returns its bytes.
96
+ # @param buffer [Buffer] source buffer
97
+ # @param staging [Buffer, nil] reusable map-read staging buffer
98
+ # @return [String] copied bytes
86
99
  def read_buffer(buffer, offset: 0, size: nil, device: nil, staging: nil)
87
100
  device ||= @device
88
101
  raise ArgumentError, "device is required when the queue has no owning device" unless device
@@ -90,6 +103,7 @@ module WGPU
90
103
  size ||= buffer.size - offset
91
104
  owns_staging = staging.nil?
92
105
  staging ||= Buffer.new(device, size: size, usage: [:map_read, :copy_dst])
106
+ validate_readback_staging!(staging, size)
93
107
  encoder = CommandEncoder.new(device)
94
108
  encoder.copy_buffer_to_buffer(
95
109
  source: buffer,
@@ -101,20 +115,32 @@ module WGPU
101
115
  command_buffer = encoder.finish
102
116
  submit([command_buffer])
103
117
 
118
+ map_requested = true
104
119
  staging.map_sync(:read)
105
120
  staging.read_mapped_data(size:)
106
121
  ensure
107
- staging&.unmap if staging && staging.map_state == :mapped
108
- command_buffer&.release
109
- encoder&.release
110
- staging&.release if owns_staging
122
+ cleanup_readback_resources(
123
+ staging:,
124
+ unmap_staging: map_requested,
125
+ owns_staging:,
126
+ command_buffer:,
127
+ encoder:,
128
+ active_error: $!
129
+ )
111
130
  end
112
131
 
132
+ # Copies a texture region to mapped staging memory and returns its padded bytes.
133
+ # @param source [Hash] source texture, origin, aspect, and optional format
134
+ # @param data_layout [Hash] destination byte layout
135
+ # @param size [Hash, Array] extent to read
136
+ # @param staging [Buffer, nil] reusable map-read staging buffer
137
+ # @return [String] copied bytes
113
138
  def read_texture(source:, data_layout:, size:, device: nil, staging: nil)
114
139
  device ||= @device
115
140
  raise ArgumentError, "device is required when the queue has no owning device" unless device
116
141
 
117
- width, height, depth = texture_extent(size)
142
+ extent = DescriptorHelpers.extent_3d(size)
143
+ width, height, depth = extent.values
118
144
  bytes_per_row = data_layout[:bytes_per_row]
119
145
  raise ArgumentError, "data_layout[:bytes_per_row] is required" unless bytes_per_row
120
146
 
@@ -125,10 +151,12 @@ module WGPU
125
151
  )
126
152
  format = source[:format] || source[:texture].format
127
153
  aspect = source[:aspect] || :all
128
- minimum_bytes_per_row = TextureFormat.bytes_per_row(width, format, aspect:)
154
+ tight_bytes_per_row = TextureFormat.bytes_per_row(width, format, aspect:)
155
+ minimum_bytes_per_row = TextureFormat.aligned_bytes_per_row(width, format, aspect:)
129
156
  if bytes_per_row < minimum_bytes_per_row
130
157
  raise ArgumentError,
131
- "bytes_per_row must be at least #{minimum_bytes_per_row} for width #{width} and #{format.inspect}"
158
+ "bytes_per_row must be at least #{minimum_bytes_per_row} for width #{width} and #{format.inspect} " \
159
+ "(tight row is #{tight_bytes_per_row} bytes)"
132
160
  end
133
161
 
134
162
  rows_per_image = data_layout[:rows_per_image] || height
@@ -136,6 +164,7 @@ module WGPU
136
164
 
137
165
  owns_staging = staging.nil?
138
166
  staging ||= Buffer.new(device, size: buffer_size, usage: [:map_read, :copy_dst])
167
+ validate_readback_staging!(staging, buffer_size)
139
168
  encoder = CommandEncoder.new(device)
140
169
  encoder.copy_texture_to_buffer(
141
170
  source: source,
@@ -150,25 +179,42 @@ module WGPU
150
179
  command_buffer = encoder.finish
151
180
  submit([command_buffer])
152
181
 
182
+ map_requested = true
153
183
  staging.map_sync(:read)
154
184
  staging.read_mapped_data(size: buffer_size)
155
185
  ensure
156
- staging&.unmap if staging && staging.map_state == :mapped
157
- command_buffer&.release
158
- encoder&.release
159
- staging&.release if owns_staging
186
+ cleanup_readback_resources(
187
+ staging:,
188
+ unmap_staging: map_requested,
189
+ owns_staging:,
190
+ command_buffer:,
191
+ encoder:,
192
+ active_error: $!
193
+ )
160
194
  end
161
195
 
196
+ # Waits until all previously submitted queue work completes.
197
+ # @param device [Device, nil] device used to drive callback progress
198
+ # @param timeout [Numeric, nil] maximum wait time in seconds
199
+ # @return [Symbol] native completion status
162
200
  def on_submitted_work_done(device: nil, timeout: nil)
201
+ timeout = AsyncWaiter.normalize_timeout(timeout)
163
202
  device ||= @device
164
203
  instance = device&.adapter&.instance
165
204
  status_holder = { done: false, status: nil }
166
205
 
206
+ callback_lifetime_release = device_callback_lifetime_lease
207
+ callback_token = nil
167
208
  callback = FFI::Function.new(
168
209
  :void, [:uint32, :pointer, :pointer]
169
210
  ) do |status, _userdata1, _userdata2|
170
- status_holder[:done] = true
171
- status_holder[:status] = Native::QueueWorkDoneStatus[status]
211
+ begin
212
+ status_holder[:status] = Native::QueueWorkDoneStatus[status]
213
+ status_holder[:done] = true
214
+ ensure
215
+ CallbackKeepalive.release(self, callback_token)
216
+ callback_lifetime_release.call
217
+ end
172
218
  end
173
219
 
174
220
  callback_info = Native::QueueWorkDoneCallbackInfo.new
@@ -179,28 +225,37 @@ module WGPU
179
225
  callback_info[:userdata2] = nil
180
226
 
181
227
  callback_token = CallbackKeepalive.retain(self, callback)
182
- begin
183
- future = Native.wgpuQueueOnSubmittedWorkDone(@handle, callback_info)
184
- AsyncWaiter.wait(
185
- status_holder: status_holder,
186
- instance: instance,
187
- device: device,
188
- future: future,
189
- timeout: timeout
190
- )
191
- ensure
192
- CallbackKeepalive.release(self, callback_token)
193
- end
228
+ future =
229
+ begin
230
+ Native.wgpuQueueOnSubmittedWorkDone(@handle, callback_info)
231
+ rescue StandardError
232
+ CallbackKeepalive.release(self, callback_token)
233
+ callback_lifetime_release.call
234
+ raise
235
+ end
236
+ AsyncWaiter.wait(
237
+ status_holder: status_holder,
238
+ instance: instance,
239
+ device: device,
240
+ future: future,
241
+ timeout: timeout
242
+ )
194
243
 
195
244
  status_holder[:status]
196
245
  end
197
246
 
247
+ # Waits for prior queue work on a background task.
248
+ # @return [AsyncTask] task yielding the native completion status
198
249
  def on_submitted_work_done_async(device: nil, timeout: nil)
199
250
  AsyncTask.new do
200
251
  on_submitted_work_done(device: device, timeout: timeout)
201
252
  end
202
253
  end
203
254
 
255
+ # Releases the native queue handle.
256
+ #
257
+ # Calling this method more than once has no effect.
258
+ # @return [void]
204
259
  def release
205
260
  return if @handle.null?
206
261
  Native.wgpuQueueRelease(@handle)
@@ -209,12 +264,57 @@ module WGPU
209
264
 
210
265
  private
211
266
 
212
- def texture_extent(size)
213
- if size.is_a?(Array)
214
- [size.fetch(0), size[1] || 1, size[2] || 1]
215
- else
216
- [size.fetch(:width), size[:height] || 1, size[:depth_or_array_layers] || 1]
267
+ def validate_readback_staging!(staging, required_size)
268
+ if staging.size < required_size
269
+ raise ArgumentError,
270
+ "staging buffer size must be at least #{required_size} bytes (got #{staging.size})"
271
+ end
272
+
273
+ required_usage = Native::BufferUsage.fetch(:map_read) | Native::BufferUsage.fetch(:copy_dst)
274
+ unless (staging.usage & required_usage) == required_usage
275
+ raise ArgumentError, "staging buffer usage must include :map_read and :copy_dst"
217
276
  end
277
+
278
+ return if staging.map_state == :unmapped
279
+
280
+ raise ArgumentError, "staging buffer must be unmapped before readback"
281
+ end
282
+
283
+ def cleanup_readback_resources(
284
+ staging:,
285
+ unmap_staging:,
286
+ owns_staging:,
287
+ command_buffer:,
288
+ encoder:,
289
+ active_error:
290
+ )
291
+ cleanup_error = nil
292
+
293
+ begin
294
+ staging&.unmap if unmap_staging && staging && staging.map_state == :mapped
295
+ rescue StandardError => e
296
+ cleanup_error ||= e
297
+ ensure
298
+ begin
299
+ command_buffer&.release
300
+ rescue StandardError => e
301
+ cleanup_error ||= e
302
+ ensure
303
+ begin
304
+ encoder&.release
305
+ rescue StandardError => e
306
+ cleanup_error ||= e
307
+ ensure
308
+ begin
309
+ staging&.release if owns_staging
310
+ rescue StandardError => e
311
+ cleanup_error ||= e
312
+ end
313
+ end
314
+ end
315
+ end
316
+
317
+ raise cleanup_error if cleanup_error && active_error.nil?
218
318
  end
219
319
 
220
320
  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
@@ -15,12 +18,15 @@ module WGPU
15
18
  desc[:label][:data] = nil
16
19
  desc[:label][:length] = 0
17
20
 
18
- handle = Native.wgpuInstanceCreateSurface(instance.handle, desc)
21
+ handle = Native.wgpuInstanceCreateSurface(NativeResource.checked_handle(instance, expected_class: Instance), desc)
19
22
  raise SurfaceError, "Failed to create surface from Metal layer" if handle.null?
20
23
 
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
@@ -33,12 +39,15 @@ module WGPU
33
39
  desc[:label][:data] = nil
34
40
  desc[:label][:length] = 0
35
41
 
36
- handle = Native.wgpuInstanceCreateSurface(instance.handle, desc)
42
+ handle = Native.wgpuInstanceCreateSurface(NativeResource.checked_handle(instance, expected_class: Instance), desc)
37
43
  raise SurfaceError, "Failed to create surface from Windows HWND" if handle.null?
38
44
 
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
@@ -51,12 +60,15 @@ module WGPU
51
60
  desc[:label][:data] = nil
52
61
  desc[:label][:length] = 0
53
62
 
54
- handle = Native.wgpuInstanceCreateSurface(instance.handle, desc)
63
+ handle = Native.wgpuInstanceCreateSurface(NativeResource.checked_handle(instance, expected_class: Instance), desc)
55
64
  raise SurfaceError, "Failed to create surface from Xlib window" if handle.null?
56
65
 
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
@@ -69,12 +81,15 @@ module WGPU
69
81
  desc[:label][:data] = nil
70
82
  desc[:label][:length] = 0
71
83
 
72
- handle = Native.wgpuInstanceCreateSurface(instance.handle, desc)
84
+ handle = Native.wgpuInstanceCreateSurface(NativeResource.checked_handle(instance, expected_class: Instance), desc)
73
85
  raise SurfaceError, "Failed to create surface from Wayland surface" if handle.null?
74
86
 
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,10 +97,12 @@ 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
- config[:device] = device.handle
105
+ config[:device] = NativeResource.checked_handle(device, expected_class: Device)
89
106
  config[:format] = Native::EnumHelper.coerce(Native::TextureFormat, format, name: "surface format")
90
107
  config[:usage] = normalize_usage(usage)
91
108
  config[:width] = width
@@ -114,8 +131,10 @@ module WGPU
114
131
  )
115
132
 
116
133
  Native.wgpuSurfaceConfigure(@handle, config)
134
+ release_device_callback_lifetime
117
135
  @configured = true
118
136
  @device = device
137
+ attach_device_callback_lifetime(device)
119
138
  @config = {
120
139
  device: device,
121
140
  format: format,
@@ -128,12 +147,20 @@ module WGPU
128
147
  }
129
148
  end
130
149
 
150
+ # Removes the current surface configuration.
151
+ # @return [void]
131
152
  def unconfigure
132
153
  Native.wgpuSurfaceUnconfigure(@handle)
154
+ release_device_callback_lifetime
133
155
  @configured = false
156
+ @device = nil
134
157
  @config = nil
135
158
  end
136
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
137
164
  def current_texture
138
165
  raise SurfaceError, "Surface is not configured" unless @configured
139
166
 
@@ -150,29 +177,46 @@ module WGPU
150
177
  raise SurfaceError, "Surface returned null texture"
151
178
  end
152
179
 
153
- Texture.from_handle(texture_ptr, surface_status: status)
180
+ Texture.from_handle(texture_ptr, surface_status: status, device: @device)
154
181
  end
155
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
156
188
  def get_current_texture
157
189
  current_texture
158
190
  end
159
191
 
192
+ # Presents the current surface texture.
193
+ # @return [void]
160
194
  def present
161
195
  Native.wgpuSurfacePresent(@handle)
162
196
  end
163
197
 
198
+ # Returns the most recent surface configuration.
199
+ #
200
+ # @return [Hash, nil] configuration options, or +nil+ when unconfigured
164
201
  def get_configuration
165
202
  @config
166
203
  end
167
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
168
209
  def get_preferred_format(adapter)
169
210
  caps = capabilities(adapter)
170
211
  caps[:formats].first || :bgra8_unorm
171
212
  end
172
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]
173
217
  def capabilities(adapter)
174
218
  caps = Native::SurfaceCapabilities.new
175
- Native.wgpuSurfaceGetCapabilities(@handle, adapter.handle, caps)
219
+ Native.wgpuSurfaceGetCapabilities(@handle, NativeResource.checked_handle(adapter, expected_class: Adapter), caps)
176
220
 
177
221
  formats = []
178
222
  if caps[:format_count] > 0 && !caps[:formats].null?
@@ -205,6 +249,10 @@ module WGPU
205
249
  Native.wgpuSurfaceCapabilitiesFreeMembers(caps) if caps
206
250
  end
207
251
 
252
+ # Releases the native surface handle.
253
+ #
254
+ # Calling this method more than once has no effect.
255
+ # @return [void]
208
256
  def release
209
257
  return if @handle.null?
210
258
  Native.wgpuSurfaceRelease(@handle)
@@ -13,6 +13,10 @@ module WGPU
13
13
 
14
14
  module_function
15
15
 
16
+ # Encodes Ruby values in little-endian native transfer format.
17
+ # @param values [Array] values to encode
18
+ # @param type [Symbol] element type
19
+ # @return [String] encoded bytes
16
20
  def pack(values, type: :f32)
17
21
  format, = format_for(type)
18
22
  Array(values).pack(format)
@@ -20,6 +24,10 @@ module WGPU
20
24
  raise ArgumentError, "value is out of range for #{type}: #{e.message}"
21
25
  end
22
26
 
27
+ # Decodes little-endian transfer bytes into Ruby values.
28
+ # @param bytes [String] encoded bytes
29
+ # @param type [Symbol] element type
30
+ # @return [Array]
23
31
  def unpack(bytes, type: :f32)
24
32
  format, byte_size = format_for(type)
25
33
  unless (bytes.bytesize % byte_size).zero?
@@ -29,10 +37,15 @@ module WGPU
29
37
  bytes.unpack(format)
30
38
  end
31
39
 
40
+ # Returns the byte width of one typed value.
41
+ # @param type [Symbol] element type
42
+ # @return [Integer]
32
43
  def byte_size(type)
33
44
  format_for(type).last
34
45
  end
35
46
 
47
+ # Converts data to an FFI pointer and reports its byte length.
48
+ # @return [Array(FFI::Pointer, Integer)] pointer and byte length
36
49
  def to_pointer(data, type: :f32)
37
50
  return [data, pointer_size(data)] if data.is_a?(FFI::Pointer)
38
51
 
@@ -42,6 +55,9 @@ module WGPU
42
55
  [pointer, bytes.bytesize]
43
56
  end
44
57
 
58
+ # Validates a non-negative aligned byte value.
59
+ # @return [Integer] normalized value
60
+ # @raise [ArgumentError] if negative or misaligned
45
61
  def validate_alignment!(value, alignment, name:)
46
62
  integer = Integer(value)
47
63
  raise ArgumentError, "#{name} must be non-negative" if integer.negative?