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,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "library"
4
+
5
+ module Zaniah
6
+ module FFI
7
+ # Runtime protocol metadata replaces generated C bindings. Core interfaces
8
+ # are exported by libwayland-client; these extensions use protocol v1.
9
+ class Wayland
10
+ P, I, U, V = Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_UINT, Fiddle::TYPE_VOID
11
+ PROTOCOLS = {
12
+ "xdg_wm_base" => [[
13
+ ["destroy", ""], ["create_positioner", "n", "xdg_positioner"],
14
+ ["get_xdg_surface", "no", "xdg_surface", "wl_surface"], ["pong", "u"]
15
+ ], [["ping", "u"]]],
16
+ "xdg_positioner" => [[], []], "xdg_popup" => [[], []],
17
+ "xdg_surface" => [[
18
+ ["destroy", ""], ["get_toplevel", "n", "xdg_toplevel"],
19
+ ["get_popup", "n?oo", "xdg_popup", "xdg_surface", "xdg_positioner"],
20
+ ["set_window_geometry", "iiii"], ["ack_configure", "u"]
21
+ ], [["configure", "u"]]],
22
+ "xdg_toplevel" => [[
23
+ ["destroy", ""], ["set_parent", "?o", "xdg_toplevel"], ["set_title", "s"], ["set_app_id", "s"],
24
+ ["show_window_menu", "ouii", "wl_seat"], ["move", "ou", "wl_seat"], ["resize", "ouu", "wl_seat"],
25
+ ["set_max_size", "ii"], ["set_min_size", "ii"], ["set_maximized", ""], ["unset_maximized", ""],
26
+ ["set_fullscreen", "?o", "wl_output"], ["unset_fullscreen", ""], ["set_minimized", ""]
27
+ ], [["configure", "iia"], ["close", ""]]],
28
+ "zwp_text_input_manager_v3" => [[["destroy", ""], ["get_text_input", "no", "zwp_text_input_v3", "wl_seat"]], []],
29
+ "zwp_text_input_v3" => [[
30
+ ["destroy", ""], ["enable", ""], ["disable", ""], ["set_surrounding_text", "sii"],
31
+ ["set_text_change_cause", "u"], ["set_content_type", "uu"], ["set_cursor_rectangle", "iiii"], ["commit", ""]
32
+ ], [["enter", "o", "wl_surface"], ["leave", "o", "wl_surface"], ["preedit_string", "?sii"],
33
+ ["commit_string", "?s"], ["delete_surrounding_text", "uu"], ["done", "u"]]],
34
+ "zxdg_decoration_manager_v1" => [[["destroy", ""], ["get_toplevel_decoration", "no", "zxdg_toplevel_decoration_v1", "xdg_toplevel"]], []],
35
+ "zxdg_toplevel_decoration_v1" => [[["destroy", ""], ["set_mode", "u"], ["unset_mode", ""]], [["configure", "u"]]],
36
+ "zxdg_output_manager_v1" => [[["destroy", ""], ["get_xdg_output", "no", "zxdg_output_v1", "wl_output"]], []],
37
+ "zxdg_output_v1" => [[["destroy", ""]], [["logical_position", "ii"], ["logical_size", "ii"], ["done", ""], ["name", "2s"], ["description", "2s"]]]
38
+ }.freeze
39
+ PROTOCOL_VERSIONS = {"zxdg_output_manager_v1" => 3, "zxdg_output_v1" => 3}.freeze
40
+ attr_reader :library, :display
41
+
42
+ def initialize
43
+ @library = Library.new("libwayland-client.so.0")
44
+ @interfaces, @keep, @listeners, @objects = {}, [], [], []
45
+ PROTOCOLS.each_key { |name| @interfaces[name] = Fiddle::Pointer.malloc(40, Fiddle::RUBY_FREE) }
46
+ PROTOCOLS.each do |name, (requests, events)|
47
+ bytes = [keep_string(name), PROTOCOL_VERSIONS.fetch(name, 1), requests.length, messages(requests), events.length, messages(events)].pack("JiiJi x4J")
48
+ @interfaces[name][0, 40] = bytes
49
+ end
50
+ @display = @library.fn(:wl_display_connect, [P], P).call(0)
51
+ raise Zaniah::Error, "cannot connect to WAYLAND_DISPLAY" if @display.null?
52
+ end
53
+ def interface(name) = @interfaces[name] ||= Fiddle::Pointer.new(@library.handle["#{name}_interface"])
54
+ def keep_string(text)
55
+ string = text + "\0"
56
+ @keep << string
57
+ Fiddle::Pointer[string].to_i
58
+ end
59
+ def messages(definitions)
60
+ return 0 if definitions.empty?
61
+ data = definitions.map do |name, signature, *interfaces|
62
+ count = signature.delete("?0123456789").length
63
+ types = Array.new(count, 0)
64
+ interfaces.each_with_index { |type, i| types[i] = interface(type).to_i if type }
65
+ packed = types.pack("J*")
66
+ @keep << packed
67
+ [keep_string(name), keep_string(signature), Fiddle::Pointer[packed].to_i].pack("J3")
68
+ end.join
69
+ @keep << data
70
+ Fiddle::Pointer[data].to_i
71
+ end
72
+ def version(proxy) = @library.fn(:wl_proxy_get_version, [P], U).call(proxy)
73
+ def request(proxy, opcode, *arguments, new_interface: nil, version: nil, destroy: false)
74
+ strings = []
75
+ values = arguments.map do |value|
76
+ if value.is_a?(String)
77
+ strings << value + "\0"
78
+ Fiddle::Pointer[strings.last].to_i
79
+ else
80
+ value.to_i & 0xffffffffffffffff
81
+ end
82
+ end.pack("Q*")
83
+ object = @library.fn(:wl_proxy_marshal_array_flags, [P, U, P, U, U, P], P).call(proxy, opcode,
84
+ new_interface ? interface(new_interface) : 0, version || self.version(proxy), destroy ? 1 : 0, values.empty? ? 0 : values)
85
+ @objects << object if new_interface && !object.null?
86
+ @objects.delete(proxy) if destroy
87
+ object
88
+ end
89
+ def listen(proxy, events)
90
+ @objects << proxy unless @objects.include?(proxy)
91
+ callbacks = events.map do |types, block|
92
+ Fiddle::Closure::BlockCaller.new(V, [P, P, *types]) do |_data, object, *arguments|
93
+ begin
94
+ block&.call(object, *arguments)
95
+ rescue StandardError => error
96
+ @error = error
97
+ end
98
+ end
99
+ end
100
+ table = callbacks.map(&:to_i).pack("J*")
101
+ result = @library.fn(:wl_proxy_add_listener, [P, P, P], I).call(proxy, table, 0)
102
+ raise Zaniah::Error, "Wayland listener registration failed" unless result.zero?
103
+ @listeners << [table, callbacks]
104
+ end
105
+ def roundtrip
106
+ result = @library.fn(:wl_display_roundtrip, [P], I).call(@display)
107
+ raise @error if @error
108
+ raise Zaniah::Error, "Wayland compositor disconnected" if result.negative?
109
+ end
110
+ def poll(timeout: 0)
111
+ until @library.fn(:wl_display_prepare_read, [P], I).call(@display).zero?
112
+ result = @library.fn(:wl_display_dispatch_pending, [P], I).call(@display)
113
+ raise @error if @error
114
+ raise Zaniah::Error, "Wayland compositor disconnected" if result.negative?
115
+ end
116
+ @library.fn(:wl_display_flush, [P], I).call(@display)
117
+ fd = @library.fn(:wl_display_get_fd, [P], I).call(@display)
118
+ ready = IO.select([IO.for_fd(fd, autoclose: false)], nil, nil, timeout)
119
+ if ready
120
+ result = @library.fn(:wl_display_read_events, [P], I).call(@display)
121
+ raise Zaniah::Error, "Wayland compositor disconnected" if result.negative?
122
+ else
123
+ @library.fn(:wl_display_cancel_read, [P], V).call(@display)
124
+ end
125
+ @library.fn(:wl_display_dispatch_pending, [P], I).call(@display)
126
+ raise @error if @error
127
+ end
128
+ def destroy(proxy)
129
+ return unless proxy && !proxy.to_i.zero?
130
+ @library.fn(:wl_proxy_destroy, [P], V).call(proxy)
131
+ @objects.delete(proxy)
132
+ end
133
+ def close
134
+ @objects.reverse_each { |object| @library.fn(:wl_proxy_destroy, [P], V).call(object) }
135
+ @objects.clear
136
+ @library.fn(:wl_display_disconnect, [P], V).call(@display)
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ class FrameContext
5
+ attr_reader :window, :scene, :dispatcher, :text_system
6
+
7
+ def initialize(window)
8
+ @window, @scene = window, window.scene
9
+ @dispatcher, @text_system = window.dispatcher, window.text_system
10
+ end
11
+
12
+ def state(key, &initial) = @window.element_state(key, &initial)
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "point"
4
+ require_relative "size"
5
+ require_relative "edges"
6
+ require_relative "corners"
7
+ require_relative "bounds"
8
+ require_relative "length"
9
+ require_relative "device_pixels"
10
+ require_relative "length_units"
11
+ require_relative "color"
12
+ require_relative "hsla"
13
+
14
+ module Zaniah
15
+ extend LengthUnits
16
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ module GPU
5
+ class Buffer
6
+ attr_reader :size, :usage, :data
7
+
8
+ def initialize(size, usage: :vertex, mutable: true)
9
+ raise ArgumentError, "invalid buffer size" unless size.is_a?(Integer) && size >= 0
10
+ @size, @usage, @mutable = size, usage, mutable
11
+ @data = "\0".b * size
12
+ end
13
+
14
+ def write(bytes, offset: 0)
15
+ raise ArgumentError, "buffer write out of bounds" if offset.negative? || offset + bytes.bytesize > @size
16
+ @data[offset, bytes.bytesize] = bytes
17
+ self
18
+ end
19
+
20
+ def release = @data.clear
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ module GPU
5
+ class FrameEncoder
6
+ def initialize(device, clear)
7
+ @device, @clear, @scene, @textures = device, clear, Scene.new, {}
8
+ end
9
+
10
+ def set_pipeline(pipeline) = @pipeline = pipeline
11
+ def set_uniforms(**values) = @uniforms = values
12
+ def set_texture(slot, texture) = @textures[slot] = texture
13
+ def set_scissor(bounds) = @scissor = bounds
14
+
15
+ def draw_instanced(vertex_count:, instance_buffer:, instance_count:)
16
+ raise ArgumentError, "expected quad vertices" unless vertex_count == 4
17
+ raise ArgumentError, "pipeline not set" unless @pipeline
18
+ draw = lambda do
19
+ values = instance_buffer.data.unpack("f*")
20
+ if @pipeline.shader == :quad
21
+ raise ArgumentError, "instance buffer too small" if values.length < instance_count * Scene::QUAD_STRIDE
22
+ instance_count.times do |index|
23
+ quad = values.slice(index * Scene::QUAD_STRIDE, Scene::QUAD_STRIDE)
24
+ @scene.quad(*quad.first(4), color: quad[4, 4], radius: quad[8, 4], border_width: quad[12], border_color: quad[13, 4])
25
+ end
26
+ elsif [:mono_sprite, :poly_sprite].include?(@pipeline.shader)
27
+ raise ArgumentError, "instance buffer too small" if values.length < instance_count * 12
28
+ texture = @textures.fetch(0)
29
+ instance_count.times do |index|
30
+ sprite = values.slice(index * 12, 12)
31
+ @scene.sprite(*sprite.first(4), texture: texture, color: sprite[4, 4], source: Bounds.new(*sprite[8, 4]))
32
+ end
33
+ else
34
+ raise ArgumentError, "unsupported instanced shader #{@pipeline.shader}"
35
+ end
36
+ end
37
+ @scissor ? @scene.clip(@scissor, &draw) : draw.call
38
+ end
39
+
40
+ def present = @device.render(@scene, clear: @clear)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zaniah
4
+ module GPU
5
+ # Consecutive commands only are batched, preserving alpha and layer order.
6
+ module InstancePacking
7
+ STRIDE = 24
8
+ def self.pack(scene)
9
+ data, batches, bytes = [], [], "".b
10
+ scene.each_command do |kind, offset, clip|
11
+ texture = nil
12
+ first = bytes.bytesize / (STRIDE * 4) + data.length / STRIDE
13
+ count = 1
14
+ case kind
15
+ when :quad
16
+ q, i = scene.quads, offset
17
+ data.push(q[i], q[i+1], q[i+2], q[i+3], q[i+4], q[i+5], q[i+6], q[i+7],
18
+ q[i+8], q[i+9], q[i+10], q[i+11], q[i+13], q[i+14], q[i+15], q[i+16], q[i+12], 0, 0, 0, 0, 0, 0, 0)
19
+ when :sprite
20
+ s, i = scene.sprite_data, offset
21
+ texture = scene.textures.fetch(s[i+8])
22
+ data.push(s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5], s[i+6], s[i+7],
23
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, texture.format == :r8 ? 1 : 2, 0, 0,
24
+ s[i+9].to_f / texture.width, s[i+10].to_f / texture.height,
25
+ s[i+11].to_f / texture.width, s[i+12].to_f / texture.height)
26
+ when :sprite_batch
27
+ unless data.empty?
28
+ bytes << data.pack("f*")
29
+ data.clear
30
+ end
31
+ batch = scene.sprite_batches.fetch(offset)
32
+ bytes << batch.bytes
33
+ kind, texture = :sprite, batch.texture
34
+ count = batch.bytes.bytesize / (STRIDE * 4)
35
+ when :triangle
36
+ p, i = scene.paths, offset
37
+ data.push(p[i], p[i+1], p[i+2], p[i+3], p[i+6], p[i+7], p[i+8], p[i+9],
38
+ p[i+4], p[i+5], 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0)
39
+ else raise ArgumentError, "unsupported primitive #{kind}"
40
+ end
41
+ previous = batches.last&.first
42
+ if previous && previous[0] == kind && previous[1].equal?(texture) && previous[2] == clip
43
+ batches.last[2] += count
44
+ else
45
+ batches << [[kind, texture, clip], first, count]
46
+ end
47
+ end
48
+ bytes << data.pack("f*") unless data.empty?
49
+ [bytes, batches]
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "instance_packing"
4
+ require_relative "../ffi/objc"
5
+
6
+ module Zaniah
7
+ module GPU
8
+ class Metal
9
+ O = FFI::ObjC
10
+ LIB = FFI::Library.new("/System/Library/Frameworks/Metal.framework/Metal")
11
+ POINTER_ARGS = [:pointer].freeze
12
+ ULONG_ARGS = [:ulong].freeze
13
+ BUFFER_ARGS = [:pointer, :ulong, :ulong].freeze
14
+ TEXTURE_ARGS = [:pointer, :ulong].freeze
15
+ DRAW_ARGS = [:ulong, :ulong, :ulong, :ulong].freeze
16
+ CLEAR_ARGS = [:clear_color].freeze
17
+ attr_reader :width, :height, :draw_calls
18
+ SOURCE = <<~MSL.freeze
19
+ #include <metal_stdlib>
20
+ using namespace metal;
21
+ struct Vertex { float4 position [[position]]; float2 local; float2 size;
22
+ float4 color; float4 radii; float4 border; float4 info; float2 uv; };
23
+ vertex Vertex zaniah_vertex(uint v [[vertex_id]], uint instance [[instance_id]],
24
+ const device float *data [[buffer(0)]], constant float2 &viewport [[buffer(1)]]) {
25
+ const device float *p = data + instance * 24;
26
+ float2 corners[4] = {float2(0,0),float2(1,0),float2(0,1),float2(1,1)};
27
+ float2 corner = corners[v], size = float2(p[2],p[3]);
28
+ float2 position = float2(p[0],p[1]) + corner * size;
29
+ if (p[17] == 3) position = v == 0 ? float2(p[0],p[1]) : v == 1 ? float2(p[2],p[3]) : float2(p[8],p[9]);
30
+ Vertex out;
31
+ out.position = float4(position.x / viewport.x * 2 - 1, 1 - position.y / viewport.y * 2, 0, 1);
32
+ out.local = corner * size; out.size = size;
33
+ out.color = float4(p[4],p[5],p[6],p[7]); out.radii = float4(p[8],p[9],p[10],p[11]);
34
+ out.border = float4(p[12],p[13],p[14],p[15]); out.info = float4(p[16],p[17],p[18],p[19]);
35
+ out.uv = float2(p[20],p[21]) + corner * float2(p[22],p[23]); return out;
36
+ }
37
+ fragment float4 zaniah_fragment(Vertex in [[stage_in]], texture2d<float> atlas [[texture(0)]]) {
38
+ float4 color = in.color;
39
+ if (in.info.y == 0) {
40
+ float radius = in.local.y < in.size.y/2 ? (in.local.x < in.size.x/2 ? in.radii.x : in.radii.y) : (in.local.x < in.size.x/2 ? in.radii.w : in.radii.z);
41
+ radius = clamp(radius, 0.0f, min(in.size.x,in.size.y)/2);
42
+ float2 q = abs(in.local - in.size/2) - in.size/2 + radius;
43
+ float distance = length(max(q,0.0f)) + min(max(q.x,q.y),0.0f) - radius;
44
+ float coverage = clamp(0.5f - distance, 0.0f, 1.0f);
45
+ if (in.info.x > 0 && distance >= -in.info.x) color = in.border;
46
+ color.a *= coverage;
47
+ } else if (in.info.y == 1 || in.info.y == 2) {
48
+ constexpr sampler texture_sampler(coord::normalized, address::clamp_to_edge, filter::nearest);
49
+ float4 sample = atlas.sample(texture_sampler,in.uv);
50
+ if (in.info.y == 1) color.a *= sample.r; else color *= sample;
51
+ }
52
+ return float4(color.rgb * color.a, color.a);
53
+ }
54
+ MSL
55
+
56
+ def initialize(window)
57
+ @window, @textures = window, {}
58
+ @device = LIB.fn(:MTLCreateSystemDefaultDevice, [], Fiddle::TYPE_VOIDP).call.to_i
59
+ raise Error, "Metal device unavailable" if @device.zero?
60
+ @queue = O.send(@device, "newCommandQueue")
61
+ O.send(window.layer, "setDevice:", @device, args: [:pointer], result: :void)
62
+ O.send(window.layer, "setPixelFormat:", 80, args: [:ulong], result: :void)
63
+ O.send(window.layer, "setFramebufferOnly:", 0, args: [:bool], result: :void)
64
+ @pipeline = compile_pipeline(SOURCE)
65
+ # Metal render-pass descriptors are reusable; only the drawable target
66
+ # and a changed clear color need updating between encoders.
67
+ @render_pass = O.new("MTLRenderPassDescriptor")
68
+ @color_attachment = O.send(O.send(@render_pass, "colorAttachments"), "objectAtIndexedSubscript:", 0, args: ULONG_ARGS)
69
+ O.send(@color_attachment, "setLoadAction:", 2, args: ULONG_ARGS, result: :void)
70
+ O.send(@color_attachment, "setStoreAction:", 1, args: ULONG_ARGS, result: :void)
71
+ @clear_input = Object.new
72
+ @scissor = Array.new(4, 0)
73
+ @scissor_signature = FFI::Struct::Signature.new([:pointer, :pointer, :scissor], :void)
74
+ @scissor_selector = O.selector("setScissorRect:")
75
+ @white = create_texture(1, 1, data: "\xff".b * 4)
76
+ resize(window.content_size.width, window.content_size.height)
77
+ end
78
+
79
+ def compile_pipeline(source)
80
+ error = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, Fiddle::RUBY_FREE)
81
+ error[0, Fiddle::SIZEOF_VOIDP] = [0].pack("J")
82
+ library = O.send(@device, "newLibraryWithSource:options:error:", O.string(source), 0, error,
83
+ args: [:pointer] * 3)
84
+ raise Error, native_error(error, "Metal shader compilation failed") if library.zero?
85
+ vertex = O.send(library, "newFunctionWithName:", O.string("zaniah_vertex"), args: [:pointer])
86
+ fragment = O.send(library, "newFunctionWithName:", O.string("zaniah_fragment"), args: [:pointer])
87
+ descriptor = O.new("MTLRenderPipelineDescriptor")
88
+ O.send(descriptor, "setVertexFunction:", vertex, args: [:pointer], result: :void)
89
+ O.send(descriptor, "setFragmentFunction:", fragment, args: [:pointer], result: :void)
90
+ attachment = O.send(O.send(descriptor, "colorAttachments"), "objectAtIndexedSubscript:", 0, args: [:ulong])
91
+ O.send(attachment, "setPixelFormat:", 80, args: [:ulong], result: :void)
92
+ O.send(attachment, "setBlendingEnabled:", 1, args: [:bool], result: :void)
93
+ {"setSourceRGBBlendFactor:" => 1, "setSourceAlphaBlendFactor:" => 1,
94
+ "setDestinationRGBBlendFactor:" => 5, "setDestinationAlphaBlendFactor:" => 5}.each do |name, value|
95
+ O.send(attachment, name, value, args: [:ulong], result: :void)
96
+ end
97
+ pipeline = O.send(@device, "newRenderPipelineStateWithDescriptor:error:", descriptor, error, args: [:pointer, :pointer])
98
+ raise Error, native_error(error, "Metal pipeline creation failed") if pipeline.zero?
99
+ pipeline
100
+ ensure
101
+ [descriptor, vertex, fragment, library].compact.each { |object| O.release(object) }
102
+ end
103
+
104
+ def resize(width, height)
105
+ raise ArgumentError, "invalid viewport" unless width.positive? && height.positive?
106
+ @width, @height = width.to_i, height.to_i
107
+ @scale = @window.scale_factor
108
+ @pixel_width, @pixel_height = (width * @scale).round, (height * @scale).round
109
+ @viewport_bytes = [@width, @height].pack("f2")
110
+ @viewport_pointer = Fiddle::Pointer[@viewport_bytes]
111
+ @full_scissor = [0, 0, @pixel_width, @pixel_height].freeze
112
+ O.send(@window.layer, "setContentsScale:", @scale, args: [:double], result: :void)
113
+ O.send(@window.layer, "setDrawableSize:", [@pixel_width, @pixel_height], args: [:size], result: :void)
114
+ end
115
+ def create_texture(width, height, **options) = Texture.new(width, height, **options)
116
+ def create_buffer(size, **options) = Buffer.new(size, **options)
117
+ def create_pipeline(shader:, blend: :premultiplied) = Pipeline.new(shader, blend)
118
+ def begin_frame(clear: "#0000") = FrameEncoder.new(self, clear)
119
+
120
+ def render(scene, clear: "#0000")
121
+ pool = O.new("NSAutoreleasePool")
122
+ @draw_calls = 0
123
+ drawable = O.send(@window.layer, "nextDrawable")
124
+ return if drawable.zero?
125
+ target = O.send(drawable, "texture")
126
+ O.release(@last_texture) if @last_texture
127
+ @last_texture = O.send(target, "retain")
128
+ command = O.send(@queue, "commandBuffer")
129
+ O.send(@color_attachment, "setTexture:", target, args: POINTER_ARGS, result: :void)
130
+ unless clear == @clear_input
131
+ O.send(@color_attachment, "setClearColor:", Color.parse(clear).premultiplied, args: CLEAR_ARGS, result: :void)
132
+ @clear_input = clear.is_a?(String) || clear.is_a?(Array) ? clear.dup.freeze : clear
133
+ end
134
+ encoder = O.send(command, "renderCommandEncoderWithDescriptor:", @render_pass, args: POINTER_ARGS)
135
+ O.send(encoder, "setRenderPipelineState:", @pipeline, args: POINTER_ARGS, result: :void)
136
+ bytes, batches = InstancePacking.pack(scene)
137
+ unless bytes.empty?
138
+ buffer = O.send(@device, "newBufferWithBytes:length:options:", Fiddle::Pointer[bytes], bytes.bytesize, 0, args: BUFFER_ARGS)
139
+ O.send(encoder, "setVertexBytes:length:atIndex:", @viewport_pointer, @viewport_bytes.bytesize, 1, args: BUFFER_ARGS, result: :void)
140
+ batches.each do |(kind, texture, clip), first, count|
141
+ scissor = @full_scissor
142
+ if clip
143
+ left, top = [clip.x, 0].max, [clip.y, 0].max
144
+ width, height = [clip.right, @width].min - left, [clip.bottom, @height].min - top
145
+ next unless width.positive? && height.positive?
146
+ scissor = @scissor
147
+ scissor[0], scissor[1] = (left * @scale).to_i, (top * @scale).to_i
148
+ scissor[2], scissor[3] = (width * @scale).to_i, (height * @scale).to_i
149
+ end
150
+ next if scissor[2].zero? || scissor[3].zero?
151
+ # MTLScissorRect is four NSUInteger values, the same layout as two ranges.
152
+ @scissor_signature.call(O::MESSAGE_SEND, encoder, @scissor_selector, scissor)
153
+ O.send(encoder, "setVertexBuffer:offset:atIndex:", buffer, first * InstancePacking::STRIDE * 4, 0, args: BUFFER_ARGS, result: :void)
154
+ O.send(encoder, "setFragmentTexture:atIndex:", native_texture(texture || @white), 0, args: TEXTURE_ARGS, result: :void)
155
+ O.send(encoder, "drawPrimitives:vertexStart:vertexCount:instanceCount:", kind == :triangle ? 3 : 4, 0, kind == :triangle ? 3 : 4, count, args: DRAW_ARGS, result: :void)
156
+ @draw_calls += 1
157
+ end
158
+ end
159
+ O.send(encoder, "endEncoding", result: :void)
160
+ O.send(command, "presentDrawable:", drawable, args: POINTER_ARGS, result: :void)
161
+ O.send(command, "commit", result: :void)
162
+ O.release(@last_command) if @last_command
163
+ @last_command = O.send(command, "retain")
164
+ ensure
165
+ O.release(buffer) if buffer
166
+ O.release(pool) if pool
167
+ end
168
+
169
+ def native_texture(texture)
170
+ entry = @textures[texture]
171
+ unless entry
172
+ descriptor = O.send(O.klass("MTLTextureDescriptor"), "texture2DDescriptorWithPixelFormat:width:height:mipmapped:", texture.format == :r8 ? 10 : 70, texture.width, texture.height, 0, args: [:ulong, :ulong, :ulong, :bool])
173
+ O.send(descriptor, "setStorageMode:", 0, args: [:ulong], result: :void)
174
+ entry = @textures[texture] = [O.send(@device, "newTextureWithDescriptor:", descriptor, args: [:pointer]), nil]
175
+ raise Error, "Metal texture allocation failed" if entry[0].zero?
176
+ end
177
+ digest = texture.revision
178
+ if entry[1] != digest
179
+ region = [0, 0, 0, texture.width, texture.height, 1]
180
+ O.send(entry[0], "replaceRegion:mipmapLevel:withBytes:bytesPerRow:", region, 0, Fiddle::Pointer[texture.data], texture.width * (texture.format == :r8 ? 1 : 4), args: [:region, :ulong, :pointer, :ulong], result: :void)
181
+ entry[1] = digest
182
+ end
183
+ entry[0]
184
+ end
185
+
186
+ def pixels
187
+ return "\0".b * (@pixel_width * @pixel_height * 4) unless @last_texture
188
+ O.send(@last_command, "waitUntilCompleted", result: :void)
189
+ status = O.send(@last_command, "status", result: :ulong)
190
+ raise Error, O.text(O.send(O.send(@last_command, "error"), "localizedDescription")) if status == 5
191
+ bytes = "\0".b * (@pixel_width * @pixel_height * 4)
192
+ O.send(@last_texture, "getBytes:bytesPerRow:fromRegion:mipmapLevel:", Fiddle::Pointer[bytes], @pixel_width * 4, [0, 0, 0, @pixel_width, @pixel_height, 1], 0, args: [:pointer, :ulong, :region, :ulong], result: :void)
193
+ bytes.unpack("C*").each_slice(4).flat_map { |b, g, r, 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*")
194
+ end
195
+ def write_png(path) = PNG.write(path, @pixel_width, @pixel_height, pixels)
196
+ def release
197
+ O.send(@last_command, "waitUntilCompleted", result: :void) if @last_command
198
+ @textures.each_value { |entry| O.release(entry[0]) }
199
+ @textures.clear
200
+ [@last_command, @last_texture, @render_pass, @pipeline, @queue, @device].compact.each { |object| O.release(object) }
201
+ @last_command = @last_texture = @render_pass = nil
202
+ end
203
+ def native_error(pointer, fallback)
204
+ object = pointer[0, Fiddle::SIZEOF_VOIDP].unpack1("J")
205
+ object.zero? ? fallback : O.text(O.send(object, "localizedDescription"))
206
+ end
207
+ end
208
+ end
209
+ end