webvas 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +8 -2
- data/LICENSE.txt +1 -1
- data/README.md +82 -48
- data/docs/performance.md +12 -0
- data/docs/security.md +9 -0
- data/docs/spikes.md +18 -0
- data/exe/webvas +271 -0
- data/js/bridge.js +18 -0
- data/js/loader.js +22 -8
- data/lib/webvas/bridge.rb +5 -0
- data/lib/webvas/shader.rb +36 -0
- data/lib/webvas/version.rb +1 -1
- data/lib/webvas.rb +6 -0
- data/patches/psych-wasi.patch +9 -0
- data/runtime/Gemfile +22 -0
- data/runtime/Gemfile.lock +101 -0
- data/script/build_site +52 -0
- data/sig/webvas.rbs +11 -0
- data/site/app.js +466 -0
- data/site/index.html +56 -0
- data/site/loader-example.html +26 -0
- data/site/style.css +89 -0
- data/{js → site}/worker.js +30 -2
- metadata +40 -5
data/js/loader.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const runtimeUrl = loader.dataset.runtime || new URL("./assets/webvas.wasm", base).href;
|
|
7
7
|
const workerUrl = new URL("./worker.js", base).href;
|
|
8
8
|
const bridgeUrl = new URL("./bridge.js", base).href;
|
|
9
|
+
const shaderUrl = new URL("./wgsl-runner.js", base).href;
|
|
9
10
|
|
|
10
11
|
async function run(sourceElement) {
|
|
11
12
|
const selector = sourceElement.dataset.canvas || "#screen";
|
|
@@ -19,22 +20,35 @@
|
|
|
19
20
|
canvas.insertAdjacentElement("afterend", status);
|
|
20
21
|
const report = message => { status.textContent = message; };
|
|
21
22
|
report("Loading Ruby runtime…");
|
|
23
|
+
|
|
24
|
+
const gpu = document.querySelector("#gpu") || document.createElement("canvas");
|
|
25
|
+
gpu.hidden = true;
|
|
26
|
+
if (!gpu.isConnected) {
|
|
27
|
+
gpu.id = "gpu";
|
|
28
|
+
gpu.hidden = true;
|
|
29
|
+
canvas.insertAdjacentElement("afterend", gpu);
|
|
30
|
+
}
|
|
22
31
|
if (canvas.tabIndex < 0) canvas.tabIndex = 0;
|
|
23
32
|
|
|
24
33
|
const response = await fetch(workerUrl, { credentials: "omit" });
|
|
25
34
|
if (!response.ok) throw new Error(`Worker download failed: ${response.status}`);
|
|
26
35
|
const workerBlobUrl = URL.createObjectURL(new Blob([await response.text()], { type: "text/javascript" }));
|
|
27
36
|
const worker = new Worker(workerBlobUrl, { type: "module", name: "webvas" });
|
|
37
|
+
const screenSelector = selector;
|
|
28
38
|
worker.addEventListener("message", event => {
|
|
29
39
|
const message = event.data || {};
|
|
30
40
|
if (message.type === "webvas:loading") report(message.message || "Loading Ruby runtime…");
|
|
31
41
|
else if (message.type === "webvas:ready") {
|
|
32
42
|
URL.revokeObjectURL(workerBlobUrl);
|
|
33
43
|
worker.postMessage({ type: "webvas:run", source: sourceElement.textContent });
|
|
34
|
-
}
|
|
44
|
+
}
|
|
45
|
+
else if (message.type === "webvas:started") report("Ruby sketch is running");
|
|
35
46
|
else if (message.type === "webvas:error") report([message.message, message.backtrace].filter(Boolean).join("\n"));
|
|
36
|
-
else if (message.type === "webvas:pixelated" && message.selector ===
|
|
47
|
+
else if (message.type === "webvas:pixelated" && message.selector === screenSelector) {
|
|
37
48
|
canvas.style.imageRendering = message.enabled ? "pixelated" : "auto";
|
|
49
|
+
} else if (message.type === "webvas:mode") {
|
|
50
|
+
canvas.hidden = message.canvas !== screenSelector;
|
|
51
|
+
gpu.hidden = message.canvas !== "#gpu";
|
|
38
52
|
}
|
|
39
53
|
});
|
|
40
54
|
worker.addEventListener("error", event => {
|
|
@@ -44,12 +58,11 @@
|
|
|
44
58
|
|
|
45
59
|
const forward = event => {
|
|
46
60
|
if (event.type === "wheel") event.preventDefault();
|
|
47
|
-
if (event.type === "keydown" && event.code === "Space") event.preventDefault();
|
|
48
61
|
if (event.type === "pointerdown") {
|
|
49
62
|
canvas.focus({ preventScroll: true });
|
|
50
63
|
canvas.setPointerCapture(event.pointerId);
|
|
51
64
|
}
|
|
52
|
-
const rect =
|
|
65
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
53
66
|
worker.postMessage({
|
|
54
67
|
type: "webvas:input",
|
|
55
68
|
event: {
|
|
@@ -66,11 +79,12 @@
|
|
|
66
79
|
canvas.addEventListener(type, forward, type === "wheel" ? { passive: false } : undefined);
|
|
67
80
|
}
|
|
68
81
|
|
|
69
|
-
const
|
|
82
|
+
const screen = canvas.transferControlToOffscreen();
|
|
83
|
+
const gpuCanvas = gpu.transferControlToOffscreen();
|
|
70
84
|
worker.postMessage({
|
|
71
|
-
type: "webvas:init",
|
|
72
|
-
bridgeUrl, wasmUrl: new URL(runtimeUrl, document.baseURI).href
|
|
73
|
-
}, [
|
|
85
|
+
type: "webvas:init", screen, gpu: gpuCanvas, screenSelector, gpuSelector: "#gpu",
|
|
86
|
+
bridgeUrl, shaderUrl, wasmUrl: new URL(runtimeUrl, document.baseURI).href
|
|
87
|
+
}, [screen, gpuCanvas]);
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
const start = () => document.querySelectorAll('script[type="text/ruby"][data-webvas]').forEach(element => {
|
data/lib/webvas/bridge.rb
CHANGED
|
@@ -37,5 +37,10 @@ module Webvas
|
|
|
37
37
|
def show_error(message, backtrace)
|
|
38
38
|
@api.showError(String(message), Array(backtrace).join("\n"))
|
|
39
39
|
end
|
|
40
|
+
|
|
41
|
+
def run_shader(canvas, source, layout, uniforms)
|
|
42
|
+
@api.shaderMode(canvas)
|
|
43
|
+
JS.global[:GlazeWGSL].run(canvas, source, layout, uniforms)
|
|
44
|
+
end
|
|
40
45
|
end
|
|
41
46
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Webvas
|
|
6
|
+
class Shader
|
|
7
|
+
DEFAULT_LAYOUT = {
|
|
8
|
+
fields: {
|
|
9
|
+
resolution: { offset: 0, type: :vec2 },
|
|
10
|
+
time: { offset: 8, type: :float },
|
|
11
|
+
frame: { offset: 12, type: :int },
|
|
12
|
+
mouse: { offset: 16, type: :vec4 }
|
|
13
|
+
},
|
|
14
|
+
size: 32
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :source, :canvas, :layout
|
|
18
|
+
|
|
19
|
+
def initialize(source, canvas: "#screen", layout: nil)
|
|
20
|
+
@source = String(source)
|
|
21
|
+
@canvas = String(canvas)
|
|
22
|
+
@layout = layout || DEFAULT_LAYOUT
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run(canvas: @canvas, bridge: Bridge.new, &uniforms)
|
|
26
|
+
stop
|
|
27
|
+
callback = proc { |time| JSON.generate(uniforms ? uniforms.call(Float(time)) : {}) }
|
|
28
|
+
@runner = bridge.run_shader(String(canvas), @source, JSON.generate(@layout), callback)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def stop
|
|
32
|
+
@runner.stop if @runner
|
|
33
|
+
@runner = nil
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/webvas/version.rb
CHANGED
data/lib/webvas.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "webvas/backend"
|
|
|
7
7
|
require_relative "webvas/input"
|
|
8
8
|
require_relative "webvas/key_map"
|
|
9
9
|
require_relative "webvas/runner"
|
|
10
|
+
require_relative "webvas/shader"
|
|
10
11
|
|
|
11
12
|
module Webvas
|
|
12
13
|
class Error < StandardError; end
|
|
@@ -36,4 +37,9 @@ module Webvas
|
|
|
36
37
|
Bridge.new.show_error(error.message, error.backtrace || [])
|
|
37
38
|
end
|
|
38
39
|
|
|
40
|
+
def self.run_shader(shader, canvas: "#gpu", &uniforms)
|
|
41
|
+
raise ArgumentError, "shader must be a Webvas::Shader" unless shader.is_a?(Shader)
|
|
42
|
+
|
|
43
|
+
shader.run(canvas:, &uniforms)
|
|
44
|
+
end
|
|
39
45
|
end
|
data/runtime/Gemfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
source "https://rubygems.org"
|
|
2
|
+
|
|
3
|
+
gem "ruby_wasm", "~> 2.10.1"
|
|
4
|
+
gem "js", "~> 2.10.1"
|
|
5
|
+
|
|
6
|
+
[
|
|
7
|
+
["webvas", ".."],
|
|
8
|
+
["glaze", "../../glaze"],
|
|
9
|
+
["gesso", "../../gesso"],
|
|
10
|
+
["glyphic", "../../glyphic"],
|
|
11
|
+
["larb", "../../larb"],
|
|
12
|
+
["rbgl", "../../rbgl"],
|
|
13
|
+
["rlsl", "../../rlsl"],
|
|
14
|
+
["tessel", "../../tessel"]
|
|
15
|
+
].each do |name, relative_path|
|
|
16
|
+
path = File.expand_path(relative_path, __dir__)
|
|
17
|
+
if File.file?(File.join(path, "#{name}.gemspec"))
|
|
18
|
+
gem name, path: path
|
|
19
|
+
else
|
|
20
|
+
gem name
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: ../../gesso
|
|
3
|
+
specs:
|
|
4
|
+
gesso (0.1.0)
|
|
5
|
+
glyphic (>= 0.1.0)
|
|
6
|
+
larb (>= 1.0.0)
|
|
7
|
+
rbgl (>= 1.0.0)
|
|
8
|
+
rlsl (>= 1.0.0)
|
|
9
|
+
tessel (>= 0.1.0)
|
|
10
|
+
|
|
11
|
+
PATH
|
|
12
|
+
remote: ../../glaze
|
|
13
|
+
specs:
|
|
14
|
+
glaze (0.1.0)
|
|
15
|
+
rbgl (>= 1.0.0)
|
|
16
|
+
rlsl (>= 1.0.0)
|
|
17
|
+
tessel (>= 0.1.0)
|
|
18
|
+
|
|
19
|
+
PATH
|
|
20
|
+
remote: ../../glyphic
|
|
21
|
+
specs:
|
|
22
|
+
glyphic (0.1.0)
|
|
23
|
+
tessel (>= 0.1.0)
|
|
24
|
+
|
|
25
|
+
PATH
|
|
26
|
+
remote: ../../larb
|
|
27
|
+
specs:
|
|
28
|
+
larb (1.0.1)
|
|
29
|
+
|
|
30
|
+
PATH
|
|
31
|
+
remote: ../../rbgl
|
|
32
|
+
specs:
|
|
33
|
+
rbgl (1.0.0)
|
|
34
|
+
larb (~> 1.0)
|
|
35
|
+
|
|
36
|
+
PATH
|
|
37
|
+
remote: ../../rlsl
|
|
38
|
+
specs:
|
|
39
|
+
rlsl (1.0.0)
|
|
40
|
+
prism (>= 1.0.0)
|
|
41
|
+
|
|
42
|
+
PATH
|
|
43
|
+
remote: ../../tessel
|
|
44
|
+
specs:
|
|
45
|
+
tessel (0.2.0)
|
|
46
|
+
|
|
47
|
+
PATH
|
|
48
|
+
remote: ..
|
|
49
|
+
specs:
|
|
50
|
+
webvas (0.3.0)
|
|
51
|
+
base64 (~> 0.2)
|
|
52
|
+
glaze (>= 0.1.0, < 0.2)
|
|
53
|
+
rbgl (>= 1.0.0, < 2)
|
|
54
|
+
|
|
55
|
+
GEM
|
|
56
|
+
remote: https://rubygems.org/
|
|
57
|
+
specs:
|
|
58
|
+
base64 (0.3.0)
|
|
59
|
+
js (2.10.1)
|
|
60
|
+
logger (1.7.0)
|
|
61
|
+
prism (1.9.0)
|
|
62
|
+
ruby_wasm (2.10.1-arm64-darwin)
|
|
63
|
+
logger
|
|
64
|
+
ruby_wasm (2.10.1-x86_64-linux)
|
|
65
|
+
logger
|
|
66
|
+
|
|
67
|
+
PLATFORMS
|
|
68
|
+
arm64-darwin-24
|
|
69
|
+
arm64-darwin-25
|
|
70
|
+
x86_64-linux
|
|
71
|
+
|
|
72
|
+
DEPENDENCIES
|
|
73
|
+
gesso!
|
|
74
|
+
glaze!
|
|
75
|
+
glyphic!
|
|
76
|
+
js (~> 2.10.1)
|
|
77
|
+
larb!
|
|
78
|
+
rbgl!
|
|
79
|
+
rlsl!
|
|
80
|
+
ruby_wasm (~> 2.10.1)
|
|
81
|
+
tessel!
|
|
82
|
+
webvas!
|
|
83
|
+
|
|
84
|
+
CHECKSUMS
|
|
85
|
+
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
86
|
+
gesso (0.1.0)
|
|
87
|
+
glaze (0.1.0)
|
|
88
|
+
glyphic (0.1.0)
|
|
89
|
+
js (2.10.1) sha256=c5fb88c60ea936234f818ff240f2ab0c42a57c25ab9b575211fcc4d702d059fb
|
|
90
|
+
larb (1.0.1)
|
|
91
|
+
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
92
|
+
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
93
|
+
rbgl (1.0.0)
|
|
94
|
+
rlsl (1.0.0)
|
|
95
|
+
ruby_wasm (2.10.1-arm64-darwin) sha256=f106a9b4d8b273592471005460f8c54d89a8913e4fecb9dbab3bd3fc8c20bbc6
|
|
96
|
+
ruby_wasm (2.10.1-x86_64-linux) sha256=728e29dc688246572b56dcef1c7b131431d07865320c9b8434dd1ccdfd63db2b
|
|
97
|
+
tessel (0.2.0)
|
|
98
|
+
webvas (0.3.0)
|
|
99
|
+
|
|
100
|
+
BUNDLED WITH
|
|
101
|
+
2.6.9
|
data/script/build_site
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "cgi"
|
|
6
|
+
require "rubygems"
|
|
7
|
+
require "uri"
|
|
8
|
+
require "zlib"
|
|
9
|
+
|
|
10
|
+
root = File.expand_path("..", __dir__)
|
|
11
|
+
destination = File.expand_path(ARGV.fetch(0, File.join(root, "public")))
|
|
12
|
+
runtime_url = ARGV.fetch(1, "./assets/webvas.wasm")
|
|
13
|
+
wasm = ARGV[2] || File.join(root, "build", "webvas.wasm")
|
|
14
|
+
uri = URI.parse(runtime_url)
|
|
15
|
+
if uri.host && (uri.scheme != "https" || uri.userinfo)
|
|
16
|
+
abort "Runtime URL must use HTTPS"
|
|
17
|
+
end
|
|
18
|
+
glaze_runner = File.expand_path("../glaze/lib/glaze/webgpu_runner.js", root)
|
|
19
|
+
unless File.file?(glaze_runner)
|
|
20
|
+
glaze = Gem::Specification.find_by_name("glaze")
|
|
21
|
+
glaze_runner = File.join(glaze.full_gem_path, "lib", "glaze", "webgpu_runner.js")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
abort "Build the WebAssembly runtime first: #{wasm}" if runtime_url.start_with?(".", "/") && !File.file?(wasm)
|
|
25
|
+
abort "Missing shared Glaze WebGPU runner: #{glaze_runner}" unless File.file?(glaze_runner)
|
|
26
|
+
|
|
27
|
+
FileUtils.rm_rf(destination)
|
|
28
|
+
FileUtils.cp_r(File.join(root, "site"), destination)
|
|
29
|
+
FileUtils.cp(File.join(root, "js", "bridge.js"), File.join(destination, "bridge.js"))
|
|
30
|
+
FileUtils.cp(File.join(root, "js", "loader.js"), File.join(destination, "loader.js"))
|
|
31
|
+
FileUtils.cp(glaze_runner, File.join(destination, "wgsl-runner.js"))
|
|
32
|
+
index = File.join(destination, "index.html")
|
|
33
|
+
html = File.read(index)
|
|
34
|
+
html.sub!(%r{(<meta name="webvas-runtime" content=")[^"]*(">)}, "\\1#{CGI.escapeHTML(runtime_url)}\\2")
|
|
35
|
+
if uri.host
|
|
36
|
+
html.sub!(/connect-src ([^;]+)/) do
|
|
37
|
+
sources = Regexp.last_match(1)
|
|
38
|
+
"connect-src #{sources} https://#{uri.host}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
File.write(index, html)
|
|
42
|
+
|
|
43
|
+
if runtime_url.start_with?(".", "/")
|
|
44
|
+
abort "Build the WebAssembly runtime first: #{wasm}" unless File.file?(wasm)
|
|
45
|
+
FileUtils.mkdir_p(File.join(destination, "assets"))
|
|
46
|
+
FileUtils.cp(wasm, File.join(destination, "assets", "webvas.wasm"))
|
|
47
|
+
raw_size = File.size(wasm)
|
|
48
|
+
compressed_size = Zlib::Deflate.deflate(File.binread(wasm), Zlib::BEST_COMPRESSION).bytesize
|
|
49
|
+
puts "Webvas runtime: #{raw_size} bytes (#{compressed_size} deflate bytes)"
|
|
50
|
+
else
|
|
51
|
+
puts "Webvas runtime: #{runtime_url}"
|
|
52
|
+
end
|
data/sig/webvas.rbs
CHANGED
|
@@ -13,6 +13,7 @@ module Webvas
|
|
|
13
13
|
def close: (untyped handle) -> untyped
|
|
14
14
|
def request_animation_frame: (Proc callback) -> untyped
|
|
15
15
|
def show_error: (String message, Array[String] backtrace) -> untyped
|
|
16
|
+
def run_shader: (String canvas, String source, String layout, Proc uniforms) -> untyped
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
class Backend < RBGL::GUI::Backend
|
|
@@ -43,6 +44,16 @@ module Webvas
|
|
|
43
44
|
def request_animation_frame: (Proc callback) -> untyped
|
|
44
45
|
end
|
|
45
46
|
|
|
47
|
+
class Shader
|
|
48
|
+
attr_reader source: String
|
|
49
|
+
attr_reader canvas: String
|
|
50
|
+
attr_reader layout: Hash[Symbol, untyped]
|
|
51
|
+
def initialize: (String source, ?canvas: String, ?layout: Hash[Symbol, untyped]) -> void
|
|
52
|
+
def run: (?canvas: String, ?bridge: Bridge) ?{ (Float) -> Hash[Symbol, untyped] } -> untyped
|
|
53
|
+
def stop: () -> nil
|
|
54
|
+
end
|
|
55
|
+
|
|
46
56
|
def self.run: (RBGL::GUI::Window window, ?scheduler: Scheduler, ?on_error: Proc) { (RBGL::Engine::Context, Float) -> void } -> Proc
|
|
57
|
+
def self.run_shader: (Shader shader, ?canvas: String) ?{ (Float) -> Hash[Symbol, untyped] } -> untyped
|
|
47
58
|
def self.report_error: (StandardError error) -> untyped
|
|
48
59
|
end
|