diff options
| author | Andrés Cuervo <andrescuervor@gmail.com> | 2023-10-27 16:50:16 +0000 |
|---|---|---|
| committer | Andrés Cuervo <andrescuervor@gmail.com> | 2023-10-27 16:50:16 +0000 |
| commit | ccbcc1e7bf7dbf3fb118a4a06371fe4d0f2147ff (patch) | |
| tree | 9980a884f927312bf6e64a28f3066fb1dd146ce6 /virtual-programs | |
| parent | Remove extraneous debug info (diff) | |
| parent | outline: Slight optimization (diff) | |
| download | folk-ccbcc1e7bf7dbf3fb118a4a06371fe4d0f2147ff.tar.gz folk-ccbcc1e7bf7dbf3fb118a4a06371fe4d0f2147ff.zip | |
Merge branch 'main' into ac/editor
Diffstat (limited to 'virtual-programs')
31 files changed, 732 insertions, 609 deletions
diff --git a/virtual-programs/archive/calibration-overlays.folk b/virtual-programs/_archive/calibration-overlays.folk index 6575168d..6575168d 100644 --- a/virtual-programs/archive/calibration-overlays.folk +++ b/virtual-programs/_archive/calibration-overlays.folk diff --git a/virtual-programs/archive/hello-name.folk b/virtual-programs/_archive/hello-name.folk index be31ba5c..be31ba5c 100644 --- a/virtual-programs/archive/hello-name.folk +++ b/virtual-programs/_archive/hello-name.folk diff --git a/virtual-programs/archive/highlight.folk b/virtual-programs/_archive/highlight.folk index 768e4bc9..768e4bc9 100644 --- a/virtual-programs/archive/highlight.folk +++ b/virtual-programs/_archive/highlight.folk diff --git a/virtual-programs/archive/log.folk b/virtual-programs/_archive/log.folk index 872f3fe2..872f3fe2 100644 --- a/virtual-programs/archive/log.folk +++ b/virtual-programs/_archive/log.folk diff --git a/virtual-programs/archive/metrics.folk b/virtual-programs/_archive/metrics.folk index e5389512..e5389512 100644 --- a/virtual-programs/archive/metrics.folk +++ b/virtual-programs/_archive/metrics.folk diff --git a/virtual-programs/archive/points-up-test.folk b/virtual-programs/_archive/points-up-test.folk index c4fcff4d..c4fcff4d 100644 --- a/virtual-programs/archive/points-up-test.folk +++ b/virtual-programs/_archive/points-up-test.folk diff --git a/virtual-programs/archive/points-up.folk b/virtual-programs/_archive/points-up.folk index e5c12126..e5c12126 100644 --- a/virtual-programs/archive/points-up.folk +++ b/virtual-programs/_archive/points-up.folk diff --git a/virtual-programs/archive/test.folk b/virtual-programs/_archive/test.folk index 9b5c75f0..9b5c75f0 100644 --- a/virtual-programs/archive/test.folk +++ b/virtual-programs/_archive/test.folk diff --git a/virtual-programs/archive/welcome.folk b/virtual-programs/_archive/welcome.folk index 6c08ed47..6c08ed47 100644 --- a/virtual-programs/archive/welcome.folk +++ b/virtual-programs/_archive/welcome.folk diff --git a/virtual-programs/apriltags.folk b/virtual-programs/apriltags.folk index 7e190a5b..49b83e5a 100644 --- a/virtual-programs/apriltags.folk +++ b/virtual-programs/apriltags.folk @@ -3,21 +3,13 @@ if {$::isLaptop} return set tagfamily "tagStandard52h13" # Plain detector. Runs on entire camera frame. -set mainDetectorProcess [On process { +set mainDetectorProcess [Start process { source pi/AprilTags.tcl - # TODO: Clean this up. We retract these so that we don't bounce - # statements back to the main Folk process that it sends us. - Retract /anyone/ wishes $::thisProcess shares all wishes - Retract /anyone/ wishes $::thisProcess shares all claims Wish $::thisProcess receives statements like \ [list /someone/ claims the camera frame is /grayFrame/ at /timestamp/] Wish $::thisProcess shares statements like \ - [list /someone/ wishes /process/ receives statements like /pattern/] - Wish $::thisProcess shares statements like \ [list /someone/ claims /process/ detects tags /tags/ at /timestamp/ in time /aprilTime/] - Wish $::thisProcess shares statements like \ - [list /someone/ claims $::thisProcess has pid /pid/] set detector [AprilTags new $tagfamily] When the camera frame is /grayFrame/ at /timestamp/ { @@ -30,22 +22,14 @@ set mainDetectorProcess [On process { # Incremental detector. Looks at regions where there were tags in the # old camera frame. -On process { +Start process { source pi/AprilTags.tcl - # TODO: Clean this up. We retract these so that we don't bounce - # statements back to the main Folk process that it sends us. - Retract /anyone/ wishes $::thisProcess shares all wishes - Retract /anyone/ wishes $::thisProcess shares all claims Wish $::thisProcess receives statements like \ [list /someone/ claims the camera frame is /grayFrame/ at /timestamp/] Wish $::thisProcess receives statements like \ [list /someone/ claims $mainDetectorProcess detects tags /tags/ at /timestamp/ in time /aprilTime/] Wish $::thisProcess shares statements like \ - [list /someone/ wishes /process/ receives statements like /pattern/] - Wish $::thisProcess shares statements like \ - [list /someone/ claims $::thisProcess has pid /pid/] - Wish $::thisProcess shares statements like \ [list /someone/ claims $::thisProcess detects tags /tags/ at /timestamp/ in time /aprilTime/] Wish $::thisProcess shares statements like \ [list /someone/ wishes /something/ is labelled /text/] diff --git a/virtual-programs/camera.folk b/virtual-programs/camera.folk index 0e1710e7..f997d419 100644 --- a/virtual-programs/camera.folk +++ b/virtual-programs/camera.folk @@ -14,7 +14,10 @@ if {$::thisNode eq "folk-interact"} { set width $::Camera::WIDTH set height $::Camera::HEIGHT -On process { +Start process "camera" { + Wish $::thisProcess shares statements like \ + [list /someone/ claims the camera /...anything/] + source pi/Camera.tcl Camera::init $width $height diff --git a/virtual-programs/connections.folk b/virtual-programs/connections.folk index c66fba1d..4e002910 100644 --- a/virtual-programs/connections.folk +++ b/virtual-programs/connections.folk @@ -17,8 +17,8 @@ When /anyone/ wishes /source/ is connected to /sink/ & \ set angle [expr {atan2(-[lindex $direction 1], [lindex $direction 0]) - 3.14159/2}] set color grey - Display::stroke [list $source $sink ] 2 $color - shape 3 $c 30 $angle $color true + Wish to draw a stroke with points [list $source $sink] width 2 color $color + Wish to draw a shape with sides 3 center $c radius 30 radians $angle color $color filled true } set speed 75 @@ -40,7 +40,7 @@ When /anyone/ wishes /source/ is dynamically connected to /sink/ & \ lassign [vec2 scale [vec2 add $source $sink] 0.5] cx cy - Display::stroke [list $source $sink ] 1 white + Wish to draw a stroke with points [list $source $sink] width 1 color white When the clock time is /t/ { @@ -50,7 +50,7 @@ When /anyone/ wishes /source/ is dynamically connected to /sink/ & \ for {set p $offset} {$p < $distance} {incr p $spacing} { set c [vec2 add $source [vec2 scale $direction $p]] set s [expr {min($maxsize, 0.20*min($p, $distance - $p))}] - shape 3 $c $s $angle white true + Wish to draw a shape with sides 3 center $c radius $s radians $angle color white filled true } } } diff --git a/virtual-programs/display.folk b/virtual-programs/display.folk index da12bc83..2853c065 100644 --- a/virtual-programs/display.folk +++ b/virtual-programs/display.folk @@ -1,529 +1,185 @@ -namespace eval ::Display { - variable WIDTH - variable HEIGHT - variable LAYER 0 - if {$::isLaptop} { - set WIDTH 640; set HEIGHT 480 - } else { - regexp {mode "(\d+)x(\d+)"} [exec fbset] -> WIDTH HEIGHT - } - - proc drawOnTop {func args} { - set ::Display::LAYER 1 - uplevel [list $func {*}$args] - set ::Display::LAYER 0 - } - - # Create proxy versions of drawing primitives for the main Folk - # process (that will forward those draw commands to the display - # subprocess). - foreach func {stroke circle text fillTriangle fillQuad fillPolygon} { - proc $func args { - set func [lindex [info level 0] 0] - uplevel [list Wish display runs [list $func {*}$args] on layer $::Display::LAYER] - } - } - - Claim the display Display has width $::Display::WIDTH height $::Display::HEIGHT +namespace eval Colors { source "pi/Colors.tcl" } +proc ::getColor {color} { + if {[info exists Colors::$color]} { return [set Colors::$color] } \ + else { return $Colors::white } } -On process { +Start process "display" { puts "Display pid: [pid]" source pi/Gpu.tcl Gpu::init Gpu::ImageManager::imageManagerInit - namespace eval Display { - namespace eval Colors { source "pi/Colors.tcl" } - - variable rotate [Gpu::fn {vec2 v float a} vec2 { - float s = sin(a); - float c = cos(a); - mat2 m = mat2(c, s, -s, c); - return m * v; - }] - variable cross2d [Gpu::fn {vec2 a vec2 b} float { - return a.x*b.y - a.y*b.x; - }] - # See https://www.shadertoy.com/view/lsBSDm - variable invBilinear [Gpu::fn {vec2 p vec2 a vec2 b vec2 c vec2 d fn cross2d} vec2 { - vec2 res = vec2(-1.0); - - vec2 e = b-a; - vec2 f = d-a; - vec2 g = a-b+c-d; - vec2 h = p-a; - - float k2 = cross2d( g, f ); - float k1 = cross2d( e, f ) + cross2d( h, g ); - float k0 = cross2d( h, e ); - - // if edges are parallel, this is a linear equation - k2 /= k0; k1 /= k0; k0 = 1.0; - if( abs(k2)<0.001*abs(k0) ) + # TODO: Share these functions through the database somehow, + # instead of keeping them in globals. + + set ::rotate [Gpu::fn {vec2 v float a} vec2 { + float s = sin(a); + float c = cos(a); + mat2 m = mat2(c, s, -s, c); + return m * v; + }] + set ::cross2d [Gpu::fn {vec2 a vec2 b} float { + return a.x*b.y - a.y*b.x; + }] + # See https://www.shadertoy.com/view/lsBSDm + set ::invBilinear [Gpu::fn {vec2 p vec2 a vec2 b vec2 c vec2 d fn ::cross2d} vec2 { + vec2 res = vec2(-1.0); + + vec2 e = b-a; + vec2 f = d-a; + vec2 g = a-b+c-d; + vec2 h = p-a; + + float k2 = cross2d( g, f ); + float k1 = cross2d( e, f ) + cross2d( h, g ); + float k0 = cross2d( h, e ); + + // if edges are parallel, this is a linear equation + k2 /= k0; k1 /= k0; k0 = 1.0; + if( abs(k2)<0.001*abs(k0) ) + { + res = vec2( (h.x*k1+f.x*k0)/(e.x*k1-g.x*k0), -k0/k1 ); + } + // otherwise, it's a quadratic + else + { + float w = k1*k1 - 4.0*k0*k2; + if( w<0.0 ) return vec2(-1.0); + w = sqrt( w ); + + float ik2 = 0.5/k2; + float v = (-k1 - w)*ik2; + float u = (h.x - f.x*v)/(e.x + g.x*v); + + if( u<0.0 || u>1.0 || v<0.0 || v>1.0 ) { - res = vec2( (h.x*k1+f.x*k0)/(e.x*k1-g.x*k0), -k0/k1 ); - } - // otherwise, it's a quadratic - else - { - float w = k1*k1 - 4.0*k0*k2; - if( w<0.0 ) return vec2(-1.0); - w = sqrt( w ); - - float ik2 = 0.5/k2; - float v = (-k1 - w)*ik2; - float u = (h.x - f.x*v)/(e.x + g.x*v); - - if( u<0.0 || u>1.0 || v<0.0 || v>1.0 ) - { - v = (-k1 + w)*ik2; - u = (h.x - f.x*v)/(e.x + g.x*v); - } - res = vec2( u, v ); - } - return res; - }] - - namespace eval font { - proc load {name} { - set csvFd [open "vendor/fonts/$name.csv" r]; set csv [read $csvFd]; close $csvFd - set glyphInfos [dict create] - foreach line [split $csv "\n"] { - set info [lassign [split $line ,] glyph] - lassign $info advance \ - planeLeft planeBottom planeRight planeTop \ - atlasLeft atlasBottom atlasRight atlasTop - dict set glyphInfos $glyph \ - [list $advance \ - [list $planeLeft $planeBottom $planeRight $planeTop] \ - [list $atlasLeft $atlasBottom $atlasRight $atlasTop]] - } - - set im [image load "[pwd]/vendor/fonts/$name.png"] - set gim [Gpu::ImageManager::copyImageToGpu $im] - - return [list $glyphInfos $im $gim] - } - proc hasGlyphInfo {font charCode} { dict exists [lindex $font 0] $charCode } - proc glyphInfo {font charCode} { dict get [lindex $font 0] $charCode } - proc atlasImage {font} { lindex $font 1 } - proc gpuAtlasImage {font} { lindex $font 2 } - - namespace export * - namespace ensemble create - } - variable glyphMsd [Gpu::fn {sampler2D atlas vec4 atlasGlyphBounds vec2 glyphUv} vec4 { - vec2 atlasUv = mix(atlasGlyphBounds.xw, atlasGlyphBounds.zy, glyphUv); - return texture(atlas, vec2(atlasUv.x, 1.0-atlasUv.y)); - }] - variable median [Gpu::fn {float r float g float b} float { - return max(min(r, g), min(max(r, g), b)); - }] - variable glyph [Gpu::pipeline {sampler2D atlas vec2 atlasSize - vec4 atlasGlyphBounds - vec4 planeGlyphBounds - vec2 pos float radians float em - fn rotate} { - float left = planeGlyphBounds[0] * em; - float bottom = planeGlyphBounds[1] * em; - float right = planeGlyphBounds[2] * em; - float top = planeGlyphBounds[3] * em; - vec2 a = pos + rotate(vec2(left, -top), -radians); - vec2 b = pos + rotate(vec2(right, -top), -radians); - vec2 c = pos + rotate(vec2(right, -bottom), -radians); - vec2 d = pos + rotate(vec2(left, -bottom), -radians); - - vec2 vertices[4] = vec2[4](a, b, d, c); - return vertices[gl_VertexIndex]; - } {fn rotate fn invBilinear fn glyphMsd fn median} { - float left = planeGlyphBounds[0] * em; - float bottom = planeGlyphBounds[1] * em; - float right = planeGlyphBounds[2] * em; - float top = planeGlyphBounds[3] * em; - vec2 a = pos + rotate(vec2(left, -top), -radians); - vec2 b = pos + rotate(vec2(right, -top), -radians); - vec2 c = pos + rotate(vec2(right, -bottom), -radians); - vec2 d = pos + rotate(vec2(left, -bottom), -radians); - - vec2 glyphUv = invBilinear(gl_FragCoord.xy, a, b, c, d); - if( max( abs(glyphUv.x-0.5), abs(glyphUv.y-0.5))>=0.5 ) { - return vec4(0, 0, 0, 0); - } - vec3 msd = glyphMsd(atlas, atlasGlyphBounds/atlasSize.xyxy, glyphUv).rgb; - float sd = median(msd.r, msd.g, msd.b); - float screenPxDistance = 4.5*(sd - 0.5); - float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0); - return mix(vec4(0, 0, 0, 0), vec4(1, 1, 1, 1), opacity); - }] - - proc start {} { Gpu::drawStart } - - proc getColor {color} { - if {[info exists Colors::$color]} { return [set Colors::$color] } \ - else { return $Colors::white } - } - - variable line [Gpu::pipeline {vec2 from vec2 to float thickness vec4 color} { - vec2 vertices[4] = vec2[4]( - min(from, to) - thickness, - vec2(max(from.x, to.x) + thickness, min(from.y, to.y) - thickness), - vec2(min(from.x, to.x) - thickness, max(from.y, to.y) + thickness), - max(from, to) + thickness - ); - return vertices[gl_VertexIndex]; - } { - float l = length(to - from); - vec2 d = (to - from) / l; - vec2 q = (gl_FragCoord.xy - (from + to)*0.5); - q = mat2(d.x, -d.y, d.y, d.x) * q; - q = abs(q) - vec2(l, thickness)*0.5; - float dist = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0); - - return dist < 0.0 ? color : vec4(0, 0, 0, 0); - }] - proc stroke {points width color} { - variable line - for {set i 0} {$i < [expr {[llength $points] - 1}]} {incr i} { - set from [lindex $points $i] - set to [lindex $points [expr $i+1]] - Gpu::draw $line $from $to $width [getColor $color] - } - } - - variable arc [Gpu::pipeline {vec2 center float start float arclen float radius float thickness vec4 color} { - float r = radius + thickness; - vec2 vertices[4] = vec2[4]( - center - r, - vec2(center.x + r, center.y - r), - vec2(center.x - r, center.y + r), - center + r - ); - return vertices[gl_VertexIndex]; - } { - #define M_TWO_PI 6.283185307179586 - start = clamp(start, 0, M_TWO_PI); - arclen = clamp(arclen, 0, M_TWO_PI); - - float dist = length(gl_FragCoord.xy - center) - radius; - float angle = atan(-(gl_FragCoord.y - center.y), gl_FragCoord.x - center.x); - - // Shift angle from [-pi, pi) to [0, 2*pi] - angle = (angle < 0) ? (angle + M_TWO_PI) : angle; - float end = start + arclen; - - return ((dist < thickness && dist > 0.0) && - ((end < M_TWO_PI && angle > start && angle < end) || - (end >= M_TWO_PI && (angle > start || angle < end-M_TWO_PI)))) ? color : vec4(0, 0, 0, 0); - - }] - - proc arc {x y start arclen radius thickness color} { - variable arc - Gpu::draw $arc [list $x $y] $start $arclen $radius $thickness [getColor $color] - } - - # TODO: Fix bool support. - variable circle [Gpu::pipeline {vec2 center float radius float thickness vec4 color int filled} { - float r = radius + thickness; - vec2 vertices[4] = vec2[4]( - center - r, - vec2(center.x + r, center.y - r), - vec2(center.x - r, center.y + r), - center + r - ); - return vertices[gl_VertexIndex]; - } { - float dist = length(gl_FragCoord.xy - center) - radius; - if (filled == 1) { - return (dist < thickness) ? color : vec4(0, 0, 0, 0); - } else { - return (dist < thickness && dist > 0.0) ? color : vec4(0, 0, 0, 0); - } - }] - proc circle {x y radius thickness color {filled false}} { - variable circle - Gpu::draw $circle [list $x $y] $radius $thickness [getColor $color] [ne $filled "false"] - } - - set ::FontCache [dict create] - - # load all fonts into the fontCache - foreach fontPath [list {*}[glob {*}vendor/fonts/*.png]] { - set fontName "" - regexp {vendor/fonts/(.*).png} $fontPath whole_match fontName - if {!($fontName eq "")} { - puts "Loaded $fontName into font cache" - set fontdata [font load $fontName] - dict set ::FontCache $fontName $fontdata - } - } - - proc textExtent {text scale {font "PTSans-Regular"}} { - if {!([dict exists $::FontCache $font])} { - throw {DISPLAY FONT {font doesn't exist}} "$font doesn't exist" - return - } - set font [dict get $::FontCache $font] - - set em [* $scale 25.0] - set x 0; set y 0 - set width 0 - for {set i 0} {$i < [string length $text]} {incr i} { - set char [string index $text $i] - if {$char eq "\n"} { - set y [+ $y $em]; set x 0; continue - } - set charCode [scan $char %c] - if {[font hasGlyphInfo $font $charCode]} { - set glyphInfo [font glyphInfo $font $charCode] - } else { - set glyphInfo [font glyphInfo $font [scan ? %c]] - } - lassign $glyphInfo advance planeBounds atlasBounds - set x [+ $x [* $advance $em]] - if {$x > $width} { set width $x } - } - return [list $width [+ $y $em]] - } - proc text {x0 y0 scale text radians {font "PTSans-Regular"}} { - if {!([dict exists $::FontCache $font])} { - throw {DISPLAY FONT {font doesn't exist}} "$font doesn't exist" - return - } - set font [dict get $::FontCache $font] - - - variable glyph - set fontAtlas [font gpuAtlasImage $font] - set fontAtlasSize [list [::image width [font atlasImage $font]] \ - [::image height [font atlasImage $font]]] - - set extent [vec2 rotate [textExtent $text $scale] $radians] - - set em [* $scale 25.0] - - # TODO: Add text alignment/anchor options (right now, this - # setup centers the text). - set x0 [expr {$x0 - [lindex $extent 0]/2}] - set y0 [expr {$y0 - [lindex $extent 1]/2}] - set x $x0; set y $y0 - - set lineNum 0 - for {set i 0} {$i < [string length $text]} {incr i} { - set char [string index $text $i] - if {$char eq "\n"} { - incr lineNum - lassign [vec2 add [list $x0 $y0] \ - [vec2 rotate [list 0 [* $lineNum $em]] $radians]] x y - continue - } - set charCode [scan $char %c] - if {[font hasGlyphInfo $font $charCode]} { - set glyphInfo [font glyphInfo $font $charCode] - } else { - set glyphInfo [font glyphInfo $font [scan ? %c]] - } - lassign $glyphInfo advance planeBounds atlasBounds - if {$char ne " "} { - Gpu::draw $glyph $fontAtlas $fontAtlasSize \ - $atlasBounds $planeBounds [list $x $y] $radians $em - } - lassign [vec2 add [list $x $y] \ - [vec2 rotate [list [* $advance $em] 0] $radians]] x y + v = (-k1 + w)*ik2; + u = (h.x - f.x*v)/(e.x + g.x*v); } + res = vec2( u, v ); } + return res; + }] - variable fillTriangle [Gpu::pipeline {vec2 p0 vec2 p1 vec2 p2 vec4 color} { - vec2 vertices[4] = vec2[4](p0, p1, p2, p0); - return vertices[gl_VertexIndex]; - } { - return color; - }] - proc fillTriangle {p0 p1 p2 color} { - variable fillTriangle - Gpu::draw $fillTriangle $p0 $p1 $p2 [getColor $color] - } - - proc fillQuad {p0 p1 p2 p3 color} { - fillTriangle $p1 $p2 $p3 $color - fillTriangle $p0 $p1 $p3 $color - } - - proc fillPolygon {points color} { - set num_points [llength $points] - if {$num_points < 3} { - error "At least 3 points are required to form a polygon." - } elseif {$num_points == 3} { - eval fillTriangle $points $color - } elseif {$num_points == 4} { - eval fillQuad $points $color - } else { - # Get the first point in the list as the "base" point of the triangles - set p0 [lindex $points 0] - - for {set i 1} {$i < $num_points - 1} {incr i} { - set p1 [lindex $points $i] - set p2 [lindex $points [expr {$i+1}]] - fillTriangle $p0 $p1 $p2 $color - } - } - } - - variable image [Gpu::pipeline {sampler2D image vec2 imageSize - vec2 pos float radians float scale - fn rotate} { - vec2 a = pos + rotate(-imageSize/2, -radians); - vec2 b = pos + rotate(vec2(imageSize.x, -imageSize.y)/2, -radians); - vec2 c = pos + rotate(imageSize/2, -radians); - vec2 d = pos + rotate(vec2(-imageSize.x, imageSize.y)/2, -radians); - vec2 vertices[4] = vec2[4](a, b, d, c); - return vertices[gl_VertexIndex]; - } {fn invBilinear fn rotate} { - vec2 a = pos + rotate(-imageSize/2, -radians); - vec2 b = pos + rotate(vec2(imageSize.x, -imageSize.y)/2, -radians); - vec2 c = pos + rotate(imageSize/2, -radians); - vec2 d = pos + rotate(vec2(-imageSize.x, imageSize.y)/2, -radians); - vec2 p = gl_FragCoord.xy; - vec2 uv = invBilinear(p, a, b, c, d); - if( max( abs(uv.x-0.5), abs(uv.y-0.5))<0.5 ) { - return texture(image, uv); - } - return vec4(0.0, 0.0, 0.0, 0.0); - }] - variable imCache [dict create] - variable IMCACHE_MAX_IMAGES [- $Gpu::ImageManager::GPU_MAX_IMAGES [dict size $::FontCache]] - proc checkImCacheAndCopyIfNeeded {imDrawSet} { - variable imCache - variable IMCACHE_MAX_IMAGES - - dict for {im v} $imDrawSet { - if {![dict exists $imCache $im]} { continue } - # Check for staleness and remove from cache if so. - lassign [dict get $imCache $im] gim expectedVersion - set version [Heap::folkHeapGetVersion [string map {uint8_t void} [::image_t data_ptr $im]]] - if {$expectedVersion != $version} { - Gpu::ImageManager::freeGpuImage $gim - dict unset imCache $im - } - } - - set notInCache [dictset difference $imDrawSet $imCache] - set notInDrawSet [dictset difference $imCache $imDrawSet] - - set numImagesToCopy [dictset size $notInCache] - if {$numImagesToCopy > 0} { - if {[dictset size $imCache] + $numImagesToCopy > $IMCACHE_MAX_IMAGES} { - set numImagesToEvict \ - [expr {[dictset size $imCache] + $numImagesToCopy - $IMCACHE_MAX_IMAGES}] - - # What can we safely evict? - # - Anything that's stale - # - Anything that's not in use - set numImagesEvicted 0 - dict for {im v} $imCache { - if {$numImagesEvicted == $numImagesToEvict} { break } - - lassign $v gim expectedVersion - set version [Heap::folkHeapGetVersion [string map {uint8_t void} [::image_t data_ptr $im]]] - if {$expectedVersion != $version} { - Gpu::ImageManager::freeGpuImage $gim - dict unset imCache $im - incr numImagesEvicted - continue - } - if {![dict exists $imDrawSet $im]} { - Gpu::ImageManager::freeGpuImage $gim - dict unset imCache $im - incr numImagesEvicted - } - } - } - - dict for {im _} $notInCache { - # TODO: This is unsafe (has a race condition) -- we're - # not locking the image, so version and gim may diverge. - set version [Heap::folkHeapGetVersion [string map {uint8_t void} [::image_t data_ptr $im]]] - set gim [Gpu::ImageManager::copyImageToGpu $im] - dict set imCache $im [list $gim $version] - } - } - } - proc image {x y im radians {scale 1.0}} { - # TODO: Implement scale. - variable image - variable imCache - lassign [dict get $imCache $im] gim - Gpu::draw $image $gim [list [::image width $im] [::image height $im]] \ - [list $x $y] $radians $scale - } - - proc end {} { Gpu::drawEnd; Gpu::poll } + namespace eval Colors { source "pi/Colors.tcl" } + proc ::getColor {color} { + if {[info exists Colors::$color]} { return [set Colors::$color] } \ + else { return $Colors::white } } - # TODO: Clean this up. We retract these so that we don't bounce - # statements back to the main Folk process that it sends us. - Retract /anyone/ wishes $::thisProcess shares all wishes - Retract /anyone/ wishes $::thisProcess shares all claims - Wish $::thisProcess shares statements like \ - [list /someone/ wishes /process/ receives statements like /pattern/] - Wish $::thisProcess shares statements like \ - [list /someone/ claims $::thisProcess has pid /pid/] Wish $::thisProcess receives statements like \ - [list /someone/ wishes display runs /command/ on layer /layer/] - Wish $::thisProcess receives statements like \ - [list /someone/ wishes display runs /command/] + [list /someone/ wishes the GPU /...anything/] Wish $::thisProcess shares statements like \ [list /someone/ claims the display time is /displayTime/] + Wish $::thisProcess shares statements like \ + [list /someone/ has error /err/ with info /errorInfo/] - while true { - set displayList [list] - foreach match [Statements::findMatches {/someone/ wishes display runs /command/ on layer /layer/}] { - lappend displayList [list [dict get $match layer] [dict get $match command]] - } - foreach match [Statements::findMatches {/someone/ wishes display runs /command/}] { - lappend displayList [list 0 [dict get $match command]] - } - - proc lcomp {a b} { - set layerA [lindex $a 0] - set layerB [lindex $b 0] - if {$layerA == $layerB} { - expr {[lindex $a 1 0] == "Display::text"} - } else { - expr {$layerA - $layerB} - } + set ::pipelines [dict create] + When /wisher/ wishes the GPU compiles pipeline /name/ /source/ { + try { + dict set ::pipelines $name [Gpu::pipeline {*}$source] + } on error e { + puts stderr $::errorInfo + Say $wisher has error $e with info $::errorInfo } + } - set displayCommands [lmap sublist [lsort -command lcomp $displayList] {lindex $sublist 1}] - - set imDrawSet [dictset create] - foreach command $displayCommands { - if {[lindex $command 0] eq "Display::image"} { - set im [lindex $command 3] - dictset add imDrawSet $im + while true { + set ::displayList [dict create] ;# Keys are layer numbers; values are lists of commands + Step + foreach match [Statements::findMatches {/wisher/ wishes the GPU draws pipeline /name/ with /...options/}] { + try { + set name [dict get $match name] + set pipeline [dict get $::pipelines [dict get $match name]] + + set options [dict get $match options] + set layer [dict_getdef $options layer 0] + if {[dict exists $options instances]} { + set instances [dict get $options instances] + } else { + set instances [list [dict get $options arguments]] + } + foreach instance $instances { + dict lappend ::displayList $layer [list Gpu::draw $pipeline {*}$instance] + } + } on error e { + Say [dict get $match wisher] has error $e with info $::errorInfo } } - Display::checkImCacheAndCopyIfNeeded $imDrawSet set renderTime [baretime { - Display::start - foreach command $displayCommands { - try { {*}$command } \ - on error e { puts stderr $::errorInfo } + Gpu::drawStart + foreach layer [lsort -real [dict keys $::displayList]] { + set layerDisplayList [dict get $::displayList $layer] + foreach displayCommand $layerDisplayList { + try { {*}$displayCommand } \ + on error e { puts stderr $::errorInfo } + } } - Display::end + Gpu::drawEnd }] Commit { Claim the display time is "render $renderTime us ($::stepTime)" } - Step } } -set ::Display::displayTime none set ::displayTime none When the display time is /displayTime/ { set ::Display::displayTime $displayTime set ::displayTime $displayTime } + +namespace eval ::Display { + variable displayTime none + + variable WIDTH + variable HEIGHT + variable LAYER 0 + if {$::isLaptop} { + set WIDTH 640; set HEIGHT 480 + } else { + regexp {mode "(\d+)x(\d+)"} [exec fbset] -> WIDTH HEIGHT + } + + # TODO: Remove these / expel them to a shim page; these are only + # for backward compatibility now. + proc warnDeprecated {} { + upvar 2 this this + if {![info exists this]} {set this "<unknown>"} + set func [lindex [info level -1] 0] + set w "$func was deprecated in Oct 2023; use GPU wishes/shaders" + Claim $this has warning $w with info $w + } + proc stroke {points width color} { + warnDeprecated + Wish to draw a stroke with points $points width $width color $color + } + proc circle {x y radius thickness color {filled false}} { + warnDeprecated + Wish to draw a circle with x $x y $y radius $radius thickness $thickness color $color filled $filled + } + proc text {x0 y0 scale text radians {font "PTSans-Regular"}} { + warnDeprecated + Wish to draw text with x $x0 y $y0 scale $scale text $text radians $radians font $font + } + proc fillTriangle {p0 p1 p2 color} { + warnDeprecated + Wish to draw a triangle with p0 $p0 p1 $p1 p2 $p2 color $color + } + proc fillQuad {p0 p1 p2 p3 color} { + warnDeprecated + Wish to draw a quad with p0 $p0 p1 $p1 p2 $p2 p3 $p3 color $color + } + proc fillPolygon {points color} { + warnDeprecated + Wish to draw a polygon with points $points color $color + } + proc image {x y im radians {scale 1.0}} { + warnDeprecated + Wish to draw an image with x $x y $y image $im radians $radians scale $scale + } + + Claim the display Display has width $::Display::WIDTH height $::Display::HEIGHT +} diff --git a/virtual-programs/display/arc.folk b/virtual-programs/display/arc.folk new file mode 100644 index 00000000..e2412bf0 --- /dev/null +++ b/virtual-programs/display/arc.folk @@ -0,0 +1,39 @@ +# Example: +# When $this has region /r/ { +# lassign [region centroid $r] x y +# Wish to draw an arc with x $x y $y start 0 arclen 1 thickness 3 radius 100 color green +# } + +Wish the GPU compiles pipeline "arc" {{vec2 center float start float arclen float radius float thickness vec4 color} { + float r = radius + thickness; + vec2 vertices[4] = vec2[4]( + center - r, + vec2(center.x + r, center.y - r), + vec2(center.x - r, center.y + r), + center + r + ); + return vertices[gl_VertexIndex]; +} { + #define M_TWO_PI 6.283185307179586 + start = clamp(start, 0, M_TWO_PI); + arclen = clamp(arclen, 0, M_TWO_PI); + + float dist = length(gl_FragCoord.xy - center) - radius; + float angle = atan(-(gl_FragCoord.y - center.y), gl_FragCoord.x - center.x); + + // Shift angle from [-pi, pi) to [0, 2*pi] + angle = (angle < 0) ? (angle + M_TWO_PI) : angle; + float end = start + arclen; + + return ((dist < thickness && dist > 0.0) && + ((end < M_TWO_PI && angle > start && angle < end) || + (end >= M_TWO_PI && (angle > start || angle < end-M_TWO_PI)))) ? color : vec4(0, 0, 0, 0); + +}} + +When /someone/ wishes to draw an arc with /...options/ { + dict with options { + Wish the GPU draws pipeline "arc" with arguments \ + [list [list $x $y] $start $arclen $radius $thickness [getColor $color]] + } +} diff --git a/virtual-programs/display/circle.folk b/virtual-programs/display/circle.folk new file mode 100644 index 00000000..03e4def7 --- /dev/null +++ b/virtual-programs/display/circle.folk @@ -0,0 +1,31 @@ +Wish the GPU compiles pipeline "circle" { + {vec2 center float radius float thickness vec4 color int filled} { + float r = radius + thickness; + vec2 vertices[4] = vec2[4]( + center - r, + vec2(center.x + r, center.y - r), + vec2(center.x - r, center.y + r), + center + r + ); + return vertices[gl_VertexIndex]; + } { + float dist = length(gl_FragCoord.xy - center) - radius; + if (filled == 1) { + return (dist < thickness) ? color : vec4(0, 0, 0, 0); + } else { + return (dist < thickness && dist > 0.0) ? color : vec4(0, 0, 0, 0); + } + } +} + +When /someone/ wishes to draw a circle with /...options/ { + set center [dict_getdef $options center ""] + if {$center eq ""} { set center [list [dict get $options x] [dict get $options y]] } + set radius [dict get $options radius] + set thickness [dict get $options thickness] + set color [getColor [dict get $options color]] + set filled [dict_getdef $options filled false] + + Wish the GPU draws pipeline "circle" with arguments \ + [list $center $radius $thickness $color [expr {$filled eq false ? 0 : 1}]] +} diff --git a/virtual-programs/display/fill.folk b/virtual-programs/display/fill.folk new file mode 100644 index 00000000..f3057068 --- /dev/null +++ b/virtual-programs/display/fill.folk @@ -0,0 +1,51 @@ +Wish the GPU compiles pipeline "fillTriangle" { + {vec2 p0 vec2 p1 vec2 p2 vec4 color} { + vec2 vertices[4] = vec2[4](p0, p1, p2, p0); + return vertices[gl_VertexIndex]; + } { + return color; + } +} + +When /someone/ wishes to draw a triangle with /...options/ { + dict with options { + Wish the GPU draws pipeline "fillTriangle" with arguments \ + [list $p0 $p1 $p2 [getColor $color]] + } +} +When /someone/ wishes to draw a quad with /...options/ { + dict with options { + if {![info exists layer]} { set layer 0 } + Wish the GPU draws pipeline "fillTriangle" with arguments \ + [list $p1 $p2 $p3 [getColor $color]] layer $layer + Wish the GPU draws pipeline "fillTriangle" with arguments \ + [list $p0 $p1 $p3 [getColor $color]] layer $layer + } +} +When /someone/ wishes to draw a polygon with /...options/ { + set points [dict get $options points] + set color [dict get $options color] + + set num_points [llength $points] + if {$num_points < 3} { + error "At least 3 points are required to form a polygon." + } elseif {$num_points == 3} { + Wish to draw a triangle with \ + p0 [lindex $points 0] p1 [lindex $points 1] p2 [lindex $points 2] \ + color $color + } elseif {$num_points == 4} { + Wish to draw a quad with \ + p0 [lindex $points 0] p1 [lindex $points 1] p2 [lindex $points 2] p3 [lindex $points 3] \ + color $color + } else { + # Get the first point in the list as the "base" point of the triangles + set p0 [lindex $points 0] + + for {set i 1} {$i < $num_points - 1} {incr i} { + set p1 [lindex $points $i] + set p2 [lindex $points [expr {$i+1}]] + Wish the GPU draws pipeline "fillTriangle" with arguments \ + [list $p0 $p1 $p2 [getColor $color]] + } + } +} diff --git a/virtual-programs/display/fns.folk b/virtual-programs/display/fns.folk new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/virtual-programs/display/fns.folk diff --git a/virtual-programs/display/image.folk b/virtual-programs/display/image.folk new file mode 100644 index 00000000..23246d45 --- /dev/null +++ b/virtual-programs/display/image.folk @@ -0,0 +1,118 @@ +On process "display" { + set invBilinear $::invBilinear + set rotate $::rotate + # TODO: Do this with a wish, instead of hard-coding the global dict. + dict set ::pipelines "image" [Gpu::pipeline {sampler2D image vec2 imageSize + vec2 pos float radians float scale + fn rotate} { + vec2 a = pos + rotate(-imageSize/2, -radians); + vec2 b = pos + rotate(vec2(imageSize.x, -imageSize.y)/2, -radians); + vec2 c = pos + rotate(imageSize/2, -radians); + vec2 d = pos + rotate(vec2(-imageSize.x, imageSize.y)/2, -radians); + vec2 vertices[4] = vec2[4](a, b, d, c); + return vertices[gl_VertexIndex]; + } {fn invBilinear fn rotate} { + vec2 a = pos + rotate(-imageSize/2, -radians); + vec2 b = pos + rotate(vec2(imageSize.x, -imageSize.y)/2, -radians); + vec2 c = pos + rotate(imageSize/2, -radians); + vec2 d = pos + rotate(vec2(-imageSize.x, imageSize.y)/2, -radians); + vec2 p = gl_FragCoord.xy; + vec2 uv = invBilinear(p, a, b, c, d); + if( max( abs(uv.x-0.5), abs(uv.y-0.5))<0.5 ) { + return texture(image, uv); + } + return vec4(0.0, 0.0, 0.0, 0.0); + }] + + When the GPU has loaded /nfonts/ fonts { + namespace eval ::ImageCache { + # Backing store: stores pairs of (GPU image handle, heap slot version). + variable cache [dict create] + variable CACHE_MAX_SIZE [- $Gpu::ImageManager::GPU_MAX_IMAGES [uplevel {set nfonts}]] + + proc getOrInsert {im} { + variable cache + variable CACHE_MAX_SIZE + if {[dict exists $cache $im]} { + lassign [dict get $cache $im] gim cachedVersion + set version [Heap::folkHeapGetVersion [string map {uint8_t void} [::image_t data_ptr $im]]] + if {$version == $cachedVersion} { + # Bump this image to end of cache since it's + # most-recently-accessed. + dict unset cache $im + dict set cache $im [list $gim $cachedVersion] + return $gim + } else { + # This image is stale. Don't retain it. + remove $im + } + } + if {[dict size $cache] >= $CACHE_MAX_SIZE} { + evict + } + if {[dict size $cache] >= $CACHE_MAX_SIZE} { + puts stderr "image: Warning: Out of slots in GPU image cache." + } + set version [Heap::folkHeapGetVersion [string map {uint8_t void} [::image_t data_ptr $im]]] + set gim [Gpu::ImageManager::copyImageToGpu $im] + dict set cache $im [list $gim $version] + return $gim + } + + proc evict {} { + variable cache + variable CACHE_MAX_SIZE + set numToEvict [expr {([dict size $cache] + 1) - $CACHE_MAX_SIZE}] + set numEvicted [list] + # Evict stale. + dict for {im v} $cache { + lassign $v gim expectedVersion + set version [Heap::folkHeapGetVersion [string map {uint8_t void} [::image_t data_ptr $im]]] + if {$expectedVersion != $version} { + Gpu::ImageManager::freeGpuImage $gim + lappend numEvicted $im + } + } + foreach im $numEvicted { dict unset cache $im } + # Evict old. + dict for {im v} $cache { + if {$numToEvict - [llength $numEvicted] <= 0} { + break + } + lassign $v gim + Gpu::ImageManager::freeGpuImage $gim + lappend numEvicted $im + } + foreach im $numEvicted { dict unset cache $im } + } + + proc remove {im} { + variable cache + if {[dict exists $cache $im]} { + lassign [dict get $cache $im] gim + Gpu::ImageManager::freeGpuImage $gim + } + dict unset cache $im + } + } + } + + Wish $::thisProcess receives statements like \ + [list /someone/ wishes to draw an image with /...options/] + When /someone/ wishes to draw an image with /...options/ { + if {[dict exists $options center]} { + set center [dict get $options center] + } else { + set center [list [dict get $options x] [dict get $options y]] + } + set im [dict get $options image] + set radians [dict get $options radians] + set scale [dict_getdef $options scale 1.0] + + set gim [ImageCache::getOrInsert $im] + + Wish the GPU draws pipeline "image" with arguments \ + [list $gim [list [image_t width $im] [image_t height $im]] \ + $center $radians $scale] + } +} diff --git a/virtual-programs/display/stroke.folk b/virtual-programs/display/stroke.folk new file mode 100644 index 00000000..f212f471 --- /dev/null +++ b/virtual-programs/display/stroke.folk @@ -0,0 +1,34 @@ +Wish the GPU compiles pipeline "line" { + {vec2 from vec2 to float thickness vec4 color} { + vec2 vertices[4] = vec2[4]( + min(from, to) - thickness, + vec2(max(from.x, to.x) + thickness, min(from.y, to.y) - thickness), + vec2(min(from.x, to.x) - thickness, max(from.y, to.y) + thickness), + max(from, to) + thickness + ); + return vertices[gl_VertexIndex]; + } { + float l = length(to - from); + vec2 d = (to - from) / l; + vec2 q = (gl_FragCoord.xy - (from + to)*0.5); + q = mat2(d.x, -d.y, d.y, d.x) * q; + q = abs(q) - vec2(l, thickness)*0.5; + float dist = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0); + + return dist < 0.0 ? color : vec4(0, 0, 0, 0); + } +} + +When /someone/ wishes to draw a stroke with /...options/ { + set points [dict get $options points] + set width [dict get $options width] + set color [getColor [dict get $options color]] + + set instances [list] + for {set i 0} {$i < [expr {[llength $points] - 1}]} {incr i} { + set from [lindex $points $i] + set to [lindex $points [expr $i+1]] + lappend instances [list $from $to $width $color] + } + Wish the GPU draws pipeline "line" with instances $instances +} diff --git a/virtual-programs/display/text.folk b/virtual-programs/display/text.folk new file mode 100644 index 00000000..858a9dc7 --- /dev/null +++ b/virtual-programs/display/text.folk @@ -0,0 +1,207 @@ +On process "display" { + namespace eval font { + set cc [c create] + $cc include <math.h> + defineImageType $cc + $cc struct Font { + image_t atlasImage; + int gpuAtlasImage; + // TODO: This only handles ASCII, obviously. + Tcl_Obj* glyphInfos[128]; + } + + proc load {name} { + set csvFd [open "vendor/fonts/$name.csv" r]; set csv [read $csvFd]; close $csvFd + set fields [list ] + # HACK: Create list of null glyphs to initialize. + set glyphInfos [list] + for {set i 0} {$i < 128} {incr i} { + lappend glyphInfos {} + } + + foreach line [split $csv "\n"] { + set values [lassign [split $line ,] glyph] + if {![string is integer -strict $glyph]} { continue } + + lassign $values advance \ + planeLeft planeBottom planeRight planeTop \ + atlasLeft atlasBottom atlasRight atlasTop + lset glyphInfos $glyph \ + [list $advance \ + [list $planeLeft $planeBottom $planeRight $planeTop] \ + [list $atlasLeft $atlasBottom $atlasRight $atlasTop]] + } + + set im [image load "[pwd]/vendor/fonts/$name.png"] + set gim [Gpu::ImageManager::copyImageToGpu $im] + + return [dict create atlasImage $im gpuAtlasImage $gim glyphInfos $glyphInfos] + } + $cc struct vec2f { float x; float y; } + $cc proc vec2f_add {vec2f a vec2f b} vec2f { + return (vec2f) { a.x + b.x, a.y + b.y }; + } + $cc proc vec2f_rotate {vec2f a float radians} vec2f { + return (vec2f) { + a.x*cosf(radians) + a.y*sinf(radians), + -a.x*sinf(radians) + a.y*cosf(radians) + }; + } + $cc proc textExtent {Font* font char* text float scale} vec2f { + float em = scale * 25.0; + float x = 0; float y = 0; + float width = 0; + for (int i = 0; text[i] != 0; i++) { + int ch = text[i]; + if (ch == '\n') { + y = y + em; x = 0; continue; + } + if (ch >= sizeof(font->glyphInfos)/sizeof(font->glyphInfos[0])) { + ch = '?'; + } + Tcl_Obj* glyphInfo = font->glyphInfos[ch]; + Tcl_Obj* advanceObj; Tcl_ListObjIndex(NULL, glyphInfo, 0, &advanceObj); + double advance; Tcl_GetDoubleFromObj(NULL, advanceObj, &advance); + x = x + advance * em; + if (x > width) { width = x; } + } + return (vec2f) { width, y + em }; + } + $cc proc textShape {Font* font char* text + float x0 float y0 float scale float radians} Tcl_Obj* { + Tcl_Obj* gpuAtlasImageSize = Tcl_ObjPrintf("%d %d", font->atlasImage.width, font->atlasImage.height); + + vec2f extent = vec2f_rotate(textExtent(font, text, scale), radians); + float em = scale * 25.0; + + vec2f p0 = { x0 - extent.x/2.0, y0 - extent.y/2.0 }; + vec2f p = p0; + + int lineNum = 0; + Tcl_Obj* instances = Tcl_NewListObj(0, NULL); // List of instances. + for (int i = 0; text[i] != 0; i++) { + int ch = text[i]; + if (ch == '\n') { + lineNum++; + p = vec2f_add(p0, vec2f_rotate((vec2f) {0, lineNum * em}, radians)); + continue; + } + if (ch >= sizeof(font->glyphInfos)/sizeof(font->glyphInfos[0])) { + ch = '?'; + } + Tcl_Obj* glyphInfo = font->glyphInfos[ch]; + Tcl_Obj* advanceObj; Tcl_ListObjIndex(NULL, glyphInfo, 0, &advanceObj); + double advance; Tcl_GetDoubleFromObj(NULL, advanceObj, &advance); + if (ch != ' ') { + // Append to list of instances. + Tcl_Obj* planeBounds; Tcl_ListObjIndex(NULL, glyphInfo, 1, &planeBounds); + Tcl_Obj* atlasBounds; Tcl_ListObjIndex(NULL, glyphInfo, 2, &atlasBounds); + Tcl_Obj* pv[] = {Tcl_NewDoubleObj(p.x), Tcl_NewDoubleObj(p.y)}; + Tcl_Obj* pObj = Tcl_NewListObj(2, pv); + Tcl_Obj* args[] = { + Tcl_NewIntObj(font->gpuAtlasImage), + gpuAtlasImageSize, + atlasBounds, + planeBounds, + pObj, Tcl_NewDoubleObj(radians), Tcl_NewDoubleObj(em) + }; + Tcl_Obj* instance = Tcl_NewListObj(sizeof(args)/sizeof(args[0]), args); + Tcl_ListObjAppendElement(NULL, instances, instance); + } + p = vec2f_add(p, vec2f_rotate((vec2f) {advance * em, 0}, radians)); + } + return instances; + } + $cc compile + + namespace export * + namespace ensemble create + } + set ::FontCache [dict create] + # load all fonts into the fontCache + foreach fontPath [list {*}[glob vendor/fonts/*.png]] { + set fontName "" + regexp {vendor/fonts/(.*).png} $fontPath -> fontName + if {!($fontName eq "")} { + puts "Loaded $fontName into font cache" + set fontdata [font load $fontName] + dict set ::FontCache $fontName $fontdata + } + } + Claim the GPU has loaded [dict size $::FontCache] fonts + + set rotate $::rotate + set invBilinear $::invBilinear + set glyphMsd [Gpu::fn {sampler2D atlas vec4 atlasGlyphBounds vec2 glyphUv} vec4 { + vec2 atlasUv = mix(atlasGlyphBounds.xw, atlasGlyphBounds.zy, glyphUv); + return texture(atlas, vec2(atlasUv.x, 1.0-atlasUv.y)); + }] + set median [Gpu::fn {float r float g float b} float { + return max(min(r, g), min(max(r, g), b)); + }] + # TODO: Do this with a wish, instead of hard-coding the global dict. + dict set ::pipelines "glyph" [Gpu::pipeline \ + {sampler2D atlas vec2 atlasSize + vec4 atlasGlyphBounds + vec4 planeGlyphBounds + vec2 pos float radians float em + fn rotate} { + float left = planeGlyphBounds[0] * em; + float bottom = planeGlyphBounds[1] * em; + float right = planeGlyphBounds[2] * em; + float top = planeGlyphBounds[3] * em; + vec2 a = pos + rotate(vec2(left, -top), -radians); + vec2 b = pos + rotate(vec2(right, -top), -radians); + vec2 c = pos + rotate(vec2(right, -bottom), -radians); + vec2 d = pos + rotate(vec2(left, -bottom), -radians); + + vec2 vertices[4] = vec2[4](a, b, d, c); + return vertices[gl_VertexIndex]; + } {fn rotate fn invBilinear fn glyphMsd fn median} { + float left = planeGlyphBounds[0] * em; + float bottom = planeGlyphBounds[1] * em; + float right = planeGlyphBounds[2] * em; + float top = planeGlyphBounds[3] * em; + vec2 a = pos + rotate(vec2(left, -top), -radians); + vec2 b = pos + rotate(vec2(right, -top), -radians); + vec2 c = pos + rotate(vec2(right, -bottom), -radians); + vec2 d = pos + rotate(vec2(left, -bottom), -radians); + + vec2 glyphUv = invBilinear(gl_FragCoord.xy, a, b, c, d); + if( max( abs(glyphUv.x-0.5), abs(glyphUv.y-0.5))>=0.5 ) { + return vec4(0, 0, 0, 0); + } + vec3 msd = glyphMsd(atlas, atlasGlyphBounds/atlasSize.xyxy, glyphUv).rgb; + float sd = median(msd.r, msd.g, msd.b); + float screenPxDistance = 4.5*(sd - 0.5); + float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0); + return mix(vec4(0, 0, 0, 0), vec4(1, 1, 1, 1), opacity); + }] + + Wish $::thisProcess receives statements like \ + [list /someone/ wishes to draw text with /...options/] + + When (non-capturing) /someone/ wishes to draw text with /...options/ { + if {[dict exists $options center]} { + lassign [dict get $options center] x0 y0 + } else { + set x0 [dict get $options x] + set y0 [dict get $options y] + } + set scale [dict_getdef $options scale 1.0] + set font [dict_getdef $options font "PTSans-Regular"] + set text [dict get $options text] + set radians [dict get $options radians] + + if {!([dict exists $::FontCache $font])} { + throw {DISPLAY FONT {font doesn't exist}} "$font doesn't exist" + } + set font [dict get $::FontCache $font] + + set instances [font textShape $font $text $x0 $y0 $scale $radians] + + # We need to batch into one wish so we don't deal with n^2 + # checks for existing statements for n glyphs. + Wish the GPU draws pipeline "glyph" with instances $instances + } +} diff --git a/virtual-programs/errors.folk b/virtual-programs/errors.folk index 28028710..32e34e73 100644 --- a/virtual-programs/errors.folk +++ b/virtual-programs/errors.folk @@ -6,6 +6,14 @@ When /p/ has error /err/ with info /info/ { Wish $p is outlined red } } - Wish $p is titled $err } + +When /p/ has warning /w/ with info /info/ { + When the clock time is /t/ { + if {[expr {(int($t * 5)) % 2}] != 1} { + Wish $p is outlined yellow + } + } + Wish $p is titled $w +} diff --git a/virtual-programs/images.folk b/virtual-programs/images.folk index e748cd11..d8bcaf55 100644 --- a/virtual-programs/images.folk +++ b/virtual-programs/images.folk @@ -36,8 +36,10 @@ namespace eval ::image { $cc include <string.h> if {[namespace exists ::Heap]} { $cc import ::Heap::cc folkHeapAlloc as folkHeapAlloc + $cc import ::Heap::cc folkHeapFree as folkHeapFree } else { $cc code { #define folkHeapAlloc malloc } + $cc code { #define folkHeapFree free } } $cc code { @@ -258,6 +260,10 @@ namespace eval ::image { return ret; } + $cc proc freeImage {image_t im} void { + folkHeapFree(im.data); + } + $cc proc freeJpeg {image_t im} void { // TODO: Free the JPEG. // ckfree(im.data); @@ -334,7 +340,7 @@ When /someone/ wishes /p/ displays camera slice /slice/ & /p/ has region /r/ { # set scale [expr {$Display::WIDTH / $Camera::WIDTH}] # Use 1x scale instead of $scale so the projected tag doesn't redetect. # TODO: Mask the tag out? - Wish display runs [list Display::image {*}$center $slice 0 1] + Wish to draw an image with center $center image $slice radians 0 scale 1 } When /someone/ wishes /p/ displays image /im/ { @@ -348,7 +354,7 @@ When /someone/ wishes /p/ displays image /im/ { # set scale [expr {min($width / [image width $im], # $height / [image height $im])}] # Wish $p is labelled $im - Wish display runs [list Display::image {*}$center $im [region angle $r] 1] + Wish to draw an image with center $center image $im radians [region angle $r] } # On unmatch { # # HACK: Leaves time for the display to finish trying to display this. diff --git a/virtual-programs/label.folk b/virtual-programs/label.folk index 440bbf48..88a86c18 100644 --- a/virtual-programs/label.folk +++ b/virtual-programs/label.folk @@ -1,15 +1,12 @@ When /thing/ has region /region/ { lassign [region centroid $region] x y - # set width [region width $region] - # set height [region height $region] set radians [region angle $region] When the collected matches for [list /someone/ wishes $thing is labelled /text/ with font /font/] are /matches/ { set text [join [lmap match $matches {dict get $match text}] "\n"] if {$text eq ""} { return } - set scale 1 - Display::text $x $y $scale $text $radians [dict get $match font] + Wish to draw text with x $x y $y text $text radians $radians font [dict get $match font] } } diff --git a/virtual-programs/mask-tags.folk b/virtual-programs/mask-tags.folk index 4f0ae4b2..749bb26c 100644 --- a/virtual-programs/mask-tags.folk +++ b/virtual-programs/mask-tags.folk @@ -12,5 +12,5 @@ When tag /something/ has corners /corners/ { set p1 [lindex $corners 1] set p2 [lindex $corners 2] set p3 [lindex $corners 3] - Display::drawOnTop Display::fillQuad $p0 $p1 $p2 $p3 black + Wish to draw a quad with p0 $p0 p1 $p1 p2 $p2 p3 $p3 color black layer 1 }
\ No newline at end of file diff --git a/virtual-programs/new-program-web-editor.folk b/virtual-programs/new-program-web-editor.folk index a7c176cc..f972cfc7 100644 --- a/virtual-programs/new-program-web-editor.folk +++ b/virtual-programs/new-program-web-editor.folk @@ -152,12 +152,14 @@ Wish the web server handles route "/new" with handler { Assert web claims {${program}} has program code {${code}} if {$::isLaptop} { Step } `); - setTimeout(() => { - send(` + [500, 1000, 3000].forEach(timeout => { + setTimeout(() => { + send(` set errors [Statements::findMatches [list {${program}} has error /err/ with info /errorInfo/]] ::websocket::send $chan text [join [list "Error:" {*}[lmap e $errors {dict get $e errorInfo}]] "\n"] `); - }, 500); + }, timeout); + }); } let jobid; function handlePrint() { diff --git a/virtual-programs/outline.folk b/virtual-programs/outline.folk index f62ab0ba..1d6411cb 100644 --- a/virtual-programs/outline.folk +++ b/virtual-programs/outline.folk @@ -1,9 +1,12 @@ -proc loopRegion {edges vertices weight color} { - foreach edge $edges { - set from [lindex $vertices [lindex $edge 0]] - set to [lindex $vertices [lindex $edge 1]] - Display::stroke [list $from $to] $weight $color - } +proc loopRegion {edges vertices width color} { + set instances [list] + set color [getColor $color] + foreach edge $edges { + set from [lindex $vertices [lindex $edge 0]] + set to [lindex $vertices [lindex $edge 1]] + lappend instances [list $from $to $width $color] + } + Wish the GPU draws pipeline "line" with instances $instances } When the collected matches for [list /someone/ wishes /thing/ is outlined /color/] are /matches/ { @@ -27,4 +30,4 @@ When /someone/ wishes /thing/ is outlined thick /color/ & /thing/ has region /re # FIXME: assumes path lassign $region vertices edges loopRegion $edges $vertices 6 $color -}
\ No newline at end of file +} diff --git a/virtual-programs/shapes.folk b/virtual-programs/shapes.folk index 08106475..10589f38 100644 --- a/virtual-programs/shapes.folk +++ b/virtual-programs/shapes.folk @@ -1,7 +1,14 @@ -# numPoints 2 => line -# numPoints 3 => triangle -# numPoints 4 => square -proc shape {numPoints c r {angle 0} {color white} {filled false} args} { +# sides 2 => line +# sides 3 => triangle +# sides 4 => square +When /someone/ wishes to draw a shape with /...options/ { + set numPoints [dict get $options sides] + set c [dict get $options center] + set r [dict get $options radius] + set radians [dict_getdef $options radians 0] + set color [dict_getdef $options color white] + set filled [dict_getdef $options filled false] + set p [list 0 0] set center $p set points [list $p] @@ -19,24 +26,34 @@ proc shape {numPoints c r {angle 0} {color white} {filled false} args} { set points [lmap v $points { set v [vec2 sub $v $center] - set v [vec2 rotate $v $angle] + set v [vec2 rotate $v $radians] set v [vec2 add $v $c] set v }] if {$filled} { - Display::fillPolygon $points $color + Wish to draw a polygon with points $points color $color } else { - Display::stroke $points 1 $color + Wish to draw a stroke with points $points width 1 color $color } } - -When /someone/ wishes /p/ draws a /color/ /shape/ offset /offsetVector/ & /p/ has region /r/ { +set shapes [dict create triangle 3 square 4 pentagon 5 hexagon 6 \ + septagon 7 octagon 8 nonagon 9] +When /someone/ wishes /p/ draws a /shape/ { + # TODO: This is a hack because rest pattern doesn't match empty + # sequence at end. + Wish $p draws a $shape with color white +} +When /someone/ wishes /p/ draws an /shape/ { Wish $p draws a $shape } +When /someone/ wishes /p/ draws a /shape/ with /...options/ & /p/ has region /r/ { lassign [region centroid $r] x y set width [region width $r] set height [region height $r] - lassign $offsetVector offsetX offsetY + lassign [dict_getdef $options offset {0 0}] offsetX offsetY + set radius [dict_getdef $options radius 50] + set color [dict_getdef $options color white] + set filled [dict_getdef $options filled false] if {$offsetX != 0} { set x [expr {$x + $offsetX}] @@ -48,74 +65,39 @@ When /someone/ wishes /p/ draws a /color/ /shape/ offset /offsetVector/ & /p/ ha set angle [region angle $r] set p [list $x $y] - # puts "drawing a $shape at $x $y with width $width and height $height" - set radius 50 - switch -nocase $shape { - triangle { - shape 3 $p $radius $angle $color - } - square { - shape 4 $p $radius $angle $color - } - pentagon { - shape 5 $p $radius $angle $color - } - hexagon { - shape 6 $p $radius $angle $color - } - septagon { - shape 7 $p $radius $angle $color - } - octagon { - shape 8 $p $radius $angle $color - } - nonagon { - shape 9 $p $radius $angle $color - } - circle { - Display::circle $x $y $radius 5 $color - } - default { - shape 2 $p $radius $angle $color - } + if {$shape eq "circle"} { + Wish to draw a circle with center $p radius $radius thickness 5 color $color filled $filled + } elseif {[dict exists $shapes $shape]} { + Wish to draw a shape with sides [dict get $shapes $shape] \ + center $p radius $radius radians $angle color $color filled $filled + } else { + Wish to draw a shape with sides 2 \ + center $p radius $radius radians $angle color $color filled $filled } } - -set defaultColor white - -When /someone/ wishes /p/ draws a /color/ /shape/ & /p/ has region /r/ { - Wish $p draws a $color $shape offset {0 0} -} - -When /someone/ wishes /p/ draws a /shape/ & /p/ has region /r/ { - Wish $p draws a $defaultColor $shape -} - -When /someone/ wishes /p/ draws a /shape/ offset /offsetVector/ { - Wish $p draws a $defaultColor $shape offset $offsetVector +When /someone/ wishes /p/ draws an /shape/ with /...options/ { + Wish $p draws a $shape with {*}$options } Claim $this has demo { Wish $this draws a circle - Wish $this draws a skyblue triangle - Wish $this draws a green triangle offset {280 0} - Wish $this draws a gold pentagon offset {200 0} - Wish $this draws a red octagon offset {250 80} + Wish $this draws a triangle with color skyblue + Wish $this draws a triangle with color green offset {280 0} + Wish $this draws a pentagon with color gold offset {200 0} + Wish $this draws an octagon with color red offset {250 80} When the clock time is /t/ { set offsetVector [list [sin $t] [cos $t]] set offsetVector [::vec2::scale $offsetVector 105] - Wish $this draws a palegoldenrod circle offset $offsetVector + Wish $this draws a circle with color palegoldenrod offset $offsetVector } # This toggles a square between filled and unfilled - # TODO cwervo: Support `Wish $this draws a circle with options /optionsDict/` to make this cleaner - # optionsDisct: {size number?: 50, filled boolean?: false, color string?: white, thickness string? white } When $this has region /r/ & the clock time is /t/ { lassign [region centroid $r] x y - set fill [expr {round(sin($t) * 2) % 2 == 0 ? true : false}] + set fill [expr {round(sin($t) * 2) % 2 == 0}] set y [- $y 150] - shape 4 [list [- $x 100] $y] 0 60 white $fill + Wish to draw a shape with sides 4 center [list [- $x 100] $y] radius 60 color white filled $fill } Wish $this is outlined white diff --git a/virtual-programs/sprites.folk b/virtual-programs/sprites.folk index 0e4fc87e..48f8a1b1 100644 --- a/virtual-programs/sprites.folk +++ b/virtual-programs/sprites.folk @@ -23,7 +23,7 @@ When /anyone/ wishes /p/ draws sprite /path/ with /frameCount/ frames and /colum set subimage [image subimage $im $x $y $spriteWidth $spriteHeight] set center [region centroid $r] set angle [region angle $r] - Wish display runs [list Display::image {*}$center $subimage $angle 1] + Wish to draw an image with center $center image $subimage radians $angle } } diff --git a/virtual-programs/tags-and-calibration.folk b/virtual-programs/tags-and-calibration.folk index ce368f00..dbe58f16 100644 --- a/virtual-programs/tags-and-calibration.folk +++ b/virtual-programs/tags-and-calibration.folk @@ -9,10 +9,11 @@ namespace import ::math::linearalgebra::add \ ::math::linearalgebra::solvePGauss When the camera frame is /f/ { - Claim camera Camera has width [dict get $f width] height [dict get $f height] + Claim the camera Camera has width [dict get $f width] height [dict get $f height] } -When camera /camera/ has width /cameraWidth/ height /cameraHeight/ { +When the display /display/ has width /displayWidth/ height /displayHeight/ &\ + the camera /camera/ has width /cameraWidth/ height /cameraHeight/ { puts "Evaluating calibration for $camera ($cameraWidth x $cameraHeight)" # camera (tag) point -> projection point # calibration for Hex @@ -27,10 +28,10 @@ When camera /camera/ has width /cameraWidth/ height /cameraHeight/ { set points [list] foreach correspondence $generatedCalibration::points { lassign $correspondence cx cy dx dy - lappend points [list [expr {double($cx)/$generatedCalibration::cameraWidth*$Camera::WIDTH}] \ - [expr {double($cy)/$generatedCalibration::cameraHeight*$Camera::HEIGHT}] \ - [expr {double($dx)/$generatedCalibration::displayWidth*$Display::WIDTH}] \ - [expr {double($dy)/$generatedCalibration::displayHeight*$Display::HEIGHT}]] + lappend points [list [expr {double($cx)/$generatedCalibration::cameraWidth*$cameraWidth}] \ + [expr {double($cy)/$generatedCalibration::cameraHeight*$cameraHeight}] \ + [expr {double($dx)/$generatedCalibration::displayWidth*$displayWidth}] \ + [expr {double($dy)/$generatedCalibration::displayHeight*$displayHeight}]] } } if {[llength $points] < 4} { diff --git a/virtual-programs/title.folk b/virtual-programs/title.folk index fccacc17..3187ed3c 100644 --- a/virtual-programs/title.folk +++ b/virtual-programs/title.folk @@ -14,7 +14,7 @@ When /thing/ has region /region/ { if {$text eq ""} { return } set scale 1 - Display::text {*}$top $scale $text $radians + Wish to draw text with center $top scale $scale text $text radians $radians } When the collected matches for [list /someone/ wishes $thing is footnoted /text/] are /matches/ { @@ -22,6 +22,6 @@ When /thing/ has region /region/ { if {$text eq ""} { return } set scale 1 - Display::text {*}$bot $scale $text $radians + Wish to draw text with center $bot scale $scale text $text radians $radians } } diff --git a/virtual-programs/web-editor.folk b/virtual-programs/web-editor.folk index 245b6043..fb899560 100644 --- a/virtual-programs/web-editor.folk +++ b/virtual-programs/web-editor.folk @@ -96,13 +96,14 @@ Wish the web server handles route {/page/(.*)$} with handler { send(`EditVirtualProgram file_name {${code}}`) } - - setTimeout(() => { - send(` + [500, 1000, 3000].forEach(timeout => { + setTimeout(() => { + send(` set errors [Statements::findMatches [list program_id has error /err/ with info /errorInfo/]] ::websocket::send $chan text [join [list "Error:" {*}[lmap e $errors {dict get $e errorInfo}]] "\n"] `); - }, 500); + }, 500); + }); } let jobid; |
