wgpu 0.1.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.
@@ -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,54 @@
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
+
10
+ entry_point = compute[:entry_point] || "main"
11
+ entry_point_ptr = FFI::MemoryPointer.from_string(entry_point)
12
+
13
+ desc = Native::ComputePipelineDescriptor.new
14
+ desc[:next_in_chain] = nil
15
+ if label
16
+ label_ptr = FFI::MemoryPointer.from_string(label)
17
+ desc[:label][:data] = label_ptr
18
+ desc[:label][:length] = label.bytesize
19
+ else
20
+ desc[:label][:data] = nil
21
+ desc[:label][:length] = 0
22
+ end
23
+ desc[:layout] = layout.handle
24
+
25
+ desc[:compute][:next_in_chain] = nil
26
+ desc[:compute][:module] = compute[:module].handle
27
+ desc[:compute][:entry_point][:data] = entry_point_ptr
28
+ desc[:compute][:entry_point][:length] = entry_point.bytesize
29
+ desc[:compute][:constant_count] = 0
30
+ desc[:compute][:constants] = nil
31
+
32
+ device.push_error_scope(:validation)
33
+ @handle = Native.wgpuDeviceCreateComputePipeline(device.handle, desc)
34
+ error = device.pop_error_scope
35
+
36
+ if @handle.null? || (error[:type] && error[:type] != :no_error)
37
+ msg = error[:message] || "Failed to create compute pipeline"
38
+ raise PipelineError, msg
39
+ end
40
+ end
41
+
42
+ def get_bind_group_layout(index)
43
+ handle = Native.wgpuComputePipelineGetBindGroupLayout(@handle, index)
44
+ raise PipelineError, "Failed to get bind group layout at index #{index}" if handle.null?
45
+ BindGroupLayout.from_handle(handle)
46
+ end
47
+
48
+ def release
49
+ return if @handle.null?
50
+ Native.wgpuComputePipelineRelease(@handle)
51
+ @handle = FFI::Pointer::NULL
52
+ end
53
+ end
54
+ 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,248 @@
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] = layout.handle
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
+
83
+ buffers = vertex[:buffers] || []
84
+ if buffers.empty?
85
+ vertex_state[:buffer_count] = 0
86
+ vertex_state[:buffers] = nil
87
+ else
88
+ buffer_layouts = setup_vertex_buffer_layouts(buffers)
89
+ vertex_state[:buffer_count] = buffers.size
90
+ vertex_state[:buffers] = buffer_layouts
91
+ end
92
+ end
93
+
94
+ def setup_vertex_buffer_layouts(buffers)
95
+ layouts_ptr = FFI::MemoryPointer.new(Native::VertexBufferLayout, buffers.size)
96
+ @pointers << layouts_ptr
97
+
98
+ buffers.each_with_index do |buf, i|
99
+ layout = Native::VertexBufferLayout.new(layouts_ptr + i * Native::VertexBufferLayout.size)
100
+ layout[:array_stride] = buf[:array_stride]
101
+ layout[:step_mode] = buf[:step_mode] || :vertex
102
+
103
+ attrs = buf[:attributes] || []
104
+ if attrs.empty?
105
+ layout[:attribute_count] = 0
106
+ layout[:attributes] = nil
107
+ else
108
+ attrs_ptr = FFI::MemoryPointer.new(Native::VertexAttribute, attrs.size)
109
+ @pointers << attrs_ptr
110
+ attrs.each_with_index do |attr, j|
111
+ a = Native::VertexAttribute.new(attrs_ptr + j * Native::VertexAttribute.size)
112
+ a[:format] = attr[:format]
113
+ a[:offset] = attr[:offset]
114
+ a[:shader_location] = attr[:shader_location]
115
+ end
116
+ layout[:attribute_count] = attrs.size
117
+ layout[:attributes] = attrs_ptr
118
+ end
119
+ end
120
+
121
+ layouts_ptr
122
+ end
123
+
124
+ def setup_primitive_state(primitive_state, primitive)
125
+ primitive_state[:next_in_chain] = nil
126
+ primitive_state[:topology] = primitive[:topology] || :triangle_list
127
+ primitive_state[:strip_index_format] = primitive[:strip_index_format] || :undefined
128
+ primitive_state[:front_face] = primitive[:front_face] || :ccw
129
+ primitive_state[:cull_mode] = primitive[:cull_mode] || :none
130
+ primitive_state[:unclipped_depth] = primitive[:unclipped_depth] ? 1 : 0
131
+ end
132
+
133
+ def setup_multisample_state(multisample_state, multisample)
134
+ multisample_state[:next_in_chain] = nil
135
+ multisample_state[:count] = multisample[:count] || 1
136
+ multisample_state[:mask] = multisample[:mask] || 0xFFFFFFFF
137
+ multisample_state[:alpha_to_coverage_enabled] = multisample[:alpha_to_coverage_enabled] ? 1 : 0
138
+ end
139
+
140
+ def setup_depth_stencil_state(depth_stencil)
141
+ ds = Native::DepthStencilState.new
142
+ @pointers << ds
143
+ ds[:next_in_chain] = nil
144
+ ds[:format] = depth_stencil[:format]
145
+ ds[:depth_write_enabled] = depth_stencil[:depth_write_enabled] ? 1 : 0
146
+ ds[:depth_compare] = depth_stencil[:depth_compare] || :always
147
+
148
+ setup_stencil_face(ds[:stencil_front], depth_stencil[:stencil_front] || {})
149
+ setup_stencil_face(ds[:stencil_back], depth_stencil[:stencil_back] || {})
150
+
151
+ ds[:stencil_read_mask] = depth_stencil[:stencil_read_mask] || 0xFFFFFFFF
152
+ ds[:stencil_write_mask] = depth_stencil[:stencil_write_mask] || 0xFFFFFFFF
153
+ ds[:depth_bias] = depth_stencil[:depth_bias] || 0
154
+ ds[:depth_bias_slope_scale] = depth_stencil[:depth_bias_slope_scale] || 0.0
155
+ ds[:depth_bias_clamp] = depth_stencil[:depth_bias_clamp] || 0.0
156
+
157
+ ds.to_ptr
158
+ end
159
+
160
+ def setup_stencil_face(face, config)
161
+ face[:compare] = config[:compare] || :always
162
+ face[:fail_op] = config[:fail_op] || :keep
163
+ face[:depth_fail_op] = config[:depth_fail_op] || :keep
164
+ face[:pass_op] = config[:pass_op] || :keep
165
+ end
166
+
167
+ def setup_fragment_state(fragment)
168
+ frag = Native::FragmentState.new
169
+ @pointers << frag
170
+ frag[:next_in_chain] = nil
171
+ frag[:module] = fragment[:module].handle
172
+
173
+ entry_point = fragment[:entry_point] || "main"
174
+ entry_ptr = FFI::MemoryPointer.from_string(entry_point)
175
+ @pointers << entry_ptr
176
+ frag[:entry_point][:data] = entry_ptr
177
+ frag[:entry_point][:length] = entry_point.bytesize
178
+
179
+ frag[:constant_count] = 0
180
+ frag[:constants] = nil
181
+
182
+ targets = fragment[:targets] || []
183
+ if targets.empty?
184
+ frag[:target_count] = 0
185
+ frag[:targets] = nil
186
+ else
187
+ targets_ptr = setup_color_targets(targets)
188
+ frag[:target_count] = targets.size
189
+ frag[:targets] = targets_ptr
190
+ end
191
+
192
+ frag.to_ptr
193
+ end
194
+
195
+ def setup_color_targets(targets)
196
+ targets_ptr = FFI::MemoryPointer.new(Native::ColorTargetState, targets.size)
197
+ @pointers << targets_ptr
198
+
199
+ targets.each_with_index do |target, i|
200
+ ct = Native::ColorTargetState.new(targets_ptr + i * Native::ColorTargetState.size)
201
+ ct[:next_in_chain] = nil
202
+ ct[:format] = target[:format]
203
+ ct[:write_mask] = normalize_write_mask(target[:write_mask])
204
+
205
+ if target[:blend]
206
+ blend_ptr = setup_blend_state(target[:blend])
207
+ ct[:blend] = blend_ptr
208
+ else
209
+ ct[:blend] = nil
210
+ end
211
+ end
212
+
213
+ targets_ptr
214
+ end
215
+
216
+ def setup_blend_state(blend)
217
+ bs = Native::BlendState.new
218
+ @pointers << bs
219
+
220
+ color = blend[:color] || {}
221
+ bs[:color][:operation] = color[:operation] || :add
222
+ bs[:color][:src_factor] = color[:src_factor] || :one
223
+ bs[:color][:dst_factor] = color[:dst_factor] || :zero
224
+
225
+ alpha = blend[:alpha] || {}
226
+ bs[:alpha][:operation] = alpha[:operation] || :add
227
+ bs[:alpha][:src_factor] = alpha[:src_factor] || :one
228
+ bs[:alpha][:dst_factor] = alpha[:dst_factor] || :zero
229
+
230
+ bs.to_ptr
231
+ end
232
+
233
+ def normalize_write_mask(mask)
234
+ case mask
235
+ when nil
236
+ Native::ColorWriteMask[:all]
237
+ when Integer
238
+ mask
239
+ when Symbol
240
+ Native::ColorWriteMask[mask]
241
+ when Array
242
+ mask.reduce(0) { |acc, m| acc | Native::ColorWriteMask[m] }
243
+ else
244
+ raise ArgumentError, "Invalid write_mask: #{mask}"
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ class ShaderModule
5
+ attr_reader :handle
6
+
7
+ def initialize(device, label: nil, code:)
8
+ @device = device
9
+ @code_ptr = FFI::MemoryPointer.from_string(code)
10
+
11
+ wgsl_desc = Native::ShaderSourceWGSL.new
12
+ wgsl_desc[:chain][:next] = nil
13
+ wgsl_desc[:chain][:s_type] = :shader_source_wgsl
14
+ wgsl_desc[:code][:data] = @code_ptr
15
+ wgsl_desc[:code][:length] = code.bytesize
16
+
17
+ desc = Native::ShaderModuleDescriptor.new
18
+ desc[:next_in_chain] = wgsl_desc.to_ptr
19
+ if label
20
+ label_ptr = FFI::MemoryPointer.from_string(label)
21
+ desc[:label][:data] = label_ptr
22
+ desc[:label][:length] = label.bytesize
23
+ else
24
+ desc[:label][:data] = nil
25
+ desc[:label][:length] = 0
26
+ end
27
+
28
+ device.push_error_scope(:validation)
29
+ @handle = Native.wgpuDeviceCreateShaderModule(device.handle, desc)
30
+ error = device.pop_error_scope
31
+
32
+ if @handle.null? || (error[:type] && error[:type] != :no_error)
33
+ msg = error[:message] || "Failed to create shader module"
34
+ raise ShaderError, msg
35
+ end
36
+ end
37
+
38
+ def get_compilation_info
39
+ result_holder = { done: false, status: nil, messages: [] }
40
+
41
+ callback = FFI::Function.new(
42
+ :void, [:uint32, :pointer, :pointer, :pointer]
43
+ ) do |status, compilation_info_ptr, _userdata1, _userdata2|
44
+ result_holder[:done] = true
45
+ result_holder[:status] = Native::CompilationInfoRequestStatus[status]
46
+
47
+ unless compilation_info_ptr.null?
48
+ info = Native::CompilationInfo.new(compilation_info_ptr)
49
+ count = info[:message_count]
50
+ if count > 0 && !info[:messages].null?
51
+ count.times do |i|
52
+ msg_ptr = info[:messages] + (i * Native::CompilationMessage.size)
53
+ msg = Native::CompilationMessage.new(msg_ptr)
54
+ message_text = if msg[:message][:data] && !msg[:message][:data].null? && msg[:message][:length] > 0
55
+ msg[:message][:data].read_string(msg[:message][:length])
56
+ else
57
+ ""
58
+ end
59
+ result_holder[:messages] << {
60
+ type: msg[:type],
61
+ message: message_text,
62
+ line_num: msg[:line_num],
63
+ line_pos: msg[:line_pos],
64
+ offset: msg[:offset],
65
+ length: msg[:length]
66
+ }
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ callback_info = Native::CompilationInfoCallbackInfo.new
73
+ callback_info[:next_in_chain] = nil
74
+ callback_info[:mode] = 1
75
+ callback_info[:callback] = callback
76
+ callback_info[:userdata1] = nil
77
+ callback_info[:userdata2] = nil
78
+
79
+ Native.wgpuShaderModuleGetCompilationInfo(@handle, callback_info)
80
+
81
+ until result_holder[:done]
82
+ Native.wgpuDevicePoll(@device.handle, 0, nil)
83
+ sleep(0.001)
84
+ end
85
+
86
+ {
87
+ status: result_holder[:status],
88
+ messages: result_holder[:messages]
89
+ }
90
+ end
91
+
92
+ def release
93
+ return if @handle.null?
94
+ Native.wgpuShaderModuleRelease(@handle)
95
+ @handle = FFI::Pointer::NULL
96
+ end
97
+ end
98
+ end