Skip to content

Commit

Permalink
gleval: add CachedSDF3 implementations and modify Render interface to…
Browse files Browse the repository at this point in the history
… accept userdata
  • Loading branch information
soypat committed Aug 31, 2024
1 parent b381839 commit 4658186
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 11 deletions.
9 changes: 5 additions & 4 deletions examples/fibonacci-showerhead/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,11 @@ func run() error {
defer fpvis.Close()

err = gsdfaux.Render(object, gsdfaux.RenderConfig{
STLOutput: fpstl,
VisualOutput: fpvis,
Resolution: object.Bounds().Diagonal() / 200,
UseGPU: *useGPU,
STLOutput: fpstl,
VisualOutput: fpvis,
Resolution: object.Bounds().Diagonal() / 200,
UseGPU: *useGPU,
EnableCaching: !*useGPU, // Has many unions, part can likely benefit from caching when using CPU.
})

return err
Expand Down
18 changes: 18 additions & 0 deletions glbuild/glbuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,15 @@ func (c3 *CachedShader3D) ForEach2DChild(userData any, fn func(userData any, s *
return err
}

// Evaluate implements the gleval.SDF3 interface.
func (c3 *CachedShader3D) Evaluate(pos []ms3.Vec, dist []float32, userData any) error {
sdf, ok := c3.Shader.(sdf3)
if !ok {
return fmt.Errorf("%T does not implement gleval.SDF3", c3.Shader)
}
return sdf.Evaluate(pos, dist, userData)
}

var _ Shader2D = (*CachedShader2D)(nil) // Interface implementation compile-time check.

// CachedShader2D implements the Shader2D interface with results it caches for another Shader2D on a call to RefreshCache.
Expand Down Expand Up @@ -650,6 +659,15 @@ func (c2 *CachedShader2D) AppendShaderBody(b []byte) []byte {
return append(b, c2.data[c2.bodyOffset:]...)
}

// Evaluate implements the gleval.SDF2 interface.
func (c2 *CachedShader2D) Evaluate(pos []ms2.Vec, dist []float32, userData any) error {
sdf, ok := c2.Shader.(sdf2)
if !ok {
return fmt.Errorf("%T does not implement gleval.SDF2", c2.Shader)
}
return sdf.Evaluate(pos, dist, userData)
}

type nameOverloadShader3D struct {
Shader Shader3D
name []byte
Expand Down
177 changes: 177 additions & 0 deletions gleval/gleval.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package gleval
import (
"errors"
"fmt"
"slices"

"github.com/chewxy/math32"
"github.com/soypat/glgl/math/ms2"
"github.com/soypat/glgl/math/ms3"
)
Expand Down Expand Up @@ -96,3 +98,178 @@ func NormalsCentralDiff(s SDF3, pos []ms3.Vec, normals []ms3.Vec, step float32,
}
return nil
}

type BlockCachedSDF3 struct {
sdf SDF3
mul ms3.Vec
m map[[3]int]float32
posbuf []ms3.Vec
distbuf []float32
idxbuf []int
hits uint64
evals uint64
}

// Reset resets the SDF3 and reuses the underlying buffers for future SDF evaluations. It also resets statistics such as evaluations and cache hits.
func (c3 *BlockCachedSDF3) Reset(sdf SDF3, res ms3.Vec) error {
if res.X <= 0 || res.Y <= 0 || res.Z <= 0 {
return errors.New("invalid resolution for BlockCachedSDF3")
}
if c3.m == nil {
c3.m = make(map[[3]int]float32)
} else {
clear(c3.m)
}
// bb := sdf.Bounds()
// Ncells := ms3.DivElem(bb.Size(), res)
*c3 = BlockCachedSDF3{
sdf: sdf,
mul: ms3.DivElem(ms3.Vec{X: 1, Y: 1, Z: 1}, res),
m: c3.m,
posbuf: c3.posbuf[:0],
distbuf: c3.distbuf[:0],
idxbuf: c3.idxbuf[:0],
}
return nil
}

// CacheHits returns total amount of cached evalutions done throughout the SDF's lifetime.
func (c3 *BlockCachedSDF3) CacheHits() uint64 {
return c3.hits
}

// Evaluations returns total evaluations performed succesfully during sdf's lifetime, including cached.
func (c3 *BlockCachedSDF3) Evaluations() uint64 {
return c3.evals
}

// Evaluate implements the [SDF3] interface with cached evaluation.
func (c3 *BlockCachedSDF3) Evaluate(pos []ms3.Vec, dist []float32, userData any) error {
bb := c3.sdf.Bounds()
seekPos := c3.posbuf[:0]
idx := c3.idxbuf[:0]
mul := c3.mul
for i, p := range pos {
tp := ms3.MulElem(mul, ms3.Sub(p, bb.Min))
k := [3]int{
int(tp.X),
int(tp.Y),
int(tp.Z),
}
d, cached := c3.m[k]
if cached {
dist[i] = d
} else {
seekPos = append(seekPos, p)
idx = append(idx, i)
}
}
if len(idx) > 0 {
// Renew buffers in case they were grown.
c3.idxbuf = idx
c3.posbuf = seekPos
c3.distbuf = slices.Grow(c3.distbuf[:0], len(seekPos))
seekDist := c3.distbuf[:len(seekPos)]
err := c3.sdf.Evaluate(seekPos, seekDist, userData)
if err != nil {
return err
}
// Add new entries to cache.
for i, p := range seekPos {
tp := ms3.MulElem(mul, ms3.Sub(p, bb.Min))
k := [3]int{
int(tp.X),
int(tp.Y),
int(tp.Z),
}
c3.m[k] = seekDist[i]
}
// Fill original buffer with new distances.
for i, d := range seekDist {
dist[idx[i]] = d
}
}
c3.evals += uint64(len(dist))
c3.hits += uint64(len(dist) - len(seekPos))
return nil
}

// Bounds returns the SDF's bounding box such that all of the shape is contained within.
func (c3 *BlockCachedSDF3) Bounds() ms3.Box {
return c3.sdf.Bounds()
}

type cachedExactSDF3 struct {
SDF SDF3
m map[[3]uint32]float32
posbuf []ms3.Vec
distbuf []float32
idxbuf []int
hits uint64
evals uint64
}

// CacheHits returns total amount of cached evalutions done throughout the SDF's lifetime.
func (c3 *cachedExactSDF3) CacheHits() uint64 {
return c3.hits
}

// Evaluations returns total evaluations performed succesfully during sdf's lifetime, including cached.
func (c3 *cachedExactSDF3) Evaluations() uint64 {
return c3.evals
}

// Evaluate implements the [SDF3] interface with cached evaluation.
func (c3 *cachedExactSDF3) Evaluate(pos []ms3.Vec, dist []float32, userData any) error {
if c3.m == nil {
c3.m = make(map[[3]uint32]float32)
}
seekPos := c3.posbuf[:0]
idx := c3.idxbuf[:0]
for i, p := range pos {
k := [3]uint32{
math32.Float32bits(p.X),
math32.Float32bits(p.Y),
math32.Float32bits(p.Z),
}
d, cached := c3.m[k]
if cached {
dist[i] = d
} else {
seekPos = append(seekPos, p)
idx = append(idx, i)
}
}
if len(idx) > 0 {
// Renew buffers in case they were grown.
c3.idxbuf = idx
c3.posbuf = seekPos
c3.distbuf = slices.Grow(c3.distbuf[:0], len(seekPos))
seekDist := c3.distbuf[:len(seekPos)]
err := c3.SDF.Evaluate(seekPos, seekDist, userData)
if err != nil {
return err
}
// Add new entries to cache.
for i, p := range seekPos {
k := [3]uint32{
math32.Float32bits(p.X),
math32.Float32bits(p.Y),
math32.Float32bits(p.Z),
}
c3.m[k] = seekDist[i]
}
// Fill original buffer with new distances.
for i, d := range seekDist {
dist[idx[i]] = d
}
}
c3.evals += uint64(len(dist))
c3.hits += uint64(len(dist) - len(seekPos))
return nil
}

// Bounds returns the SDF's bounding box such that all of the shape is contained within.
func (c3 *cachedExactSDF3) Bounds() ms3.Box {
return c3.SDF.Bounds()
}
6 changes: 3 additions & 3 deletions glrender/glrender.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ import (
const sqrt3 = 1.73205080757

type Renderer interface {
ReadTriangles(dst []ms3.Triangle) (n int, err error)
ReadTriangles(dst []ms3.Triangle, userData any) (n int, err error)
}

// RenderAll reads the full contents of a Renderer and returns the slice read.
// It does not return error on io.EOF, like the io.RenderAll implementation.
func RenderAll(r Renderer) ([]ms3.Triangle, error) {
func RenderAll(r Renderer, userData any) ([]ms3.Triangle, error) {
const startSize = 4096
var err error
var nt int
result := make([]ms3.Triangle, 0, startSize)
buf := make([]ms3.Triangle, startSize)
for {
nt, err = r.ReadTriangles(buf)
nt, err = r.ReadTriangles(buf, userData)
if err == nil || err == io.EOF {
result = append(result, buf[:nt]...)
}
Expand Down
2 changes: 1 addition & 1 deletion glrender/octree.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func (oc *Octree) Reset(s gleval.SDF3, cubeResolution float32) error {
return nil
}

func (oc *Octree) ReadTriangles(dst []ms3.Triangle) (n int, err error) {
func (oc *Octree) ReadTriangles(dst []ms3.Triangle, userData any) (n int, err error) {
if len(dst) < 5 {
return 0, io.ErrShortBuffer
}
Expand Down
27 changes: 24 additions & 3 deletions gsdfaux/gsdfaux.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type RenderConfig struct {
Resolution float32
UseGPU bool
Silent bool
// EnableCaching uses [gleval.BlockCachedSDF3] to omit potential evaluations.
// Can cut down on times for very complex SDFs, mainly when using CPU.
EnableCaching bool
}

// Render is an auxiliary function to aid users in getting setup in using gsdf quickly.
Expand Down Expand Up @@ -72,7 +75,19 @@ func Render(s glbuild.Shader3D, cfg RenderConfig) (err error) {
if err != nil || sdf == nil {
return fmt.Errorf("instantiating SDF: %s", err)
}

if cfg.EnableCaching {
var cache gleval.BlockCachedSDF3
cacheRes := cfg.Resolution / 2
err = cache.Reset(sdf, ms3.Vec{X: cacheRes, Y: cacheRes, Z: cacheRes})
if err != nil {
return err
}
sdf = &cache
defer func() {
pcnt := percentUint64(cache.CacheHits(), cache.Evaluations())
log("SDF caching omitted", pcnt, "percent of", cache.Evaluations(), "SDF evaluations")
}()
}
log("instantiating evaluation SDF took", watch())
const size = 1 << 12
renderer, err := glrender.NewOctreeRenderer(sdf, cfg.Resolution, size)
Expand Down Expand Up @@ -107,14 +122,16 @@ func Render(s glbuild.Shader3D, cfg RenderConfig) (err error) {
}

if cfg.STLOutput != nil {
maybeVP, _ := gleval.GetVecPool(sdf)
watch = stopwatch()
triangles, err := glrender.RenderAll(renderer)
triangles, err := glrender.RenderAll(renderer, maybeVP)
if err != nil {
return fmt.Errorf("rendering triangles: %s", err)
}

e := sdf.(interface{ Evaluations() uint64 })
omitted := 8 * renderer.TotalPruned()
percentOmit := math.Trunc(10000*float32(omitted)/float32(e.Evaluations()+omitted)) / 100
percentOmit := percentUint64(omitted, e.Evaluations()+omitted)
log("evaluated SDF", e.Evaluations(), "times and rendered", len(triangles), "triangles in", watch(), "with", percentOmit, "percent evaluations omitted")

watch = stopwatch()
Expand Down Expand Up @@ -207,3 +224,7 @@ func ColorConversionInigoQuilez(characteristicDistance float32) func(float32) co
}
}
}

func percentUint64(num, denom uint64) float32 {
return math.Trunc(10000*float32(num)/float32(denom)) / 100
}

0 comments on commit 4658186

Please sign in to comment.