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,13 +4,18 @@ module WGPU
4
4
  class ComputePipeline
5
5
  attr_reader :handle
6
6
 
7
+ # Creates a compute pipeline.
8
+ # @param device [Device] owning device
9
+ # @param layout [PipelineLayout, :auto, nil] pipeline layout
10
+ # @param compute [Hash] programmable compute stage descriptor
11
+ # @raise [PipelineError] if native validation or creation fails
7
12
  def initialize(device, label: nil, layout:, compute:)
8
13
  @device = device
9
14
  desc, @pointers = build_descriptor(label:, layout:, compute:)
10
15
 
11
- device.push_error_scope(:validation)
12
- @handle = Native.wgpuDeviceCreateComputePipeline(device.handle, desc)
13
- error = device.pop_error_scope
16
+ error = device.send(:capture_error_scope) do
17
+ @handle = Native.wgpuDeviceCreateComputePipeline(NativeResource.checked_handle(device, expected_class: Device), desc)
18
+ end
14
19
 
15
20
  if @handle.null? || (error[:type] && error[:type] != :no_error)
16
21
  msg = error[:message] || "Failed to create compute pipeline"
@@ -18,12 +23,20 @@ module WGPU
18
23
  end
19
24
  end
20
25
 
26
+ # Returns the bind group layout inferred or assigned at an index.
27
+ # @param index [Integer] bind group index
28
+ # @return [BindGroupLayout]
29
+ # @raise [PipelineError] if no native layout is returned
21
30
  def get_bind_group_layout(index)
22
31
  handle = Native.wgpuComputePipelineGetBindGroupLayout(@handle, index)
23
32
  raise PipelineError, "Failed to get bind group layout at index #{index}" if handle.null?
24
- BindGroupLayout.from_handle(handle)
33
+ BindGroupLayout.from_handle(handle, device: @device)
25
34
  end
26
35
 
36
+ # Releases the native compute pipeline handle.
37
+ #
38
+ # Calling this method more than once has no effect.
39
+ # @return [void]
27
40
  def release
28
41
  return if @handle.null?
29
42
  Native.wgpuComputePipelineRelease(@handle)
@@ -40,51 +53,26 @@ module WGPU
40
53
  context: "compute pipeline stage"
41
54
  )
42
55
  @pointers = []
43
- entry_point = compute[:entry_point] || "main"
44
- entry_point_ptr = FFI::MemoryPointer.from_string(entry_point)
45
- @pointers << entry_point_ptr
46
56
 
47
57
  desc = Native::ComputePipelineDescriptor.new
48
58
  desc[:next_in_chain] = nil
49
59
  DescriptorHelpers.set_label(desc, label, keepalive: @pointers)
50
60
  desc[:layout] = normalize_layout(layout)
51
61
  desc[:compute][:next_in_chain] = nil
52
- desc[:compute][:module] = compute.fetch(:module).handle
53
- desc[:compute][:entry_point][:data] = entry_point_ptr
54
- desc[:compute][:entry_point][:length] = entry_point.bytesize
55
- desc[:compute][:constant_count] = 0
56
- desc[:compute][:constants] = nil
57
- setup_constants(desc[:compute], compute[:constants])
62
+ desc[:compute][:module] = NativeResource.checked_handle(compute.fetch(:module), expected_class: ShaderModule)
63
+ DescriptorHelpers.set_nullable_string_view(
64
+ desc[:compute][:entry_point],
65
+ compute[:entry_point],
66
+ keepalive: @pointers
67
+ )
68
+ DescriptorHelpers.set_constants(desc[:compute], compute[:constants], keepalive: @pointers)
58
69
 
59
70
  [desc, @pointers]
60
71
  end
61
72
 
62
73
  def normalize_layout(layout)
63
74
  return nil if layout.nil? || layout == :auto || layout == "auto"
64
- layout.handle
65
- end
66
-
67
- def setup_constants(stage_desc, constants)
68
- return if constants.nil? || constants.empty?
69
-
70
- constants_ptr = FFI::MemoryPointer.new(Native::ConstantEntry, constants.size)
71
- @pointers << constants_ptr
72
-
73
- constants.each_with_index do |(key, value), i|
74
- entry_ptr = constants_ptr + (i * Native::ConstantEntry.size)
75
- entry = Native::ConstantEntry.new(entry_ptr)
76
- entry[:next_in_chain] = nil
77
-
78
- key_str = key.to_s
79
- key_ptr = FFI::MemoryPointer.from_string(key_str)
80
- @pointers << key_ptr
81
- entry[:key][:data] = key_ptr
82
- entry[:key][:length] = key_str.bytesize
83
- entry[:value] = value.to_f
84
- end
85
-
86
- stage_desc[:constant_count] = constants.size
87
- stage_desc[:constants] = constants_ptr
75
+ NativeResource.checked_handle(layout, expected_class: PipelineLayout)
88
76
  end
89
77
  end
90
78
  end
@@ -4,12 +4,16 @@ module WGPU
4
4
  class PipelineLayout
5
5
  attr_reader :handle
6
6
 
7
+ # Creates a pipeline layout from ordered bind group layouts.
8
+ # @param device [Device] owning device
9
+ # @param bind_group_layouts [Array<BindGroupLayout>] layouts by group index
10
+ # @raise [PipelineError] if native validation or creation fails
7
11
  def initialize(device, label: nil, bind_group_layouts:)
8
12
  @device = device
9
13
 
10
14
  layouts = Array(bind_group_layouts)
11
15
  layouts_ptr = FFI::MemoryPointer.new(:pointer, layouts.size)
12
- layouts_ptr.write_array_of_pointer(layouts.map(&:handle))
16
+ layouts_ptr.write_array_of_pointer(layouts.map { |layout| NativeResource.checked_handle(layout, expected_class: BindGroupLayout) })
13
17
 
14
18
  desc = Native::PipelineLayoutDescriptor.new
15
19
  desc[:next_in_chain] = nil
@@ -24,9 +28,9 @@ module WGPU
24
28
  desc[:bind_group_layout_count] = layouts.size
25
29
  desc[:bind_group_layouts] = layouts_ptr
26
30
 
27
- device.push_error_scope(:validation)
28
- @handle = Native.wgpuDeviceCreatePipelineLayout(device.handle, desc)
29
- error = device.pop_error_scope
31
+ error = device.send(:capture_error_scope) do
32
+ @handle = Native.wgpuDeviceCreatePipelineLayout(NativeResource.checked_handle(device, expected_class: Device), desc)
33
+ end
30
34
 
31
35
  if @handle.null? || (error[:type] && error[:type] != :no_error)
32
36
  msg = error[:message] || "Failed to create pipeline layout"
@@ -34,6 +38,10 @@ module WGPU
34
38
  end
35
39
  end
36
40
 
41
+ # Releases the native pipeline layout handle.
42
+ #
43
+ # Calling this method more than once has no effect.
44
+ # @return [void]
37
45
  def release
38
46
  return if @handle.null?
39
47
  Native.wgpuPipelineLayoutRelease(@handle)
@@ -4,6 +4,12 @@ module WGPU
4
4
  class RenderPipeline
5
5
  attr_reader :handle
6
6
 
7
+ # Creates a render pipeline.
8
+ # @param device [Device] owning device
9
+ # @param layout [PipelineLayout, :auto, nil] pipeline layout
10
+ # @param vertex [Hash] programmable vertex stage descriptor
11
+ # @param fragment [Hash, nil] optional fragment stage descriptor
12
+ # @raise [PipelineError] if native validation or creation fails
7
13
  def initialize(device, label: nil, layout:, vertex:, primitive: {}, depth_stencil: nil, multisample: {}, fragment: nil)
8
14
  @device = device
9
15
  desc, @pointers = build_descriptor(
@@ -16,9 +22,9 @@ module WGPU
16
22
  fragment:
17
23
  )
18
24
 
19
- device.push_error_scope(:validation)
20
- @handle = Native.wgpuDeviceCreateRenderPipeline(device.handle, desc)
21
- error = device.pop_error_scope
25
+ error = device.send(:capture_error_scope) do
26
+ @handle = Native.wgpuDeviceCreateRenderPipeline(NativeResource.checked_handle(device, expected_class: Device), desc)
27
+ end
22
28
 
23
29
  if @handle.null? || (error[:type] && error[:type] != :no_error)
24
30
  msg = error[:message] || "Failed to create render pipeline"
@@ -26,12 +32,20 @@ module WGPU
26
32
  end
27
33
  end
28
34
 
35
+ # Returns the bind group layout inferred or assigned at an index.
36
+ # @param index [Integer] bind group index
37
+ # @return [BindGroupLayout]
38
+ # @raise [PipelineError] if no native layout is returned
29
39
  def get_bind_group_layout(index)
30
40
  handle = Native.wgpuRenderPipelineGetBindGroupLayout(@handle, index)
31
41
  raise PipelineError, "Failed to get bind group layout at index #{index}" if handle.null?
32
- BindGroupLayout.from_handle(handle)
42
+ BindGroupLayout.from_handle(handle, device: @device)
33
43
  end
34
44
 
45
+ # Releases the native render pipeline handle.
46
+ #
47
+ # Calling this method more than once has no effect.
48
+ # @return [void]
35
49
  def release
36
50
  return if @handle.null?
37
51
  Native.wgpuRenderPipelineRelease(@handle)
@@ -64,17 +78,14 @@ module WGPU
64
78
  context: "render pipeline vertex descriptor"
65
79
  )
66
80
  vertex_state[:next_in_chain] = nil
67
- vertex_state[:module] = vertex[:module].handle
68
-
69
- entry_point = vertex[:entry_point] || "main"
70
- entry_ptr = FFI::MemoryPointer.from_string(entry_point)
71
- @pointers << entry_ptr
72
- vertex_state[:entry_point][:data] = entry_ptr
73
- vertex_state[:entry_point][:length] = entry_point.bytesize
81
+ vertex_state[:module] = NativeResource.checked_handle(vertex[:module], expected_class: ShaderModule)
74
82
 
75
- vertex_state[:constant_count] = 0
76
- vertex_state[:constants] = nil
77
- setup_constants(vertex_state, vertex[:constants])
83
+ DescriptorHelpers.set_nullable_string_view(
84
+ vertex_state[:entry_point],
85
+ vertex[:entry_point],
86
+ keepalive: @pointers
87
+ )
88
+ DescriptorHelpers.set_constants(vertex_state, vertex[:constants], keepalive: @pointers)
78
89
 
79
90
  buffers = vertex[:buffers] || []
80
91
  if buffers.empty?
@@ -254,17 +265,14 @@ module WGPU
254
265
  frag = Native::FragmentState.new
255
266
  @pointers << frag
256
267
  frag[:next_in_chain] = nil
257
- frag[:module] = fragment[:module].handle
268
+ frag[:module] = NativeResource.checked_handle(fragment[:module], expected_class: ShaderModule)
258
269
 
259
- entry_point = fragment[:entry_point] || "main"
260
- entry_ptr = FFI::MemoryPointer.from_string(entry_point)
261
- @pointers << entry_ptr
262
- frag[:entry_point][:data] = entry_ptr
263
- frag[:entry_point][:length] = entry_point.bytesize
264
-
265
- frag[:constant_count] = 0
266
- frag[:constants] = nil
267
- setup_constants(frag, fragment[:constants])
270
+ DescriptorHelpers.set_nullable_string_view(
271
+ frag[:entry_point],
272
+ fragment[:entry_point],
273
+ keepalive: @pointers
274
+ )
275
+ DescriptorHelpers.set_constants(frag, fragment[:constants], keepalive: @pointers)
268
276
 
269
277
  targets = fragment[:targets] || []
270
278
  if targets.empty?
@@ -361,30 +369,7 @@ module WGPU
361
369
 
362
370
  def normalize_layout(layout)
363
371
  return nil if layout.nil? || layout == :auto || layout == "auto"
364
- layout.handle
365
- end
366
-
367
- def setup_constants(stage_state, constants)
368
- return if constants.nil? || constants.empty?
369
-
370
- constants_ptr = FFI::MemoryPointer.new(Native::ConstantEntry, constants.size)
371
- @pointers << constants_ptr
372
-
373
- constants.each_with_index do |(key, value), i|
374
- entry_ptr = constants_ptr + (i * Native::ConstantEntry.size)
375
- entry = Native::ConstantEntry.new(entry_ptr)
376
- entry[:next_in_chain] = nil
377
-
378
- key_str = key.to_s
379
- key_ptr = FFI::MemoryPointer.from_string(key_str)
380
- @pointers << key_ptr
381
- entry[:key][:data] = key_ptr
382
- entry[:key][:length] = key_str.bytesize
383
- entry[:value] = value.to_f
384
- end
385
-
386
- stage_state[:constant_count] = constants.size
387
- stage_state[:constants] = constants_ptr
372
+ NativeResource.checked_handle(layout, expected_class: PipelineLayout)
388
373
  end
389
374
  end
390
375
  end
@@ -4,6 +4,12 @@ module WGPU
4
4
  class ShaderModule
5
5
  attr_reader :handle
6
6
 
7
+ # Creates a shader module from WGSL, GLSL, or SPIR-V input.
8
+ # @param device [Device] owning device
9
+ # @param code [String, Array<Integer>, nil] shader source or binary
10
+ # @param spirv [String, Array<Integer>, nil] explicit SPIR-V input
11
+ # @param validate [Boolean] whether to fetch and raise compilation errors
12
+ # @raise [ShaderError] if native validation, creation, or compilation fails
7
13
  def initialize(device, label: nil, code: nil, spirv: nil, compilation_hints: [], validate: false)
8
14
  @device = device
9
15
  @pointers = []
@@ -24,9 +30,9 @@ module WGPU
24
30
  desc[:label][:length] = 0
25
31
  end
26
32
 
27
- device.push_error_scope(:validation)
28
- @handle = Native.wgpuDeviceCreateShaderModule(device.handle, desc)
29
- error = device.pop_error_scope
33
+ error = device.send(:capture_error_scope) do
34
+ @handle = Native.wgpuDeviceCreateShaderModule(NativeResource.checked_handle(device, expected_class: Device), desc)
35
+ end
30
36
 
31
37
  if @handle.null? || (error[:type] && error[:type] != :no_error)
32
38
  msg = error[:message] || "Failed to create shader module"
@@ -43,6 +49,9 @@ module WGPU
43
49
  end
44
50
  end
45
51
 
52
+ # Fetches compiler diagnostics for the shader.
53
+ # @return [Hash] request status and compilation messages
54
+ # @raise [ShaderError] if the pinned native library cannot provide diagnostics
46
55
  def get_compilation_info
47
56
  unless Native.compilation_info_available?
48
57
  raise ShaderError,
@@ -52,34 +61,41 @@ module WGPU
52
61
  result_holder = { done: false, status: nil, messages: [] }
53
62
  instance = @device.adapter&.instance
54
63
 
64
+ callback_lifetime_release = device_callback_lifetime_lease
65
+ callback_token = nil
55
66
  callback = FFI::Function.new(
56
67
  :void, [:uint32, :pointer, :pointer, :pointer]
57
68
  ) do |status, compilation_info_ptr, _userdata1, _userdata2|
58
- result_holder[:done] = true
59
- result_holder[:status] = Native::CompilationInfoRequestStatus[status]
60
-
61
- unless compilation_info_ptr.null?
62
- info = Native::CompilationInfo.new(compilation_info_ptr)
63
- count = info[:message_count]
64
- if count > 0 && !info[:messages].null?
65
- count.times do |i|
66
- msg_ptr = info[:messages] + (i * Native::CompilationMessage.size)
67
- msg = Native::CompilationMessage.new(msg_ptr)
68
- message_text = if msg[:message][:data] && !msg[:message][:data].null? && msg[:message][:length] > 0
69
- msg[:message][:data].read_string(msg[:message][:length])
70
- else
71
- ""
72
- end
73
- result_holder[:messages] << CompilationMessage.new(
74
- type: msg[:type],
75
- message: message_text,
76
- line_num: msg[:line_num],
77
- line_pos: msg[:line_pos],
78
- offset: msg[:offset],
79
- length: msg[:length]
80
- )
69
+ begin
70
+ result_holder[:status] = Native::CompilationInfoRequestStatus[status]
71
+
72
+ unless compilation_info_ptr.null?
73
+ info = Native::CompilationInfo.new(compilation_info_ptr)
74
+ count = info[:message_count]
75
+ if count > 0 && !info[:messages].null?
76
+ count.times do |i|
77
+ msg_ptr = info[:messages] + (i * Native::CompilationMessage.size)
78
+ msg = Native::CompilationMessage.new(msg_ptr)
79
+ message_text = if msg[:message][:data] && !msg[:message][:data].null? && msg[:message][:length] > 0
80
+ msg[:message][:data].read_string(msg[:message][:length])
81
+ else
82
+ ""
83
+ end
84
+ result_holder[:messages] << CompilationMessage.new(
85
+ type: msg[:type],
86
+ message: message_text,
87
+ line_num: msg[:line_num],
88
+ line_pos: msg[:line_pos],
89
+ offset: msg[:offset],
90
+ length: msg[:length]
91
+ )
92
+ end
81
93
  end
82
94
  end
95
+ result_holder[:done] = true
96
+ ensure
97
+ CallbackKeepalive.release(self, callback_token)
98
+ callback_lifetime_release.call
83
99
  end
84
100
  end
85
101
 
@@ -91,12 +107,15 @@ module WGPU
91
107
  callback_info[:userdata2] = nil
92
108
 
93
109
  callback_token = CallbackKeepalive.retain(self, callback)
94
- begin
95
- future = Native.wgpuShaderModuleGetCompilationInfo(@handle, callback_info)
96
- AsyncWaiter.wait(status_holder: result_holder, instance: instance, device: @device, future: future)
97
- ensure
98
- CallbackKeepalive.release(self, callback_token)
99
- end
110
+ future =
111
+ begin
112
+ Native.wgpuShaderModuleGetCompilationInfo(@handle, callback_info)
113
+ rescue StandardError
114
+ CallbackKeepalive.release(self, callback_token)
115
+ callback_lifetime_release.call
116
+ raise
117
+ end
118
+ AsyncWaiter.wait(status_holder: result_holder, instance: instance, device: @device, future: future)
100
119
 
101
120
  {
102
121
  status: result_holder[:status],
@@ -104,10 +123,16 @@ module WGPU
104
123
  }
105
124
  end
106
125
 
126
+ # Fetches compiler diagnostics on a background task.
127
+ # @return [AsyncTask] task yielding compilation information
107
128
  def get_compilation_info_async
108
129
  AsyncTask.new { get_compilation_info }
109
130
  end
110
131
 
132
+ # Releases the native shader module handle.
133
+ #
134
+ # Calling this method more than once has no effect.
135
+ # @return [void]
111
136
  def release
112
137
  return if @handle.null?
113
138
  Native.wgpuShaderModuleRelease(@handle)