Skip to content

Commit

Permalink
perf(apu): Optimize APU using a ring buffer
Browse files Browse the repository at this point in the history
  • Loading branch information
gabe565 committed Jan 29, 2025
1 parent 1202554 commit ec6e69d
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 24 deletions.
34 changes: 10 additions & 24 deletions internal/apu/apu.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package apu

import (
"bytes"
"log/slog"
"math"
"sync"

"gabe565.com/gones/internal/config"
"gabe565.com/gones/internal/consts"
Expand Down Expand Up @@ -59,20 +57,21 @@ func New(conf *config.Config) *APU {
Enabled: true,
SampleRate: DefaultSampleRate,
conf: &conf.Audio,
buf: newRingBuffer(BufferCap),

Square: [2]Square{{Channel1: true}, {}},
Noise: Noise{ShiftRegister: 1},

FramePeriod: 4,
}
a.buf.Grow(BufferCap)
return a
}

type APU struct {
Enabled bool `msgpack:"-"`
SampleRate float64 `msgpack:"-"`
conf *config.Audio
buf *ringBuffer

Square [2]Square
Triangle Triangle
Expand All @@ -85,9 +84,6 @@ type APU struct {

IRQEnabled bool `msgpack:"alias:IrqEnabled"`
IRQPending bool `msgpack:"alias:IrqPending"`

buf bytes.Buffer
mu sync.Mutex
}

func (a *APU) WriteMem(addr uint16, data byte) {
Expand Down Expand Up @@ -265,31 +261,21 @@ func (a *APU) output() float32 {
func (a *APU) sendSample() {
result := a.output()
b := math.Float32bits(result)
a.mu.Lock()
defer a.mu.Unlock()
if a.buf.Len() < BufferCap {
a.buf.Write([]byte{
byte(b), byte(b >> 8), byte(b >> 16), byte(b >> 24),
byte(b), byte(b >> 8), byte(b >> 16), byte(b >> 24),
})
}
a.buf.Write([]byte{
byte(b), byte(b >> 8), byte(b >> 16), byte(b >> 24),
byte(b), byte(b >> 8), byte(b >> 16), byte(b >> 24),
})
}

func (a *APU) Clear() {
a.mu.Lock()
defer a.mu.Unlock()
a.buf.Reset()
}

func (a *APU) Read(p []byte) (int, error) {
a.mu.Lock()
n, err := a.buf.Read(p)
a.mu.Unlock()
if err != nil {
if n == 0 {
clear(p)
}
n := a.buf.Read(p)
if n == 0 {
clear(p)
return len(p), nil
}
return n, err
return n, nil
}
132 changes: 132 additions & 0 deletions internal/apu/ringbuffer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package apu

import (
"sync"
)

type ringBuffer struct {
buf []byte
size int
r, w int
full bool
mu sync.Mutex
}

func newRingBuffer(size int) *ringBuffer {
return &ringBuffer{
buf: make([]byte, size),
size: size,
}
}

func (r *ringBuffer) Read(p []byte) int {
r.mu.Lock()
defer r.mu.Unlock()

var n int
switch {
case r.w == r.r && !r.full:
// Buffer empty
return 0
case r.w > r.r:
// Writer ahead of reader
if n = r.w - r.r; n > len(p) {
n = len(p)
}
copy(p, r.buf[r.r:r.r+n])
default:
// Reader ahead of writer
if n = r.size - r.r + r.w; n > len(p) {
n = len(p)
}

if r.r+n <= r.size {
// End of buffer has enough elements
copy(p, r.buf[r.r:r.r+n])
} else {
// End of buffer does not have enough elements; read will wrap around
c1 := r.size - r.r
copy(p, r.buf[r.r:r.size])
c2 := n - c1
copy(p[c1:], r.buf[0:c2])
}

r.full = false
}
r.r = (r.r + n) % r.size
return n
}

func (r *ringBuffer) Write(p []byte) {
r.mu.Lock()
defer r.mu.Unlock()

n := len(p)
switch {
case r.free() < len(p):
// Buffer too full; discard write
return
case r.w >= r.r:
// Writer ahead of reader
if c1 := r.size - r.w; c1 >= n {
// Slice fits in the end of the buffer
copy(r.buf[r.w:], p)
r.w += n
} else {
// Slice does not fit in the end of the buffer; write will wrap around
copy(r.buf[r.w:], p[:c1])
c2 := n - c1
copy(r.buf[0:], p[c1:])
r.w = c2
}
default:
// Reader is ahead of writer
copy(r.buf[r.w:], p)
r.w += n
}

if r.w == r.size {
r.w = 0
}
r.full = r.w == r.r
}

func (r *ringBuffer) len() int {
r.mu.Lock()
defer r.mu.Unlock()

switch {
case r.w == r.r:
if r.full {
return r.size
}
return 0
case r.w > r.r:
return r.w - r.r
default:
return r.size - r.r + r.w
}
}

func (r *ringBuffer) free() int {
switch {
case r.w == r.r:
if r.full {
return 0
}
return r.size
case r.w < r.r:
return r.r - r.w
default:
return r.size - r.w + r.r
}
}

func (r *ringBuffer) Reset() {
r.mu.Lock()
defer r.mu.Unlock()

r.r = 0
r.w = 0
r.full = false
}
40 changes: 40 additions & 0 deletions internal/apu/ringbuffer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package apu

import (
"testing"

"github.com/stretchr/testify/assert"
)

func Test_ringBuffer(t *testing.T) {
const bufSize = 16

buf := newRingBuffer(16)
p := make([]byte, bufSize)
n := buf.Read(p)
assert.Equal(t, 0, n)
assert.Equal(t, p, make([]byte, bufSize))
assert.Equal(t, bufSize, buf.free())
assert.Equal(t, 0, buf.len())

const message = "hello world"
for range 3 {
buf.Write([]byte(message))
assert.Equal(t, bufSize-len(message), buf.free())
assert.Equal(t, len(message), buf.len())

n = buf.Read(p)
assert.Equal(t, len(message), n)
assert.Equal(t, []byte(message), p[:n])
assert.Equal(t, bufSize, buf.free())
assert.Equal(t, 0, buf.len())
}

buf.Write([]byte(message))
p = make([]byte, 5)
n = buf.Read(p)
assert.Equal(t, bufSize-len(message)+5, buf.free())
assert.Equal(t, len(message)-5, buf.len())
assert.Equal(t, 5, n)
assert.Equal(t, []byte("hello"), p[:n])
}

0 comments on commit ec6e69d

Please sign in to comment.