Add configurable debouncer for spammy Libretro callbacks

This commit is contained in:
sergystepanov 2023-10-25 21:41:36 +03:00 committed by GitHub
parent 07f40351fa
commit 7f2f1d70b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 131 additions and 86 deletions

View file

@ -139,6 +139,9 @@ emulator:
libretro:
# use zip compression for emulator save states
saveCompression: true
# Sets a limiter function for some spammy core callbacks.
# 0 - disabled, otherwise -- time in milliseconds for ignoring repeated calls except the last.
debounceMs: 0
# Libretro cores logging level: DEBUG = 0, INFO, WARN, ERROR, DUMMY = INT_MAX
logLevel: 1
cores:

View file

@ -32,6 +32,7 @@ type LibretroConfig struct {
}
List map[string]LibretroCoreConfig
}
DebounceMs int
SaveCompression bool
LogLevel int
}

View file

@ -46,21 +46,15 @@ func (c *Caged) ReloadFrontend() {
c.base = frontend
}
func (c *Caged) HandleOnSystemAvInfo(fn func()) {
c.base.SetOnAV(func() {
w, h := c.ViewportCalc()
c.SetViewport(w, h)
fn()
})
}
// VideoChangeCb adds a callback when video params are changed by the app.
func (c *Caged) VideoChangeCb(fn func()) { c.base.SetVideoChangeCb(fn) }
func (c *Caged) Load(game games.GameMetadata, path string) error {
c.Emulator.LoadCore(game.System)
if err := c.Emulator.LoadGame(game.FullPath(path)); err != nil {
return err
}
w, h := c.ViewportCalc()
c.SetViewport(w, h)
c.ViewportRecalculate()
return nil
}
@ -87,7 +81,7 @@ func (c *Caged) EnableCloudStorage(uid string, storage cloud.Storage) {
func (c *Caged) PixFormat() uint32 { return c.Emulator.PixFormat() }
func (c *Caged) Rotation() uint { return c.Emulator.Rotation() }
func (c *Caged) AudioSampleRate() int { return c.Emulator.AudioSampleRate() }
func (c *Caged) ViewportSize() (int, int) { return c.Emulator.ViewportSize() }
func (c *Caged) ViewportSize() (int, int) { return c.base.ViewportSize() }
func (c *Caged) Scale() float64 { return c.Emulator.Scale() }
func (c *Caged) SendControl(port int, data []byte) { c.base.Input(port, data) }
func (c *Caged) Start() { go c.Emulator.Start() }

View file

@ -30,11 +30,8 @@ type Emulator interface {
IsPortrait() bool
// Start is called after LoadGame
Start()
// SetViewport sets viewport size
SetViewport(width int, height int)
// ViewportCalc calculates the viewport size with the aspect ratio and scale
ViewportCalc() (nw int, nh int)
ViewportSize() (w, h int)
// ViewportRecalculate calculates output resolution with aspect and scale
ViewportRecalculate()
RestoreGameState() error
// SetSessionId sets distinct name for the game session (in order to save/load it later)
SetSessionId(name string)
@ -112,8 +109,13 @@ func NewFrontend(conf config.Emulator, log *logger.Logger) (*Frontend, error) {
nano := nanoarch.NewNano(path)
log = log.Extend(log.With().Str("m", "Libretro"))
ll := log.Extend(log.Level(logger.Level(conf.Libretro.LogLevel)).With())
nano.SetLogger(ll)
level := logger.Level(conf.Libretro.LogLevel)
if level == logger.DebugLevel {
level = logger.TraceLevel
nano.SetLogger(log.Extend(log.Level(level).With()))
} else {
nano.SetLogger(log)
}
// Check if room is on local storage, if not, pull from GCS to local storage
log.Info().Msgf("Local storage path: %v", conf.Storage)
@ -139,6 +141,12 @@ func NewFrontend(conf config.Emulator, log *logger.Logger) (*Frontend, error) {
}
f.linkNano(nano)
if conf.Libretro.DebounceMs > 0 {
t := time.Duration(conf.Libretro.DebounceMs) * time.Millisecond
f.nano.SetVideoDebounce(t)
f.log.Debug().Msgf("set debounce time: %v", t)
}
return f, nil
}
@ -212,7 +220,7 @@ func (f *Frontend) linkNano(nano *nanoarch.Nanoarch) {
f.nano.OnAudio = f.handleAudio
}
func (f *Frontend) SetOnAV(fn func()) { f.nano.OnSystemAvInfo = fn }
func (f *Frontend) SetVideoChangeCb(fn func()) { f.nano.OnSystemAvInfo = fn }
func (f *Frontend) Start() {
f.log.Debug().Msgf("frontend start")
@ -264,7 +272,7 @@ func (f *Frontend) Start() {
func (f *Frontend) AudioSampleRate() int { return f.nano.AudioSampleRate() }
func (f *Frontend) FPS() int { return f.nano.VideoFramerate() }
func (f *Frontend) Flipped() bool { return f.nano.IsGL() }
func (f *Frontend) FrameSize() (int, int) { return f.nano.GeometryBase() }
func (f *Frontend) FrameSize() (int, int) { return f.nano.BaseWidth(), f.nano.BaseHeight() }
func (f *Frontend) HasSave() bool { return os.Exists(f.HashPath()) }
func (f *Frontend) HashPath() string { return f.storage.GetSavePath() }
func (f *Frontend) Input(player int, data []byte) { f.input.setInput(player, data) }
@ -279,14 +287,14 @@ func (f *Frontend) Scale() float64 { return f.scale }
func (f *Frontend) SetAudioCb(cb func(app.Audio)) { f.onAudio = cb }
func (f *Frontend) SetSessionId(name string) { f.storage.SetMainSaveName(name) }
func (f *Frontend) SetVideoCb(ff func(app.Video)) { f.onVideo = ff }
func (f *Frontend) SetViewport(w, h int) { f.mu.Lock(); f.vw, f.vh = w, h; f.mu.Unlock() }
func (f *Frontend) Tick() { f.mu.Lock(); f.nano.Run(); f.mu.Unlock() }
func (f *Frontend) ToggleMultitap() { f.nano.ToggleMultitap() }
func (f *Frontend) ViewportRecalculate() { f.mu.Lock(); f.vw, f.vh = f.ViewportCalc(); f.mu.Unlock() }
func (f *Frontend) ViewportSize() (int, int) { return f.vw, f.vh }
func (f *Frontend) ViewportCalc() (nw int, nh int) {
w, h := f.FrameSize()
f.log.Debug().Msgf("Viewport source size: %dx%d", w, h)
nw, nh = w, h
aspect, aw, ah := f.conf.AspectRatio.Keep, f.conf.AspectRatio.Width, f.conf.AspectRatio.Height
// calc the aspect ratio
@ -298,29 +306,24 @@ func (f *Frontend) ViewportCalc() (nw int, nh int) {
nw = aw
nh = int(math.Round(float64(aw)/ratio/2) * 2)
}
f.log.Debug().Msgf("Viewport aspect change: %dx%d (%f) -> %dx%d", aw, ah, ratio, nw, nh)
} else {
nw, nh = w, h
}
if f.IsPortrait() {
nw, nh = nh, nw
f.log.Debug().Msgf("Set portrait mode")
}
f.log.Info().Msgf("Viewport final size: %dx%d", nw, nh)
f.log.Debug().Msgf("viewport: %dx%d -> %dx%d", w, h, nw, nh)
return
}
func (f *Frontend) Close() {
f.log.Debug().Msgf("frontend close")
close(f.done)
f.mui.Lock()
defer f.mui.Unlock()
f.nano.Close()
f.mui.Unlock()
f.log.Debug().Msgf("frontend closed")
}

View file

@ -128,8 +128,7 @@ func (emu *TestFrontend) loadRom(game string) {
if err != nil {
log.Fatal(err)
}
w, h := emu.FrameSize()
emu.SetViewport(w, h)
emu.ViewportRecalculate()
}
// Shutdown closes the emulator and cleans its resources.
@ -228,31 +227,26 @@ func TestLoad(t *testing.T) {
mock := DefaultFrontend(test.room, test.system, test.rom)
fmt.Printf("[%-14v] ", "initial")
mock.dumpState()
for ticks := test.frames; ticks > 0; ticks-- {
mock.Tick()
}
fmt.Printf("[%-14v] ", fmt.Sprintf("emulated %d", test.frames))
mock.dumpState()
if err := mock.Save(); err != nil {
t.Errorf("Save fail %v", err)
}
fmt.Printf("[%-14v] ", "saved")
snapshot1, _ := mock.dumpState()
for ticks := test.frames; ticks > 0; ticks-- {
mock.Tick()
}
fmt.Printf("[%-14v] ", fmt.Sprintf("emulated %d", test.frames))
mock.dumpState()
if err := mock.Load(); err != nil {
t.Errorf("Load fail %v", err)
}
fmt.Printf("[%-14v] ", "restored")
snapshot2, _ := mock.dumpState()
if snapshot1 != snapshot2 {

View file

@ -5,6 +5,7 @@ import (
"fmt"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"unsafe"
@ -47,13 +48,15 @@ type Nanoarch struct {
enabled bool
value C.unsigned
}
options *map[string]string
reserved chan struct{} // limits concurrent use
Rot uint
serializeSize C.size_t
stopped atomic.Bool
sysAvInfo C.struct_retro_system_av_info
sysInfo C.struct_retro_system_info
options *map[string]string
reserved chan struct{} // limits concurrent use
Rot uint
serializeSize C.size_t
stopped atomic.Bool
sys struct {
av C.struct_retro_system_av_info
i C.struct_retro_system_info
}
tickTime int64
cSaveDirectory *C.char
cSystemDirectory *C.char
@ -69,6 +72,7 @@ type Nanoarch struct {
vfr bool
sdlCtx *graphics.SDL
hackSkipHwContextDestroy bool
limiter func(func())
log *logger.Logger
}
@ -119,6 +123,7 @@ func (p PixFmt) String() string {
var Nan0 = Nanoarch{
reserved: make(chan struct{}, 1), // this thing forbids concurrent use of the emulator
stopped: atomic.Bool{},
limiter: func(fn func()) { fn() },
Handlers: Handlers{
OnDpad: func(uint, uint) int16 { return 0 },
OnKeyPress: func(uint, int) int { return 0 },
@ -139,18 +144,15 @@ func NewNano(localPath string) *Nanoarch {
return nano
}
func (n *Nanoarch) AudioSampleRate() int { return int(n.sysAvInfo.timing.sample_rate) }
func (n *Nanoarch) VideoFramerate() int { return int(n.sysAvInfo.timing.fps) }
func (n *Nanoarch) IsPortrait() bool { return n.Rot == 90 || n.Rot == 270 }
func (n *Nanoarch) GeometryBase() (int, int) {
return int(n.sysAvInfo.geometry.base_width), int(n.sysAvInfo.geometry.base_height)
}
func (n *Nanoarch) GeometryMax() (int, int) {
return int(n.sysAvInfo.geometry.max_width), int(n.sysAvInfo.geometry.max_height)
}
func (n *Nanoarch) WaitReady() { <-n.reserved }
func (n *Nanoarch) Close() { n.stopped.Store(true); n.reserved <- struct{}{} }
func (n *Nanoarch) SetLogger(log *logger.Logger) { n.log = log }
func (n *Nanoarch) AudioSampleRate() int { return int(n.sys.av.timing.sample_rate) }
func (n *Nanoarch) VideoFramerate() int { return int(n.sys.av.timing.fps) }
func (n *Nanoarch) IsPortrait() bool { return 90 == n.Rot%180 }
func (n *Nanoarch) BaseWidth() int { return int(n.sys.av.geometry.base_width) }
func (n *Nanoarch) BaseHeight() int { return int(n.sys.av.geometry.base_height) }
func (n *Nanoarch) WaitReady() { <-n.reserved }
func (n *Nanoarch) Close() { n.stopped.Store(true); n.reserved <- struct{}{} }
func (n *Nanoarch) SetLogger(log *logger.Logger) { n.log = log }
func (n *Nanoarch) SetVideoDebounce(t time.Duration) { n.limiter = NewLimit(t) }
func (n *Nanoarch) CoreLoad(meta Metadata) {
var err error
@ -219,16 +221,16 @@ func (n *Nanoarch) CoreLoad(meta Metadata) {
C.bridge_retro_init(retroInit)
}
C.bridge_retro_get_system_info(retroGetSystemInfo, &n.sysInfo)
C.bridge_retro_get_system_info(retroGetSystemInfo, &n.sys.i)
n.log.Debug().Msgf("System >>> %s (%s) [%s] nfp: %v",
C.GoString(n.sysInfo.library_name), C.GoString(n.sysInfo.library_version),
C.GoString(n.sysInfo.valid_extensions), bool(n.sysInfo.need_fullpath))
C.GoString(n.sys.i.library_name), C.GoString(n.sys.i.library_version),
C.GoString(n.sys.i.valid_extensions), bool(n.sys.i.need_fullpath))
}
func (n *Nanoarch) LoadGame(path string) error {
game := C.struct_retro_game_info{}
big := bool(n.sysInfo.need_fullpath) // big ROMs are loaded by cores later
big := bool(n.sys.i.need_fullpath) // big ROMs are loaded by cores later
if big {
size, err := os.StatSize(path)
if err != nil {
@ -256,17 +258,17 @@ func (n *Nanoarch) LoadGame(path string) error {
return fmt.Errorf("core failed to load ROM: %v", path)
}
C.bridge_retro_get_system_av_info(retroGetSystemAVInfo, &n.sysAvInfo)
C.bridge_retro_get_system_av_info(retroGetSystemAVInfo, &n.sys.av)
n.log.Info().Msgf("System A/V >>> %vx%v (%vx%v), [%vfps], AR [%v], audio [%vHz]",
n.sysAvInfo.geometry.base_width, n.sysAvInfo.geometry.base_height,
n.sysAvInfo.geometry.max_width, n.sysAvInfo.geometry.max_height,
n.sysAvInfo.timing.fps, n.sysAvInfo.geometry.aspect_ratio, n.sysAvInfo.timing.sample_rate,
n.sys.av.geometry.base_width, n.sys.av.geometry.base_height,
n.sys.av.geometry.max_width, n.sys.av.geometry.max_height,
n.sys.av.timing.fps, n.sys.av.geometry.aspect_ratio, n.sys.av.timing.sample_rate,
)
n.serializeSize = C.bridge_retro_serialize_size(retroSerializeSize)
n.log.Info().Msgf("Save file size: %v", byteCountBinary(int64(n.serializeSize)))
Nan0.tickTime = int64(time.Second / time.Duration(n.sysAvInfo.timing.fps))
Nan0.tickTime = int64(time.Second / time.Duration(n.sys.av.timing.fps))
if n.vfr {
n.log.Info().Msgf("variable framerate (VFR) is enabled")
}
@ -274,10 +276,9 @@ func (n *Nanoarch) LoadGame(path string) error {
n.stopped.Store(false)
if n.Video.gl.enabled {
//setRotation(image.F180) // flip Y coordinates of OpenGL
bufS := uint(n.sysAvInfo.geometry.max_width*n.sysAvInfo.geometry.max_height) * n.Video.PixFmt.BPP
bufS := uint(n.sys.av.geometry.max_width*n.sys.av.geometry.max_height) * n.Video.PixFmt.BPP
graphics.SetBuffer(int(bufS))
n.log.Info().Msgf("Set buffer: %v", byteCountBinary(int64(bufS)))
n.log.Debug().Msgf("Set buffer: %v", byteCountBinary(int64(bufS)))
if n.LibCo {
C.same_thread(C.init_video_cgo)
} else {
@ -414,9 +415,6 @@ func printOpenGLDriverInfo() {
openGLInfo.Grow(128)
openGLInfo.WriteString(fmt.Sprintf("\n[OpenGL] Version: %v\n", graphics.GetGLVersionInfo()))
openGLInfo.WriteString(fmt.Sprintf("[OpenGL] Vendor: %v\n", graphics.GetGLVendorInfo()))
// This string is often the name of the GPU.
// In the case of Mesa3d, it would be i.e "Gallium 0.4 on NVA8".
// It might even say "Direct3D" if the Windows Direct3D wrapper is being used.
openGLInfo.WriteString(fmt.Sprintf("[OpenGL] Renderer: %v\n", graphics.GetGLRendererInfo()))
openGLInfo.WriteString(fmt.Sprintf("[OpenGL] GLSL Version: %v", graphics.GetGLSLInfo()))
Nan0.log.Debug().Msg(openGLInfo.String())
@ -655,7 +653,7 @@ func coreLog(level C.enum_retro_log_level, msg *C.char) {
switch level {
// with debug level cores have too much logs
case C.RETRO_LOG_DEBUG:
Nan0.log.Debug().MsgFunc(func() string { return m(msg) })
Nan0.log.Trace().MsgFunc(func() string { return m(msg) })
case C.RETRO_LOG_INFO:
Nan0.log.Info().MsgFunc(func() string { return m(msg) })
case C.RETRO_LOG_WARN:
@ -688,18 +686,27 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool {
switch cmd {
case C.RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO:
av := *(*C.struct_retro_system_av_info)(data)
Nan0.log.Info().Msgf(">>> SET SYS AV INFO: %v", av)
Nan0.sysAvInfo = av
go func() {
if Nan0.OnSystemAvInfo != nil {
Nan0.OnSystemAvInfo()
}
}()
Nan0.sys.av = *(*C.struct_retro_system_av_info)(data)
Nan0.log.Debug().Msgf(">>> system av change: %v", Nan0.sys.av)
if Nan0.OnSystemAvInfo != nil {
go Nan0.OnSystemAvInfo()
}
return true
case C.RETRO_ENVIRONMENT_SET_GEOMETRY:
geom := *(*C.struct_retro_game_geometry)(data)
Nan0.log.Info().Msgf(">>> GEOMETRY: %v", geom)
Nan0.log.Debug().Msgf(">>> geometry change: %v", geom)
// some cores are eager to change resolution too many times
// in a small period of time, thus we have some debouncer here
Nan0.limiter(func() {
lw := Nan0.sys.av.geometry.base_width
lh := Nan0.sys.av.geometry.base_height
if lw != geom.base_width || lh != geom.base_height {
Nan0.sys.av.geometry = geom
if Nan0.OnSystemAvInfo != nil {
go Nan0.OnSystemAvInfo()
}
}
})
return true
case C.RETRO_ENVIRONMENT_SET_ROTATION:
setRotation((*(*uint)(data) % 4) * 90)
@ -739,7 +746,7 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool {
//window.SetShouldClose(true)
return false
case C.RETRO_ENVIRONMENT_GET_VARIABLE:
if (*Nan0.options) == nil {
if Nan0.options == nil || *Nan0.options == nil {
return false
}
rv := (*C.struct_retro_variable)(data)
@ -818,8 +825,8 @@ func initVideo() {
sdl, err := graphics.NewSDLContext(graphics.Config{
Ctx: context,
W: int(Nan0.sysAvInfo.geometry.max_width),
H: int(Nan0.sysAvInfo.geometry.max_height),
W: int(Nan0.sys.av.geometry.max_width),
H: int(Nan0.sys.av.geometry.max_height),
GLAutoContext: Nan0.Video.gl.autoCtx,
GLVersionMajor: uint(Nan0.Video.hw.version_major),
GLVersionMinor: uint(Nan0.Video.hw.version_minor),
@ -849,3 +856,23 @@ func deinitVideo() {
Nan0.Video.gl.autoCtx = false
Nan0.hackSkipHwContextDestroy = false
}
type limit struct {
d time.Duration
t *time.Timer
mu sync.Mutex
}
func NewLimit(d time.Duration) func(f func()) {
l := &limit{d: d}
return func(f func()) { l.push(f) }
}
func (d *limit) push(f func()) {
d.mu.Lock()
defer d.mu.Unlock()
if d.t != nil {
d.t.Stop()
}
d.t = time.AfterFunc(d.d, f)
}

View file

@ -0,0 +1,22 @@
package nanoarch
import (
"sync/atomic"
"testing"
"time"
)
func TestLimit(t *testing.T) {
c := atomic.Int32{}
lim := NewLimit(50 * time.Millisecond)
for i := 0; i < 10; i++ {
lim(func() {
c.Add(1)
})
}
if c.Load() > 1 {
t.Errorf("should be just 1")
}
}

View file

@ -140,12 +140,13 @@ func (c *coordinator) HandleGameStart(rq api.StartGameRequest[com.Uid], w *Worke
m.SetPixFmt(app.PixFormat())
m.SetRot(app.Rotation())
app.HandleOnSystemAvInfo(func() {
// recreate the video encoder
app.VideoChangeCb(func() {
app.ViewportRecalculate()
m.VideoW, m.VideoH = app.ViewportSize()
m.VideoScale = app.Scale()
err := m.Reinit()
if err != nil {
c.log.Error().Err(err).Msgf("av reinit fail")
if err := m.Reinit(); err != nil {
c.log.Error().Err(err).Msgf("reinit fail")
}
})