mirror of
https://github.com/libretro/ludo.git
synced 2026-11-12 14:35:18 -10:00
Compare commits
1
Commits
master
...
kivutar/pacing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd1ede2321 |
No files matched your search
+312
-55
@@ -3,7 +3,9 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"unsafe"
|
||||
@@ -14,16 +16,31 @@ import (
|
||||
"golang.org/x/mobile/exp/audio/al"
|
||||
)
|
||||
|
||||
const bufSize = 1024 * 8
|
||||
const (
|
||||
bufSize = 1024
|
||||
bytesPerFrame = 4
|
||||
framesPerBuffer = bufSize / bytesPerFrame
|
||||
outputRate = 48000
|
||||
targetLatencyMs = 96
|
||||
minNumBuffers = 2
|
||||
audioMaxTimingSkew = 0.05
|
||||
maxPlaybackRateSkew = 0.005
|
||||
)
|
||||
|
||||
var (
|
||||
source al.Source
|
||||
buffers []al.Buffer
|
||||
rate int32
|
||||
numBuffers int32
|
||||
tmpBuf [bufSize]byte
|
||||
tmpBufPtr int32
|
||||
resPtr int32
|
||||
source al.Source
|
||||
buffers []al.Buffer
|
||||
freeBufs []al.Buffer
|
||||
rate int32
|
||||
inputRate float64
|
||||
videoSyncRate float64
|
||||
numBuffers int32
|
||||
queueBytes int32
|
||||
inputBuf []int16
|
||||
readPhase float64
|
||||
tmpBuf [bufSize]byte
|
||||
srcRatioOrig float64
|
||||
srcRatioCurr float64
|
||||
)
|
||||
|
||||
// Effects are sound effects
|
||||
@@ -31,9 +48,17 @@ var Effects map[string]*Effect
|
||||
|
||||
// SetVolume sets the audio volume
|
||||
func SetVolume(vol float32) {
|
||||
if source == 0 {
|
||||
return
|
||||
}
|
||||
source.SetGain(vol)
|
||||
}
|
||||
|
||||
// Stop clears queued game audio without tearing down the OpenAL device.
|
||||
func Stop() {
|
||||
clearQueue()
|
||||
}
|
||||
|
||||
// Init initializes the audio device
|
||||
func Init() {
|
||||
err := al.OpenDevice()
|
||||
@@ -55,20 +80,76 @@ func Init() {
|
||||
// Reconfigure initializes the audio package. It sets the number of buffers, the
|
||||
// volume and the source for the games.
|
||||
func Reconfigure(r int32) {
|
||||
rate = r
|
||||
numBuffers = 4
|
||||
releaseSource()
|
||||
|
||||
log.Printf("[OpenAL]: Using %v buffers of %v bytes.\n", numBuffers, bufSize)
|
||||
rate = outputRate
|
||||
inputRate = float64(r)
|
||||
numBuffers = queueBufferCount(rate)
|
||||
queueBytes = numBuffers * bufSize
|
||||
updateInputRate()
|
||||
|
||||
log.Printf(
|
||||
"[OpenAL]: Using %v buffers of %v bytes (~%v ms queued).\n",
|
||||
numBuffers,
|
||||
bufSize,
|
||||
queueLatencyMs(rate, queueBytes),
|
||||
)
|
||||
|
||||
source = al.GenSources(1)[0]
|
||||
buffers = al.GenBuffers(int(numBuffers))
|
||||
resPtr = numBuffers
|
||||
tmpBufPtr = 0
|
||||
freeBufs = append(freeBufs[:0], buffers...)
|
||||
inputBuf = inputBuf[:0]
|
||||
readPhase = 0
|
||||
tmpBuf = [bufSize]byte{}
|
||||
|
||||
source.SetGain(settings.Current.AudioVolume)
|
||||
}
|
||||
|
||||
func releaseSource() {
|
||||
if source == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
clearQueue()
|
||||
al.DeleteSources(source)
|
||||
source = 0
|
||||
|
||||
if len(buffers) > 0 {
|
||||
al.DeleteBuffers(buffers...)
|
||||
}
|
||||
|
||||
buffers = nil
|
||||
freeBufs = nil
|
||||
queueBytes = 0
|
||||
inputBuf = nil
|
||||
readPhase = 0
|
||||
tmpBuf = [bufSize]byte{}
|
||||
inputRate = 0
|
||||
videoSyncRate = 0
|
||||
srcRatioOrig = 0
|
||||
srcRatioCurr = 0
|
||||
}
|
||||
|
||||
func clearQueue() {
|
||||
if source == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
al.StopSources(source)
|
||||
|
||||
queued := source.BuffersQueued()
|
||||
if queued > 0 {
|
||||
unqueued := make([]al.Buffer, queued)
|
||||
source.UnqueueBuffers(unqueued...)
|
||||
}
|
||||
|
||||
freeBufs = append(freeBufs[:0], buffers...)
|
||||
inputBuf = inputBuf[:0]
|
||||
readPhase = 0
|
||||
tmpBuf = [bufSize]byte{}
|
||||
srcRatioCurr = srcRatioOrig
|
||||
}
|
||||
|
||||
func min(a, b int32) int32 {
|
||||
if a < b {
|
||||
return a
|
||||
@@ -76,61 +157,237 @@ func min(a, b int32) int32 {
|
||||
return b
|
||||
}
|
||||
|
||||
func alUnqueueBuffers() bool {
|
||||
func clampFloat64(v, low, high float64) float64 {
|
||||
if v < low {
|
||||
return low
|
||||
}
|
||||
if v > high {
|
||||
return high
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func queueBufferCount(rate int32) int32 {
|
||||
totalBytes := int32(math.Ceil(float64(rate*bytesPerFrame*targetLatencyMs) / 1000.0))
|
||||
buffers := int32(math.Ceil(float64(totalBytes) / float64(bufSize)))
|
||||
|
||||
if buffers < minNumBuffers {
|
||||
return minNumBuffers
|
||||
}
|
||||
return buffers
|
||||
}
|
||||
|
||||
func adjustedInputRate(inputRate, inputFPS, targetVideoSyncRate float64) float64 {
|
||||
if inputRate <= 0 {
|
||||
return 0
|
||||
}
|
||||
if inputFPS <= 0 || targetVideoSyncRate <= 0 {
|
||||
return inputRate
|
||||
}
|
||||
|
||||
timingSkew := math.Abs(1.0 - inputFPS/targetVideoSyncRate)
|
||||
if timingSkew <= audioMaxTimingSkew {
|
||||
return inputRate * targetVideoSyncRate / inputFPS
|
||||
}
|
||||
|
||||
return inputRate
|
||||
}
|
||||
|
||||
func updateInputRate() {
|
||||
if inputRate <= 0 || rate <= 0 {
|
||||
srcRatioOrig = 0
|
||||
srcRatioCurr = 0
|
||||
return
|
||||
}
|
||||
|
||||
syncInputRate := adjustedInputRate(inputRate, state.CoreFPS, videoSyncRate)
|
||||
if syncInputRate <= 0 {
|
||||
syncInputRate = inputRate
|
||||
}
|
||||
|
||||
srcRatioOrig = float64(rate) / syncInputRate
|
||||
srcRatioCurr = srcRatioOrig
|
||||
}
|
||||
|
||||
// SetVideoTiming updates the current video timing used for audio rate control.
|
||||
func SetVideoTiming(refreshHz float64, swapInterval int) {
|
||||
syncRate := refreshHz
|
||||
if swapInterval > 1 {
|
||||
syncRate /= float64(swapInterval)
|
||||
}
|
||||
|
||||
if math.Abs(syncRate-videoSyncRate) < 0.01 {
|
||||
return
|
||||
}
|
||||
|
||||
videoSyncRate = syncRate
|
||||
updateInputRate()
|
||||
}
|
||||
|
||||
func queueLatencyMs(rate, bytes int32) int32 {
|
||||
if rate <= 0 {
|
||||
return 0
|
||||
}
|
||||
return int32(math.Round(float64(bytes*1000) / float64(rate*bytesPerFrame)))
|
||||
}
|
||||
|
||||
func pendingQueueBytes(queued, processed, offset int32) int32 {
|
||||
active := queued - processed
|
||||
if active <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
bytes := active * bufSize
|
||||
if offset > 0 {
|
||||
bytes -= min(offset, bufSize)
|
||||
}
|
||||
if bytes < 0 {
|
||||
return 0
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
func queuedWholeBufferBytes() int32 {
|
||||
if source == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
queued := source.BuffersQueued()
|
||||
processed := source.BuffersProcessed()
|
||||
return pendingQueueBytes(queued, processed, 0)
|
||||
}
|
||||
|
||||
func sourceWriteAvail() int32 {
|
||||
alUnqueueBuffers()
|
||||
|
||||
avail := queueBytes - queuedWholeBufferBytes()
|
||||
if avail < 0 {
|
||||
return 0
|
||||
}
|
||||
if avail > queueBytes {
|
||||
return queueBytes
|
||||
}
|
||||
return avail
|
||||
}
|
||||
|
||||
func currentResampleStep(writeAvail int32) float64 {
|
||||
if rate <= 0 || queueBytes <= 0 || srcRatioOrig <= 0 {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
halfSize := float64(queueBytes) / 2
|
||||
direction := 0.0
|
||||
if halfSize > 0 {
|
||||
direction = (float64(writeAvail) - halfSize) / halfSize
|
||||
}
|
||||
|
||||
adjust := 1.0 + maxPlaybackRateSkew*clampFloat64(direction, -1.0, 1.0)
|
||||
srcRatioCurr = srcRatioOrig * adjust
|
||||
return 1.0 / srcRatioCurr
|
||||
}
|
||||
|
||||
func alUnqueueBuffers() int32 {
|
||||
if source == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
val := source.BuffersProcessed()
|
||||
|
||||
if val <= 0 {
|
||||
return false
|
||||
return 0
|
||||
}
|
||||
|
||||
source.UnqueueBuffers(buffers[resPtr:val]...)
|
||||
resPtr += val
|
||||
return true
|
||||
unqueued := make([]al.Buffer, val)
|
||||
source.UnqueueBuffers(unqueued...)
|
||||
freeBufs = append(freeBufs, unqueued...)
|
||||
return val
|
||||
}
|
||||
|
||||
func alGetBuffer() al.Buffer {
|
||||
if resPtr == 0 {
|
||||
for {
|
||||
if alUnqueueBuffers() {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
resPtr--
|
||||
return buffers[resPtr]
|
||||
}
|
||||
|
||||
func fillInternalBuf(buf []byte) int32 {
|
||||
readSize := min(bufSize-tmpBufPtr, int32(len(buf)))
|
||||
copy(tmpBuf[tmpBufPtr:], buf[:readSize])
|
||||
tmpBufPtr += readSize
|
||||
return readSize
|
||||
}
|
||||
|
||||
func write(buf []byte, size int32) int32 {
|
||||
written := int32(0)
|
||||
|
||||
if state.FastForward {
|
||||
return size
|
||||
}
|
||||
|
||||
for size > 0 {
|
||||
|
||||
rc := fillInternalBuf(buf[written:])
|
||||
|
||||
written += rc
|
||||
size -= rc
|
||||
|
||||
if tmpBufPtr != bufSize {
|
||||
for len(freeBufs) == 0 {
|
||||
if alUnqueueBuffers() > 0 {
|
||||
break
|
||||
}
|
||||
|
||||
buffer := alGetBuffer()
|
||||
// OpenAL does not provide a blocking dequeue API, so keep the queue
|
||||
// short and sleep briefly while waiting for the next buffer to retire.
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
|
||||
last := len(freeBufs) - 1
|
||||
buffer := freeBufs[last]
|
||||
freeBufs = freeBufs[:last]
|
||||
return buffer
|
||||
}
|
||||
|
||||
func appendInput(buf []byte) int32 {
|
||||
readSize := int32(len(buf))
|
||||
for i := 0; i+1 < len(buf); i += 2 {
|
||||
inputBuf = append(inputBuf, int16(binary.LittleEndian.Uint16(buf[i:])))
|
||||
}
|
||||
return readSize
|
||||
}
|
||||
|
||||
func availableInputFrames() int {
|
||||
return len(inputBuf) / 2
|
||||
}
|
||||
|
||||
func requiredInputFrames(step float64) int {
|
||||
return int(math.Ceil(readPhase+step*float64(framesPerBuffer-1))) + 2
|
||||
}
|
||||
|
||||
func renderOutputBuffer(step float64) {
|
||||
for frame := 0; frame < framesPerBuffer; frame++ {
|
||||
base := int(readPhase)
|
||||
frac := readPhase - float64(base)
|
||||
|
||||
left0 := float64(inputBuf[base*2])
|
||||
right0 := float64(inputBuf[base*2+1])
|
||||
left1 := float64(inputBuf[(base+1)*2])
|
||||
right1 := float64(inputBuf[(base+1)*2+1])
|
||||
|
||||
left := int16(math.Round(left0 + (left1-left0)*frac))
|
||||
right := int16(math.Round(right0 + (right1-right0)*frac))
|
||||
|
||||
offset := frame * bytesPerFrame
|
||||
binary.LittleEndian.PutUint16(tmpBuf[offset:], uint16(left))
|
||||
binary.LittleEndian.PutUint16(tmpBuf[offset+2:], uint16(right))
|
||||
|
||||
readPhase += step
|
||||
}
|
||||
|
||||
discardFrames := int(readPhase)
|
||||
if discardFrames <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
discardSamples := discardFrames * 2
|
||||
copy(inputBuf, inputBuf[discardSamples:])
|
||||
inputBuf = inputBuf[:len(inputBuf)-discardSamples]
|
||||
readPhase -= float64(discardFrames)
|
||||
}
|
||||
|
||||
func write(buf []byte, size int32) int32 {
|
||||
if state.FastForward {
|
||||
clearQueue()
|
||||
return size
|
||||
}
|
||||
|
||||
if source == 0 || len(buffers) == 0 {
|
||||
return size
|
||||
}
|
||||
|
||||
written := appendInput(buf[:size])
|
||||
|
||||
for {
|
||||
writeAvail := sourceWriteAvail()
|
||||
step := currentResampleStep(writeAvail)
|
||||
if availableInputFrames() < requiredInputFrames(step) {
|
||||
break
|
||||
}
|
||||
buffer := alGetBuffer()
|
||||
renderOutputBuffer(step)
|
||||
buffer.BufferData(al.FormatStereo16, tmpBuf[:], rate)
|
||||
tmpBufPtr = 0
|
||||
source.QueueBuffers(buffer)
|
||||
|
||||
if source.State() != al.Playing {
|
||||
|
||||
+98
-39
@@ -1,58 +1,117 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_alUnqueueBuffers(t *testing.T) {
|
||||
t.Run("Return false if no buffers were processed", func(t *testing.T) {
|
||||
got := alUnqueueBuffers()
|
||||
if got {
|
||||
t.Errorf("alUnqueueBuffers() = %v, want %v", got, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Sample(t *testing.T) {
|
||||
t.Run("Doesn't crash when called", func(t *testing.T) {
|
||||
Sample(-30000, -30000)
|
||||
Sample( 30000, 30000)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_fillInternalBuf(t *testing.T) {
|
||||
Reconfigure(48000)
|
||||
type args struct {
|
||||
buf []byte
|
||||
size int32
|
||||
}
|
||||
func TestPendingQueueBytes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int32
|
||||
name string
|
||||
queued int32
|
||||
processed int32
|
||||
offset int32
|
||||
want int32
|
||||
}{
|
||||
{
|
||||
name: "Fill the buffer partially",
|
||||
args: args{
|
||||
buf: make([]byte, bufSize),
|
||||
size: 6000,
|
||||
},
|
||||
want: 6000,
|
||||
name: "Nothing queued",
|
||||
queued: 0,
|
||||
processed: 0,
|
||||
offset: 0,
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "Fill the buffer fully",
|
||||
args: args{
|
||||
buf: make([]byte, bufSize),
|
||||
size: 6000,
|
||||
},
|
||||
want: 2192,
|
||||
name: "Processed buffers do not count as pending",
|
||||
queued: 4,
|
||||
processed: 2,
|
||||
offset: 0,
|
||||
want: 2 * bufSize,
|
||||
},
|
||||
{
|
||||
name: "Subtract current playback offset",
|
||||
queued: 3,
|
||||
processed: 1,
|
||||
offset: 128,
|
||||
want: 2*bufSize - 128,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := fillInternalBuf(tt.args.buf[:tt.args.size]); got != tt.want {
|
||||
t.Errorf("fillInternalBuf() = %v, want %v", got, tt.want)
|
||||
got := pendingQueueBytes(tt.queued, tt.processed, tt.offset)
|
||||
if got != tt.want {
|
||||
t.Errorf("pendingQueueBytes() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueBufferCount(t *testing.T) {
|
||||
tests := []struct {
|
||||
rate int32
|
||||
want int32
|
||||
}{
|
||||
{rate: 32040, want: 13},
|
||||
{rate: 48000, want: 18},
|
||||
{rate: 96000, want: 36},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := queueBufferCount(tt.rate); got != tt.want {
|
||||
t.Errorf("queueBufferCount(%v) = %v, want %v", tt.rate, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentPlaybackRate(t *testing.T) {
|
||||
rate = outputRate
|
||||
inputRate = 32768
|
||||
videoSyncRate = 60.0
|
||||
srcRatioOrig = 0
|
||||
srcRatioCurr = 0
|
||||
updateInputRate()
|
||||
queueBytes = 5 * bufSize
|
||||
nominalStep := 1.0 / srcRatioOrig
|
||||
|
||||
t.Run("Low write availability speeds playback up", func(t *testing.T) {
|
||||
got := currentResampleStep(0)
|
||||
if got <= nominalStep {
|
||||
t.Errorf("currentResampleStep() = %v, want > %v", got, nominalStep)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("High write availability slows playback down", func(t *testing.T) {
|
||||
got := currentResampleStep(queueBytes)
|
||||
if got >= nominalStep {
|
||||
t.Errorf("currentResampleStep() = %v, want < %v", got, nominalStep)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Half-full queue stays at nominal rate", func(t *testing.T) {
|
||||
got := currentResampleStep(queueBytes / 2)
|
||||
if math.Abs(got-nominalStep) > 1e-9 {
|
||||
t.Errorf("currentResampleStep() = %v, want %v", got, nominalStep)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdjustedInputRate(t *testing.T) {
|
||||
got := adjustedInputRate(32768, 59.7275, 60.0)
|
||||
want := 32768 * 60.0 / 59.7275
|
||||
if math.Abs(got-want) > 1e-6 {
|
||||
t.Fatalf("adjustedInputRate() = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
got = adjustedInputRate(32768, 50.0, 60.0)
|
||||
if got != 32768 {
|
||||
t.Fatalf("adjustedInputRate() = %v, want %v outside timing skew window", got, 32768)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredInputFrames(t *testing.T) {
|
||||
readPhase = 0.25
|
||||
got := requiredInputFrames(1.0)
|
||||
if got != framesPerBuffer+2 {
|
||||
t.Errorf("requiredInputFrames() = %v, want %v", got, framesPerBuffer+2)
|
||||
}
|
||||
}
|
||||
+20
-6
@@ -6,6 +6,7 @@ package core
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -33,6 +34,21 @@ func Init(v *video.Video) {
|
||||
vid = v
|
||||
}
|
||||
|
||||
func applySystemAVInfo(avi libretro.SystemAVInfo) {
|
||||
vid.Geom = avi.Geometry
|
||||
|
||||
if avi.Timing.FPS > 0 {
|
||||
state.CoreFPS = avi.Timing.FPS
|
||||
}
|
||||
|
||||
if avi.Timing.SampleRate > 0 {
|
||||
audio.Reconfigure(int32(math.Round(avi.Timing.SampleRate)))
|
||||
if state.Core != nil && state.Core.AudioCallback != nil {
|
||||
state.Core.AudioCallback.SetState(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load loads a libretro core
|
||||
func Load(sofile string) error {
|
||||
// In case a core is already loaded, we need to close it properly
|
||||
@@ -178,8 +194,7 @@ func LoadGame(gamePath string) error {
|
||||
}
|
||||
|
||||
avi := state.Core.GetSystemAVInfo()
|
||||
|
||||
vid.Geom = avi.Geometry
|
||||
applySystemAVInfo(avi)
|
||||
|
||||
// Append the library name to the window title.
|
||||
if len(si.LibraryName) > 0 {
|
||||
@@ -187,10 +202,6 @@ func LoadGame(gamePath string) error {
|
||||
}
|
||||
|
||||
input.Init(vid)
|
||||
audio.Reconfigure(int32(avi.Timing.SampleRate))
|
||||
if state.Core.AudioCallback != nil {
|
||||
state.Core.AudioCallback.SetState(true)
|
||||
}
|
||||
|
||||
state.CoreRunning = true
|
||||
state.FastForward = false
|
||||
@@ -216,6 +227,7 @@ func Unload() {
|
||||
state.CorePath = ""
|
||||
state.Core = nil
|
||||
Options = nil
|
||||
state.CoreFPS = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,9 +235,11 @@ func Unload() {
|
||||
func UnloadGame() {
|
||||
if state.CoreRunning {
|
||||
savefiles.SaveSRAM()
|
||||
audio.Stop()
|
||||
state.Core.UnloadGame()
|
||||
state.GamePath = ""
|
||||
state.CoreRunning = false
|
||||
state.CoreFPS = 0
|
||||
vid.ResetPitch()
|
||||
vid.ResetRot()
|
||||
}
|
||||
|
||||
+1
-2
@@ -171,8 +171,7 @@ func environment(cmd uint32, data unsafe.Pointer) bool {
|
||||
case libretro.EnvironmentSetGeometry:
|
||||
vid.Geom = libretro.GetGeometry(data)
|
||||
case libretro.EnvironmentSetSystemAVInfo:
|
||||
avi := libretro.GetSystemAVInfo(data)
|
||||
vid.Geom = avi.Geometry
|
||||
applySystemAVInfo(libretro.GetSystemAVInfo(data))
|
||||
case libretro.EnvironmentGetFastforwarding:
|
||||
libretro.SetBool(data, state.FastForward)
|
||||
case libretro.EnvironmentGetLanguage:
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
@@ -30,12 +31,148 @@ func init() {
|
||||
|
||||
var frame = 0
|
||||
|
||||
const (
|
||||
defaultCoreFPS = 60.0
|
||||
maxLoopDelta = 250 * time.Millisecond
|
||||
maxPacingLagFrames = 2
|
||||
maxAutoSwapInt = 4
|
||||
pacingMatchSkew = 0.02
|
||||
)
|
||||
|
||||
func coreFrameDuration() time.Duration {
|
||||
fps := state.CoreFPS
|
||||
if fps <= 0 {
|
||||
fps = defaultCoreFPS
|
||||
}
|
||||
|
||||
return time.Duration(float64(time.Second) / fps)
|
||||
}
|
||||
|
||||
func frameTimeUsec(frameDuration time.Duration) int64 {
|
||||
if state.Core != nil && state.Core.FrameTimeCallback != nil {
|
||||
if state.FastForward && state.Core.FrameTimeCallback.Reference > 0 {
|
||||
return state.Core.FrameTimeCallback.Reference
|
||||
}
|
||||
|
||||
usec := frameDuration / time.Microsecond
|
||||
if usec > 0 {
|
||||
return int64(usec)
|
||||
}
|
||||
|
||||
if state.Core.FrameTimeCallback.Reference > 0 {
|
||||
return state.Core.FrameTimeCallback.Reference
|
||||
}
|
||||
}
|
||||
|
||||
return int64(coreFrameDuration() / time.Microsecond)
|
||||
}
|
||||
|
||||
func runCoreFrame(frameDuration time.Duration) {
|
||||
if state.Core == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if state.Core.FrameTimeCallback != nil {
|
||||
state.Core.FrameTimeCallback.Callback(frameTimeUsec(frameDuration))
|
||||
}
|
||||
|
||||
state.Core.Run()
|
||||
}
|
||||
|
||||
func updateSwapInterval(current *int, target int) {
|
||||
if *current == target {
|
||||
return
|
||||
}
|
||||
|
||||
glfw.SwapInterval(target)
|
||||
*current = target
|
||||
}
|
||||
|
||||
func relativeSkew(a, b float64) float64 {
|
||||
if a <= 0 || b <= 0 {
|
||||
return math.Inf(1)
|
||||
}
|
||||
return math.Abs(a-b) / b
|
||||
}
|
||||
|
||||
func displayRefreshHz(vid *video.Video) float64 {
|
||||
if vid == nil || vid.Window == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
monitor := vid.Window.GetMonitor()
|
||||
if monitor == nil {
|
||||
monitors := glfw.GetMonitors()
|
||||
if len(monitors) > 0 {
|
||||
index := settings.Current.VideoMonitorIndex
|
||||
if index < 0 || index >= len(monitors) {
|
||||
index = 0
|
||||
}
|
||||
monitor = monitors[index]
|
||||
}
|
||||
}
|
||||
|
||||
if monitor == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
mode := monitor.GetVideoMode()
|
||||
if mode == nil || mode.RefreshRate <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return float64(mode.RefreshRate)
|
||||
}
|
||||
|
||||
func autoSwapInterval(refreshHz, coreFPS float64) int {
|
||||
if refreshHz <= 0 || coreFPS <= 0 {
|
||||
return 1
|
||||
}
|
||||
|
||||
bestInterval := 1
|
||||
bestSkew := relativeSkew(refreshHz, coreFPS)
|
||||
|
||||
for interval := 2; interval <= maxAutoSwapInt; interval++ {
|
||||
effectiveRefresh := refreshHz / float64(interval)
|
||||
skew := relativeSkew(effectiveRefresh, coreFPS)
|
||||
if skew <= pacingMatchSkew && skew < bestSkew {
|
||||
bestInterval = interval
|
||||
bestSkew = skew
|
||||
}
|
||||
}
|
||||
|
||||
return bestInterval
|
||||
}
|
||||
|
||||
func effectiveRefreshHz(refreshHz float64, swapInterval int) float64 {
|
||||
if refreshHz <= 0 {
|
||||
return 0
|
||||
}
|
||||
if swapInterval <= 0 {
|
||||
return refreshHz
|
||||
}
|
||||
return refreshHz / float64(swapInterval)
|
||||
}
|
||||
|
||||
func blocksOnSwap(refreshHz, coreFPS float64) bool {
|
||||
return relativeSkew(refreshHz, coreFPS) <= pacingMatchSkew
|
||||
}
|
||||
|
||||
func runLoop(vid *video.Video, m *menu.Menu) {
|
||||
var currTime time.Time
|
||||
prevTime := time.Now()
|
||||
accumulator := time.Duration(0)
|
||||
swapInterval := -1
|
||||
pacingPrimed := false
|
||||
for !vid.Window.ShouldClose() {
|
||||
currTime = time.Now()
|
||||
dt := float32(currTime.Sub(prevTime)) / 1000000000
|
||||
loopDelta := currTime.Sub(prevTime)
|
||||
prevTime = currTime
|
||||
if loopDelta > maxLoopDelta {
|
||||
loopDelta = maxLoopDelta
|
||||
}
|
||||
|
||||
dt := float32(loopDelta) / 1000000000
|
||||
glfw.PollEvents()
|
||||
m.ProcessHotkeys()
|
||||
ntf.Process(dt)
|
||||
@@ -43,33 +180,79 @@ func runLoop(vid *video.Video, m *menu.Menu) {
|
||||
m.UpdatePalette()
|
||||
input.Poll()
|
||||
if !state.MenuActive {
|
||||
ranFrames := 0
|
||||
if state.CoreRunning {
|
||||
state.Core.Run()
|
||||
if state.Core.FrameTimeCallback != nil {
|
||||
state.Core.FrameTimeCallback.Callback(state.Core.FrameTimeCallback.Reference)
|
||||
if state.FastForward {
|
||||
updateSwapInterval(&swapInterval, 0)
|
||||
audio.SetVideoTiming(0, 0)
|
||||
runCoreFrame(coreFrameDuration())
|
||||
ranFrames = 1
|
||||
accumulator = 0
|
||||
pacingPrimed = false
|
||||
} else {
|
||||
frameDuration := coreFrameDuration()
|
||||
refreshHz := displayRefreshHz(vid)
|
||||
targetSwap := autoSwapInterval(refreshHz, state.CoreFPS)
|
||||
updateSwapInterval(&swapInterval, targetSwap)
|
||||
effectiveRefresh := effectiveRefreshHz(refreshHz, targetSwap)
|
||||
audio.SetVideoTiming(refreshHz, targetSwap)
|
||||
|
||||
if blocksOnSwap(effectiveRefresh, state.CoreFPS) {
|
||||
runCoreFrame(frameDuration)
|
||||
ranFrames = 1
|
||||
accumulator = 0
|
||||
pacingPrimed = false
|
||||
} else {
|
||||
if !pacingPrimed {
|
||||
accumulator = frameDuration
|
||||
pacingPrimed = true
|
||||
}
|
||||
|
||||
accumulator += loopDelta
|
||||
maxAccumulator := frameDuration * maxPacingLagFrames
|
||||
if accumulator > maxAccumulator {
|
||||
accumulator = maxAccumulator
|
||||
}
|
||||
|
||||
if accumulator >= frameDuration {
|
||||
runCoreFrame(frameDuration)
|
||||
accumulator -= frameDuration
|
||||
if accumulator > frameDuration {
|
||||
accumulator = frameDuration
|
||||
}
|
||||
ranFrames = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if state.Core.AudioCallback != nil {
|
||||
|
||||
if state.Core.AudioCallback != nil && ranFrames > 0 {
|
||||
state.Core.AudioCallback.Callback()
|
||||
}
|
||||
} else {
|
||||
updateSwapInterval(&swapInterval, 1)
|
||||
audio.SetVideoTiming(0, 0)
|
||||
accumulator = 0
|
||||
pacingPrimed = false
|
||||
}
|
||||
|
||||
vid.Render()
|
||||
frame++
|
||||
if frame%600 == 0 { // save sram about every 10 sec
|
||||
savefiles.SaveSRAM()
|
||||
for i := 0; i < ranFrames; i++ {
|
||||
frame++
|
||||
if frame%600 == 0 { // save sram about every 10 sec
|
||||
savefiles.SaveSRAM()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateSwapInterval(&swapInterval, 1)
|
||||
audio.SetVideoTiming(0, 0)
|
||||
accumulator = 0
|
||||
pacingPrimed = false
|
||||
m.Update(dt)
|
||||
vid.Render()
|
||||
m.Render(dt)
|
||||
}
|
||||
m.RenderNotifications()
|
||||
if state.FastForward {
|
||||
glfw.SwapInterval(0)
|
||||
} else {
|
||||
glfw.SwapInterval(1)
|
||||
}
|
||||
vid.Window.SwapBuffers()
|
||||
prevTime = currTime
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAutoSwapInterval(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
refreshHz float64
|
||||
coreFPS float64
|
||||
want int
|
||||
}{
|
||||
{name: "60Hz stays at swap 1", refreshHz: 60, coreFPS: 59.73, want: 1},
|
||||
{name: "120Hz prefers swap 2 for 60fps", refreshHz: 120, coreFPS: 60, want: 2},
|
||||
{name: "144Hz prefers swap 3 for 48fps", refreshHz: 144, coreFPS: 48, want: 3},
|
||||
{name: "144Hz keeps swap 1 for 60fps", refreshHz: 144, coreFPS: 60, want: 1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := autoSwapInterval(tt.refreshHz, tt.coreFPS); got != tt.want {
|
||||
t.Fatalf("autoSwapInterval(%v, %v) = %v, want %v", tt.refreshHz, tt.coreFPS, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlocksOnSwap(t *testing.T) {
|
||||
if got := blocksOnSwap(60.0, 59.73); !got {
|
||||
t.Fatalf("blocksOnSwap() = %v, want %v", got, true)
|
||||
}
|
||||
|
||||
if got := blocksOnSwap(144.0, 60.0); got {
|
||||
t.Fatalf("blocksOnSwap() = %v, want %v", got, false)
|
||||
}
|
||||
}
|
||||
@@ -33,3 +33,6 @@ var LudOS bool
|
||||
|
||||
// FastForward will run the core as fast as possible
|
||||
var FastForward bool
|
||||
|
||||
// CoreFPS is the current emulated video rate declared by the core.
|
||||
var CoreFPS float64
|
||||
Reference in new issue
Block a user