Skip to content

Commit e90f404

Browse files
committed
Adding disk image persistence, with initial GWT and TeaVM implementations that save to the OPFS (Origin Private File System).
1 parent 8f62fa3 commit e90f404

17 files changed

Lines changed: 944 additions & 33 deletions

core/src/main/java/emu/jvic/Machine.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import emu.jvic.io.Via6522;
1313
import emu.jvic.io.SerialBus;
1414
import emu.jvic.io.disk.C1541Drive;
15+
import emu.jvic.io.disk.persistence.DiskImagePersistenceSession;
16+
import emu.jvic.io.disk.persistence.NoOpDiskImagePersistenceSession;
1517
import emu.jvic.io.tape.C1530Datasette;
1618
import emu.jvic.memory.Memory;
1719
import emu.jvic.memory.RamType;
@@ -88,7 +90,18 @@ public Machine(SoundGenerator soundGenerator, KeyboardMatrix keyboardMatrix, Pix
8890
public Callable<Queue<char[]>> init(
8991
byte[] basicRom, byte[] kernalRom, byte[] charRom, byte[] dos1541Rom,
9092
Program program, MachineType machineType, RamType ramType, String palette) {
91-
93+
return init(basicRom, kernalRom, charRom, dos1541Rom, program, machineType, ramType,
94+
palette, null);
95+
}
96+
97+
/**
98+
* Initialises the machine using an optional pre-resolved disk persistence session.
99+
*/
100+
public Callable<Queue<char[]>> init(
101+
byte[] basicRom, byte[] kernalRom, byte[] charRom, byte[] dos1541Rom,
102+
Program program, MachineType machineType, RamType ramType, String palette,
103+
DiskImagePersistenceSession diskImagePersistenceSession) {
104+
92105
byte[] programData = (program != null? program.getProgramData() : null);
93106
Snapshot snapshot = null;
94107
Callable<Queue<char[]>> autoLoadRunnable = null;
@@ -175,8 +188,13 @@ public Callable<Queue<char[]>> init(
175188
} else if ("PRG".equals(programType)) {
176189
autoLoadRunnable = memory.loadBasicProgram(programData, true);
177190
} else if ("DISK".equals(programType)) {
191+
DiskImagePersistenceSession persistenceSession = diskImagePersistenceSession;
192+
if (persistenceSession == null) {
193+
persistenceSession = new NoOpDiskImagePersistenceSession(programData);
194+
}
195+
byte[] startupDiskImage = persistenceSession.getStartupDiskImage();
178196
// Insert the disk ready to be booted.
179-
c1541Drive.insertDisk(programData, true);
197+
c1541Drive.insertDisk(startupDiskImage, true, persistenceSession);
180198
autoLoadRunnable = new Callable<Queue<char[]>>() {
181199
public Queue<char[]> call() throws Exception {
182200
String autoRunCmds = program.getAppConfigItem().getAutoRunCommand();

core/src/main/java/emu/jvic/io/disk/C1541Drive.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import emu.jvic.io.SerialBus;
55
import emu.jvic.io.Via6522;
66
import emu.jvic.io.disk.GcrDiskImage.Sector;
7+
import emu.jvic.io.disk.persistence.DiskImagePersistenceSession;
78
import emu.jvic.memory.Memory;
89
import emu.jvic.memory.RamChip;
910
import emu.jvic.memory.RomChip;
@@ -126,6 +127,11 @@ public class C1541Drive {
126127
*/
127128
private SerialBus serialBus;
128129

130+
/**
131+
* Persistence session for the currently inserted disk image.
132+
*/
133+
private DiskImagePersistenceSession diskPersistenceSession;
134+
129135
/**
130136
* Constructor for C1541Drive.
131137
*
@@ -148,8 +154,14 @@ public C1541Drive(SerialBus serialBus, byte[] dos1541Rom) {
148154
* @param diskData The draw .d64 disk image data to insert.
149155
* @param warmup Whether or not to run emulation for half a second.
150156
*/
151-
public void insertDisk(byte[] diskData, boolean warmup) {
157+
public void insertDisk(byte[] diskData, boolean warmup,
158+
DiskImagePersistenceSession diskPersistenceSession) {
159+
if (this.diskPersistenceSession != null) {
160+
this.diskPersistenceSession.close();
161+
}
162+
152163
disk = new GcrDiskImage(diskData);
164+
this.diskPersistenceSession = diskPersistenceSession;
153165

154166
// There is no track 0. Tracks start at 1, sectors at 0.
155167
currentTrack = 1;
@@ -171,6 +183,10 @@ public void insertDisk(byte[] diskData, boolean warmup) {
171183
* Acts as if the disk has been ejected.
172184
*/
173185
public void ejectDisk() {
186+
if (diskPersistenceSession != null) {
187+
diskPersistenceSession.close();
188+
diskPersistenceSession = null;
189+
}
174190
disk = null;
175191
}
176192

@@ -271,6 +287,9 @@ private Memory createMemory(final Cpu6502 cpu, final Via6522 via1, final Via6522
271287
private void flushWrites() {
272288
if (bytesWritten > 0) {
273289
currentSector.commitWrites();
290+
if (diskPersistenceSession != null) {
291+
diskPersistenceSession.onDiskChanged(disk.copyRawImage());
292+
}
274293
}
275294
bytesWritten = 0;
276295
}

core/src/main/java/emu/jvic/io/disk/GcrDiskImage.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,19 @@ public Sector getSector(int track, int sector) {
407407
public int getSectorCount(int track) {
408408
return TRACK_OFFSETS[track][0];
409409
}
410+
411+
/**
412+
* Returns a copy of the current raw .d64 disk image bytes.
413+
*
414+
* @return a copy of the raw disk image.
415+
*/
416+
public byte[] copyRawImage() {
417+
byte[] copy = new byte[rawImage.length];
418+
for (int i = 0; i < rawImage.length; i++) {
419+
copy[i] = (byte)(rawImage[i] & 0xff);
420+
}
421+
return copy;
422+
}
410423

411424
/**
412425
* This class represents a Sector within the GCR disk image. It stores details such as the
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package emu.jvic.io.disk.persistence;
2+
3+
import java.util.function.Consumer;
4+
5+
import emu.jvic.config.AppConfigItem;
6+
7+
/**
8+
* Resolves a persistence session for a disk image before the drive is initialised.
9+
*/
10+
public interface DiskImagePersistence {
11+
12+
void resolve(AppConfigItem appConfigItem, byte[] originalDiskImage,
13+
Consumer<DiskImagePersistenceSession> onResolved);
14+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package emu.jvic.io.disk.persistence;
2+
3+
/**
4+
* Represents the persistence lifecycle for a single mounted disk image.
5+
*/
6+
public interface DiskImagePersistenceSession {
7+
8+
byte[] getStartupDiskImage();
9+
10+
boolean isPersistent();
11+
12+
void onDiskChanged(byte[] diskImageBytes);
13+
14+
void close();
15+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package emu.jvic.io.disk.persistence;
2+
3+
/**
4+
* Deterministic key used to map a logical disk program to an OPFS location.
5+
*/
6+
public class DiskPersistenceKey {
7+
8+
private final String programKey;
9+
private final String originalDiskHash;
10+
11+
public DiskPersistenceKey(String programKey, String originalDiskHash) {
12+
this.programKey = programKey;
13+
this.originalDiskHash = originalDiskHash;
14+
}
15+
16+
public String getProgramKey() {
17+
return programKey;
18+
}
19+
20+
public String getOriginalDiskHash() {
21+
return originalDiskHash;
22+
}
23+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package emu.jvic.io.disk.persistence;
2+
3+
import emu.jvic.config.AppConfigItem;
4+
5+
/**
6+
* Diagnostic metadata stored next to a persisted disk image.
7+
*/
8+
public class DiskPersistenceMetadata {
9+
10+
private final int schemaVersion = 1;
11+
private final String programKey;
12+
private final String programIdSource;
13+
private final String gameId;
14+
private final String name;
15+
private final String filePath;
16+
private final String entryName;
17+
private final String fileType;
18+
private final String machineType;
19+
private final String originalDiskHash;
20+
private final int originalDiskSize;
21+
private String persistedDiskHash;
22+
private long createdAtEpochMs;
23+
private long updatedAtEpochMs;
24+
private long persistenceActivatedAtEpochMs;
25+
26+
public DiskPersistenceMetadata(AppConfigItem appConfigItem, DiskPersistenceKey key,
27+
String programIdSource, int originalDiskSize) {
28+
this.programKey = key.getProgramKey();
29+
this.programIdSource = programIdSource;
30+
this.gameId = appConfigItem.getGameId();
31+
this.name = appConfigItem.getName();
32+
this.filePath = appConfigItem.getFilePath();
33+
this.entryName = appConfigItem.getEntryName();
34+
this.fileType = appConfigItem.getFileType();
35+
this.machineType = appConfigItem.getMachineType();
36+
this.originalDiskHash = key.getOriginalDiskHash();
37+
this.originalDiskSize = originalDiskSize;
38+
}
39+
40+
public void setPersistedDiskHash(String persistedDiskHash) {
41+
this.persistedDiskHash = persistedDiskHash;
42+
}
43+
44+
public void setCreatedAtEpochMs(long createdAtEpochMs) {
45+
this.createdAtEpochMs = createdAtEpochMs;
46+
}
47+
48+
public void setUpdatedAtEpochMs(long updatedAtEpochMs) {
49+
this.updatedAtEpochMs = updatedAtEpochMs;
50+
}
51+
52+
public void setPersistenceActivatedAtEpochMs(long persistenceActivatedAtEpochMs) {
53+
this.persistenceActivatedAtEpochMs = persistenceActivatedAtEpochMs;
54+
}
55+
56+
public String toJson() {
57+
StringBuilder json = new StringBuilder(512);
58+
json.append('{');
59+
appendNumber(json, "schemaVersion", schemaVersion);
60+
appendString(json, "programKey", programKey);
61+
appendString(json, "programIdSource", programIdSource);
62+
appendString(json, "gameId", gameId);
63+
appendString(json, "name", name);
64+
appendString(json, "filePath", filePath);
65+
appendString(json, "entryName", entryName);
66+
appendString(json, "fileType", fileType);
67+
appendString(json, "machineType", machineType);
68+
appendString(json, "originalDiskHash", originalDiskHash);
69+
appendString(json, "persistedDiskHash", persistedDiskHash);
70+
appendNumber(json, "originalDiskSize", originalDiskSize);
71+
appendNumber(json, "createdAtEpochMs", createdAtEpochMs);
72+
appendNumber(json, "updatedAtEpochMs", updatedAtEpochMs);
73+
appendNumber(json, "persistenceActivatedAtEpochMs", persistenceActivatedAtEpochMs);
74+
json.append('}');
75+
return json.toString();
76+
}
77+
78+
private void appendString(StringBuilder json, String name, String value) {
79+
if (json.length() > 1) {
80+
json.append(',');
81+
}
82+
json.append('"').append(name).append('"').append(':');
83+
if (value == null) {
84+
json.append("null");
85+
} else {
86+
json.append('"').append(escapeJson(value)).append('"');
87+
}
88+
}
89+
90+
private void appendNumber(StringBuilder json, String name, long value) {
91+
if (json.length() > 1) {
92+
json.append(',');
93+
}
94+
json.append('"').append(name).append('"').append(':').append(value);
95+
}
96+
97+
private String escapeJson(String value) {
98+
StringBuilder escaped = new StringBuilder(value.length() + 8);
99+
for (int i = 0; i < value.length(); i++) {
100+
char ch = value.charAt(i);
101+
switch (ch) {
102+
case '\\':
103+
escaped.append("\\\\");
104+
break;
105+
case '"':
106+
escaped.append("\\\"");
107+
break;
108+
case '\n':
109+
escaped.append("\\n");
110+
break;
111+
case '\r':
112+
escaped.append("\\r");
113+
break;
114+
case '\t':
115+
escaped.append("\\t");
116+
break;
117+
default:
118+
escaped.append(ch);
119+
break;
120+
}
121+
}
122+
return escaped.toString();
123+
}
124+
}

0 commit comments

Comments
 (0)