wgpu 1.0.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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-APACHE +190 -0
  3. data/LICENSE-MIT +21 -0
  4. data/README.md +228 -0
  5. data/ext/wgpu/Makefile +7 -0
  6. data/ext/wgpu/extconf.rb +161 -0
  7. data/lib/wgpu/async_task.rb +55 -0
  8. data/lib/wgpu/commands/command_buffer.rb +17 -0
  9. data/lib/wgpu/commands/command_encoder.rb +201 -0
  10. data/lib/wgpu/commands/compute_pass.rb +89 -0
  11. data/lib/wgpu/commands/render_bundle.rb +18 -0
  12. data/lib/wgpu/commands/render_bundle_encoder.rb +148 -0
  13. data/lib/wgpu/commands/render_pass.rb +207 -0
  14. data/lib/wgpu/core/adapter.rb +186 -0
  15. data/lib/wgpu/core/canvas_context.rb +104 -0
  16. data/lib/wgpu/core/device.rb +397 -0
  17. data/lib/wgpu/core/instance.rb +81 -0
  18. data/lib/wgpu/core/queue.rb +197 -0
  19. data/lib/wgpu/core/surface.rb +221 -0
  20. data/lib/wgpu/error.rb +16 -0
  21. data/lib/wgpu/native/callbacks.rb +26 -0
  22. data/lib/wgpu/native/enums.rb +529 -0
  23. data/lib/wgpu/native/functions.rb +419 -0
  24. data/lib/wgpu/native/loader.rb +61 -0
  25. data/lib/wgpu/native/structs.rb +646 -0
  26. data/lib/wgpu/pipeline/bind_group.rb +80 -0
  27. data/lib/wgpu/pipeline/bind_group_layout.rb +121 -0
  28. data/lib/wgpu/pipeline/compute_pipeline.rb +88 -0
  29. data/lib/wgpu/pipeline/pipeline_layout.rb +43 -0
  30. data/lib/wgpu/pipeline/render_pipeline.rb +278 -0
  31. data/lib/wgpu/pipeline/shader_module.rb +202 -0
  32. data/lib/wgpu/resources/buffer.rb +228 -0
  33. data/lib/wgpu/resources/query_set.rb +45 -0
  34. data/lib/wgpu/resources/sampler.rb +47 -0
  35. data/lib/wgpu/resources/texture.rb +136 -0
  36. data/lib/wgpu/resources/texture_view.rb +49 -0
  37. data/lib/wgpu/version.rb +5 -0
  38. data/lib/wgpu/window.rb +177 -0
  39. data/lib/wgpu.rb +36 -0
  40. metadata +125 -0
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ class BindGroupLayout
5
+ attr_reader :handle
6
+
7
+ def self.from_handle(handle)
8
+ layout = allocate
9
+ layout.instance_variable_set(:@handle, handle)
10
+ layout.instance_variable_set(:@device, nil)
11
+ layout
12
+ end
13
+
14
+ def initialize(device, label: nil, entries:)
15
+ @device = device
16
+
17
+ entries_array = entries.map { |e| create_entry(e) }
18
+ entries_ptr = FFI::MemoryPointer.new(Native::BindGroupLayoutEntry, entries_array.size)
19
+ entries_array.each_with_index do |entry, i|
20
+ offset = i * Native::BindGroupLayoutEntry.size
21
+ (entries_ptr + offset).put_bytes(0, entry.pointer.read_bytes(Native::BindGroupLayoutEntry.size))
22
+ end
23
+
24
+ desc = Native::BindGroupLayoutDescriptor.new
25
+ desc[:next_in_chain] = nil
26
+ if label
27
+ label_ptr = FFI::MemoryPointer.from_string(label)
28
+ desc[:label][:data] = label_ptr
29
+ desc[:label][:length] = label.bytesize
30
+ else
31
+ desc[:label][:data] = nil
32
+ desc[:label][:length] = 0
33
+ end
34
+ desc[:entry_count] = entries_array.size
35
+ desc[:entries] = entries_ptr
36
+
37
+ device.push_error_scope(:validation)
38
+ @handle = Native.wgpuDeviceCreateBindGroupLayout(device.handle, desc)
39
+ error = device.pop_error_scope
40
+
41
+ if @handle.null? || (error[:type] && error[:type] != :no_error)
42
+ msg = error[:message] || "Failed to create bind group layout"
43
+ raise PipelineError, msg
44
+ end
45
+ end
46
+
47
+ def release
48
+ return if @handle.null?
49
+ Native.wgpuBindGroupLayoutRelease(@handle)
50
+ @handle = FFI::Pointer::NULL
51
+ end
52
+
53
+ private
54
+
55
+ def create_entry(entry_hash)
56
+ entry = Native::BindGroupLayoutEntry.new
57
+ entry[:next_in_chain] = nil
58
+ entry[:binding] = entry_hash[:binding]
59
+ entry[:visibility] = normalize_visibility(entry_hash[:visibility])
60
+
61
+ entry[:buffer][:next_in_chain] = nil
62
+ entry[:buffer][:type] = :binding_not_used
63
+ entry[:buffer][:has_dynamic_offset] = 0
64
+ entry[:buffer][:min_binding_size] = 0
65
+
66
+ entry[:sampler][:next_in_chain] = nil
67
+ entry[:sampler][:type] = :binding_not_used
68
+
69
+ entry[:texture][:next_in_chain] = nil
70
+ entry[:texture][:sample_type] = :binding_not_used
71
+ entry[:texture][:view_dimension] = :undefined
72
+ entry[:texture][:multisampled] = 0
73
+
74
+ entry[:storage_texture][:next_in_chain] = nil
75
+ entry[:storage_texture][:access] = :binding_not_used
76
+ entry[:storage_texture][:format] = :undefined
77
+ entry[:storage_texture][:view_dimension] = :undefined
78
+
79
+ if entry_hash[:buffer]
80
+ buffer_info = entry_hash[:buffer]
81
+ entry[:buffer][:type] = buffer_info[:type] || :storage
82
+ entry[:buffer][:has_dynamic_offset] = buffer_info[:has_dynamic_offset] ? 1 : 0
83
+ entry[:buffer][:min_binding_size] = buffer_info[:min_binding_size] || 0
84
+ end
85
+
86
+ if entry_hash[:sampler]
87
+ sampler_info = entry_hash[:sampler]
88
+ entry[:sampler][:type] = sampler_info[:type] || :filtering
89
+ end
90
+
91
+ if entry_hash[:texture]
92
+ texture_info = entry_hash[:texture]
93
+ entry[:texture][:sample_type] = texture_info[:sample_type] || :float
94
+ entry[:texture][:view_dimension] = texture_info[:view_dimension] || :d2
95
+ entry[:texture][:multisampled] = texture_info[:multisampled] ? 1 : 0
96
+ end
97
+
98
+ if entry_hash[:storage_texture]
99
+ st_info = entry_hash[:storage_texture]
100
+ entry[:storage_texture][:access] = st_info[:access] || :write_only
101
+ entry[:storage_texture][:format] = st_info[:format]
102
+ entry[:storage_texture][:view_dimension] = st_info[:view_dimension] || :d2
103
+ end
104
+
105
+ entry
106
+ end
107
+
108
+ def normalize_visibility(visibility)
109
+ case visibility
110
+ when Integer
111
+ visibility
112
+ when Symbol
113
+ Native::ShaderStage[visibility]
114
+ when Array
115
+ visibility.reduce(0) { |acc, v| acc | Native::ShaderStage[v] }
116
+ else
117
+ raise ArgumentError, "Invalid visibility: #{visibility}"
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ class ComputePipeline
5
+ attr_reader :handle
6
+
7
+ def initialize(device, label: nil, layout:, compute:)
8
+ @device = device
9
+ @pointers = []
10
+
11
+ entry_point = compute[:entry_point] || "main"
12
+ entry_point_ptr = FFI::MemoryPointer.from_string(entry_point)
13
+ @pointers << entry_point_ptr
14
+
15
+ desc = Native::ComputePipelineDescriptor.new
16
+ desc[:next_in_chain] = nil
17
+ if label
18
+ label_ptr = FFI::MemoryPointer.from_string(label)
19
+ @pointers << label_ptr
20
+ desc[:label][:data] = label_ptr
21
+ desc[:label][:length] = label.bytesize
22
+ else
23
+ desc[:label][:data] = nil
24
+ desc[:label][:length] = 0
25
+ end
26
+ desc[:layout] = normalize_layout(layout)
27
+
28
+ desc[:compute][:next_in_chain] = nil
29
+ desc[:compute][:module] = compute.fetch(:module).handle
30
+ desc[:compute][:entry_point][:data] = entry_point_ptr
31
+ desc[:compute][:entry_point][:length] = entry_point.bytesize
32
+ desc[:compute][:constant_count] = 0
33
+ desc[:compute][:constants] = nil
34
+ setup_constants(desc[:compute], compute[:constants])
35
+
36
+ device.push_error_scope(:validation)
37
+ @handle = Native.wgpuDeviceCreateComputePipeline(device.handle, desc)
38
+ error = device.pop_error_scope
39
+
40
+ if @handle.null? || (error[:type] && error[:type] != :no_error)
41
+ msg = error[:message] || "Failed to create compute pipeline"
42
+ raise PipelineError, msg
43
+ end
44
+ end
45
+
46
+ def get_bind_group_layout(index)
47
+ handle = Native.wgpuComputePipelineGetBindGroupLayout(@handle, index)
48
+ raise PipelineError, "Failed to get bind group layout at index #{index}" if handle.null?
49
+ BindGroupLayout.from_handle(handle)
50
+ end
51
+
52
+ def release
53
+ return if @handle.null?
54
+ Native.wgpuComputePipelineRelease(@handle)
55
+ @handle = FFI::Pointer::NULL
56
+ end
57
+
58
+ private
59
+
60
+ def normalize_layout(layout)
61
+ return nil if layout.nil? || layout == :auto || layout == "auto"
62
+ layout.handle
63
+ end
64
+
65
+ def setup_constants(stage_desc, constants)
66
+ return if constants.nil? || constants.empty?
67
+
68
+ constants_ptr = FFI::MemoryPointer.new(Native::ConstantEntry, constants.size)
69
+ @pointers << constants_ptr
70
+
71
+ constants.each_with_index do |(key, value), i|
72
+ entry_ptr = constants_ptr + (i * Native::ConstantEntry.size)
73
+ entry = Native::ConstantEntry.new(entry_ptr)
74
+ entry[:next_in_chain] = nil
75
+
76
+ key_str = key.to_s
77
+ key_ptr = FFI::MemoryPointer.from_string(key_str)
78
+ @pointers << key_ptr
79
+ entry[:key][:data] = key_ptr
80
+ entry[:key][:length] = key_str.bytesize
81
+ entry[:value] = value.to_f
82
+ end
83
+
84
+ stage_desc[:constant_count] = constants.size
85
+ stage_desc[:constants] = constants_ptr
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ class PipelineLayout
5
+ attr_reader :handle
6
+
7
+ def initialize(device, label: nil, bind_group_layouts:)
8
+ @device = device
9
+
10
+ layouts = Array(bind_group_layouts)
11
+ layouts_ptr = FFI::MemoryPointer.new(:pointer, layouts.size)
12
+ layouts_ptr.write_array_of_pointer(layouts.map(&:handle))
13
+
14
+ desc = Native::PipelineLayoutDescriptor.new
15
+ desc[:next_in_chain] = nil
16
+ if label
17
+ label_ptr = FFI::MemoryPointer.from_string(label)
18
+ desc[:label][:data] = label_ptr
19
+ desc[:label][:length] = label.bytesize
20
+ else
21
+ desc[:label][:data] = nil
22
+ desc[:label][:length] = 0
23
+ end
24
+ desc[:bind_group_layout_count] = layouts.size
25
+ desc[:bind_group_layouts] = layouts_ptr
26
+
27
+ device.push_error_scope(:validation)
28
+ @handle = Native.wgpuDeviceCreatePipelineLayout(device.handle, desc)
29
+ error = device.pop_error_scope
30
+
31
+ if @handle.null? || (error[:type] && error[:type] != :no_error)
32
+ msg = error[:message] || "Failed to create pipeline layout"
33
+ raise PipelineError, msg
34
+ end
35
+ end
36
+
37
+ def release
38
+ return if @handle.null?
39
+ Native.wgpuPipelineLayoutRelease(@handle)
40
+ @handle = FFI::Pointer::NULL
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ class RenderPipeline
5
+ attr_reader :handle
6
+
7
+ def initialize(device, label: nil, layout:, vertex:, primitive: {}, depth_stencil: nil, multisample: {}, fragment: nil)
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
33
+
34
+ device.push_error_scope(:validation)
35
+ @handle = Native.wgpuDeviceCreateRenderPipeline(device.handle, desc)
36
+ error = device.pop_error_scope
37
+
38
+ if @handle.null? || (error[:type] && error[:type] != :no_error)
39
+ msg = error[:message] || "Failed to create render pipeline"
40
+ raise PipelineError, msg
41
+ end
42
+ end
43
+
44
+ def get_bind_group_layout(index)
45
+ handle = Native.wgpuRenderPipelineGetBindGroupLayout(@handle, index)
46
+ raise PipelineError, "Failed to get bind group layout at index #{index}" if handle.null?
47
+ BindGroupLayout.from_handle(handle)
48
+ end
49
+
50
+ def release
51
+ return if @handle.null?
52
+ Native.wgpuRenderPipelineRelease(@handle)
53
+ @handle = FFI::Pointer::NULL
54
+ end
55
+
56
+ private
57
+
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
68
+ end
69
+
70
+ def setup_vertex_state(vertex_state, vertex)
71
+ vertex_state[:next_in_chain] = nil
72
+ vertex_state[:module] = vertex[:module].handle
73
+
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
+
84
+ buffers = vertex[:buffers] || []
85
+ if buffers.empty?
86
+ vertex_state[:buffer_count] = 0
87
+ vertex_state[:buffers] = nil
88
+ else
89
+ buffer_layouts = setup_vertex_buffer_layouts(buffers)
90
+ vertex_state[:buffer_count] = buffers.size
91
+ vertex_state[:buffers] = buffer_layouts
92
+ end
93
+ end
94
+
95
+ def setup_vertex_buffer_layouts(buffers)
96
+ layouts_ptr = FFI::MemoryPointer.new(Native::VertexBufferLayout, buffers.size)
97
+ @pointers << layouts_ptr
98
+
99
+ 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
102
+ layout[:array_stride] = buf[:array_stride]
103
+
104
+ attrs = buf[:attributes] || []
105
+ if attrs.empty?
106
+ layout[:attribute_count] = 0
107
+ layout[:attributes] = nil
108
+ else
109
+ attrs_ptr = FFI::MemoryPointer.new(Native::VertexAttribute, attrs.size)
110
+ @pointers << attrs_ptr
111
+ attrs.each_with_index do |attr, j|
112
+ a = Native::VertexAttribute.new(attrs_ptr + j * Native::VertexAttribute.size)
113
+ a[:format] = attr[:format]
114
+ a[:offset] = attr[:offset]
115
+ a[:shader_location] = attr[:shader_location]
116
+ end
117
+ layout[:attribute_count] = attrs.size
118
+ layout[:attributes] = attrs_ptr
119
+ end
120
+ end
121
+
122
+ layouts_ptr
123
+ end
124
+
125
+ def setup_primitive_state(primitive_state, primitive)
126
+ 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
131
+ primitive_state[:unclipped_depth] = primitive[:unclipped_depth] ? 1 : 0
132
+ end
133
+
134
+ def setup_multisample_state(multisample_state, multisample)
135
+ multisample_state[:next_in_chain] = nil
136
+ multisample_state[:count] = multisample[:count] || 1
137
+ multisample_state[:mask] = multisample[:mask] || 0xFFFFFFFF
138
+ multisample_state[:alpha_to_coverage_enabled] = multisample[:alpha_to_coverage_enabled] ? 1 : 0
139
+ end
140
+
141
+ def setup_depth_stencil_state(depth_stencil)
142
+ ds = Native::DepthStencilState.new
143
+ @pointers << ds
144
+ ds[:next_in_chain] = nil
145
+ ds[:format] = depth_stencil[:format]
146
+ ds[:depth_write_enabled] = depth_stencil[:depth_write_enabled] ? 1 : 0
147
+ ds[:depth_compare] = depth_stencil[:depth_compare] || :always
148
+
149
+ setup_stencil_face(ds[:stencil_front], depth_stencil[:stencil_front] || {})
150
+ setup_stencil_face(ds[:stencil_back], depth_stencil[:stencil_back] || {})
151
+
152
+ ds[:stencil_read_mask] = depth_stencil[:stencil_read_mask] || 0xFFFFFFFF
153
+ ds[:stencil_write_mask] = depth_stencil[:stencil_write_mask] || 0xFFFFFFFF
154
+ ds[:depth_bias] = depth_stencil[:depth_bias] || 0
155
+ ds[:depth_bias_slope_scale] = depth_stencil[:depth_bias_slope_scale] || 0.0
156
+ ds[:depth_bias_clamp] = depth_stencil[:depth_bias_clamp] || 0.0
157
+
158
+ ds.to_ptr
159
+ end
160
+
161
+ 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
166
+ end
167
+
168
+ def setup_fragment_state(fragment)
169
+ frag = Native::FragmentState.new
170
+ @pointers << frag
171
+ frag[:next_in_chain] = nil
172
+ frag[:module] = fragment[:module].handle
173
+
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])
183
+
184
+ targets = fragment[:targets] || []
185
+ if targets.empty?
186
+ frag[:target_count] = 0
187
+ frag[:targets] = nil
188
+ else
189
+ targets_ptr = setup_color_targets(targets)
190
+ frag[:target_count] = targets.size
191
+ frag[:targets] = targets_ptr
192
+ end
193
+
194
+ frag.to_ptr
195
+ end
196
+
197
+ def setup_color_targets(targets)
198
+ targets_ptr = FFI::MemoryPointer.new(Native::ColorTargetState, targets.size)
199
+ @pointers << targets_ptr
200
+
201
+ targets.each_with_index do |target, i|
202
+ ct = Native::ColorTargetState.new(targets_ptr + i * Native::ColorTargetState.size)
203
+ ct[:next_in_chain] = nil
204
+ ct[:format] = target[:format]
205
+ ct[:write_mask] = normalize_write_mask(target[:write_mask])
206
+
207
+ if target[:blend]
208
+ blend_ptr = setup_blend_state(target[:blend])
209
+ ct[:blend] = blend_ptr
210
+ else
211
+ ct[:blend] = nil
212
+ end
213
+ end
214
+
215
+ targets_ptr
216
+ end
217
+
218
+ def setup_blend_state(blend)
219
+ bs = Native::BlendState.new
220
+ @pointers << bs
221
+
222
+ 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
226
+
227
+ 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
231
+
232
+ bs.to_ptr
233
+ end
234
+
235
+ 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
248
+ end
249
+
250
+ def normalize_layout(layout)
251
+ return nil if layout.nil? || layout == :auto || layout == "auto"
252
+ layout.handle
253
+ 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
+ end
278
+ end