diff --git a/.gitignore b/.gitignore
index 5efdd07ae..0a5c1e2a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
*.swo
.DS_Store
Session.vim
+*/testdata
vendor/*
tags
test_roms/*
diff --git a/Makefile b/Makefile
index 5baeaaf38..e3c365d1e 100644
--- a/Makefile
+++ b/Makefile
@@ -84,7 +84,7 @@ vet: check_awk
'
### testing targets
-.PHONY: test race race_debug
+.PHONY: test race race_debug fuzz
test:
# testing with shuffle preferred but it's only available in go 1.17 onwards
@@ -100,6 +100,10 @@ race: generate test
race_debug: generate test
$(goBinary) run -race gopher2600.go debug $(profilingRom)
+fuzz: generate test
+# fuzz testing cannot work with multiple packages so we must list the ones
+# we want to include
+ $(goBinary) test -fuzztime=30s -fuzz . ./crunched/
### profiling targets
.PHONY: profile profile_cpu profile_cpu_play profile_cpu_debug profile_mem_play profile_mem_debug profile_trace
diff --git a/crunched/crunched.go b/crunched/crunched.go
new file mode 100644
index 000000000..3f0e07692
--- /dev/null
+++ b/crunched/crunched.go
@@ -0,0 +1,43 @@
+// This file is part of Gopher2600.
+//
+// Gopher2600 is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Gopher2600 is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Gopher2600. If not, see .
+
+package crunched
+
+// Data provides the interface to a crunched data type
+type Data interface {
+ // IsCrunched returns true if data is currently crunched
+ IsCrunched() bool
+
+ // Size returns the uncrunched size and the current size of the data. If the
+ // data is currently crunched then the two values will be the same
+ Size() (int, int)
+
+ // Data returns a pointer to the uncrunched data
+ Data() *[]byte
+
+ // Snapshot makes a copy of the data and crunching it if required. The data will
+ // be uncrunched automatically when Data() function is called
+ Snapshot() Data
+}
+
+// Inspection provides the interface to the crunched data type and provides the
+// ability to inspect the data in its current form
+type Inspection interface {
+ Data
+
+ // Inspect returns data in the current state. In other words, the data will
+ // not be decrunched as it would be with the Data() function
+ Inspect() *[]byte
+}
diff --git a/crunched/doc.go b/crunched/doc.go
new file mode 100644
index 000000000..0b5c1f452
--- /dev/null
+++ b/crunched/doc.go
@@ -0,0 +1,21 @@
+// This file is part of Gopher2600.
+//
+// Gopher2600 is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Gopher2600 is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Gopher2600. If not, see .
+
+// Package crunched provides the Data interface. Implementations of this
+// interface can store data in a crunched and uncrunched state.
+//
+// The intention is that implementations crunch data after a call to Snapshot()
+// and transparently uncrunch it on a call to Data().
+package crunched
diff --git a/crunched/quick.go b/crunched/quick.go
new file mode 100644
index 000000000..0256222b9
--- /dev/null
+++ b/crunched/quick.go
@@ -0,0 +1,156 @@
+// This file is part of Gopher2600.
+//
+// Gopher2600 is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Gopher2600 is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Gopher2600. If not, see .
+
+package crunched
+
+type quick struct {
+ crunched bool
+ data []byte
+ uncrunchedSize int
+}
+
+// NewQuick returns an implementation of the Data interface that is intended to
+// perform quickly on both crunching and decrunching.
+//
+// For simplicity, the minimum amount of data allocated will be 4 bytes.
+func NewQuick(size int) Data {
+ size = max(size, 4)
+ return &quick{
+ data: make([]byte, size),
+ uncrunchedSize: size,
+ }
+}
+
+// IsCrunched returns true if data is currently crunched
+//
+// This function implements the Data interface
+func (c *quick) IsCrunched() bool {
+ return c.crunched
+}
+
+// Size returns the uncrunched size and the current size of the data. If the
+// data is currently crunched then the two values will be the same
+//
+// This function implements the Data interface
+func (c *quick) Size() (int, int) {
+ return c.uncrunchedSize, len(c.data)
+}
+
+// Data returns a pointer to the uncrunched data
+//
+// This function implements the Data interface
+func (c *quick) Data() *[]byte {
+ if c.crunched {
+ // sanity check. with the current RLE method the number of bytes in the
+ // crunched data should be a multiple of two
+ if len(c.data)&0x01 == 0x01 {
+ panic("crunched data should have an even number of bytes")
+ }
+
+ // make a reference to the crunched data before creating space for the
+ // uncrunched data
+ working := c.data
+ c.data = make([]byte, c.uncrunchedSize)
+
+ // undo the RLE process
+ var idx int
+ for i := 0; i < len(working); i += 2 {
+ for r := 0; r <= int(working[i+1]); r++ {
+ c.data[idx] = working[i]
+ idx++
+ }
+ }
+
+ // data is now uncrunched
+ c.crunched = false
+ }
+
+ return &c.data
+}
+
+// Snapshot makes a copy of the data and crunching it if required. The data will
+// be uncrunched automatically when Data() function is called
+//
+// This function implements the Data interface
+func (c *quick) Snapshot() Data {
+ d := *c
+
+ if !d.crunched {
+ working := make([]byte, d.uncrunchedSize)
+
+ var ct int
+ var idx int
+ working[idx] = c.data[0]
+
+ // assume crunching has succeeded unless explicitely told otherwise
+ d.crunched = true
+
+ // very basic RLE algorithm:
+ // 1) each byte is followed by a count value
+ // 2) maximum count value is 255
+ for _, v := range c.data[1:] {
+ if v == working[idx] && ct < 255 {
+ ct++
+ } else {
+ // check that the crunched data isn't getting too large. we'll
+ // be adding two bytes to the crunch stream so the check here is
+ // to make sure that won't overflow the size of the array
+ if idx >= len(working)-2 {
+ d.crunched = false
+ break // for loop
+ }
+
+ // output count to the crunch stream
+ idx++
+ working[idx] = byte(ct)
+
+ // output new byte to crunch stream
+ idx++
+ working[idx] = v
+
+ // count will begin again with the new byte
+ ct = 0
+ }
+ }
+
+ // if the data has been crunched then allocate just enough memory to
+ // store the crunched data before returning
+ if d.crunched {
+ idx++
+ working[idx] = byte(ct)
+ d.data = make([]byte, idx+1)
+ copy(d.data, working[:idx+1])
+ return &d
+ }
+
+ // if data is not crunched then we intentionally fall through to the
+ // plain data copy below
+ }
+
+ // copy data as it exists now. this may be crunched data or uncrunched data.
+ // it doesn't matter either way
+ d.data = make([]byte, len(c.data))
+ copy(d.data, c.data)
+
+ return &d
+}
+
+// Inspect returns data in the current state. In other words, the data will
+// not be decrunched as it would be with the Data() function
+//
+// This function implements the Peep interface
+func (c *quick) Inspect() *[]byte {
+ return &c.data
+}
diff --git a/crunched/quick_test.go b/crunched/quick_test.go
new file mode 100644
index 000000000..6625d61c3
--- /dev/null
+++ b/crunched/quick_test.go
@@ -0,0 +1,137 @@
+// This file is part of Gopher2600.
+//
+// Gopher2600 is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Gopher2600 is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Gopher2600. If not, see .
+
+package crunched_test
+
+import (
+ "crypto/md5"
+ "math/rand"
+ "testing"
+
+ "github.com/jetsetilly/gopher2600/crunched"
+ "github.com/jetsetilly/gopher2600/test"
+)
+
+func TestEmptyData_Quick(t *testing.T) {
+ // create 100 bytes of empty data
+ qa := crunched.NewQuick(100)
+
+ // take hash of data before crunching
+ preCrunchHash := md5.Sum(*qa.Data())
+
+ // data should not be crunched
+ test.ExpectFailure(t, qa.IsCrunched())
+
+ // take a snapshot of the data
+ qb := qa.Snapshot()
+
+ // the snapshotted data should be crunched
+ test.ExpectSuccess(t, qb.IsCrunched())
+
+ // the original data should be left uncrunched
+ test.ExpectSuccess(t, !qa.IsCrunched())
+
+ // inspect the crunched data
+ inspection := qb.(crunched.Inspection).Inspect()
+ expectedData := []byte{0, 99}
+ test.DemandEquality(t, len(*inspection), len(expectedData))
+ for i, v := range *inspection {
+ test.ExpectEquality(t, v, expectedData[i])
+ }
+
+ // check that hash of uncrunched data is the same as it was before
+ postCrunchedHash := md5.Sum(*qb.Data())
+ test.ExpectEquality(t, preCrunchHash, postCrunchedHash)
+
+ // obtaining the data from the snapshot should leave the data in the
+ // snapshot in an uncrunched state
+ test.ExpectSuccess(t, !qb.IsCrunched())
+}
+
+func TestUncompressableData_quick(t *testing.T) {
+ // create 256 bytes of empty data
+ qa := crunched.NewQuick(256)
+
+ // insert data that can't be compressed by the quick method
+ data := qa.Data()
+ for i := 0; i < len(*data); i++ {
+ (*data)[i] = byte(i)
+ }
+
+ // take hash of data before crunching
+ preCrunchHash := md5.Sum(*data)
+
+ // take a snapshot of the data
+ qb := qa.Snapshot()
+
+ // the snapshotted data should not be crunched
+ test.ExpectSuccess(t, !qb.IsCrunched())
+
+ // check that hash of uncrunched data is the same as it was before
+ postCrunchedHash := md5.Sum(*qb.Data())
+ test.ExpectEquality(t, preCrunchHash, postCrunchedHash)
+}
+
+func TestEmptyData_ExampleData(t *testing.T) {
+ // create 100 bytes of empty data
+ qa := crunched.NewQuick(20)
+
+ // insert data that can't be compressed by the quick method
+ data := qa.Data()
+ copy(*data, []byte{1, 2, 3, 3, 3, 3, 4, 4, 5, 6})
+
+ // snapshot should successfully crunch the data
+ qb := qa.Snapshot()
+ test.ExpectSuccess(t, qb.IsCrunched())
+
+ inspection := qb.(crunched.Inspection).Inspect()
+
+ expectedData := []byte{1, 0, 2, 0, 3, 3, 4, 1, 5, 0, 6, 0, 0, 9}
+ test.DemandEquality(t, len(*inspection), len(expectedData))
+ for i, v := range *inspection {
+ test.ExpectEquality(t, v, expectedData[i])
+ }
+}
+
+func FuzzQuick(f *testing.F) {
+ f.Fuzz(func(t *testing.T, size uint) {
+ qa := crunched.NewQuick(int(size))
+
+ // insert data that can't be compressed by the quick method
+ data := qa.Data()
+ (*data)[0] = byte(rand.Intn(255))
+ for i := 1; i < len(*data); i++ {
+ b := byte(rand.Intn(255))
+ for b == (*data)[i-1] {
+ b = byte(rand.Intn(255))
+ }
+ (*data)[i] = b
+ }
+
+ // take hash of data before crunching
+ preCrunchHash := md5.Sum(*data)
+
+ // take a snapshot of the data
+ qb := qa.Snapshot()
+
+ // the snapshotted data should not be crunched because it should be
+ // impossible with the data we've given it
+ test.ExpectSuccess(t, !qb.IsCrunched())
+
+ // check that hash of uncrunched data is the same as it was before
+ postCrunchedHash := md5.Sum(*qb.Data())
+ test.ExpectEquality(t, preCrunchHash, postCrunchedHash)
+ })
+}
diff --git a/hardware/memory/cartridge/elf/memory.go b/hardware/memory/cartridge/elf/memory.go
index 980f6686b..56b9b8815 100644
--- a/hardware/memory/cartridge/elf/memory.go
+++ b/hardware/memory/cartridge/elf/memory.go
@@ -24,6 +24,7 @@ import (
"github.com/jetsetilly/gopher2600/coprocessor"
"github.com/jetsetilly/gopher2600/coprocessor/developer/faults"
+ "github.com/jetsetilly/gopher2600/crunched"
"github.com/jetsetilly/gopher2600/environment"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/arm"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/arm/architecture"
@@ -103,7 +104,7 @@ type elfMemory struct {
gpio *gpio
// RAM memory for the ARM
- sram []byte
+ sram crunched.Data
sramOrigin uint32
sramMemtop uint32
@@ -172,6 +173,21 @@ func newElfMemory(env *environment.Environment) *elfMemory {
// always using PlusCart model for now
mem.model = architecture.NewMap(architecture.PlusCart)
+
+ // SRAM creation
+ const sramSize = 0x10000 // 64kb of SRAM
+ mem.sram = crunched.NewQuick(sramSize)
+ mem.sramOrigin = mem.model.SRAMOrigin
+ mem.sramMemtop = mem.sramOrigin + sramSize
+
+ // randomise sram data
+ if mem.env.Prefs.RandomState.Get().(bool) {
+ data := mem.sram.Data()
+ for i := range *data {
+ (*data)[i] = uint8(mem.env.Random.NoRewind(0xff))
+ }
+ }
+
return mem
}
@@ -621,18 +637,6 @@ func (mem *elfMemory) decode(ef *elf.File) error {
logger.Logf(mem.env, "ELF", "strongarm: %08x to %08x (%d)",
mem.strongArmOrigin, mem.strongArmMemtop, len(mem.strongArmProgram))
- // SRAM creation
- mem.sram = make([]byte, 0x10000) // 64k SRAM
- mem.sramOrigin = mem.model.SRAMOrigin
- mem.sramMemtop = mem.sramOrigin + uint32(len(mem.sram))
-
- // randomise sram data
- if mem.env.Prefs.RandomState.Get().(bool) {
- for i := range mem.sram {
- mem.sram[i] = uint8(mem.env.Random.NoRewind(0xff))
- }
- }
-
// runInitialisation() must be run once ARM has been created
return nil
@@ -744,8 +748,7 @@ func (mem *elfMemory) Snapshot() *elfMemory {
// sram is likely to have changed. it would be nice to have a compressed
// form of memory here or one that records deltas from previous snapshots
- m.sram = make([]byte, len(mem.sram))
- copy(m.sram, mem.sram)
+ m.sram = mem.sram.Snapshot()
// strongarm functions are a map and so require an explicit make()
m.strongArmFunctions = make(map[uint32]strongArmFunctionSpec)
@@ -831,7 +834,7 @@ func (mem *elfMemory) mapAddress(addr uint32, write bool) (*[]byte, uint32) {
}
if addr >= mem.sramOrigin && addr <= mem.sramMemtop {
- return &mem.sram, mem.sramOrigin
+ return mem.sram.Data(), mem.sramOrigin
}
if addr >= mem.strongArmOrigin && addr <= mem.strongArmMemtop {
@@ -914,7 +917,7 @@ func (mem *elfMemory) Segments() []mapper.CartStaticSegment {
func (mem *elfMemory) Reference(segment string) ([]uint8, bool) {
switch segment {
case "SRAM":
- return mem.sram, true
+ return *mem.sram.Data(), true
case "StrongARM Program":
return mem.strongArmProgram, true
default:
diff --git a/test/expected.go b/test/expected.go
index 200f032f8..272424f98 100644
--- a/test/expected.go
+++ b/test/expected.go
@@ -17,6 +17,19 @@ package test
import "testing"
+// DemandEquility is used to test equality between one value and another. If the
+// test fails it is a testing fatility
+//
+// This is particular useful if the values being tested are used in further
+// tests and so must be correct. For example, testing that the lengths of two
+// slices are equal before iterating over them in unison
+func DemandEquality[T comparable](t *testing.T, value T, expectedValue T) {
+ t.Helper()
+ if value != expectedValue {
+ t.Fatalf("equality test of type %T failed: %v does not equal %v)", value, value, expectedValue)
+ }
+}
+
// ExpectEquality is used to test equality between one value and another
func ExpectEquality[T comparable](t *testing.T, value T, expectedValue T) {
t.Helper()