wgpu 1.0.0 → 1.2.0

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/README.md +26 -3
  4. data/docs/README.md +22 -0
  5. data/docs/api_coverage.md +132 -0
  6. data/docs/async.md +31 -0
  7. data/docs/bind_groups.md +25 -0
  8. data/docs/buffer_data.md +37 -0
  9. data/docs/command_encoding.md +24 -0
  10. data/docs/errors.md +40 -0
  11. data/docs/getting_started_compute.md +61 -0
  12. data/docs/getting_started_rendering.md +62 -0
  13. data/docs/installation.md +94 -0
  14. data/docs/pipeline_descriptors.md +38 -0
  15. data/docs/releasing.md +22 -0
  16. data/docs/resource_lifetime.md +94 -0
  17. data/docs/shaders.md +32 -0
  18. data/docs/texture_readback.md +19 -0
  19. data/docs/troubleshooting.md +48 -0
  20. data/docs/upgrading_wgpu_native.md +26 -0
  21. data/ext/wgpu/extconf.rb +10 -142
  22. data/lib/wgpu/commands/command_buffer.rb +11 -0
  23. data/lib/wgpu/commands/command_encoder.rb +65 -8
  24. data/lib/wgpu/commands/compute_pass.rb +10 -0
  25. data/lib/wgpu/commands/render_bundle_encoder.rb +10 -3
  26. data/lib/wgpu/commands/render_pass.rb +34 -8
  27. data/lib/wgpu/core/adapter.rb +49 -20
  28. data/lib/wgpu/core/async_waiter.rb +92 -0
  29. data/lib/wgpu/core/canvas_context.rb +0 -2
  30. data/lib/wgpu/core/device.rb +160 -52
  31. data/lib/wgpu/core/instance.rb +9 -5
  32. data/lib/wgpu/core/queue.rb +81 -57
  33. data/lib/wgpu/core/surface.rb +16 -17
  34. data/lib/wgpu/data_types.rb +67 -0
  35. data/lib/wgpu/descriptor_helpers.rb +39 -0
  36. data/lib/wgpu/error.rb +55 -0
  37. data/lib/wgpu/logging.rb +63 -0
  38. data/lib/wgpu/native/abi_verifier.rb +109 -0
  39. data/lib/wgpu/native/callbacks.rb +9 -6
  40. data/lib/wgpu/native/capabilities.rb +31 -0
  41. data/lib/wgpu/native/distribution.rb +113 -0
  42. data/lib/wgpu/native/enum_helper.rb +63 -0
  43. data/lib/wgpu/native/enums.rb +82 -13
  44. data/lib/wgpu/native/functions.rb +17 -8
  45. data/lib/wgpu/native/installer.rb +192 -0
  46. data/lib/wgpu/native/loader.rb +40 -21
  47. data/lib/wgpu/native/structs.rb +19 -5
  48. data/lib/wgpu/native_resource.rb +171 -0
  49. data/lib/wgpu/pipeline/bind_group.rb +12 -0
  50. data/lib/wgpu/pipeline/bind_group_layout.rb +91 -37
  51. data/lib/wgpu/pipeline/compute_pipeline.rb +28 -26
  52. data/lib/wgpu/pipeline/render_pipeline.rb +180 -68
  53. data/lib/wgpu/pipeline/shader_module.rb +51 -12
  54. data/lib/wgpu/resources/buffer.rb +167 -84
  55. data/lib/wgpu/resources/query_set.rb +14 -12
  56. data/lib/wgpu/resources/sampler.rb +36 -20
  57. data/lib/wgpu/resources/texture.rb +44 -42
  58. data/lib/wgpu/resources/texture_view.rb +11 -3
  59. data/lib/wgpu/texture_format.rb +82 -0
  60. data/lib/wgpu/version.rb +1 -1
  61. data/lib/wgpu/window.rb +8 -1
  62. data/lib/wgpu.rb +33 -0
  63. data/sig/wgpu.rbs +381 -0
  64. metadata +33 -16
@@ -6,30 +6,15 @@ module WGPU
6
6
 
7
7
  def initialize(device, label: nil, layout:, vertex:, primitive: {}, depth_stencil: nil, multisample: {}, fragment: nil)
8
8
  @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
9
+ desc, @pointers = build_descriptor(
10
+ label:,
11
+ layout:,
12
+ vertex:,
13
+ primitive:,
14
+ depth_stencil:,
15
+ multisample:,
16
+ fragment:
17
+ )
33
18
 
34
19
  device.push_error_scope(:validation)
35
20
  @handle = Native.wgpuDeviceCreateRenderPipeline(device.handle, desc)
@@ -55,19 +40,29 @@ module WGPU
55
40
 
56
41
  private
57
42
 
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
43
+ def build_descriptor(label:, layout:, vertex:, primitive:, depth_stencil:, multisample:, fragment:)
44
+ @pointers = []
45
+ desc = Native::RenderPipelineDescriptor.new
46
+ desc[:next_in_chain] = nil
47
+ DescriptorHelpers.set_label(desc, label, keepalive: @pointers)
48
+ desc[:layout] = normalize_layout(layout)
49
+
50
+ setup_vertex_state(desc[:vertex], vertex)
51
+ setup_primitive_state(desc[:primitive], primitive)
52
+ setup_multisample_state(desc[:multisample], multisample)
53
+ desc[:depth_stencil] = depth_stencil ? setup_depth_stencil_state(depth_stencil) : nil
54
+ desc[:fragment] = fragment ? setup_fragment_state(fragment) : nil
55
+
56
+ [desc, @pointers]
68
57
  end
69
58
 
70
59
  def setup_vertex_state(vertex_state, vertex)
60
+ DescriptorHelpers.validate_keys!(
61
+ vertex,
62
+ allowed: %i[module entry_point constants buffers],
63
+ required: [:module],
64
+ context: "render pipeline vertex descriptor"
65
+ )
71
66
  vertex_state[:next_in_chain] = nil
72
67
  vertex_state[:module] = vertex[:module].handle
73
68
 
@@ -97,8 +92,18 @@ module WGPU
97
92
  @pointers << layouts_ptr
98
93
 
99
94
  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
95
+ DescriptorHelpers.validate_keys!(
96
+ buf,
97
+ allowed: %i[array_stride step_mode attributes],
98
+ required: [:array_stride],
99
+ context: "vertex buffer layout"
100
+ )
101
+ layout = Native::VertexBufferLayout.new(layouts_ptr + (i * Native::VertexBufferLayout.size))
102
+ layout[:step_mode] = Native::EnumHelper.coerce(
103
+ Native::VertexStepMode,
104
+ buf[:step_mode] || :vertex,
105
+ name: "vertex step mode"
106
+ )
102
107
  layout[:array_stride] = buf[:array_stride]
103
108
 
104
109
  attrs = buf[:attributes] || []
@@ -109,8 +114,18 @@ module WGPU
109
114
  attrs_ptr = FFI::MemoryPointer.new(Native::VertexAttribute, attrs.size)
110
115
  @pointers << attrs_ptr
111
116
  attrs.each_with_index do |attr, j|
112
- a = Native::VertexAttribute.new(attrs_ptr + j * Native::VertexAttribute.size)
113
- a[:format] = attr[:format]
117
+ DescriptorHelpers.validate_keys!(
118
+ attr,
119
+ allowed: %i[format offset shader_location],
120
+ required: %i[format offset shader_location],
121
+ context: "vertex attribute"
122
+ )
123
+ a = Native::VertexAttribute.new(attrs_ptr + (j * Native::VertexAttribute.size))
124
+ a[:format] = Native::EnumHelper.coerce(
125
+ Native::VertexFormat,
126
+ attr[:format],
127
+ name: "vertex format"
128
+ )
114
129
  a[:offset] = attr[:offset]
115
130
  a[:shader_location] = attr[:shader_location]
116
131
  end
@@ -123,15 +138,41 @@ module WGPU
123
138
  end
124
139
 
125
140
  def setup_primitive_state(primitive_state, primitive)
141
+ DescriptorHelpers.validate_keys!(
142
+ primitive,
143
+ allowed: %i[topology strip_index_format front_face cull_mode unclipped_depth],
144
+ context: "primitive state"
145
+ )
126
146
  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
147
+ primitive_state[:topology] = Native::EnumHelper.coerce(
148
+ Native::PrimitiveTopology,
149
+ primitive[:topology] || :triangle_list,
150
+ name: "primitive topology"
151
+ )
152
+ primitive_state[:strip_index_format] = Native::EnumHelper.coerce(
153
+ Native::IndexFormat,
154
+ primitive[:strip_index_format] || :undefined,
155
+ name: "strip index format"
156
+ )
157
+ primitive_state[:front_face] = Native::EnumHelper.coerce(
158
+ Native::FrontFace,
159
+ primitive[:front_face] || :ccw,
160
+ name: "front face"
161
+ )
162
+ primitive_state[:cull_mode] = Native::EnumHelper.coerce(
163
+ Native::CullMode,
164
+ primitive[:cull_mode] || :none,
165
+ name: "cull mode"
166
+ )
131
167
  primitive_state[:unclipped_depth] = primitive[:unclipped_depth] ? 1 : 0
132
168
  end
133
169
 
134
170
  def setup_multisample_state(multisample_state, multisample)
171
+ DescriptorHelpers.validate_keys!(
172
+ multisample,
173
+ allowed: %i[count mask alpha_to_coverage_enabled],
174
+ context: "multisample state"
175
+ )
135
176
  multisample_state[:next_in_chain] = nil
136
177
  multisample_state[:count] = multisample[:count] || 1
137
178
  multisample_state[:mask] = multisample[:mask] || 0xFFFFFFFF
@@ -139,12 +180,29 @@ module WGPU
139
180
  end
140
181
 
141
182
  def setup_depth_stencil_state(depth_stencil)
183
+ DescriptorHelpers.validate_keys!(
184
+ depth_stencil,
185
+ allowed: %i[
186
+ format depth_write_enabled depth_compare stencil_front stencil_back
187
+ stencil_read_mask stencil_write_mask depth_bias depth_bias_slope_scale depth_bias_clamp
188
+ ],
189
+ required: [:format],
190
+ context: "depth stencil state"
191
+ )
142
192
  ds = Native::DepthStencilState.new
143
193
  @pointers << ds
144
194
  ds[:next_in_chain] = nil
145
- ds[:format] = depth_stencil[:format]
195
+ ds[:format] = Native::EnumHelper.coerce(
196
+ Native::TextureFormat,
197
+ depth_stencil[:format],
198
+ name: "depth stencil format"
199
+ )
146
200
  ds[:depth_write_enabled] = depth_stencil[:depth_write_enabled] ? 1 : 0
147
- ds[:depth_compare] = depth_stencil[:depth_compare] || :always
201
+ ds[:depth_compare] = Native::EnumHelper.coerce(
202
+ Native::CompareFunction,
203
+ depth_stencil[:depth_compare] || :always,
204
+ name: "depth compare function"
205
+ )
148
206
 
149
207
  setup_stencil_face(ds[:stencil_front], depth_stencil[:stencil_front] || {})
150
208
  setup_stencil_face(ds[:stencil_back], depth_stencil[:stencil_back] || {})
@@ -159,13 +217,40 @@ module WGPU
159
217
  end
160
218
 
161
219
  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
220
+ DescriptorHelpers.validate_keys!(
221
+ config,
222
+ allowed: %i[compare fail_op depth_fail_op pass_op],
223
+ context: "stencil face state"
224
+ )
225
+ face[:compare] = Native::EnumHelper.coerce(
226
+ Native::CompareFunction,
227
+ config[:compare] || :always,
228
+ name: "stencil compare function"
229
+ )
230
+ face[:fail_op] = Native::EnumHelper.coerce(
231
+ Native::StencilOperation,
232
+ config[:fail_op] || :keep,
233
+ name: "stencil fail operation"
234
+ )
235
+ face[:depth_fail_op] = Native::EnumHelper.coerce(
236
+ Native::StencilOperation,
237
+ config[:depth_fail_op] || :keep,
238
+ name: "stencil depth fail operation"
239
+ )
240
+ face[:pass_op] = Native::EnumHelper.coerce(
241
+ Native::StencilOperation,
242
+ config[:pass_op] || :keep,
243
+ name: "stencil pass operation"
244
+ )
166
245
  end
167
246
 
168
247
  def setup_fragment_state(fragment)
248
+ DescriptorHelpers.validate_keys!(
249
+ fragment,
250
+ allowed: %i[module entry_point constants targets],
251
+ required: [:module],
252
+ context: "render pipeline fragment descriptor"
253
+ )
169
254
  frag = Native::FragmentState.new
170
255
  @pointers << frag
171
256
  frag[:next_in_chain] = nil
@@ -199,9 +284,19 @@ module WGPU
199
284
  @pointers << targets_ptr
200
285
 
201
286
  targets.each_with_index do |target, i|
202
- ct = Native::ColorTargetState.new(targets_ptr + i * Native::ColorTargetState.size)
287
+ DescriptorHelpers.validate_keys!(
288
+ target,
289
+ allowed: %i[format blend write_mask],
290
+ required: [:format],
291
+ context: "color target state"
292
+ )
293
+ ct = Native::ColorTargetState.new(targets_ptr + (i * Native::ColorTargetState.size))
203
294
  ct[:next_in_chain] = nil
204
- ct[:format] = target[:format]
295
+ ct[:format] = Native::EnumHelper.coerce(
296
+ Native::TextureFormat,
297
+ target[:format],
298
+ name: "color target format"
299
+ )
205
300
  ct[:write_mask] = normalize_write_mask(target[:write_mask])
206
301
 
207
302
  if target[:blend]
@@ -216,35 +311,52 @@ module WGPU
216
311
  end
217
312
 
218
313
  def setup_blend_state(blend)
314
+ DescriptorHelpers.validate_keys!(
315
+ blend,
316
+ allowed: %i[color alpha],
317
+ context: "blend state"
318
+ )
219
319
  bs = Native::BlendState.new
220
320
  @pointers << bs
221
321
 
222
322
  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
323
+ setup_blend_component(bs[:color], color, "color")
226
324
 
227
325
  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
326
+ setup_blend_component(bs[:alpha], alpha, "alpha")
231
327
 
232
328
  bs.to_ptr
233
329
  end
234
330
 
331
+ def setup_blend_component(component, config, name)
332
+ DescriptorHelpers.validate_keys!(
333
+ config,
334
+ allowed: %i[operation src_factor dst_factor],
335
+ context: "#{name} blend component"
336
+ )
337
+ component[:operation] = Native::EnumHelper.coerce(
338
+ Native::BlendOperation,
339
+ config[:operation] || :add,
340
+ name: "#{name} blend operation"
341
+ )
342
+ component[:src_factor] = Native::EnumHelper.coerce(
343
+ Native::BlendFactor,
344
+ config[:src_factor] || :one,
345
+ name: "#{name} source blend factor"
346
+ )
347
+ component[:dst_factor] = Native::EnumHelper.coerce(
348
+ Native::BlendFactor,
349
+ config[:dst_factor] || :zero,
350
+ name: "#{name} destination blend factor"
351
+ )
352
+ end
353
+
235
354
  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
355
+ Native::EnumHelper.coerce_flags(
356
+ Native::ColorWriteMask,
357
+ mask || :all,
358
+ name: "color write mask"
359
+ )
248
360
  end
249
361
 
250
362
  def normalize_layout(layout)
@@ -4,12 +4,13 @@ module WGPU
4
4
  class ShaderModule
5
5
  attr_reader :handle
6
6
 
7
- def initialize(device, label: nil, code:, compilation_hints: [])
7
+ def initialize(device, label: nil, code: nil, spirv: nil, compilation_hints: [], validate: false)
8
8
  @device = device
9
9
  @pointers = []
10
- compilation_hints # currently unused by wgpu-native API
10
+ @compilation_hints = compilation_hints
11
+ source = select_source(code, spirv)
11
12
 
12
- source_ptr = build_shader_source(code, label: label)
13
+ source_ptr = build_shader_source(source, label: label)
13
14
 
14
15
  desc = Native::ShaderModuleDescriptor.new
15
16
  desc[:next_in_chain] = source_ptr
@@ -29,12 +30,27 @@ module WGPU
29
30
 
30
31
  if @handle.null? || (error[:type] && error[:type] != :no_error)
31
32
  msg = error[:message] || "Failed to create shader module"
32
- raise ShaderError, msg
33
+ raise ShaderError, shader_error_message(msg, label)
34
+ end
35
+
36
+ return unless validate
37
+
38
+ begin
39
+ validate_compilation!(label)
40
+ rescue StandardError
41
+ release
42
+ raise
33
43
  end
34
44
  end
35
45
 
36
46
  def get_compilation_info
47
+ unless Native.compilation_info_available?
48
+ raise ShaderError,
49
+ "Shader compilation info is not implemented by wgpu-native #{Native::Distribution::VERSION}"
50
+ end
51
+
37
52
  result_holder = { done: false, status: nil, messages: [] }
53
+ instance = @device.adapter&.instance
38
54
 
39
55
  callback = FFI::Function.new(
40
56
  :void, [:uint32, :pointer, :pointer, :pointer]
@@ -54,14 +70,14 @@ module WGPU
54
70
  else
55
71
  ""
56
72
  end
57
- result_holder[:messages] << {
73
+ result_holder[:messages] << CompilationMessage.new(
58
74
  type: msg[:type],
59
75
  message: message_text,
60
76
  line_num: msg[:line_num],
61
77
  line_pos: msg[:line_pos],
62
78
  offset: msg[:offset],
63
79
  length: msg[:length]
64
- }
80
+ )
65
81
  end
66
82
  end
67
83
  end
@@ -69,16 +85,17 @@ module WGPU
69
85
 
70
86
  callback_info = Native::CompilationInfoCallbackInfo.new
71
87
  callback_info[:next_in_chain] = nil
72
- callback_info[:mode] = 1
88
+ callback_info[:mode] = AsyncWaiter.callback_mode(instance: instance)
73
89
  callback_info[:callback] = callback
74
90
  callback_info[:userdata1] = nil
75
91
  callback_info[:userdata2] = nil
76
92
 
77
- Native.wgpuShaderModuleGetCompilationInfo(@handle, callback_info)
78
-
79
- until result_holder[:done]
80
- Native.wgpuDevicePoll(@device.handle, 0, nil)
81
- sleep(0.001)
93
+ 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)
82
99
  end
83
100
 
84
101
  {
@@ -99,6 +116,28 @@ module WGPU
99
116
 
100
117
  private
101
118
 
119
+ def select_source(code, spirv)
120
+ sources = [code, spirv].reject(&:nil?)
121
+ raise ArgumentError, "provide exactly one of code: or spirv:" unless sources.one?
122
+
123
+ sources.first
124
+ end
125
+
126
+ def validate_compilation!(label)
127
+ info = get_compilation_info
128
+ errors = info[:messages].select { |message| message.type == :error }
129
+ return if errors.empty?
130
+
131
+ details = errors.map(&:to_s).join("\n")
132
+ release
133
+ raise ShaderError, shader_error_message(details, label)
134
+ end
135
+
136
+ def shader_error_message(message, label)
137
+ context = label ? " for #{label.inspect}" : ""
138
+ "Shader compilation failed#{context}: #{message}"
139
+ end
140
+
102
141
  def build_shader_source(code, label:)
103
142
  if code.is_a?(String)
104
143
  if spirv_binary?(code)