aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--alv-lib/glsl-view.moon417
-rw-r--r--alv-lib/osc.moon8
-rw-r--r--alv-lib/wgsl-view.moon327
-rw-r--r--examples/glsl-view/simple.alv12
-rw-r--r--examples/wgsl-view/simple.alv15
-rw-r--r--spec/lib/wgsl-view_spec.moon115
6 files changed, 461 insertions, 433 deletions
diff --git a/alv-lib/glsl-view.moon b/alv-lib/glsl-view.moon
deleted file mode 100644
index 6142502..0000000
--- a/alv-lib/glsl-view.moon
+++ /dev/null
@@ -1,417 +0,0 @@
-import Error, Constant, PureOp, Op, template_subst, Input, T, Array, Struct, sig, evt, const, any from require 'alv.base'
-import Message, Bundle, Timetag, add_item from require 'alv-lib._osc'
-import dns, udp from require 'socket'
-
-fmttag = (tag) -> tag\ident!\gsub '[%.:%-/\\]', '_'
-
-tryprefix = (prefix, filename) ->
- return filename unless prefix and filename\match '^%./'
- prefix .. filename
-
-GLSL_TYPES =
- num: 'float'
- bool: 'bool'
-
-for i=2,4
- GLSL_TYPES["num[#{i}]"] = "vec#{i}"
- GLSL_TYPES["bool[#{i}]"] = "bvec#{i}"
-
- for j=2,4
- GLSL_TYPES["num[#{j}][#{i}]"] = "mat#{i}x#{j}"
-
-GLSL_SAMPLER_TYPES =
- "1D": 'sampler1D'
- "2D": 'sampler2D'
- "3D": 'sampler3D'
- "CUBE_MAP": 'samplerCube'
- "RECTANGLE": 'sampler2DRect'
- "1D_ARRAY": 'sampler1DArray'
- "2D_ARRAY": 'sampler2DArray'
- "CUBE_MAP_ARRAY": 'samplerCubeArray'
- "BUFFER": 'samplerBuffer'
-
-Uniform = Struct type: T.str, val: T.str
-
-get_typestr = (type) ->
- if typestr = GLSL_TYPES[tostring type]
- return typestr
-
- if type.__class == Array
- if typestr = GLSL_TYPES[tostring type.type]
- return "#{typestr}[#{type.size}]"
-
- error Error 'type', "can't wrap #{type} in uniform"
-
-uniform = Constant.meta
- meta:
- name: 'uniform'
- summary: "override uniform binding type."
- examples: { '(uniform [glsl-type] value)' }
- description: "Label an alive value with a GLSL type:
-`(uniform 'sampler2D' 'sampler-name')` is equivalent to `{'type' 'sampler2D' 'val' 'sampler-name'}`.
-
-When absent, `glsl-type` is inferred based on the type of `value`:
-- `num`, `bool`: `uniform float/bool`
-- `num[I]`, `bool[I]`: `uniform vecI/bvecI`
-- `num[I][J]`: `uniform matIxJ`"
-
- value: class extends PureOp
- pattern: -sig.str + sig!
- type: (inputs) => Struct type: T.str, val: inputs[2]\type!
- tick: =>
- { type, val } = @unwrap_all!
- type or= get_typestr @inputs[2]\type!
- @out\set :type, :val
-
-offset= Constant.meta
- meta:
- name: 'offset'
- summary: "get the playback offset of a buffered-stream-source."
- examples: { '(offset [glsl-type] source)' }
- description: "GLSL type: float/double/int/uint"
-
- value: class extends PureOp
- pattern: -sig.str + sig!
- type: => Uniform
- tick: =>
- { type, source } = @unwrap_all!
- type or= "float"
- name = source.val
- @out\set :type, val: "/source/#{name}/offset"
-
-video_source = Constant.meta
- meta:
- name: 'video-source'
- summary: "load a video texture source."
- examples: { '(video-source [socket] type filename [format] [args])' }
- description: "Creates a texture source from a video or image file.
-
-- `type` can be one of:
- - `2D`: `sampler2D` (loads first frame only)
- - `3D` or `2D_ARRAY`: `sampler3D`/`sampler2DArray` (loads whole video)
-- `filename` is the video file to load. If it starts with `./` it will be
- resolved relative to the current alv module.
-- `format` optionally specifies the ffmpeg/avformat input format to use.
-- `args` is a struct of optional arguments for the ffmpeg/avformat input format."
-
- value: class extends Op
- pattern = -sig['osc/out'] + const.str + const.str + -const.str + -const!
- setup: (inputs, scope) =>
- { socket, type, filename, format, args } = pattern\match inputs
-
- @prefix = COPILOT.active_module.file\match'(.*/)[^/]*$'
-
- super
- socket: Input.hot socket or scope\get '*oscout*'
- type: Input.cold type
- filename: Input.cold filename
- format: format and Input.cold format
- args: args and Input.cold args
-
- samplertype = assert GLSL_SAMPLER_TYPES[@inputs.type!]
- @setup_out '=', Uniform, type: samplertype, val: "alv-#{fmttag @tag}"
-
- tick: =>
- { :socket, :type, :filename, :format, :args } = @unwrap_all!
-
- with @inputs.socket!
- filename = tryprefix @prefix, filename
- msg = Message.new{ address: "/source/alv-#{fmttag @tag}/video", types: "ss", "TEXTURE_#{type}", filename }
- msg\add 's', format if format
- add_item msg, @inputs.args\type!, args if args
- \send Message.pack msg.content
-
- -- destroy: =>
- -- name = @inputs.name!
- -- with @inputs.socket!
- -- \send Message.pack { address: "/source/#{name}/destroy", types: "" }
-
-stream_source = Constant.meta
- meta:
- name: 'stream-source'
- summary: "load a stream texture source."
- examples: { '(stream-source [socket] type uri [format] [args])' }
- description: "Creates a realtime stream from a capture source.
-
-- `type` must be `2D`.
-- `uri` is the ffmpeg URI to load. If it starts with `./` it will be resolved relative to the current alv module.
-- `format` optionally specifies the ffmpeg/avformat input format to use.
-- `args` is a struct of optional arguments for the ffmpeg/avformat input format.
-
- (stream-source 'webcam' '2D' '/dev/video0')
- (stream-source 'testpt' '2D' 'testsrc=size=1280x720:rate=30' 'lavfi')"
-
- value: class extends Op
- pattern = -sig['osc/out'] + const.str + const.str + -const.str + -const!
- setup: (inputs, scope) =>
- { socket, type, filename, format, args } = pattern\match inputs
-
- @prefix = COPILOT.active_module.file\match'(.*/)[^/]*$'
-
- super
- socket: Input.hot socket or scope\get '*oscout*'
- type: Input.cold type
- filename: Input.cold filename
- format: format and Input.cold format
- args: args and Input.cold args
-
- samplertype = assert GLSL_SAMPLER_TYPES[@inputs.type!]
- @setup_out '=', Uniform, type: samplertype, val: "alv-#{fmttag @tag}"
-
- tick: =>
- { :socket, :name, :type, :filename, :format, :args } = @unwrap_all!
-
- with @inputs.socket!
- filename = tryprefix @prefix, filename
- msg = Message.new{ address: "/source/alv-#{fmttag @tag}/stream", types: "ss", "TEXTURE_#{type}", filename }
- msg\add 's', format if format
- add_item msg, @inputs.args\type!, args if args
- \send Message.pack msg.content
-
-buffered_stream_source = Constant.meta
- meta:
- name: 'buffered-stream-source'
- summary: "load a stream as a 3d texture source."
- examples: { '(buffered-stream-source [socket] type depth uri [format] [args])' }
- description: "Creates a circular buffer 3d texture from a capture source.
-
-- `type` must be `3D` or `2D_ARRAY`.
-- `depth` is the number of frames to keep in the buffer.
-- `uri` is the ffmpeg URI to load. If it starts with `./` it will be resolved relative to the current alv module.
-- `format` optionally specifies the ffmpeg/avformat input format to use.
-- `args` is a struct of optional arguments for the ffmpeg/avformat input format.
-
- (stream-source 'webcam' '3D' 64 '/dev/video0')"
-
- value: class extends Op
- pattern = -sig['osc/out'] + const.str + const.num + const.str + -const.str + -const!
- setup: (inputs, scope) =>
- { socket, type, depth, filename, format, args } = pattern\match inputs
-
- @prefix = COPILOT.active_module.file\match'(.*/)[^/]*$'
-
- super
- socket: Input.hot socket or scope\get '*oscout*'
- type: Input.cold type
- depth: Input.cold depth
- filename: Input.cold filename
- format: format and Input.cold format
- args: args and Input.cold args
-
- samplertype = assert GLSL_SAMPLER_TYPES[@inputs.type!]
- @setup_out '=', Uniform, type: samplertype, val: "alv-#{fmttag @tag}"
-
- tick: =>
- { :socket, :name, :type, :depth, :filename, :format, :args } = @unwrap_all!
-
- with @inputs.socket!
- filename = tryprefix @prefix, filename
- msg = Message.new{ address: "/source/alv-#{fmttag @tag}/buffered-stream", types: "sis", "TEXTURE_#{type}", depth, filename }
- msg\add 's', format if format
- add_item msg, @inputs.args\type!, args if args
- \send Message.pack msg.content
-
-tsv_source = Constant.meta
- meta:
- name: 'tsv-source'
- summary: "load a texture-share-vk texture source."
- examples: { '(tsv-source [socket] name [type])' }
- description: "Creates a texture source from a texture-share-vk texture.
-
-- `name` is the tsv name of this texture source.
-- `type` defaults to `2D`."
-
- value: class extends Op
- pattern = -sig['osc/out'] + const.str + -const.str
- setup: (inputs, scope) =>
- { socket, name, type } = pattern\match inputs
-
- super
- socket: Input.hot socket or scope\get '*oscout*'
- name: Input.hot name
- type: Input.hot type or Constant.str "2D"
-
- samplertype = assert GLSL_SAMPLER_TYPES[@inputs.type!]
- @setup_out '=', Uniform, type: samplertype, val: "alv-#{fmttag @tag}"
-
- tick: =>
- { :socket, :name, :type } = @unwrap_all!
-
- with @inputs.socket!
- \send Message.pack { address: "/source/alv-#{fmttag @tag}/tsv", types: "ss", "TEXTURE_#{type}", name }
-
-freeze = Constant.meta
- meta:
- name: 'freeze'
- summary: "freeze a texture source."
- examples: { '(freeze [socket] source freeze?)' }
-
- value: class extends Op
- pattern = -sig['osc/out'] + const[Uniform] + any.bool
- setup: (inputs, scope) =>
- { socket, source, freeze } = pattern\match inputs
-
- super
- socket: Input.hot socket or scope\get '*oscout*'
- source: Input.cold source
- freeze: Input.hot freeze
-
- tick: =>
- { :socket, :source, :freeze } = @unwrap_all!
-
- with @inputs.socket!
- msg = Message.new{ address: "/source/#{source.val}/freeze", types: "" }
- add_item msg, @inputs.freeze\type!, freeze
- \send Message.pack msg.content
-
-step_ = Constant.meta
- meta:
- name: 'step!'
- summary: "step a frozen texture source."
- examples: { '(step! [socket] source [trig!])' }
-
- value: class extends Op
- pattern = -sig['osc/out'] + any[Uniform] + -evt.bang
- setup: (inputs, scope) =>
- { socket, source, trig } = pattern\match inputs
-
- super
- socket: Input.hot socket or scope\get '*oscout*'
- source: Input.hot source
- trig: trig and Input.hot trig
-
- tick: =>
- { :socket, :source, :trig } = @unwrap_all!
-
- with @inputs.socket!
- \send Message.pack { address: "/source/#{source.val}/step", types: "I" }
-
-shader_ = Constant.meta
- meta:
- name: 'shader'
- summary: "compile a GLSL shader with uniforms."
- examples: { '$shader"code…"' }
- description: "A template string formatter for [glsl-view/draw][] that returns a shader struct~.
-
-- `code…` is a template string that can contain `~-stream` template substitutions that are used as follows:
- - `str`: substituted directly as GLSL
- - another shader struct~: substituted as GLSL, with uniforms lifted
- - `{type: str val: …}`: reference to uniform of GLSL-type `type`
- - other values are implicitly converted as by [glsl-view/uniform][]"
-
- value: class extends PureOp
- pattern: const! + sig!^0
-
- name: (i) => "alv_u#{fmttag @tag}_#{i}"
-
- type: (args) =>
- uniforms = {}
- for i, val in ipairs args[2]
- type = val\type!
- if type == T.str
- continue
- else if type.__class == Struct and type.types['shader']
- for name, typ in pairs type.types['uniforms'].types
- uniforms[name] = typ
- else
- if type.__class == Struct
- assert type.types['type'] == T.str, Error 'type', "struct substituton needs type key"
- else
- type = Struct type: T.str, val: type
-
- uniforms[@name i] = type
-
- Struct shader: T.str, uniforms: Struct uniforms
-
- tick: =>
- uniforms = {}
-
- pieces = for i, input in ipairs @inputs[2]
- name, input = (@name i), input
- val, type = input!, input\type!
-
- if type == T.str
- val
- elseif type.__class == Struct and type.types['shader']
- for k, v in pairs val.uniforms
- uniforms[k] = v
- val.shader
- else
- if type.__class != Struct
- val = :val, type: get_typestr type
-
- uniforms[name] = val
- name
-
- shader = template_subst @inputs[1]!, pieces
-
- @out\set :shader, :uniforms
-
-draw = Constant.meta
- meta:
- name: 'draw'
- summary: "draw a GLSL shader via OSC."
- examples: { '(draw [socket] shader)' }
- description: "Syncs code and parameters with glsl-view via OSC.
-
-- `socket` must be an `osc/out~` or read from `*oscout*`
-- `shader` is a struct with
- - `shader` (`str`): GLSL source
- - `uniforms`: struct mapping uniform names to values"
-
- value: class extends Op
- pattern = -sig['osc/out'] + any!
-
- setup: (inputs, scope) =>
- { socket, shader } = pattern\match inputs
- super
- socket: Input.hot socket or scope\get '*oscout*'
- shader: Input.hot shader
-
- tick: =>
- { :socket, :shader } = @unwrap_all!
-
- if not shader
- return
-
- with Bundle.new Timetag.new!
- chunks = { '#version 330 core\n' }
- for key, val in pairs shader.uniforms
- typ, arrsuff = val.type\match "^(%w+)(%[%d+%])$"
- table.insert chunks, if typ and arrsuff
- print "typ and arrsuf", typ, arrsuff
- "uniform #{typ} #{key}#{arrsuff};"
- else
- "uniform #{val.type} #{key};"
- table.insert chunks, shader.shader
-
- \add content: { address: '/shader', types: 's', table.concat chunks, '\n' }
-
- types = @inputs.shader\type!\get 'uniforms'
- for key, val in pairs shader.uniforms
- type = types\get(key)\get 'val'
- msg = Message.new "/uniform/#{key}"
- add_item msg, type, val.val
- \add msg
-
- socket\send Bundle.pack .content
-
-Constant.meta
- meta:
- name: 'glsl-view'
- summary: "glsl-view integration via OSC."
-
- value:
- :uniform
-
- 'video-source': video_source
- 'stream-source': stream_source
- 'buffered-stream-source': buffered_stream_source
- 'tsv-source': tsv_source
-
- :offset
-
- :freeze, 'step!': step_
-
- shader: shader_
- :draw
diff --git a/alv-lib/osc.moon b/alv-lib/osc.moon
index 82be97e..0e51b40 100644
--- a/alv-lib/osc.moon
+++ b/alv-lib/osc.moon
@@ -1,6 +1,6 @@
import Op, PureOp, Constant, Input, T, Array, sig, const, any from require 'alv.base'
import new_message, add_item, Message from require 'alv-lib._osc'
-import dns, udp from require 'socket'
+socket = require 'socket'
losc = require 'losc'
losc_Pattern = require 'losc.pattern'
@@ -25,9 +25,9 @@ connect = Constant.meta
tick: =>
{ :host, :port } = @unwrap_all!
- ip = dns.toip host
+ ip = socket.dns.toip host
- @out\set with sock = udp!
+ @out\set with sock = socket.udp!
\setpeername ip, port
listen = Constant.meta
@@ -67,7 +67,7 @@ listen = Constant.meta
tick: =>
if @inputs.port\dirty! or @inputs.host\dirty!
- @state = with udp!
+ @state = with socket.udp!
\settimeout 0
\setoption 'reuseaddr', true
\setoption 'reuseport', true
diff --git a/alv-lib/wgsl-view.moon b/alv-lib/wgsl-view.moon
new file mode 100644
index 0000000..02d27b4
--- /dev/null
+++ b/alv-lib/wgsl-view.moon
@@ -0,0 +1,327 @@
+import RTNode, Error, Constant, PureOp, Op, template_subst, Input, T, Primitive, Array, Struct, sig, evt, const, any from require 'alv.base'
+import Message, Bundle, Timetag from require 'alv-lib._osc'
+
+SCALAR_TYPES = num: 'f32', bool: 'u32'
+get_typestr = (type) ->
+ typestr = tostring type
+
+ if wgsl = typestr\match "^wgsl/(.+)$"
+ return wgsl
+
+ if scalar = SCALAR_TYPES[typestr]
+ return scalar
+
+ if type.__class == Array
+ vecsz = type.size >= 2 and type.size <= 4 and type.size
+ if inner = vecsz and SCALAR_TYPES[tostring type.type]
+ return "vec#{vecsz}<#{inner}>"
+
+ inner = get_typestr type.type
+ return "array<#{inner}, #{type.size}>"
+
+ error Error 'type', "can't convert #{type} to WGSL type"
+
+fmttag = (tag) -> tag\ident!\gsub '[%.:%-/\\]', '_'
+add_item = (message, val, use_arrays=true) ->
+ message\add switch type val
+ when "number" then 'f', val
+ when "string" then 's', val
+ when "boolean"
+ if val then 'T' else 'F'
+ when "table"
+ message\add '[' if use_arrays
+ for inner in *val
+ add_item message, inner, use_arrays
+ message\add ']' if use_arrays
+ return
+ else
+ error "unable to send #{val}"
+
+class Shader
+ is_bindable = (typestr) ->
+ typestr == "sampler" or typestr\match "^texture_"
+
+ new: (@id, code, inputs) =>
+ @uniforms = {}
+ @bindings = {}
+ @deps = {}
+
+ pieces = for i, input in ipairs inputs
+ val, type = input!, input\type!
+
+ if type == T.str
+ val
+ elseif type == T["wgsl-v/shader"]
+ @deps[val.id] = val
+ "package::#{val.id}"
+ elseif type.__class == Primitive
+ typestr = get_typestr tostring type
+ if is_bindable typestr
+ table.insert @bindings, { :val, :typestr }
+ "b_#{#@bindings}"
+ else
+ table.insert @uniforms, { :val, :typestr }
+ "u._#{#@uniforms}"
+
+ @code = template_subst code, pieces
+
+ compile: (g, bi) =>
+ chunks = { @code, "" }
+ if #@uniforms > 0
+ table.insert chunks, "struct Uniforms {"
+ for i, uniform in pairs @uniforms
+ table.insert chunks, " _#{i}: #{uniform.typestr},"
+ table.insert chunks, "};"
+ table.insert chunks, "@group(#{g}) @binding(#{bi}) var<uniform> u: Uniforms;"
+ bi += 1
+
+ for i, binding in pairs @bindings
+ table.insert chunks, "@group(#{g}) @binding(#{bi}) var b_#{i}: #{binding.typestr};"
+ bi += 1
+
+ code = table.concat chunks, "\n"
+ code, bi
+
+ add_module_message: (bundle, g, bi) =>
+ code, bi = @compile g, bi
+ bundle\add content: { address: "/module/package::#{@id}", types: 's', code }
+ bi
+
+ add_binding_messages: (bundle) =>
+ for i, binding in ipairs @bindings
+ bundle\add Message.new { address: "/binding/package::#{@id}/b_#{i}", types: "s", binding.val }
+
+ for i, uniform in ipairs @uniforms
+ msg = Message.new "/uniform/package::#{@id}/u/_#{i}"
+ add_item msg, uniform.val
+ bundle\add msg
+
+ all_modules: (all={})=>
+ all[@id] = @
+
+ for k, v in pairs @deps
+ v\all_modules all
+
+ all
+
+ @code_eql: (a, b) ->
+ return false if not a or not b
+ return false unless a.code == b.code
+
+ for k, v in pairs a.deps
+ return false unless v == b[k]
+
+ for k, v in pairs b.deps
+ return false unless a[k]
+
+ return true
+
+sampler = Constant.meta
+ meta:
+ name: 'sampler'
+ summary: "create a sampler."
+ examples: { '(sampler [socket] filter-mode address-mode)' }
+ description: 'Creates a sampler resource.
+
+- `filter-mode` is one of `"linear"` or `"nearest"`
+- `address-mode` is one of `"clamp"`, `"repeat"` or `"mirror"`'
+
+ value: class extends Op
+ pattern = -sig['osc/out'] + sig.str + sig.str
+ setup: (inputs, scope) =>
+ { socket, filter, address } = pattern\match inputs
+
+ super
+ socket: Input.hot socket or scope\get '*oscout*'
+ filter: Input.hot filter
+ address: Input.hot address
+
+ @state = "/sampler/alv-#{fmttag @tag}"
+ @setup_out '=', T["wgsl/sampler"], @state
+
+ tick: =>
+ { :socket, :filter, :address } = @unwrap_all!
+
+ with @inputs.socket!
+ \send Message.pack { address: @state, types: "ss", filter, address }
+
+ destroy: =>
+ return unless @state
+ with @inputs.socket!
+ \send Message.pack { address: "#{@state}/destroy", types: "" }
+
+video_stream = Constant.meta
+ meta:
+ name: 'video-stream'
+ summary: "create a video stream"
+ examples: { '(video-stream [socket] args…)' }
+ description: 'Creates a video stream from ffmpeg args.'
+
+ value: class extends Op
+ pattern = -sig['osc/out'] + const!^0
+ default_prefix = RTNode result: Constant.str ""
+ setup: (inputs, scope) =>
+ { socket, rest } = pattern\match inputs
+
+ super
+ socket: Input.hot socket or scope\get '*oscout*'
+ rest: [Input.hot p for p in *rest]
+
+ bin_prefix = (scope\get '*wgsl-view-bin*', default_prefix).result!
+
+ tsvname = "alv-#{fmttag @tag}"
+ id = "/texture/#{tsvname}"
+ args = table.concat @unwrap_all!.rest, ' '
+ @state or= {
+ :tsvname, :id
+ pipe: io.popen bin_prefix .. "tsv-video-stream --name #{tsvname} -- #{args}",
+ }
+ @setup_out '=', T["wgsl/texture_2d<f32>"], id
+
+ tick: =>
+ with @inputs.socket!
+ \send Message.pack { address: @state.id, types: "s", @state.tsvname }
+
+ destroy: =>
+ return unless @state
+ with @inputs.socket!
+ \send Message.pack { address: "#{@state.id}/destroy", types: "" }
+
+tsv_tex = Constant.meta
+ meta:
+ name: 'tsv-tex'
+ summary: "load a TSV texture"
+ examples: { '(tsv-tex [socket] type name)' }
+ description: 'Load the TSV texture `name` of the given type (`1d`, 2d`, `2d_array` or `3d`).'
+
+ value: class extends Op
+ pattern = -sig['osc/out'] + const.str + sig.str
+ default_prefix = RTNode result: Constant.str ""
+ setup: (inputs, scope) =>
+ { socket, typename, tsvname } = pattern\match inputs
+
+ super
+ socket: Input.hot socket or scope\get '*oscout*'
+ typename: Input.cold typename
+ tsvname: Input.hot tsvname
+
+ @state = "/texture/alv-#{fmttag @tag}"
+ @setup_out '=', T["wgsl/texture_#{@inputs.typename!}<f32>"], @state
+
+ tick: =>
+ { :socket, :tsvname } = @unwrap_all!
+ with @inputs.socket!
+ \send Message.pack { address: @state, types: "s", tsvname }
+
+ destroy: =>
+ return unless @state
+ with @inputs.socket!
+ \send Message.pack { address: "#{@state}/destroy", types: "" }
+
+cast = Constant.meta
+ meta:
+ name: 'cast'
+ summary: "cast alive values to WGSL types."
+ examples: { '(cast [wgsl-type] value)' }
+ description: "Label an alive value with a WGSL type.:
+When absent, `wgsl-type` is inferred based on the type of `value`:
+- `num`: `f32`
+- `bool`: `u32`
+- `num[I]`: `vecI<f32>` (if `I` is in 2-4)
+- `B[I]`: `array<B, I>` (where B is cast by the same rules)"
+
+ value: class extends PureOp
+ pattern: -sig.str + sig!
+
+ type: (inputs) =>
+ typestr = if inputs[1] then inputs[1].result!
+ else get_typestr inputs[2]\type!
+ T["wgsl/" .. typestr]
+
+ tick: => @out\set @inputs[2]!
+
+shader_ = Constant.meta
+ meta:
+ name: 'shader'
+ summary: "compile a WGSL shader with uniforms."
+ examples: { '$shader"code…"' }
+ description: "A template string formatter for [wgsl-view/draw][] that returns a shader struct~.
+
+- `code…` is a template string that can contain `~-stream` template substitutions that are used as follows:
+ - `str`: substituted directly as WGSL
+ - another shader struct~: substituted as WGSL, with uniforms lifted
+ - `{type: str val: …}`: reference to uniform of WGSL-type `type`
+ - other values are implicitly converted as by [wgsl-view/cast][]"
+
+ value: class extends PureOp
+ pattern: const! + sig!^0
+ type: T["wgsl-v/shader"]
+ tick: =>
+ shader = Shader "alv_#{fmttag @tag}", @inputs[1]!, @inputs[2]
+ @out\set shader
+
+draw = Constant.meta
+ meta:
+ name: 'draw'
+ summary: "draw a WGSL shader via OSC."
+ examples: { '(draw [socket] shader)' }
+ description: "Syncs code and parameters with wgsl-view via OSC.
+
+- `socket` must be an `osc/out~` or read from `*oscout*`
+- `shader` is a struct with
+ - `shader` (`str`): WGSL source
+ - `uniforms`: struct mapping uniform names to values"
+
+ value: class extends Op
+ pattern = -sig['osc/out'] + any!
+
+ setup: (inputs, scope) =>
+ { socket, shader } = pattern\match inputs
+
+ super
+ socket: Input.hot socket or scope\get '*oscout*'
+ shader: Input.hot shader
+
+ @state or= {}
+
+ tick: =>
+ { :socket, :shader } = @unwrap_all!
+
+ if not shader
+ return
+
+ bundle = Bundle.new Timetag.new!
+ values = Bundle.new Timetag.new!
+
+ g, bi = 1, 0
+ dirty = false
+ for k, module in pairs shader\all_modules!
+ bi = module\add_module_message bundle, g, bi
+ module\add_binding_messages values
+
+ if not Shader.code_eql @state[k], module
+ dirty = true
+ @state[k] = module
+
+ if dirty
+ bundle\add content: { address: '/entrypoint', types: 's', "package::#{shader.id}" }
+
+ bundle\add values
+ socket\send Bundle.pack bundle.content
+
+Constant.meta
+ meta:
+ name: 'wgsl-view'
+ summary: "wgsl-view integration via OSC."
+
+ value:
+ '*wgsl-view-bin*': Constant.str ''
+
+ :cast
+ :sampler
+
+ 'tsv-tex': tsv_tex
+ 'video-stream': video_stream
+
+ shader: shader_
+ :draw
diff --git a/examples/glsl-view/simple.alv b/examples/glsl-view/simple.alv
deleted file mode 100644
index 02c8796..0000000
--- a/examples/glsl-view/simple.alv
+++ /dev/null
@@ -1,12 +0,0 @@
-([1]import* glsl-view time math)
-([2]import osc)
-
-([4]def *oscout* ([3]osc/connect 'localhost' 9000))
-
-([7]draw $[6]shader"
-in vec2 uv;
-out vec4 color;
-
-void main() {
- color = vec4(uv, sin(uv.x + $([5]ramp 1 tau)), 1);
-}")
diff --git a/examples/wgsl-view/simple.alv b/examples/wgsl-view/simple.alv
new file mode 100644
index 0000000..ff66602
--- /dev/null
+++ b/examples/wgsl-view/simple.alv
@@ -0,0 +1,15 @@
+([1]import* wgsl-view time math)
+([2]import osc)
+
+([4]def *oscout* ([3]osc/connect 'localhost' 9000))
+
+## let cols = $[[1 0 0] [0 1 0] [0 0 1]];
+## let col = cols[i32($([8]tick 0.5)) % 3];
+
+([7]draw $[6]shader"
+@fragment
+fn fs_main(@location(0) _uv: vec2f) -> @location(0) vec4f {
+ var uv = _uv * 2.0 - 1.0;
+ let col = 0.5 + 0.5 * cos($([5]ramp 1 tau) + uv.xyx + vec3f(0, 2, 4));
+ return vec4f(col, 1.0);
+}")
diff --git a/spec/lib/wgsl-view_spec.moon b/spec/lib/wgsl-view_spec.moon
new file mode 100644
index 0000000..3676b63
--- /dev/null
+++ b/spec/lib/wgsl-view_spec.moon
@@ -0,0 +1,115 @@
+import TestPilot from require 'spec.test_setup'
+import T, Array from require 'alv'
+import _ from require "luassert.match"
+
+socket = require "socket"
+
+describe "wgsl-view", ->
+ test = TestPilot '', '
+(import* wgsl-view)
+(import osc)
+(def *oscout* (osc/connect "localhost" 9000))
+'
+
+ local SOCK
+ _udp = socket.udp
+ stub socket, 'udp', ->
+ SOCK = mock {
+ send: ->
+ setpeername: ->
+ }
+ SOCK
+
+ describe "shader", ->
+ it "handles simple shaders", ->
+ rt = COPILOT\eval_once '$shader"just simple content"'
+ assert.is.true rt\is_const!
+ shader = rt.result!
+ assert.is.equal "just simple content", shader.code
+ assert.is.same {}, shader.uniforms
+ assert.is.same {}, shader.bindings
+ assert.is.same {}, shader.deps
+
+ it "interpolates strings", ->
+ rt = COPILOT\eval_once '$shader"just $"simple" content"'
+ assert.is.equal "just simple content", rt.result!.code
+
+ it "interpolates uniforms", ->
+ rt = COPILOT\eval_once '$[1]shader"let val = $4;"'
+ assert.is.equal "struct Uniforms {
+ _1: f32,
+};
+@group(1) @binding(0) var<uniform> u: Uniforms;
+
+let val = u._1;", rt.result!\compile 1
+
+ messages = rt.result!\get_binding_messages 1
+ assert.is.equal 1, #messages
+ assert.is.same {
+ address: '/uniform/package::alv::main_1::u/_1',
+ types: 'f',
+ 4,
+ }, messages[1].content
+
+ it "interpolates bindings", ->
+ rt = COPILOT\eval_once '$[4]shader"let s = $([5]sampler "linear" "repeat");"'
+ assert.is.equal "@group(2) @binding(1) var b_1: sampler;
+
+let s = b_1;", rt.result!\compile 2
+
+ messages = rt.result!\get_binding_messages 2
+ assert.is.equal 1, #messages
+ assert.is.same {
+ address: '/binding/package::alv::main_4::b_1',
+ types: 's',
+ '/sampler/alv-main_5',
+ }, messages[1].content
+
+ it "respects casted type", ->
+ rt = COPILOT\eval_once '$[1]shader"let val = $(cast "u32" 4);"'
+ assert.is.equal "struct Uniforms {
+ _1: u32,
+};
+@group(1) @binding(0) var<uniform> u: Uniforms;
+
+let val = u._1;", rt.result!\compile 1
+
+ messages = rt.result!\get_binding_messages 1
+ assert.is.equal 1, #messages
+ assert.is.same {
+ address: '/uniform/package::alv::main_1::u/_1',
+ types: 'f',
+ 4,
+ }, messages[1].content
+
+ describe 'cast', ->
+ f32 = T["wgsl/f32"]
+ i32 = T["wgsl/i32"]
+ u32 = T["wgsl/u32"]
+
+ it "infers types", ->
+ rt = COPILOT\eval_once '(cast 4)'
+ assert.is.equal (f32\mk_const 4), rt.result
+
+ rt = COPILOT\eval_once '(cast [1 2])'
+ assert.is.equal T["wgsl/vec2<f32>"], rt\type!
+
+ rt = COPILOT\eval_once '(cast [1 2 3 4 5])'
+ assert.is.equal T["wgsl/array<f32, 5>"], rt\type!
+
+ rt = COPILOT\eval_once '(cast [[1 2] [3 4]])'
+ assert.is.equal T["wgsl/array<vec2<f32>, 2>"], rt\type!
+
+ rt = COPILOT\eval_once '(cast [[1 2 3 4 5] [6 7 8 9 10]])'
+ assert.is.equal T["wgsl/array<array<f32, 5>, 2>"], rt\type!
+
+ rt = COPILOT\eval_once '(cast [[1 2] [3 4] [5 6] [7 8] [9 10]])'
+ assert.is.equal T["wgsl/array<vec2<f32>, 5>"], rt\type!
+
+ it "sets wgsl type", ->
+ rt = COPILOT\eval_once '(cast "i32" 4)'
+ assert.is.equal (i32\mk_const 4), rt.result
+
+ it "overrides existing type", ->
+ rt = COPILOT\eval_once '(cast "u32" (cast "i32" 4))'
+ assert.is.equal (u32\mk_const 4), rt.result