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,10 @@ 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
 
@@ -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,32 +4,23 @@ 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
- @pointers = []
10
-
11
- desc = Native::RenderPipelineDescriptor.new
12
- desc[:next_in_chain] = nil
13
- setup_label(desc, label)
14
- desc[:layout] = normalize_layout(layout)
15
-
16
- setup_vertex_state(desc[:vertex], vertex)
17
- setup_primitive_state(desc[:primitive], primitive)
18
- setup_multisample_state(desc[:multisample], multisample)
19
-
20
- if depth_stencil
21
- ds_ptr = setup_depth_stencil_state(depth_stencil)
22
- desc[:depth_stencil] = ds_ptr
23
- else
24
- desc[:depth_stencil] = nil
25
- end
26
-
27
- if fragment
28
- frag_ptr = setup_fragment_state(fragment)
29
- desc[:fragment] = frag_ptr
30
- else
31
- desc[:fragment] = nil
32
- end
15
+ desc, @pointers = build_descriptor(
16
+ label:,
17
+ layout:,
18
+ vertex:,
19
+ primitive:,
20
+ depth_stencil:,
21
+ multisample:,
22
+ fragment:
23
+ )
33
24
 
34
25
  device.push_error_scope(:validation)
35
26
  @handle = Native.wgpuDeviceCreateRenderPipeline(device.handle, desc)
@@ -41,12 +32,20 @@ module WGPU
41
32
  end
42
33
  end
43
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
44
39
  def get_bind_group_layout(index)
45
40
  handle = Native.wgpuRenderPipelineGetBindGroupLayout(@handle, index)
46
41
  raise PipelineError, "Failed to get bind group layout at index #{index}" if handle.null?
47
- BindGroupLayout.from_handle(handle)
42
+ BindGroupLayout.from_handle(handle, device: @device)
48
43
  end
49
44
 
45
+ # Releases the native render pipeline handle.
46
+ #
47
+ # Calling this method more than once has no effect.
48
+ # @return [void]
50
49
  def release
51
50
  return if @handle.null?
52
51
  Native.wgpuRenderPipelineRelease(@handle)
@@ -55,31 +54,38 @@ module WGPU
55
54
 
56
55
  private
57
56
 
58
- def setup_label(desc, label)
59
- if label
60
- ptr = FFI::MemoryPointer.from_string(label)
61
- @pointers << ptr
62
- desc[:label][:data] = ptr
63
- desc[:label][:length] = label.bytesize
64
- else
65
- desc[:label][:data] = nil
66
- desc[:label][:length] = 0
67
- end
57
+ def build_descriptor(label:, layout:, vertex:, primitive:, depth_stencil:, multisample:, fragment:)
58
+ @pointers = []
59
+ desc = Native::RenderPipelineDescriptor.new
60
+ desc[:next_in_chain] = nil
61
+ DescriptorHelpers.set_label(desc, label, keepalive: @pointers)
62
+ desc[:layout] = normalize_layout(layout)
63
+
64
+ setup_vertex_state(desc[:vertex], vertex)
65
+ setup_primitive_state(desc[:primitive], primitive)
66
+ setup_multisample_state(desc[:multisample], multisample)
67
+ desc[:depth_stencil] = depth_stencil ? setup_depth_stencil_state(depth_stencil) : nil
68
+ desc[:fragment] = fragment ? setup_fragment_state(fragment) : nil
69
+
70
+ [desc, @pointers]
68
71
  end
69
72
 
70
73
  def setup_vertex_state(vertex_state, vertex)
74
+ DescriptorHelpers.validate_keys!(
75
+ vertex,
76
+ allowed: %i[module entry_point constants buffers],
77
+ required: [:module],
78
+ context: "render pipeline vertex descriptor"
79
+ )
71
80
  vertex_state[:next_in_chain] = nil
72
81
  vertex_state[:module] = vertex[:module].handle
73
82
 
74
- entry_point = vertex[:entry_point] || "main"
75
- entry_ptr = FFI::MemoryPointer.from_string(entry_point)
76
- @pointers << entry_ptr
77
- vertex_state[:entry_point][:data] = entry_ptr
78
- vertex_state[:entry_point][:length] = entry_point.bytesize
79
-
80
- vertex_state[:constant_count] = 0
81
- vertex_state[:constants] = nil
82
- 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)
83
89
 
84
90
  buffers = vertex[:buffers] || []
85
91
  if buffers.empty?
@@ -97,8 +103,18 @@ module WGPU
97
103
  @pointers << layouts_ptr
98
104
 
99
105
  buffers.each_with_index do |buf, i|
100
- layout = Native::VertexBufferLayout.new(layouts_ptr + i * Native::VertexBufferLayout.size)
101
- layout[:step_mode] = buf[:step_mode] || :vertex
106
+ DescriptorHelpers.validate_keys!(
107
+ buf,
108
+ allowed: %i[array_stride step_mode attributes],
109
+ required: [:array_stride],
110
+ context: "vertex buffer layout"
111
+ )
112
+ layout = Native::VertexBufferLayout.new(layouts_ptr + (i * Native::VertexBufferLayout.size))
113
+ layout[:step_mode] = Native::EnumHelper.coerce(
114
+ Native::VertexStepMode,
115
+ buf[:step_mode] || :vertex,
116
+ name: "vertex step mode"
117
+ )
102
118
  layout[:array_stride] = buf[:array_stride]
103
119
 
104
120
  attrs = buf[:attributes] || []
@@ -109,8 +125,18 @@ module WGPU
109
125
  attrs_ptr = FFI::MemoryPointer.new(Native::VertexAttribute, attrs.size)
110
126
  @pointers << attrs_ptr
111
127
  attrs.each_with_index do |attr, j|
112
- a = Native::VertexAttribute.new(attrs_ptr + j * Native::VertexAttribute.size)
113
- a[:format] = attr[:format]
128
+ DescriptorHelpers.validate_keys!(
129
+ attr,
130
+ allowed: %i[format offset shader_location],
131
+ required: %i[format offset shader_location],
132
+ context: "vertex attribute"
133
+ )
134
+ a = Native::VertexAttribute.new(attrs_ptr + (j * Native::VertexAttribute.size))
135
+ a[:format] = Native::EnumHelper.coerce(
136
+ Native::VertexFormat,
137
+ attr[:format],
138
+ name: "vertex format"
139
+ )
114
140
  a[:offset] = attr[:offset]
115
141
  a[:shader_location] = attr[:shader_location]
116
142
  end
@@ -123,15 +149,41 @@ module WGPU
123
149
  end
124
150
 
125
151
  def setup_primitive_state(primitive_state, primitive)
152
+ DescriptorHelpers.validate_keys!(
153
+ primitive,
154
+ allowed: %i[topology strip_index_format front_face cull_mode unclipped_depth],
155
+ context: "primitive state"
156
+ )
126
157
  primitive_state[:next_in_chain] = nil
127
- primitive_state[:topology] = primitive[:topology] || :triangle_list
128
- primitive_state[:strip_index_format] = primitive[:strip_index_format] || :undefined
129
- primitive_state[:front_face] = primitive[:front_face] || :ccw
130
- primitive_state[:cull_mode] = primitive[:cull_mode] || :none
158
+ primitive_state[:topology] = Native::EnumHelper.coerce(
159
+ Native::PrimitiveTopology,
160
+ primitive[:topology] || :triangle_list,
161
+ name: "primitive topology"
162
+ )
163
+ primitive_state[:strip_index_format] = Native::EnumHelper.coerce(
164
+ Native::IndexFormat,
165
+ primitive[:strip_index_format] || :undefined,
166
+ name: "strip index format"
167
+ )
168
+ primitive_state[:front_face] = Native::EnumHelper.coerce(
169
+ Native::FrontFace,
170
+ primitive[:front_face] || :ccw,
171
+ name: "front face"
172
+ )
173
+ primitive_state[:cull_mode] = Native::EnumHelper.coerce(
174
+ Native::CullMode,
175
+ primitive[:cull_mode] || :none,
176
+ name: "cull mode"
177
+ )
131
178
  primitive_state[:unclipped_depth] = primitive[:unclipped_depth] ? 1 : 0
132
179
  end
133
180
 
134
181
  def setup_multisample_state(multisample_state, multisample)
182
+ DescriptorHelpers.validate_keys!(
183
+ multisample,
184
+ allowed: %i[count mask alpha_to_coverage_enabled],
185
+ context: "multisample state"
186
+ )
135
187
  multisample_state[:next_in_chain] = nil
136
188
  multisample_state[:count] = multisample[:count] || 1
137
189
  multisample_state[:mask] = multisample[:mask] || 0xFFFFFFFF
@@ -139,12 +191,29 @@ module WGPU
139
191
  end
140
192
 
141
193
  def setup_depth_stencil_state(depth_stencil)
194
+ DescriptorHelpers.validate_keys!(
195
+ depth_stencil,
196
+ allowed: %i[
197
+ format depth_write_enabled depth_compare stencil_front stencil_back
198
+ stencil_read_mask stencil_write_mask depth_bias depth_bias_slope_scale depth_bias_clamp
199
+ ],
200
+ required: [:format],
201
+ context: "depth stencil state"
202
+ )
142
203
  ds = Native::DepthStencilState.new
143
204
  @pointers << ds
144
205
  ds[:next_in_chain] = nil
145
- ds[:format] = depth_stencil[:format]
206
+ ds[:format] = Native::EnumHelper.coerce(
207
+ Native::TextureFormat,
208
+ depth_stencil[:format],
209
+ name: "depth stencil format"
210
+ )
146
211
  ds[:depth_write_enabled] = depth_stencil[:depth_write_enabled] ? 1 : 0
147
- ds[:depth_compare] = depth_stencil[:depth_compare] || :always
212
+ ds[:depth_compare] = Native::EnumHelper.coerce(
213
+ Native::CompareFunction,
214
+ depth_stencil[:depth_compare] || :always,
215
+ name: "depth compare function"
216
+ )
148
217
 
149
218
  setup_stencil_face(ds[:stencil_front], depth_stencil[:stencil_front] || {})
150
219
  setup_stencil_face(ds[:stencil_back], depth_stencil[:stencil_back] || {})
@@ -159,27 +228,51 @@ module WGPU
159
228
  end
160
229
 
161
230
  def setup_stencil_face(face, config)
162
- face[:compare] = config[:compare] || :always
163
- face[:fail_op] = config[:fail_op] || :keep
164
- face[:depth_fail_op] = config[:depth_fail_op] || :keep
165
- face[:pass_op] = config[:pass_op] || :keep
231
+ DescriptorHelpers.validate_keys!(
232
+ config,
233
+ allowed: %i[compare fail_op depth_fail_op pass_op],
234
+ context: "stencil face state"
235
+ )
236
+ face[:compare] = Native::EnumHelper.coerce(
237
+ Native::CompareFunction,
238
+ config[:compare] || :always,
239
+ name: "stencil compare function"
240
+ )
241
+ face[:fail_op] = Native::EnumHelper.coerce(
242
+ Native::StencilOperation,
243
+ config[:fail_op] || :keep,
244
+ name: "stencil fail operation"
245
+ )
246
+ face[:depth_fail_op] = Native::EnumHelper.coerce(
247
+ Native::StencilOperation,
248
+ config[:depth_fail_op] || :keep,
249
+ name: "stencil depth fail operation"
250
+ )
251
+ face[:pass_op] = Native::EnumHelper.coerce(
252
+ Native::StencilOperation,
253
+ config[:pass_op] || :keep,
254
+ name: "stencil pass operation"
255
+ )
166
256
  end
167
257
 
168
258
  def setup_fragment_state(fragment)
259
+ DescriptorHelpers.validate_keys!(
260
+ fragment,
261
+ allowed: %i[module entry_point constants targets],
262
+ required: [:module],
263
+ context: "render pipeline fragment descriptor"
264
+ )
169
265
  frag = Native::FragmentState.new
170
266
  @pointers << frag
171
267
  frag[:next_in_chain] = nil
172
268
  frag[:module] = fragment[:module].handle
173
269
 
174
- entry_point = fragment[:entry_point] || "main"
175
- entry_ptr = FFI::MemoryPointer.from_string(entry_point)
176
- @pointers << entry_ptr
177
- frag[:entry_point][:data] = entry_ptr
178
- frag[:entry_point][:length] = entry_point.bytesize
179
-
180
- frag[:constant_count] = 0
181
- frag[:constants] = nil
182
- 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)
183
276
 
184
277
  targets = fragment[:targets] || []
185
278
  if targets.empty?
@@ -199,9 +292,19 @@ module WGPU
199
292
  @pointers << targets_ptr
200
293
 
201
294
  targets.each_with_index do |target, i|
202
- ct = Native::ColorTargetState.new(targets_ptr + i * Native::ColorTargetState.size)
295
+ DescriptorHelpers.validate_keys!(
296
+ target,
297
+ allowed: %i[format blend write_mask],
298
+ required: [:format],
299
+ context: "color target state"
300
+ )
301
+ ct = Native::ColorTargetState.new(targets_ptr + (i * Native::ColorTargetState.size))
203
302
  ct[:next_in_chain] = nil
204
- ct[:format] = target[:format]
303
+ ct[:format] = Native::EnumHelper.coerce(
304
+ Native::TextureFormat,
305
+ target[:format],
306
+ name: "color target format"
307
+ )
205
308
  ct[:write_mask] = normalize_write_mask(target[:write_mask])
206
309
 
207
310
  if target[:blend]
@@ -216,63 +319,57 @@ module WGPU
216
319
  end
217
320
 
218
321
  def setup_blend_state(blend)
322
+ DescriptorHelpers.validate_keys!(
323
+ blend,
324
+ allowed: %i[color alpha],
325
+ context: "blend state"
326
+ )
219
327
  bs = Native::BlendState.new
220
328
  @pointers << bs
221
329
 
222
330
  color = blend[:color] || {}
223
- bs[:color][:operation] = color[:operation] || :add
224
- bs[:color][:src_factor] = color[:src_factor] || :one
225
- bs[:color][:dst_factor] = color[:dst_factor] || :zero
331
+ setup_blend_component(bs[:color], color, "color")
226
332
 
227
333
  alpha = blend[:alpha] || {}
228
- bs[:alpha][:operation] = alpha[:operation] || :add
229
- bs[:alpha][:src_factor] = alpha[:src_factor] || :one
230
- bs[:alpha][:dst_factor] = alpha[:dst_factor] || :zero
334
+ setup_blend_component(bs[:alpha], alpha, "alpha")
231
335
 
232
336
  bs.to_ptr
233
337
  end
234
338
 
339
+ def setup_blend_component(component, config, name)
340
+ DescriptorHelpers.validate_keys!(
341
+ config,
342
+ allowed: %i[operation src_factor dst_factor],
343
+ context: "#{name} blend component"
344
+ )
345
+ component[:operation] = Native::EnumHelper.coerce(
346
+ Native::BlendOperation,
347
+ config[:operation] || :add,
348
+ name: "#{name} blend operation"
349
+ )
350
+ component[:src_factor] = Native::EnumHelper.coerce(
351
+ Native::BlendFactor,
352
+ config[:src_factor] || :one,
353
+ name: "#{name} source blend factor"
354
+ )
355
+ component[:dst_factor] = Native::EnumHelper.coerce(
356
+ Native::BlendFactor,
357
+ config[:dst_factor] || :zero,
358
+ name: "#{name} destination blend factor"
359
+ )
360
+ end
361
+
235
362
  def normalize_write_mask(mask)
236
- case mask
237
- when nil
238
- Native::ColorWriteMask[:all]
239
- when Integer
240
- mask
241
- when Symbol
242
- Native::ColorWriteMask[mask]
243
- when Array
244
- mask.reduce(0) { |acc, m| acc | Native::ColorWriteMask[m] }
245
- else
246
- raise ArgumentError, "Invalid write_mask: #{mask}"
247
- end
363
+ Native::EnumHelper.coerce_flags(
364
+ Native::ColorWriteMask,
365
+ mask || :all,
366
+ name: "color write mask"
367
+ )
248
368
  end
249
369
 
250
370
  def normalize_layout(layout)
251
371
  return nil if layout.nil? || layout == :auto || layout == "auto"
252
372
  layout.handle
253
373
  end
254
-
255
- def setup_constants(stage_state, constants)
256
- return if constants.nil? || constants.empty?
257
-
258
- constants_ptr = FFI::MemoryPointer.new(Native::ConstantEntry, constants.size)
259
- @pointers << constants_ptr
260
-
261
- constants.each_with_index do |(key, value), i|
262
- entry_ptr = constants_ptr + (i * Native::ConstantEntry.size)
263
- entry = Native::ConstantEntry.new(entry_ptr)
264
- entry[:next_in_chain] = nil
265
-
266
- key_str = key.to_s
267
- key_ptr = FFI::MemoryPointer.from_string(key_str)
268
- @pointers << key_ptr
269
- entry[:key][:data] = key_ptr
270
- entry[:key][:length] = key_str.bytesize
271
- entry[:value] = value.to_f
272
- end
273
-
274
- stage_state[:constant_count] = constants.size
275
- stage_state[:constants] = constants_ptr
276
- end
277
374
  end
278
375
  end
@@ -4,12 +4,19 @@ module WGPU
4
4
  class ShaderModule
5
5
  attr_reader :handle
6
6
 
7
- def initialize(device, label: nil, code:, compilation_hints: [])
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
13
+ def initialize(device, label: nil, code: nil, spirv: nil, compilation_hints: [], validate: false)
8
14
  @device = device
9
15
  @pointers = []
10
- compilation_hints # currently unused by wgpu-native API
16
+ @compilation_hints = compilation_hints
17
+ source = select_source(code, spirv)
11
18
 
12
- source_ptr = build_shader_source(code, label: label)
19
+ source_ptr = build_shader_source(source, label: label)
13
20
 
14
21
  desc = Native::ShaderModuleDescriptor.new
15
22
  desc[:next_in_chain] = source_ptr
@@ -29,42 +36,66 @@ module WGPU
29
36
 
30
37
  if @handle.null? || (error[:type] && error[:type] != :no_error)
31
38
  msg = error[:message] || "Failed to create shader module"
32
- raise ShaderError, msg
39
+ raise ShaderError, shader_error_message(msg, label)
40
+ end
41
+
42
+ return unless validate
43
+
44
+ begin
45
+ validate_compilation!(label)
46
+ rescue StandardError
47
+ release
48
+ raise
33
49
  end
34
50
  end
35
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
36
55
  def get_compilation_info
56
+ unless Native.compilation_info_available?
57
+ raise ShaderError,
58
+ "Shader compilation info is not implemented by wgpu-native #{Native::Distribution::VERSION}"
59
+ end
60
+
37
61
  result_holder = { done: false, status: nil, messages: [] }
38
62
  instance = @device.adapter&.instance
39
63
 
64
+ callback_lifetime_release = device_callback_lifetime_lease
65
+ callback_token = nil
40
66
  callback = FFI::Function.new(
41
67
  :void, [:uint32, :pointer, :pointer, :pointer]
42
68
  ) do |status, compilation_info_ptr, _userdata1, _userdata2|
43
- result_holder[:done] = true
44
- result_holder[:status] = Native::CompilationInfoRequestStatus[status]
45
-
46
- unless compilation_info_ptr.null?
47
- info = Native::CompilationInfo.new(compilation_info_ptr)
48
- count = info[:message_count]
49
- if count > 0 && !info[:messages].null?
50
- count.times do |i|
51
- msg_ptr = info[:messages] + (i * Native::CompilationMessage.size)
52
- msg = Native::CompilationMessage.new(msg_ptr)
53
- message_text = if msg[:message][:data] && !msg[:message][:data].null? && msg[:message][:length] > 0
54
- msg[:message][:data].read_string(msg[:message][:length])
55
- else
56
- ""
57
- end
58
- result_holder[:messages] << {
59
- type: msg[:type],
60
- message: message_text,
61
- line_num: msg[:line_num],
62
- line_pos: msg[:line_pos],
63
- offset: msg[:offset],
64
- length: msg[:length]
65
- }
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
66
93
  end
67
94
  end
95
+ result_holder[:done] = true
96
+ ensure
97
+ CallbackKeepalive.release(self, callback_token)
98
+ callback_lifetime_release.call
68
99
  end
69
100
  end
70
101
 
@@ -75,7 +106,15 @@ module WGPU
75
106
  callback_info[:userdata1] = nil
76
107
  callback_info[:userdata2] = nil
77
108
 
78
- future = Native.wgpuShaderModuleGetCompilationInfo(@handle, callback_info)
109
+ callback_token = CallbackKeepalive.retain(self, callback)
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
79
118
  AsyncWaiter.wait(status_holder: result_holder, instance: instance, device: @device, future: future)
80
119
 
81
120
  {
@@ -84,10 +123,16 @@ module WGPU
84
123
  }
85
124
  end
86
125
 
126
+ # Fetches compiler diagnostics on a background task.
127
+ # @return [AsyncTask] task yielding compilation information
87
128
  def get_compilation_info_async
88
129
  AsyncTask.new { get_compilation_info }
89
130
  end
90
131
 
132
+ # Releases the native shader module handle.
133
+ #
134
+ # Calling this method more than once has no effect.
135
+ # @return [void]
91
136
  def release
92
137
  return if @handle.null?
93
138
  Native.wgpuShaderModuleRelease(@handle)
@@ -96,6 +141,28 @@ module WGPU
96
141
 
97
142
  private
98
143
 
144
+ def select_source(code, spirv)
145
+ sources = [code, spirv].reject(&:nil?)
146
+ raise ArgumentError, "provide exactly one of code: or spirv:" unless sources.one?
147
+
148
+ sources.first
149
+ end
150
+
151
+ def validate_compilation!(label)
152
+ info = get_compilation_info
153
+ errors = info[:messages].select { |message| message.type == :error }
154
+ return if errors.empty?
155
+
156
+ details = errors.map(&:to_s).join("\n")
157
+ release
158
+ raise ShaderError, shader_error_message(details, label)
159
+ end
160
+
161
+ def shader_error_message(message, label)
162
+ context = label ? " for #{label.inspect}" : ""
163
+ "Shader compilation failed#{context}: #{message}"
164
+ end
165
+
99
166
  def build_shader_source(code, label:)
100
167
  if code.is_a?(String)
101
168
  if spirv_binary?(code)