diff options
Diffstat (limited to 'virtual-programs/display')
| -rw-r--r-- | virtual-programs/display/arc.folk | 39 | ||||
| -rw-r--r-- | virtual-programs/display/circle.folk | 31 | ||||
| -rw-r--r-- | virtual-programs/display/fill.folk | 51 | ||||
| -rw-r--r-- | virtual-programs/display/fns.folk | 0 | ||||
| -rw-r--r-- | virtual-programs/display/image.folk | 118 | ||||
| -rw-r--r-- | virtual-programs/display/stroke.folk | 34 | ||||
| -rw-r--r-- | virtual-programs/display/text.folk | 207 |
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 + } +} |
