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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +70 -0
- data/assets/fonts/Abel-Regular.ttf +0 -0
- data/assets/fonts/OFL.txt +93 -0
- data/docs/adr/000-template.md +24 -0
- data/docs/adr/001-native-gpu-backends.md +36 -0
- data/docs/adr/002-objective-c-aggregates.md +34 -0
- data/docs/adr/003-fiddle-call-boundary.md +33 -0
- data/docs/adr/README.md +10 -0
- data/docs/native.md +66 -0
- data/docs/process_pool.md +67 -0
- data/docs/text.md +90 -0
- data/docs/vector_and_list.md +52 -0
- data/examples/ffi_bench.rb +16 -0
- data/examples/linux_smoke.rb +53 -0
- data/examples/mac_event_check.rb +61 -0
- data/examples/mac_poll_bench.rb +24 -0
- data/examples/metal_state_check.rb +72 -0
- data/examples/native_bench.rb +18 -0
- data/examples/native_drop.rb +82 -0
- data/examples/native_font.rb +15 -0
- data/examples/native_ime.rb +50 -0
- data/examples/native_smoke.rb +75 -0
- data/examples/native_text_bench.rb +83 -0
- data/examples/native_watch.rb +25 -0
- data/examples/vulkan_triangle.rb +20 -0
- data/examples/word_count_worker.rb +8 -0
- data/lib/zaniah/anchored.rb +10 -0
- data/lib/zaniah/app.rb +130 -0
- data/lib/zaniah/bounds.rb +22 -0
- data/lib/zaniah/canvas.rb +15 -0
- data/lib/zaniah/color.rb +35 -0
- data/lib/zaniah/configuration.rb +5 -0
- data/lib/zaniah/corners.rb +7 -0
- data/lib/zaniah/data_compat.rb +25 -0
- data/lib/zaniah/device_pixels.rb +7 -0
- data/lib/zaniah/div.rb +5 -0
- data/lib/zaniah/edges.rb +7 -0
- data/lib/zaniah/element.rb +106 -0
- data/lib/zaniah/entity.rb +9 -0
- data/lib/zaniah/entity_context.rb +17 -0
- data/lib/zaniah/error.rb +5 -0
- data/lib/zaniah/ffi/com.rb +24 -0
- data/lib/zaniah/ffi/library.rb +29 -0
- data/lib/zaniah/ffi/objc.rb +86 -0
- data/lib/zaniah/ffi/struct/signature.rb +46 -0
- data/lib/zaniah/ffi/struct.rb +61 -0
- data/lib/zaniah/ffi/wayland.rb +140 -0
- data/lib/zaniah/frame_context.rb +14 -0
- data/lib/zaniah/geometry.rb +16 -0
- data/lib/zaniah/gpu/buffer.rb +23 -0
- data/lib/zaniah/gpu/frame_encoder.rb +43 -0
- data/lib/zaniah/gpu/instance_packing.rb +53 -0
- data/lib/zaniah/gpu/metal.rb +209 -0
- data/lib/zaniah/gpu/open_gl.rb +210 -0
- data/lib/zaniah/gpu/pipeline.rb +7 -0
- data/lib/zaniah/gpu/software.rb +126 -0
- data/lib/zaniah/gpu/texture.rb +33 -0
- data/lib/zaniah/gpu/vulkan.rb +233 -0
- data/lib/zaniah/gpu.rb +18 -0
- data/lib/zaniah/hsla.rb +14 -0
- data/lib/zaniah/image.rb +19 -0
- data/lib/zaniah/input/action_registry.rb +12 -0
- data/lib/zaniah/input/composition.rb +7 -0
- data/lib/zaniah/input/context_predicate.rb +87 -0
- data/lib/zaniah/input/dispatcher.rb +59 -0
- data/lib/zaniah/input/file_drop.rb +7 -0
- data/lib/zaniah/input/focus_handle.rb +24 -0
- data/lib/zaniah/input/key_down.rb +7 -0
- data/lib/zaniah/input/key_up.rb +7 -0
- data/lib/zaniah/input/keymap/binding.rb +9 -0
- data/lib/zaniah/input/keymap.rb +55 -0
- data/lib/zaniah/input/keystroke.rb +20 -0
- data/lib/zaniah/input/mouse_down.rb +7 -0
- data/lib/zaniah/input/mouse_move.rb +7 -0
- data/lib/zaniah/input/mouse_up.rb +7 -0
- data/lib/zaniah/input/scroll_wheel.rb +7 -0
- data/lib/zaniah/input/text_input.rb +7 -0
- data/lib/zaniah/input.rb +17 -0
- data/lib/zaniah/layout/available_space.rb +11 -0
- data/lib/zaniah/layout/engine.rb +214 -0
- data/lib/zaniah/layout/node.rb +36 -0
- data/lib/zaniah/layout/style.rb +19 -0
- data/lib/zaniah/layout/tree.rb +16 -0
- data/lib/zaniah/layout.rb +11 -0
- data/lib/zaniah/length.rb +14 -0
- data/lib/zaniah/length_units.rb +9 -0
- data/lib/zaniah/list/height_index.rb +68 -0
- data/lib/zaniah/list.rb +101 -0
- data/lib/zaniah/overlay.rb +10 -0
- data/lib/zaniah/platform/display.rb +7 -0
- data/lib/zaniah/platform/file_event.rb +7 -0
- data/lib/zaniah/platform/headless/window.rb +185 -0
- data/lib/zaniah/platform/linux/appearance_aware.rb +60 -0
- data/lib/zaniah/platform/linux/displays.rb +55 -0
- data/lib/zaniah/platform/linux/free_type.rb +80 -0
- data/lib/zaniah/platform/linux/watcher.rb +70 -0
- data/lib/zaniah/platform/linux/wayland_display_discovery.rb +60 -0
- data/lib/zaniah/platform/linux/wayland_window.rb +369 -0
- data/lib/zaniah/platform/linux/window.rb +418 -0
- data/lib/zaniah/platform/linux.rb +3 -0
- data/lib/zaniah/platform/mac/app.rb +67 -0
- data/lib/zaniah/platform/mac/core_text.rb +94 -0
- data/lib/zaniah/platform/mac/displays.rb +23 -0
- data/lib/zaniah/platform/mac/watcher.rb +63 -0
- data/lib/zaniah/platform/mac/window.rb +298 -0
- data/lib/zaniah/platform/mac.rb +19 -0
- data/lib/zaniah/platform/tui/input_decoder.rb +145 -0
- data/lib/zaniah/platform/tui/text_renderer.rb +40 -0
- data/lib/zaniah/platform/tui/window.rb +91 -0
- data/lib/zaniah/platform/windows/terminal.rb +127 -0
- data/lib/zaniah/platform/windows/watcher.rb +91 -0
- data/lib/zaniah/platform/windows/window.rb +407 -0
- data/lib/zaniah/platform/windows.rb +3 -0
- data/lib/zaniah/platform.rb +34 -0
- data/lib/zaniah/png.rb +118 -0
- data/lib/zaniah/point.rb +8 -0
- data/lib/zaniah/process_pool/worker.rb +7 -0
- data/lib/zaniah/process_pool.rb +239 -0
- data/lib/zaniah/process_wire.rb +127 -0
- data/lib/zaniah/quad_packer.rb +15 -0
- data/lib/zaniah/scene/sprite_batch.rb +7 -0
- data/lib/zaniah/scene.rb +167 -0
- data/lib/zaniah/scene_renderer.rb +8 -0
- data/lib/zaniah/size.rb +5 -0
- data/lib/zaniah/subscription.rb +14 -0
- data/lib/zaniah/svg/path.rb +134 -0
- data/lib/zaniah/svg/rasterizable.rb +186 -0
- data/lib/zaniah/svg.rb +295 -0
- data/lib/zaniah/task/cancelled.rb +7 -0
- data/lib/zaniah/task/timeout.rb +7 -0
- data/lib/zaniah/task.rb +51 -0
- data/lib/zaniah/task_executor.rb +105 -0
- data/lib/zaniah/text.rb +24 -0
- data/lib/zaniah/text_system/atlas/entry.rb +9 -0
- data/lib/zaniah/text_system/atlas.rb +102 -0
- data/lib/zaniah/text_system/atlas_cache.rb +146 -0
- data/lib/zaniah/text_system/font_db/face.rb +9 -0
- data/lib/zaniah/text_system/font_db.rb +193 -0
- data/lib/zaniah/text_system/glyph.rb +7 -0
- data/lib/zaniah/text_system/line_layout.rb +20 -0
- data/lib/zaniah/text_system/renderer.rb +196 -0
- data/lib/zaniah/text_system/shaper.rb +430 -0
- data/lib/zaniah/text_system/typesetter/copy_error.rb +9 -0
- data/lib/zaniah/text_system/typesetter.rb +168 -0
- data/lib/zaniah/text_system.rb +9 -0
- data/lib/zaniah/unicode.rb +13 -0
- data/lib/zaniah/uniform_list.rb +30 -0
- data/lib/zaniah/version.rb +5 -0
- data/lib/zaniah/view.rb +13 -0
- data/lib/zaniah.rb +29 -0
- data/sig/app.rbs +80 -0
- data/sig/elements.rbs +129 -0
- data/sig/gpu.rbs +108 -0
- data/sig/input.rbs +114 -0
- data/sig/layout.rbs +53 -0
- data/sig/native.rbs +88 -0
- data/sig/platform.rbs +94 -0
- data/sig/scene.rbs +52 -0
- data/sig/shaper.rbs +8 -0
- data/sig/text.rbs +137 -0
- data/sig/zaniah.rbs +120 -0
- data/tools/check_dependencies.rb +52 -0
- 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,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
|
data/lib/zaniah/hsla.rb
ADDED
|
@@ -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
|