summaryrefslogtreecommitdiffstats
path: root/virtual-programs/display
diff options
context:
space:
mode:
Diffstat (limited to 'virtual-programs/display')
-rw-r--r--virtual-programs/display/arc.folk39
-rw-r--r--virtual-programs/display/circle.folk31
-rw-r--r--virtual-programs/display/fill.folk51
-rw-r--r--virtual-programs/display/fns.folk0
-rw-r--r--virtual-programs/display/image.folk118
-rw-r--r--virtual-programs/display/stroke.folk34
-rw-r--r--virtual-programs/display/text.folk207
7 files changed, 480 insertions, 0 deletions
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
+ }
+}