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()