loop.go raw

   1  // SPDX-License-Identifier: Unlicense OR MIT
   2  
   3  package app
   4  
   5  import (
   6  	"image"
   7  	"image/color"
   8  	"runtime"
   9  
  10  	"github.com/p9c/p9/pkg/gel/gio/app/internal/wm"
  11  	"github.com/p9c/p9/pkg/gel/gio/gpu"
  12  	"github.com/p9c/p9/pkg/gel/gio/op"
  13  )
  14  
  15  type renderLoop struct {
  16  	summary string
  17  	drawing bool
  18  	err     error
  19  
  20  	frames     chan frame
  21  	results    chan frameResult
  22  	refresh    chan struct{}
  23  	refreshErr chan error
  24  	ack        chan struct{}
  25  	stop       chan struct{}
  26  	stopped    chan struct{}
  27  }
  28  
  29  type frame struct {
  30  	viewport image.Point
  31  	ops      *op.Ops
  32  }
  33  
  34  type frameResult struct {
  35  	profile string
  36  	err     error
  37  }
  38  
  39  func newLoop(ctx wm.Context) (*renderLoop, error) {
  40  	l := &renderLoop{
  41  		frames:     make(chan frame),
  42  		results:    make(chan frameResult),
  43  		refresh:    make(chan struct{}),
  44  		refreshErr: make(chan error),
  45  		// Ack is buffered so GPU commands can be issued after
  46  		// ack'ing the frame.
  47  		ack:     make(chan struct{}, 1),
  48  		stop:    make(chan struct{}),
  49  		stopped: make(chan struct{}),
  50  	}
  51  	if err := l.renderLoop(ctx); err != nil {
  52  		return nil, err
  53  	}
  54  	return l, nil
  55  }
  56  
  57  func (l *renderLoop) renderLoop(ctx wm.Context) error {
  58  	// GL Operations must happen on a single OS thread, so
  59  	// pass initialization result through a channel.
  60  	initErr := make(chan error)
  61  	go func() {
  62  		defer close(l.stopped)
  63  		runtime.LockOSThread()
  64  		// Don't UnlockOSThread to avoid reuse by the Go runtime.
  65  
  66  		if err := ctx.MakeCurrent(); err != nil {
  67  			initErr <- err
  68  			return
  69  		}
  70  		g, err := gpu.New(ctx.API())
  71  		if err != nil {
  72  			initErr <- err
  73  			return
  74  		}
  75  		defer g.Release()
  76  		initErr <- nil
  77  	loop:
  78  		for {
  79  			select {
  80  			case <-l.refresh:
  81  				l.refreshErr <- ctx.MakeCurrent()
  82  			case frame := <-l.frames:
  83  				ctx.Lock()
  84  				if runtime.GOOS == "js" {
  85  					// Use transparent black when Gio is embedded, to allow mixing of Gio and
  86  					// foreign content below.
  87  					g.Clear(color.NRGBA{A: 0x00, R: 0x00, G: 0x00, B: 0x00})
  88  				} else {
  89  					g.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff})
  90  				}
  91  				g.Collect(frame.viewport, frame.ops)
  92  				// Signal that we're done with the frame ops.
  93  				l.ack <- struct{}{}
  94  				var res frameResult
  95  				res.err = g.Frame()
  96  				if res.err == nil {
  97  					res.err = ctx.Present()
  98  				}
  99  				res.profile = g.Profile()
 100  				ctx.Unlock()
 101  				l.results <- res
 102  			case <-l.stop:
 103  				break loop
 104  			}
 105  		}
 106  	}()
 107  	return <-initErr
 108  }
 109  
 110  func (l *renderLoop) Release() {
 111  	// Flush error.
 112  	l.Flush()
 113  	close(l.stop)
 114  	<-l.stopped
 115  	l.stop = nil
 116  }
 117  
 118  func (l *renderLoop) Flush() error {
 119  	if l.drawing {
 120  		st := <-l.results
 121  		l.setErr(st.err)
 122  		if st.profile != "" {
 123  			l.summary = st.profile
 124  		}
 125  		l.drawing = false
 126  	}
 127  	return l.err
 128  }
 129  
 130  func (l *renderLoop) Summary() string {
 131  	return l.summary
 132  }
 133  
 134  func (l *renderLoop) Refresh() {
 135  	if l.err != nil {
 136  		return
 137  	}
 138  	// Make sure any pending frame is complete.
 139  	l.Flush()
 140  	l.refresh <- struct{}{}
 141  	l.setErr(<-l.refreshErr)
 142  }
 143  
 144  // Draw initiates a draw of a frame. It returns a channel
 145  // than signals when the frame is no longer being accessed.
 146  func (l *renderLoop) Draw(viewport image.Point, frameOps *op.Ops) <-chan struct{} {
 147  	if l.err != nil {
 148  		l.ack <- struct{}{}
 149  		return l.ack
 150  	}
 151  	l.Flush()
 152  	l.frames <- frame{viewport, frameOps}
 153  	l.drawing = true
 154  	return l.ack
 155  }
 156  
 157  func (l *renderLoop) setErr(err error) {
 158  	if l.err == nil {
 159  		l.err = err
 160  	}
 161  }
 162