zaniah 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.
Files changed (166) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +70 -0
  5. data/assets/fonts/Abel-Regular.ttf +0 -0
  6. data/assets/fonts/OFL.txt +93 -0
  7. data/docs/adr/000-template.md +24 -0
  8. data/docs/adr/001-native-gpu-backends.md +36 -0
  9. data/docs/adr/002-objective-c-aggregates.md +34 -0
  10. data/docs/adr/003-fiddle-call-boundary.md +33 -0
  11. data/docs/adr/README.md +10 -0
  12. data/docs/native.md +66 -0
  13. data/docs/process_pool.md +67 -0
  14. data/docs/text.md +90 -0
  15. data/docs/vector_and_list.md +52 -0
  16. data/examples/ffi_bench.rb +16 -0
  17. data/examples/linux_smoke.rb +53 -0
  18. data/examples/mac_event_check.rb +61 -0
  19. data/examples/mac_poll_bench.rb +24 -0
  20. data/examples/metal_state_check.rb +72 -0
  21. data/examples/native_bench.rb +18 -0
  22. data/examples/native_drop.rb +82 -0
  23. data/examples/native_font.rb +15 -0
  24. data/examples/native_ime.rb +50 -0
  25. data/examples/native_smoke.rb +75 -0
  26. data/examples/native_text_bench.rb +83 -0
  27. data/examples/native_watch.rb +25 -0
  28. data/examples/vulkan_triangle.rb +20 -0
  29. data/examples/word_count_worker.rb +8 -0
  30. data/lib/zaniah/anchored.rb +10 -0
  31. data/lib/zaniah/app.rb +130 -0
  32. data/lib/zaniah/bounds.rb +22 -0
  33. data/lib/zaniah/canvas.rb +15 -0
  34. data/lib/zaniah/color.rb +35 -0
  35. data/lib/zaniah/configuration.rb +5 -0
  36. data/lib/zaniah/corners.rb +7 -0
  37. data/lib/zaniah/data_compat.rb +25 -0
  38. data/lib/zaniah/device_pixels.rb +7 -0
  39. data/lib/zaniah/div.rb +5 -0
  40. data/lib/zaniah/edges.rb +7 -0
  41. data/lib/zaniah/element.rb +106 -0
  42. data/lib/zaniah/entity.rb +9 -0
  43. data/lib/zaniah/entity_context.rb +17 -0
  44. data/lib/zaniah/error.rb +5 -0
  45. data/lib/zaniah/ffi/com.rb +24 -0
  46. data/lib/zaniah/ffi/library.rb +29 -0
  47. data/lib/zaniah/ffi/objc.rb +86 -0
  48. data/lib/zaniah/ffi/struct/signature.rb +46 -0
  49. data/lib/zaniah/ffi/struct.rb +61 -0
  50. data/lib/zaniah/ffi/wayland.rb +140 -0
  51. data/lib/zaniah/frame_context.rb +14 -0
  52. data/lib/zaniah/geometry.rb +16 -0
  53. data/lib/zaniah/gpu/buffer.rb +23 -0
  54. data/lib/zaniah/gpu/frame_encoder.rb +43 -0
  55. data/lib/zaniah/gpu/instance_packing.rb +53 -0
  56. data/lib/zaniah/gpu/metal.rb +209 -0
  57. data/lib/zaniah/gpu/open_gl.rb +210 -0
  58. data/lib/zaniah/gpu/pipeline.rb +7 -0
  59. data/lib/zaniah/gpu/software.rb +126 -0
  60. data/lib/zaniah/gpu/texture.rb +33 -0
  61. data/lib/zaniah/gpu/vulkan.rb +233 -0
  62. data/lib/zaniah/gpu.rb +18 -0
  63. data/lib/zaniah/hsla.rb +14 -0
  64. data/lib/zaniah/image.rb +19 -0
  65. data/lib/zaniah/input/action_registry.rb +12 -0
  66. data/lib/zaniah/input/composition.rb +7 -0
  67. data/lib/zaniah/input/context_predicate.rb +87 -0
  68. data/lib/zaniah/input/dispatcher.rb +59 -0
  69. data/lib/zaniah/input/file_drop.rb +7 -0
  70. data/lib/zaniah/input/focus_handle.rb +24 -0
  71. data/lib/zaniah/input/key_down.rb +7 -0
  72. data/lib/zaniah/input/key_up.rb +7 -0
  73. data/lib/zaniah/input/keymap/binding.rb +9 -0
  74. data/lib/zaniah/input/keymap.rb +55 -0
  75. data/lib/zaniah/input/keystroke.rb +20 -0
  76. data/lib/zaniah/input/mouse_down.rb +7 -0
  77. data/lib/zaniah/input/mouse_move.rb +7 -0
  78. data/lib/zaniah/input/mouse_up.rb +7 -0
  79. data/lib/zaniah/input/scroll_wheel.rb +7 -0
  80. data/lib/zaniah/input/text_input.rb +7 -0
  81. data/lib/zaniah/input.rb +17 -0
  82. data/lib/zaniah/layout/available_space.rb +11 -0
  83. data/lib/zaniah/layout/engine.rb +214 -0
  84. data/lib/zaniah/layout/node.rb +36 -0
  85. data/lib/zaniah/layout/style.rb +19 -0
  86. data/lib/zaniah/layout/tree.rb +16 -0
  87. data/lib/zaniah/layout.rb +11 -0
  88. data/lib/zaniah/length.rb +14 -0
  89. data/lib/zaniah/length_units.rb +9 -0
  90. data/lib/zaniah/list/height_index.rb +68 -0
  91. data/lib/zaniah/list.rb +101 -0
  92. data/lib/zaniah/overlay.rb +10 -0
  93. data/lib/zaniah/platform/display.rb +7 -0
  94. data/lib/zaniah/platform/file_event.rb +7 -0
  95. data/lib/zaniah/platform/headless/window.rb +185 -0
  96. data/lib/zaniah/platform/linux/appearance_aware.rb +60 -0
  97. data/lib/zaniah/platform/linux/displays.rb +55 -0
  98. data/lib/zaniah/platform/linux/free_type.rb +80 -0
  99. data/lib/zaniah/platform/linux/watcher.rb +70 -0
  100. data/lib/zaniah/platform/linux/wayland_display_discovery.rb +60 -0
  101. data/lib/zaniah/platform/linux/wayland_window.rb +369 -0
  102. data/lib/zaniah/platform/linux/window.rb +418 -0
  103. data/lib/zaniah/platform/linux.rb +3 -0
  104. data/lib/zaniah/platform/mac/app.rb +67 -0
  105. data/lib/zaniah/platform/mac/core_text.rb +94 -0
  106. data/lib/zaniah/platform/mac/displays.rb +23 -0
  107. data/lib/zaniah/platform/mac/watcher.rb +63 -0
  108. data/lib/zaniah/platform/mac/window.rb +298 -0
  109. data/lib/zaniah/platform/mac.rb +19 -0
  110. data/lib/zaniah/platform/tui/input_decoder.rb +145 -0
  111. data/lib/zaniah/platform/tui/text_renderer.rb +40 -0
  112. data/lib/zaniah/platform/tui/window.rb +91 -0
  113. data/lib/zaniah/platform/windows/terminal.rb +127 -0
  114. data/lib/zaniah/platform/windows/watcher.rb +91 -0
  115. data/lib/zaniah/platform/windows/window.rb +407 -0
  116. data/lib/zaniah/platform/windows.rb +3 -0
  117. data/lib/zaniah/platform.rb +34 -0
  118. data/lib/zaniah/png.rb +118 -0
  119. data/lib/zaniah/point.rb +8 -0
  120. data/lib/zaniah/process_pool/worker.rb +7 -0
  121. data/lib/zaniah/process_pool.rb +239 -0
  122. data/lib/zaniah/process_wire.rb +127 -0
  123. data/lib/zaniah/quad_packer.rb +15 -0
  124. data/lib/zaniah/scene/sprite_batch.rb +7 -0
  125. data/lib/zaniah/scene.rb +167 -0
  126. data/lib/zaniah/scene_renderer.rb +8 -0
  127. data/lib/zaniah/size.rb +5 -0
  128. data/lib/zaniah/subscription.rb +14 -0
  129. data/lib/zaniah/svg/path.rb +134 -0
  130. data/lib/zaniah/svg/rasterizable.rb +186 -0
  131. data/lib/zaniah/svg.rb +295 -0
  132. data/lib/zaniah/task/cancelled.rb +7 -0
  133. data/lib/zaniah/task/timeout.rb +7 -0
  134. data/lib/zaniah/task.rb +51 -0
  135. data/lib/zaniah/task_executor.rb +105 -0
  136. data/lib/zaniah/text.rb +24 -0
  137. data/lib/zaniah/text_system/atlas/entry.rb +9 -0
  138. data/lib/zaniah/text_system/atlas.rb +102 -0
  139. data/lib/zaniah/text_system/atlas_cache.rb +146 -0
  140. data/lib/zaniah/text_system/font_db/face.rb +9 -0
  141. data/lib/zaniah/text_system/font_db.rb +193 -0
  142. data/lib/zaniah/text_system/glyph.rb +7 -0
  143. data/lib/zaniah/text_system/line_layout.rb +20 -0
  144. data/lib/zaniah/text_system/renderer.rb +196 -0
  145. data/lib/zaniah/text_system/shaper.rb +430 -0
  146. data/lib/zaniah/text_system/typesetter/copy_error.rb +9 -0
  147. data/lib/zaniah/text_system/typesetter.rb +168 -0
  148. data/lib/zaniah/text_system.rb +9 -0
  149. data/lib/zaniah/unicode.rb +13 -0
  150. data/lib/zaniah/uniform_list.rb +30 -0
  151. data/lib/zaniah/version.rb +5 -0
  152. data/lib/zaniah/view.rb +13 -0
  153. data/lib/zaniah.rb +29 -0
  154. data/sig/app.rbs +80 -0
  155. data/sig/elements.rbs +129 -0
  156. data/sig/gpu.rbs +108 -0
  157. data/sig/input.rbs +114 -0
  158. data/sig/layout.rbs +53 -0
  159. data/sig/native.rbs +88 -0
  160. data/sig/platform.rbs +94 -0
  161. data/sig/scene.rbs +52 -0
  162. data/sig/shaper.rbs +8 -0
  163. data/sig/text.rbs +137 -0
  164. data/sig/zaniah.rbs +120 -0
  165. data/tools/check_dependencies.rb +52 -0
  166. metadata +249 -0
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "instance_packing"
4
+ require_relative "../ffi/library"
5
+
6
+ module Zaniah
7
+ module GPU
8
+ class OpenGL
9
+ I, P, V, F = Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOID, Fiddle::TYPE_FLOAT
10
+ attr_reader :width, :height, :draw_calls
11
+ VERTEX = <<~GLSL.freeze
12
+ #version 330 core
13
+ layout(location=0) in vec4 rect;
14
+ layout(location=1) in vec4 tint;
15
+ layout(location=2) in vec4 corners;
16
+ layout(location=3) in vec4 border_color;
17
+ layout(location=4) in vec4 extra;
18
+ layout(location=5) in vec4 source;
19
+ uniform vec2 viewport;
20
+ out vec2 local_position; out vec2 size; out vec4 color; out vec4 radii;
21
+ out vec4 border; out vec4 info; out vec2 uv;
22
+ void main() {
23
+ vec2 vertices[4] = vec2[4](vec2(0,0),vec2(1,0),vec2(0,1),vec2(1,1));
24
+ vec2 corner = vertices[gl_VertexID];
25
+ vec2 position = rect.xy + corner * rect.zw;
26
+ if (extra.y == 3) position = gl_VertexID == 0 ? rect.xy : gl_VertexID == 1 ? rect.zw : corners.xy;
27
+ gl_Position = vec4(position.x / viewport.x * 2 - 1, 1 - position.y / viewport.y * 2, 0, 1);
28
+ local_position = corner * rect.zw; size = rect.zw; color = tint;
29
+ radii = corners; border = border_color; info = extra; uv = source.xy + corner * source.zw;
30
+ }
31
+ GLSL
32
+ FRAGMENT = <<~GLSL.freeze
33
+ #version 330 core
34
+ in vec2 local_position; in vec2 size; in vec4 color; in vec4 radii;
35
+ in vec4 border; in vec4 info; in vec2 uv;
36
+ uniform sampler2D atlas;
37
+ out vec4 output_color;
38
+ void main() {
39
+ vec4 result = color;
40
+ if (info.y == 0) {
41
+ float radius = local_position.y < size.y/2 ? (local_position.x < size.x/2 ? radii.x : radii.y) : (local_position.x < size.x/2 ? radii.w : radii.z);
42
+ radius = clamp(radius, 0, min(size.x,size.y)/2);
43
+ vec2 q = abs(local_position - size/2) - size/2 + radius;
44
+ float distance = length(max(q,0)) + min(max(q.x,q.y),0) - radius;
45
+ float coverage = clamp(0.5 - distance, 0, 1);
46
+ if (info.x > 0 && distance >= -info.x) result = border;
47
+ result.a *= coverage;
48
+ } else if (info.y == 1 || info.y == 2) {
49
+ vec4 sample_color = texture(atlas,uv);
50
+ if (info.y == 1) result.a *= sample_color.r; else result *= sample_color;
51
+ }
52
+ output_color = vec4(result.rgb * result.a, result.a);
53
+ }
54
+ GLSL
55
+
56
+ def initialize(window, library:, resolver: nil)
57
+ @window, @library, @resolver, @functions, @textures = window, library, resolver, {}, {}
58
+ window.make_current
59
+ version = gl(:glGetString, [I], P, 0x1F02).to_s
60
+ raise Error, "OpenGL 3.3 required (#{version})" if version.to_f < 3.3
61
+ @program = compile_program(VERTEX, FRAGMENT)
62
+ @vao, @buffer = generate(:glGenVertexArrays), generate(:glGenBuffers)
63
+ gl(:glBindVertexArray, [I], V, @vao)
64
+ gl(:glBindBuffer, [I, I], V, 0x8892, @buffer)
65
+ 6.times do |location|
66
+ gl(:glEnableVertexAttribArray, [I], V, location)
67
+ gl(:glVertexAttribDivisor, [I, I], V, location, 1)
68
+ end
69
+ gl(:glEnable, [I], V, 0x0BE2)
70
+ gl(:glBlendFunc, [I, I], V, 1, 0x0303)
71
+ @viewport_location = gl(:glGetUniformLocation, [I, P], I, @program, "viewport")
72
+ @white = create_texture(1, 1, data: "\xff".b * 4)
73
+ resize(window.content_size.width, window.content_size.height)
74
+ end
75
+ def gl(name, args, result, *values)
76
+ function = @functions[name] ||= begin
77
+ address = @resolver&.call(name.to_s)
78
+ address = @library.handle[name.to_s] if !address || address.to_i <= 3 || address.to_i == -1
79
+ Fiddle::Function.new(address, args, result, name: name.to_s, need_gvl: false)
80
+ end
81
+ function.call(*values)
82
+ end
83
+ def generate(name)
84
+ bytes = [0].pack("I")
85
+ gl(name, [I, P], V, 1, bytes)
86
+ bytes.unpack1("I")
87
+ end
88
+ def compile_program(vertex, fragment)
89
+ shaders = []
90
+ [[0x8B31, vertex], [0x8B30, fragment]].each do |kind, source|
91
+ shader = gl(:glCreateShader, [I], I, kind)
92
+ pointer = [Fiddle::Pointer[source].to_i].pack("J")
93
+ length = [source.bytesize].pack("i")
94
+ gl(:glShaderSource, [I, I, P, P], V, shader, 1, pointer, length)
95
+ gl(:glCompileShader, [I], V, shader)
96
+ status = [0].pack("i")
97
+ gl(:glGetShaderiv, [I, I, P], V, shader, 0x8B81, status)
98
+ if status.unpack1("i").zero?
99
+ log = "\0" * 8192
100
+ gl(:glGetShaderInfoLog, [I, I, P, P], V, shader, log.bytesize, 0, log)
101
+ gl(:glDeleteShader, [I], V, shader)
102
+ raise Error, "OpenGL shader: #{log.delete("\0") }"
103
+ end
104
+ shaders << shader
105
+ end
106
+ program = gl(:glCreateProgram, [], I)
107
+ shaders.each { |shader| gl(:glAttachShader, [I, I], V, program, shader) }
108
+ gl(:glLinkProgram, [I], V, program)
109
+ status = [0].pack("i")
110
+ gl(:glGetProgramiv, [I, I, P], V, program, 0x8B82, status)
111
+ if status.unpack1("i").zero?
112
+ log = "\0" * 8192
113
+ gl(:glGetProgramInfoLog, [I, I, P, P], V, program, log.bytesize, 0, log)
114
+ gl(:glDeleteProgram, [I], V, program)
115
+ raise Error, "OpenGL program: #{log.delete("\0") }"
116
+ end
117
+ program
118
+ ensure
119
+ shaders&.each { |shader| gl(:glDeleteShader, [I], V, shader) }
120
+ end
121
+ def resize(width, height)
122
+ raise ArgumentError, "invalid viewport" unless width.positive? && height.positive?
123
+ @width, @height, @scale = width.to_i, height.to_i, @window.scale_factor
124
+ @pixel_width, @pixel_height = (width * @scale).round, (height * @scale).round
125
+ end
126
+ def create_texture(width, height, **options) = Texture.new(width, height, **options)
127
+ def create_buffer(size, **options) = Buffer.new(size, **options)
128
+ def create_pipeline(shader:, blend: :premultiplied) = Pipeline.new(shader, blend)
129
+ def begin_frame(clear: "#0000") = FrameEncoder.new(self, clear)
130
+
131
+ def render(scene, clear: "#0000", capture: false)
132
+ @window.make_current
133
+ @draw_calls = 0
134
+ gl(:glViewport, [I] * 4, V, 0, 0, @pixel_width, @pixel_height)
135
+ gl(:glDisable, [I], V, 0x0C11)
136
+ gl(:glClearColor, [F] * 4, V, *Color.parse(clear).premultiplied)
137
+ gl(:glClear, [I], V, 0x4000)
138
+ gl(:glEnable, [I], V, 0x0C11)
139
+ gl(:glUseProgram, [I], V, @program)
140
+ gl(:glUniform2f, [I, F, F], V, @viewport_location, @width, @height)
141
+ gl(:glBindVertexArray, [I], V, @vao)
142
+ gl(:glBindBuffer, [I, I], V, 0x8892, @buffer)
143
+ bytes, batches = InstancePacking.pack(scene)
144
+ unless bytes.empty?
145
+ gl(:glBufferData, [I, Fiddle::TYPE_SIZE_T, P, I], V, 0x8892, bytes.bytesize, 0, 0x88E0)
146
+ gl(:glBufferSubData, [I, Fiddle::TYPE_SIZE_T, Fiddle::TYPE_SIZE_T, P], V, 0x8892, 0, bytes.bytesize, bytes)
147
+ batches.each do |(kind, texture, clip), first, count|
148
+ clip = clip ? Bounds.new(0, 0, @width, @height).intersect(clip) : Bounds.new(0, 0, @width, @height)
149
+ next unless clip.width.positive? && clip.height.positive?
150
+ gl(:glScissor, [I] * 4, V, (clip.x * @scale).to_i, ((@height - clip.bottom) * @scale).to_i, (clip.width * @scale).to_i, (clip.height * @scale).to_i)
151
+ 6.times do |location|
152
+ gl(:glVertexAttribPointer, [I, I, I, I, I, P], V, location, 4, 0x1406, 0, InstancePacking::STRIDE * 4, (first * InstancePacking::STRIDE + location * 4) * 4)
153
+ end
154
+ native_texture(texture || @white)
155
+ gl(:glDrawArraysInstanced, [I] * 4, V, kind == :triangle ? 4 : 5, 0, kind == :triangle ? 3 : 4, count)
156
+ @draw_calls += 1
157
+ end
158
+ end
159
+ # Capture only when requested; glReadPixels is otherwise an avoidable stall.
160
+ @last_scene, @last_clear = scene, clear
161
+ @captured_pixels = read_pixels if capture
162
+ @window.swap_buffers
163
+ end
164
+
165
+ def native_texture(texture)
166
+ entry = @textures[texture]
167
+ unless entry
168
+ entry = @textures[texture] = [generate(:glGenTextures), nil]
169
+ gl(:glBindTexture, [I, I], V, 0x0DE1, entry[0])
170
+ gl(:glTexParameteri, [I] * 3, V, 0x0DE1, 0x2801, 0x2600)
171
+ gl(:glTexParameteri, [I] * 3, V, 0x0DE1, 0x2800, 0x2600)
172
+ gl(:glTexParameteri, [I] * 3, V, 0x0DE1, 0x2802, 0x812F)
173
+ gl(:glTexParameteri, [I] * 3, V, 0x0DE1, 0x2803, 0x812F)
174
+ gl(:glPixelStorei, [I, I], V, 0x0CF5, 1)
175
+ gl(:glTexImage2D, [I] * 8 + [P], V, 0x0DE1, 0, texture.format == :r8 ? 0x8229 : 0x8058, texture.width, texture.height, 0, texture.format == :r8 ? 0x1903 : 0x1908, 0x1401, 0)
176
+ else
177
+ gl(:glBindTexture, [I, I], V, 0x0DE1, entry[0])
178
+ end
179
+ digest = texture.revision
180
+ if entry[1] != digest
181
+ gl(:glTexSubImage2D, [I] * 8 + [P], V, 0x0DE1, 0, 0, 0, texture.width, texture.height, texture.format == :r8 ? 0x1903 : 0x1908, 0x1401, texture.data)
182
+ entry[1] = digest
183
+ end
184
+ entry[0]
185
+ end
186
+
187
+ def pixels
188
+ render(@last_scene, clear: @last_clear, capture: true) if @last_scene
189
+ @captured_pixels || "\0".b * (@pixel_width * @pixel_height * 4)
190
+ end
191
+ def read_pixels
192
+ @window.make_current
193
+ gl(:glReadBuffer, [I], V, 0x0405)
194
+ bytes = "\0".b * (@pixel_width * @pixel_height * 4)
195
+ gl(:glReadPixels, [I] * 6 + [P], V, 0, 0, @pixel_width, @pixel_height, 0x1908, 0x1401, bytes)
196
+ rows = @pixel_height.times.map { |y| bytes.byteslice((@pixel_height - y - 1) * @pixel_width * 4, @pixel_width * 4) }.join
197
+ rows.unpack("C*").each_slice(4).flat_map { |r, g, b, a| a.zero? ? [0, 0, 0, 0] : [(r * 255 / a).clamp(0, 255), (g * 255 / a).clamp(0, 255), (b * 255 / a).clamp(0, 255), a] }.pack("C*")
198
+ end
199
+ def write_png(path) = PNG.write(path, @pixel_width, @pixel_height, pixels)
200
+ def release
201
+ @window.make_current
202
+ @textures.each_value { |entry| gl(:glDeleteTextures, [I, P], V, 1, [entry[0]].pack("I")) }
203
+ @textures.clear
204
+ gl(:glDeleteBuffers, [I, P], V, 1, [@buffer].pack("I")) if @buffer
205
+ gl(:glDeleteVertexArrays, [I, P], V, 1, [@vao].pack("I")) if @vao
206
+ gl(:glDeleteProgram, [I], V, @program) if @program
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ module GPU
5
+ Pipeline = Data.define(:shader, :blend)
6
+ end
7
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ module GPU
5
+ class Software
6
+ attr_reader :width, :height, :pixels
7
+
8
+ def initialize(width, height)
9
+ resize(width, height)
10
+ end
11
+
12
+ def resize(width, height)
13
+ raise ArgumentError, "invalid viewport" unless width.positive? && height.positive? && width * height <= 32_000_000
14
+ @width, @height = width.to_i, height.to_i
15
+ @viewport = Bounds.new(0, 0, @width, @height)
16
+ @pixels = "\0".b * (@width * @height * 4)
17
+ end
18
+
19
+ def create_texture(width, height, **options) = Texture.new(width, height, **options)
20
+ def create_buffer(size, **options) = Buffer.new(size, **options)
21
+ def create_pipeline(shader:, blend: :premultiplied) = Pipeline.new(shader, blend)
22
+ def begin_frame(clear: "#0000") = FrameEncoder.new(self, clear)
23
+
24
+ def render(scene, clear: "#0000")
25
+ @pixels.replace(Color.parse(clear).to_a.map { |value| (value.clamp(0, 1) * 255).round }.pack("C4") * (@width * @height))
26
+ scene.each_command do |kind, offset, clip|
27
+ bounds = clip ? @viewport.intersect(clip) : @viewport
28
+ case kind
29
+ when :quad then draw_quad(scene.quads, offset, bounds)
30
+ when :sprite then draw_sprite(scene, offset, bounds)
31
+ when :sprite_batch
32
+ first, count = scene.expand_sprite_batch(offset)
33
+ count.times { |index| draw_sprite(scene, first + index * 13, bounds) }
34
+ when :triangle then draw_triangle(scene.paths, offset, bounds)
35
+ end
36
+ end
37
+ @pixels
38
+ end
39
+
40
+ def write_png(path) = PNG.write(path, @width, @height, @pixels)
41
+ def release = @pixels.clear
42
+
43
+ private
44
+
45
+ def draw_quad(data, offset, clip)
46
+ x, y, width, height, red, green, blue, alpha, top_left, top_right, bottom_right, bottom_left, border, border_red, border_green, border_blue, border_alpha = data.slice(offset, Scene::QUAD_STRIDE)
47
+ bounds = Bounds.new(x, y, width, height).intersect(clip)
48
+ colors = [[red, green, blue, alpha], [border_red, border_green, border_blue, border_alpha]]
49
+ top, bottom = [bounds.y.floor, 0].max, [bounds.bottom.ceil, @height].min
50
+ left, right = [bounds.x.floor, 0].max, [bounds.right.ceil, @width].min
51
+ pixel_y = top
52
+ while pixel_y < bottom
53
+ pixel_x = left
54
+ while pixel_x < right
55
+ center_x, center_y = pixel_x + 0.5, pixel_y + 0.5
56
+ radius = center_y < y + height / 2.0 ? (center_x < x + width / 2.0 ? top_left : top_right) : (center_x < x + width / 2.0 ? bottom_left : bottom_right)
57
+ radius = radius.clamp(0, [width, height].min / 2.0)
58
+ quad_x = (center_x - x - width / 2.0).abs - width / 2.0 + radius
59
+ quad_y = (center_y - y - height / 2.0).abs - height / 2.0 + radius
60
+ distance = Math.sqrt([quad_x, 0].max**2 + [quad_y, 0].max**2) + [[quad_x, quad_y].max, 0].min - radius
61
+ coverage = (0.5 - distance).clamp(0, 1)
62
+ if coverage.positive?
63
+ color = colors[border.positive? && distance >= -border ? 1 : 0]
64
+ blend(pixel_x, pixel_y, color[0], color[1], color[2], color[3] * coverage)
65
+ end
66
+ pixel_x += 1
67
+ end
68
+ pixel_y += 1
69
+ end
70
+ end
71
+
72
+ def draw_sprite(scene, offset, clip)
73
+ x, y, width, height, red, green, blue, alpha, id, source_x, source_y, source_width, source_height = scene.sprite_data.slice(offset, 13)
74
+ return if width <= 0 || height <= 0
75
+ texture = scene.textures.fetch(id)
76
+ bounds = Bounds.new(x, y, width, height).intersect(clip)
77
+ ([bounds.y.ceil, 0].max...[bounds.bottom.ceil, @height].min).each do |pixel_y|
78
+ ([bounds.x.ceil, 0].max...[bounds.right.ceil, @width].min).each do |pixel_x|
79
+ texture_x = (source_x + (pixel_x + 0.5 - x) * source_width / width).floor.clamp(0, texture.width - 1)
80
+ texture_y = (source_y + (pixel_y + 0.5 - y) * source_height / height).floor.clamp(0, texture.height - 1)
81
+ index = texture_y * texture.width + texture_x
82
+ if texture.format == :r8
83
+ blend(pixel_x, pixel_y, red, green, blue, alpha * texture.data.getbyte(index) / 255.0)
84
+ else
85
+ pixel = index * 4
86
+ blend(pixel_x, pixel_y, red * texture.data.getbyte(pixel) / 255.0,
87
+ green * texture.data.getbyte(pixel + 1) / 255.0,
88
+ blue * texture.data.getbyte(pixel + 2) / 255.0,
89
+ alpha * texture.data.getbyte(pixel + 3) / 255.0)
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ def draw_triangle(data, offset, clip)
96
+ x0, y0, x1, y1, x2, y2, red, green, blue, alpha = data.slice(offset, 10)
97
+ area = (x1 - x0) * (y2 - y0) - (y1 - y0) * (x2 - x0)
98
+ return if area.zero?
99
+ bounds = Bounds.new([x0, x1, x2].min, [y0, y1, y2].min,
100
+ [x0, x1, x2].max - [x0, x1, x2].min,
101
+ [y0, y1, y2].max - [y0, y1, y2].min).intersect(clip)
102
+ (bounds.y.floor...bounds.bottom.ceil).each do |pixel_y|
103
+ (bounds.x.floor...bounds.right.ceil).each do |pixel_x|
104
+ x, y = pixel_x + 0.5, pixel_y + 0.5
105
+ u = ((x1 - x) * (y2 - y) - (y1 - y) * (x2 - x)) / area.to_f
106
+ v = ((x2 - x) * (y0 - y) - (y2 - y) * (x0 - x)) / area.to_f
107
+ blend(pixel_x, pixel_y, red, green, blue, alpha) if u >= 0 && v >= 0 && u + v <= 1
108
+ end
109
+ end
110
+ end
111
+
112
+ def blend(x, y, red, green, blue, alpha)
113
+ return if alpha <= 0
114
+ index = (y * @width + x) * 4
115
+ alpha = alpha.clamp(0, 1)
116
+ destination_alpha = @pixels.getbyte(index + 3) / 255.0
117
+ combined_alpha = alpha + destination_alpha * (1 - alpha)
118
+ [red, green, blue].each_with_index do |value, channel|
119
+ result = (value * alpha + @pixels.getbyte(index + channel) / 255.0 * destination_alpha * (1 - alpha)) / combined_alpha
120
+ @pixels.setbyte(index + channel, (result.clamp(0, 1) * 255).round)
121
+ end
122
+ @pixels.setbyte(index + 3, (combined_alpha * 255).round)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ module GPU
5
+ class Texture
6
+ attr_reader :width, :height, :format, :data, :revision
7
+
8
+ def initialize(width, height, format: :rgba8, data: nil)
9
+ raise ArgumentError, "invalid texture" unless width.is_a?(Integer) && height.is_a?(Integer) && width.positive? && height.positive? && [:r8, :rgba8].include?(format)
10
+ @width, @height, @format = width, height, format
11
+ @revision = 0
12
+ @channels = format == :r8 ? 1 : 4
13
+ @data = data ? data.b.dup : "\0".b * (width * height * @channels)
14
+ raise ArgumentError, "incorrect texture data size" unless @data.bytesize == width * height * @channels
15
+ end
16
+
17
+ def upload(x, y, width, height, bytes)
18
+ raise ArgumentError, "texture write out of bounds" if x.negative? || y.negative? || width.negative? || height.negative? || x + width > @width || y + height > @height
19
+ raise ArgumentError, "incorrect upload size" unless bytes.bytesize == width * height * @channels
20
+ height.times do |row|
21
+ @data[((y + row) * @width + x) * @channels, width * @channels] = bytes.byteslice(row * width * @channels, width * @channels)
22
+ end
23
+ @revision += 1
24
+ self
25
+ end
26
+
27
+ def release
28
+ @data.clear
29
+ @revision += 1
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fiddle/import"
4
+ require_relative "../ffi/library"
5
+
6
+ module Zaniah
7
+ module GPU
8
+ # Minimal offscreen Vulkan path for validating the pure Ruby SPIR-V emitter.
9
+ # It renders a vertex-index triangle into RGBA8 and reads back native memory.
10
+ # Window presentation and general Scene rendering remain Metal/OpenGL work.
11
+ class Vulkan
12
+ module Types
13
+ extend Fiddle::Importer
14
+ Instance = struct ["int s_type", "void *next", "unsigned int flags", "void *application", "unsigned int layer_count", "void *layers", "unsigned int extension_count", "void *extensions"]
15
+ Queue = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int family", "unsigned int count", "void *priorities"]
16
+ Device = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int queue_count", "void *queues", "unsigned int layer_count", "void *layers", "unsigned int extension_count", "void *extensions", "void *features"]
17
+ Image = struct ["int s_type", "void *next", "unsigned int flags", "int image_type", "int format", "unsigned int width", "unsigned int height", "unsigned int depth", "unsigned int mip_levels", "unsigned int layers", "unsigned int samples", "int tiling", "unsigned int usage", "int sharing", "unsigned int family_count", "void *families", "int initial_layout"]
18
+ Buffer = struct ["int s_type", "void *next", "unsigned int flags", "unsigned long long size", "unsigned int usage", "int sharing", "unsigned int family_count", "void *families"]
19
+ Allocation = struct ["int s_type", "void *next", "unsigned long long size", "unsigned int type_index"]
20
+ ImageView = struct ["int s_type", "void *next", "unsigned int flags", "unsigned long long image", "int view_type", "int format", "int components[4]", "unsigned int aspect", "unsigned int mip_level", "unsigned int level_count", "unsigned int array_layer", "unsigned int layer_count"]
21
+ Attachment = struct ["unsigned int flags", "int format", "unsigned int samples", "int load", "int store", "int stencil_load", "int stencil_store", "int initial_layout", "int final_layout"]
22
+ Subpass = struct ["unsigned int flags", "int bind_point", "unsigned int input_count", "void *inputs", "unsigned int color_count", "void *colors", "void *resolves", "void *depth_stencil", "unsigned int preserve_count", "void *preserves"]
23
+ Dependency = struct ["unsigned int source", "unsigned int destination", "unsigned int source_stage", "unsigned int destination_stage", "unsigned int source_access", "unsigned int destination_access", "unsigned int flags"]
24
+ RenderPass = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int attachment_count", "void *attachments", "unsigned int subpass_count", "void *subpasses", "unsigned int dependency_count", "void *dependencies"]
25
+ Framebuffer = struct ["int s_type", "void *next", "unsigned int flags", "unsigned long long render_pass", "unsigned int attachment_count", "void *attachments", "unsigned int width", "unsigned int height", "unsigned int layers"]
26
+ Shader = struct ["int s_type", "void *next", "unsigned int flags", "size_t code_size", "void *code"]
27
+ Stage = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int stage", "unsigned long long shader", "void *name", "void *specialization"]
28
+ VertexInput = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int binding_count", "void *bindings", "unsigned int attribute_count", "void *attributes"]
29
+ Assembly = struct ["int s_type", "void *next", "unsigned int flags", "int topology", "unsigned int primitive_restart"]
30
+ Viewport = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int viewport_count", "void *viewports", "unsigned int scissor_count", "void *scissors"]
31
+ Rasterization = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int depth_clamp", "unsigned int discard", "int polygon_mode", "unsigned int cull_mode", "int front_face", "unsigned int depth_bias", "float depth_bias_constant", "float depth_bias_clamp", "float depth_bias_slope", "float line_width"]
32
+ Multisample = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int samples", "unsigned int sample_shading", "float min_sample_shading", "void *sample_mask", "unsigned int alpha_to_coverage", "unsigned int alpha_to_one"]
33
+ Blend = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int logic_enable", "int logic_op", "unsigned int attachment_count", "void *attachments", "float constants[4]"]
34
+ Layout = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int set_count", "void *sets", "unsigned int push_count", "void *pushes"]
35
+ Pipeline = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int stage_count", "void *stages", "void *vertex_input", "void *assembly", "void *tessellation", "void *viewport", "void *rasterization", "void *multisample", "void *depth_stencil", "void *blend", "void *dynamic", "unsigned long long layout", "unsigned long long render_pass", "unsigned int subpass", "unsigned long long base_pipeline", "int base_index"]
36
+ CommandPool = struct ["int s_type", "void *next", "unsigned int flags", "unsigned int family"]
37
+ CommandAllocation = struct ["int s_type", "void *next", "unsigned long long pool", "int level", "unsigned int count"]
38
+ CommandBegin = struct ["int s_type", "void *next", "unsigned int flags", "void *inheritance"]
39
+ RenderBegin = struct ["int s_type", "void *next", "unsigned long long render_pass", "unsigned long long framebuffer", "int x", "int y", "unsigned int width", "unsigned int height", "unsigned int clear_count", "void *clears"]
40
+ MemoryBarrier = struct ["int s_type", "void *next", "unsigned int source_access", "unsigned int destination_access"]
41
+ Fence = struct ["int s_type", "void *next", "unsigned int flags"]
42
+ Submit = struct ["int s_type", "void *next", "unsigned int wait_count", "void *wait_semaphores", "void *wait_stages", "unsigned int command_count", "void *commands", "unsigned int signal_count", "void *signal_semaphores"]
43
+ end
44
+
45
+ P, I, U, Q, V = Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_UINT, -Fiddle::TYPE_LONG_LONG, Fiddle::TYPE_VOID
46
+ attr_reader :width, :height, :pixels, :device_name
47
+
48
+ def initialize(width: 128, height: 128)
49
+ raise ArgumentError, "invalid Vulkan image dimensions" unless [width, height].all? { |n| n.is_a?(Integer) && n.positive? } && width * height <= 16_777_216
50
+ @width, @height, @arena, @resources, @memories = width, height, [], [], []
51
+ @lib = FFI::Library.new("libvulkan.so.1", "vulkan-1.dll", "libvulkan.1.dylib", "libMoltenVK.dylib")
52
+ output = keep("\0".b * 8)
53
+ check(call(:vkCreateInstance, [P, P, P], I, build(Types::Instance, s_type: 1), 0, output), "vkCreateInstance")
54
+ @instance = output.unpack1("J")
55
+ count = keep([0].pack("I"))
56
+ check(call(:vkEnumeratePhysicalDevices, [P, P, P], I, @instance, count, 0), "vkEnumeratePhysicalDevices")
57
+ raise Error, "no Vulkan physical device" if count.unpack1("I").zero?
58
+ physicals = keep("\0".b * (count.unpack1("I") * Fiddle::SIZEOF_VOIDP))
59
+ check(call(:vkEnumeratePhysicalDevices, [P, P, P], I, @instance, count, physicals), "vkEnumeratePhysicalDevices")
60
+ @physical = physicals.unpack1("J")
61
+ properties = keep("\0".b * 4096)
62
+ call(:vkGetPhysicalDeviceProperties, [P, P], V, @physical, properties)
63
+ @device_name = properties.byteslice(20, 256).split("\0", 2).first
64
+ count.replace([0].pack("I"))
65
+ call(:vkGetPhysicalDeviceQueueFamilyProperties, [P, P, P], V, @physical, count, 0)
66
+ families = keep("\0".b * (count.unpack1("I") * 24))
67
+ call(:vkGetPhysicalDeviceQueueFamilyProperties, [P, P, P], V, @physical, count, families)
68
+ @family = count.unpack1("I").times.find { |index| (families.byteslice(index * 24, 4).unpack1("I") & 1) != 0 }
69
+ raise Error, "no Vulkan graphics queue" unless @family
70
+ queue_info = build(Types::Queue, s_type: 2, family: @family, count: 1, priorities: keep([1.0].pack("f")))
71
+ info = build(Types::Device, s_type: 3, queue_count: 1, queues: queue_info)
72
+ check(call(:vkCreateDevice, [P, P, P, P], I, @physical, info, 0, output), "vkCreateDevice")
73
+ @device = output.unpack1("J")
74
+ call(:vkGetDeviceQueue, [P, U, U, P], V, @device, @family, 0, output)
75
+ @queue = output.unpack1("J")
76
+ @memory_properties = keep("\0".b * 1024)
77
+ call(:vkGetPhysicalDeviceMemoryProperties, [P, P], V, @physical, @memory_properties)
78
+ create_target
79
+ rescue StandardError, LoadError
80
+ release
81
+ raise
82
+ end
83
+
84
+ def keep(object)
85
+ @arena << object
86
+ object
87
+ end
88
+ def build(type, **values)
89
+ object = type.malloc(Fiddle::RUBY_FREE)
90
+ object.to_ptr[0, type.size] = "\0" * type.size
91
+ values.each { |name, value| object[name.to_s] = value.is_a?(String) ? Fiddle::Pointer[value] : value }
92
+ keep(object)
93
+ end
94
+ def call(name, arguments, result, *values) = @lib.fn(name, arguments, result, need_gvl: false).call(*values)
95
+ def check(result, name)
96
+ raise Error, "#{name} failed with VkResult #{result}" unless result.zero?
97
+ result
98
+ end
99
+ def create(kind, info)
100
+ output = keep("\0".b * 8)
101
+ check(call("vkCreate#{kind}", [P, P, P, P], I, @device, info, 0, output), "vkCreate#{kind}")
102
+ handle = output.unpack1("Q")
103
+ @resources << ["vkDestroy#{kind}", handle]
104
+ handle
105
+ end
106
+ def allocate_for(kind, handle, required_flags:)
107
+ requirements = keep("\0".b * 24)
108
+ call("vkGet#{kind}MemoryRequirements", [P, Q, P], V, @device, handle, requirements)
109
+ size, _alignment, bits = requirements.unpack("QQI")
110
+ count = @memory_properties.unpack1("I")
111
+ index = count.times.find do |n|
112
+ flags = @memory_properties.byteslice(4 + n * 8, 4).unpack1("I")
113
+ (bits & (1 << n)) != 0 && (flags & required_flags) == required_flags
114
+ end
115
+ raise Error, "Vulkan memory type unavailable (flags #{required_flags})" unless index
116
+ output = keep("\0".b * 8)
117
+ info = build(Types::Allocation, s_type: 5, size: size, type_index: index)
118
+ check(call(:vkAllocateMemory, [P, P, P, P], I, @device, info, 0, output), "vkAllocateMemory")
119
+ memory = output.unpack1("Q")
120
+ @memories << memory
121
+ check(call("vkBind#{kind}Memory", [P, Q, Q, Q], I, @device, handle, memory, 0), "vkBind#{kind}Memory")
122
+ memory
123
+ end
124
+ def create_target
125
+ @image = create("Image", build(Types::Image, s_type: 14, image_type: 1, format: 37,
126
+ width: @width, height: @height, depth: 1, mip_levels: 1, layers: 1, samples: 1, usage: 0x11))
127
+ allocate_for("Image", @image, required_flags: 0)
128
+ @view = create("ImageView", build(Types::ImageView, s_type: 15, image: @image, view_type: 1,
129
+ format: 37, aspect: 1, level_count: 1, layer_count: 1))
130
+ attachment = build(Types::Attachment, format: 37, samples: 1, load: 1, store: 0,
131
+ stencil_load: 2, stencil_store: 1, initial_layout: 0, final_layout: 6)
132
+ reference = keep([0, 2].pack("I2"))
133
+ subpass = build(Types::Subpass, color_count: 1, colors: reference)
134
+ incoming = build(Types::Dependency, source: 0xffffffff, destination: 0, source_stage: 1,
135
+ destination_stage: 0x400, destination_access: 0x100)
136
+ outgoing = build(Types::Dependency, source: 0, destination: 0xffffffff, source_stage: 0x400,
137
+ destination_stage: 0x1000, source_access: 0x100, destination_access: 0x800)
138
+ dependencies = keep(incoming.to_ptr[0, Types::Dependency.size] + outgoing.to_ptr[0, Types::Dependency.size])
139
+ @render_pass = create("RenderPass", build(Types::RenderPass, s_type: 38,
140
+ attachment_count: 1, attachments: attachment, subpass_count: 1, subpasses: subpass,
141
+ dependency_count: 2, dependencies: dependencies))
142
+ @framebuffer = create("Framebuffer", build(Types::Framebuffer, s_type: 37, render_pass: @render_pass,
143
+ attachment_count: 1, attachments: keep([@view].pack("Q")), width: @width, height: @height, layers: 1))
144
+ @buffer = create("Buffer", build(Types::Buffer, s_type: 12, size: @width * @height * 4, usage: 2))
145
+ @read_memory = allocate_for("Buffer", @buffer, required_flags: 6)
146
+ @pool = create("CommandPool", build(Types::CommandPool, s_type: 39, flags: 2, family: @family))
147
+ @layout = create("PipelineLayout", build(Types::Layout, s_type: 30))
148
+ end
149
+
150
+ def shader(bytes)
151
+ unless bytes.is_a?(String) && bytes.bytesize.between?(20, 16_777_216) && (bytes.bytesize % 4).zero? && bytes.unpack1("V") == 0x07230203
152
+ raise ArgumentError, "expected a SPIR-V word stream"
153
+ end
154
+ create("ShaderModule", build(Types::Shader, s_type: 16, code_size: bytes.bytesize, code: keep(bytes)))
155
+ end
156
+ def pipeline(vertex_spirv, fragment_spirv)
157
+ vertex = build(Types::Stage, s_type: 18, stage: 1, shader: shader(vertex_spirv), name: keep("main\0"))
158
+ fragment = build(Types::Stage, s_type: 18, stage: 16, shader: shader(fragment_spirv), name: keep("main\0"))
159
+ stages = keep(vertex.to_ptr[0, Types::Stage.size] + fragment.to_ptr[0, Types::Stage.size])
160
+ vertex_input = build(Types::VertexInput, s_type: 19)
161
+ assembly = build(Types::Assembly, s_type: 20, topology: 3)
162
+ viewport = keep([0, 0, @width, @height, 0, 1].pack("f6"))
163
+ scissor = keep([0, 0, @width, @height].pack("i2I2"))
164
+ viewport_state = build(Types::Viewport, s_type: 22, viewport_count: 1, viewports: viewport, scissor_count: 1, scissors: scissor)
165
+ raster = build(Types::Rasterization, s_type: 23, front_face: 1, line_width: 1.0)
166
+ multisample = build(Types::Multisample, s_type: 24, samples: 1)
167
+ blend_attachment = keep([0, 1, 0, 0, 1, 0, 0, 15].pack("I8"))
168
+ blend = build(Types::Blend, s_type: 26, attachment_count: 1, attachments: blend_attachment)
169
+ info = build(Types::Pipeline, s_type: 28, stage_count: 2, stages: stages,
170
+ vertex_input: vertex_input, assembly: assembly, viewport: viewport_state,
171
+ rasterization: raster, multisample: multisample, blend: blend, layout: @layout,
172
+ render_pass: @render_pass, base_index: -1)
173
+ output = keep("\0".b * 8)
174
+ check(call(:vkCreateGraphicsPipelines, [P, Q, U, P, P, P], I, @device, 0, 1, info, 0, output), "vkCreateGraphicsPipelines")
175
+ result = output.unpack1("Q")
176
+ @resources << [:vkDestroyPipeline, result]
177
+ result
178
+ end
179
+
180
+ def render_triangle(vertex_spirv:, fragment_spirv:)
181
+ raise IOError, "Vulkan device released" unless @device
182
+ # ponytail: validation submissions retain resources until release;
183
+ # recycle pipelines/command buffers before using this as a frame loop.
184
+ pipeline_handle = pipeline(vertex_spirv, fragment_spirv)
185
+ output = keep("\0".b * 8)
186
+ info = build(Types::CommandAllocation, s_type: 40, pool: @pool, level: 0, count: 1)
187
+ check(call(:vkAllocateCommandBuffers, [P, P, P], I, @device, info, output), "vkAllocateCommandBuffers")
188
+ command = output.unpack1("J")
189
+ check(call(:vkBeginCommandBuffer, [P, P], I, command, build(Types::CommandBegin, s_type: 42, flags: 1)), "vkBeginCommandBuffer")
190
+ render = build(Types::RenderBegin, s_type: 43, render_pass: @render_pass, framebuffer: @framebuffer,
191
+ width: @width, height: @height, clear_count: 1, clears: keep([0, 0, 0, 1].pack("f4")))
192
+ call(:vkCmdBeginRenderPass, [P, P, I], V, command, render, 0)
193
+ call(:vkCmdBindPipeline, [P, I, Q], V, command, 0, pipeline_handle)
194
+ call(:vkCmdDraw, [P, U, U, U, U], V, command, 3, 1, 0, 0)
195
+ call(:vkCmdEndRenderPass, [P], V, command)
196
+ region = keep([0, 0, 0, 1, 0, 0, 1, 0, 0, 0, @width, @height, 1].pack("Q I2 I4 i3 I3"))
197
+ call(:vkCmdCopyImageToBuffer, [P, Q, I, Q, U, P], V, command, @image, 6, @buffer, 1, region)
198
+ barrier = build(Types::MemoryBarrier, s_type: 46, source_access: 0x1000, destination_access: 0x2000)
199
+ call(:vkCmdPipelineBarrier, [P, U, U, U, U, P, U, P, U, P], V, command, 0x1000, 0x4000, 0, 1, barrier, 0, 0, 0, 0)
200
+ check(call(:vkEndCommandBuffer, [P], I, command), "vkEndCommandBuffer")
201
+ fence = create("Fence", build(Types::Fence, s_type: 8))
202
+ submit = build(Types::Submit, s_type: 4, command_count: 1, commands: keep([command].pack("J")))
203
+ check(call(:vkQueueSubmit, [P, U, P, Q], I, @queue, 1, submit, fence), "vkQueueSubmit")
204
+ check(call(:vkWaitForFences, [P, U, P, U, Q], I, @device, 1, keep([fence].pack("Q")), 1, 10_000_000_000), "vkWaitForFences")
205
+ check(call(:vkMapMemory, [P, Q, Q, Q, U, P], I, @device, @read_memory, 0, @width * @height * 4, 0, output), "vkMapMemory")
206
+ begin
207
+ @pixels = Fiddle::Pointer.new(output.unpack1("J"))[0, @width * @height * 4]
208
+ ensure
209
+ call(:vkUnmapMemory, [P, Q], V, @device, @read_memory)
210
+ end
211
+ @pixels
212
+ end
213
+ def write_png(path)
214
+ raise Error, "render a Vulkan frame before capture" unless @pixels
215
+ PNG.write(path, @width, @height, @pixels)
216
+ end
217
+ def release
218
+ if @device
219
+ call(:vkDeviceWaitIdle, [P], I, @device)
220
+ @resources.reverse_each { |name, handle| call(name, [P, Q, P], V, @device, handle, 0) }
221
+ @memories.each { |memory| call(:vkFreeMemory, [P, Q, P], V, @device, memory, 0) }
222
+ call(:vkDestroyDevice, [P, P], V, @device, 0)
223
+ @device = nil
224
+ end
225
+ call(:vkDestroyInstance, [P, P], V, @instance, 0) if @instance
226
+ @instance = nil
227
+ @resources&.clear
228
+ @memories&.clear
229
+ @arena&.clear
230
+ end
231
+ end
232
+ end
233
+ end
data/lib/zaniah/gpu.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gpu/buffer"
4
+ require_relative "gpu/texture"
5
+ require_relative "gpu/pipeline"
6
+ require_relative "gpu/frame_encoder"
7
+ require_relative "gpu/software"
8
+
9
+ module Zaniah
10
+ module GPU
11
+ def self.create(window = nil, backend: :software, width: nil, height: nil)
12
+ return window.create_device(backend) if window && backend != :software
13
+ raise ArgumentError, "backend requires a native window" unless backend == :software
14
+ size = window&.content_size
15
+ Software.new(width || size&.width || 800, height || size&.height || 600)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ HSLA = Data.define(:h, :s, :l, :a) do
5
+ def to_rgba
6
+ chroma = (1 - (2 * l - 1).abs) * s
7
+ phase = (h % 1.0) * 6
8
+ x = chroma * (1 - (phase % 2 - 1).abs)
9
+ values = [[chroma, x, 0], [x, chroma, 0], [0, chroma, x],
10
+ [0, x, chroma], [x, 0, chroma], [chroma, 0, x]][phase.floor]
11
+ Color.new(*values.map { |value| value + l - chroma / 2.0 }, a)
12
+ end
13
+ end
14
+ end