ac6502 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/dist/components/CPU.js +1170 -0
- package/dist/components/CPU.js.map +1 -0
- package/dist/components/Cart.js +23 -0
- package/dist/components/Cart.js.map +1 -0
- package/dist/components/IO/Empty.js +19 -0
- package/dist/components/IO/Empty.js.map +1 -0
- package/dist/components/IO/GPIOAttachments/GPIOAttachment.js +71 -0
- package/dist/components/IO/GPIOAttachments/GPIOAttachment.js.map +1 -0
- package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js +90 -0
- package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js.map +1 -0
- package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js +489 -0
- package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js.map +1 -0
- package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js +274 -0
- package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js.map +1 -0
- package/dist/components/IO/GPIOCard.js +597 -0
- package/dist/components/IO/GPIOCard.js.map +1 -0
- package/dist/components/IO/InputBoard.js +19 -0
- package/dist/components/IO/InputBoard.js.map +1 -0
- package/dist/components/IO/LCDCard.js +19 -0
- package/dist/components/IO/LCDCard.js.map +1 -0
- package/dist/components/IO/RAMCard.js +63 -0
- package/dist/components/IO/RAMCard.js.map +1 -0
- package/dist/components/IO/RTCCard.js +483 -0
- package/dist/components/IO/RTCCard.js.map +1 -0
- package/dist/components/IO/SerialCard.js +282 -0
- package/dist/components/IO/SerialCard.js.map +1 -0
- package/dist/components/IO/SoundCard.js +620 -0
- package/dist/components/IO/SoundCard.js.map +1 -0
- package/dist/components/IO/StorageCard.js +428 -0
- package/dist/components/IO/StorageCard.js.map +1 -0
- package/dist/components/IO/VGACard.js +9 -0
- package/dist/components/IO/VGACard.js.map +1 -0
- package/dist/components/IO/VideoCard.js +623 -0
- package/dist/components/IO/VideoCard.js.map +1 -0
- package/dist/components/IO.js +3 -0
- package/dist/components/IO.js.map +1 -0
- package/dist/components/Machine.js +310 -0
- package/dist/components/Machine.js.map +1 -0
- package/dist/components/RAM.js +24 -0
- package/dist/components/RAM.js.map +1 -0
- package/dist/components/ROM.js +23 -0
- package/dist/components/ROM.js.map +1 -0
- package/dist/index.js +441 -0
- package/dist/index.js.map +1 -0
- package/dist/tests/CPU.test.js +1626 -0
- package/dist/tests/CPU.test.js.map +1 -0
- package/dist/tests/Cart.test.js +119 -0
- package/dist/tests/Cart.test.js.map +1 -0
- package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js +339 -0
- package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js.map +1 -0
- package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js +126 -0
- package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js.map +1 -0
- package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js +779 -0
- package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js.map +1 -0
- package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js +355 -0
- package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js.map +1 -0
- package/dist/tests/IO/GPIOCard.test.js +503 -0
- package/dist/tests/IO/GPIOCard.test.js.map +1 -0
- package/dist/tests/IO/RAMCard.test.js +229 -0
- package/dist/tests/IO/RAMCard.test.js.map +1 -0
- package/dist/tests/IO/RTCCard.test.js +177 -0
- package/dist/tests/IO/RTCCard.test.js.map +1 -0
- package/dist/tests/IO/SerialCard.test.js +423 -0
- package/dist/tests/IO/SerialCard.test.js.map +1 -0
- package/dist/tests/IO/SoundCard.test.js +528 -0
- package/dist/tests/IO/SoundCard.test.js.map +1 -0
- package/dist/tests/IO/StorageCard.test.js +647 -0
- package/dist/tests/IO/StorageCard.test.js.map +1 -0
- package/dist/tests/IO/VideoCard.test.js +549 -0
- package/dist/tests/IO/VideoCard.test.js.map +1 -0
- package/dist/tests/Machine.test.js +383 -0
- package/dist/tests/Machine.test.js.map +1 -0
- package/dist/tests/RAM.test.js +160 -0
- package/dist/tests/RAM.test.js.map +1 -0
- package/dist/tests/ROM.test.js +123 -0
- package/dist/tests/ROM.test.js.map +1 -0
- package/jest.config.cjs +9 -0
- package/package.json +43 -0
- package/src/components/CPU.ts +1371 -0
- package/src/components/Cart.ts +20 -0
- package/src/components/IO/GPIOAttachments/GPIOAttachment.ts +189 -0
- package/src/components/IO/GPIOAttachments/GPIOJoystickAttachment.ts +99 -0
- package/src/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.ts +465 -0
- package/src/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.ts +287 -0
- package/src/components/IO/GPIOCard.ts +677 -0
- package/src/components/IO/RAMCard.ts +68 -0
- package/src/components/IO/RTCCard.ts +518 -0
- package/src/components/IO/SerialCard.ts +335 -0
- package/src/components/IO/SoundCard.ts +711 -0
- package/src/components/IO/StorageCard.ts +473 -0
- package/src/components/IO/VideoCard.ts +730 -0
- package/src/components/IO.ts +11 -0
- package/src/components/Machine.ts +364 -0
- package/src/components/RAM.ts +23 -0
- package/src/components/ROM.ts +19 -0
- package/src/index.ts +474 -0
- package/src/tests/CPU.test.ts +2045 -0
- package/src/tests/Cart.test.ts +149 -0
- package/src/tests/IO/GPIOAttachments/GPIOAttachment.test.ts +413 -0
- package/src/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.ts +147 -0
- package/src/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.ts +961 -0
- package/src/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.ts +449 -0
- package/src/tests/IO/GPIOCard.test.ts +644 -0
- package/src/tests/IO/RAMCard.test.ts +284 -0
- package/src/tests/IO/RTCCard.test.ts +222 -0
- package/src/tests/IO/SerialCard.test.ts +530 -0
- package/src/tests/IO/SoundCard.test.ts +659 -0
- package/src/tests/IO/StorageCard.test.ts +787 -0
- package/src/tests/IO/VideoCard.test.ts +668 -0
- package/src/tests/Machine.test.ts +437 -0
- package/src/tests/RAM.test.ts +196 -0
- package/src/tests/ROM.test.ts +154 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
import { IO } from '../IO'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TMS9918 Video Display Processor Emulation
|
|
5
|
+
*
|
|
6
|
+
* Port mapping (address bit 0):
|
|
7
|
+
* Even address (bit 0 = 0): VRAM data read/write
|
|
8
|
+
* Odd address (bit 0 = 1): Register/address write / status read
|
|
9
|
+
*
|
|
10
|
+
* Display modes:
|
|
11
|
+
* Graphics I - 32x24 tiles, 8x8 patterns, 1-of-8 color groups
|
|
12
|
+
* Graphics II - 32x24 tiles, 8x8 patterns, per-row color
|
|
13
|
+
* Text - 40x24 tiles, 6x8 patterns, no sprites
|
|
14
|
+
* Multicolor - 32x24 blocks, 4x4 colored cells
|
|
15
|
+
*
|
|
16
|
+
* Output: 256x192 active area centered in a 320x240 RGBA buffer
|
|
17
|
+
*
|
|
18
|
+
* Reference: vrEmuTms9918 by Troy Schrapel
|
|
19
|
+
* https://github.com/visrealm/vrEmuTms9918
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Display modes
|
|
23
|
+
export enum TmsMode {
|
|
24
|
+
GRAPHICS_I = 0,
|
|
25
|
+
GRAPHICS_II = 1,
|
|
26
|
+
TEXT = 2,
|
|
27
|
+
MULTICOLOR = 3,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// TMS9918 Color indices
|
|
31
|
+
export enum TmsColor {
|
|
32
|
+
TRANSPARENT = 0,
|
|
33
|
+
BLACK = 1,
|
|
34
|
+
MED_GREEN = 2,
|
|
35
|
+
LT_GREEN = 3,
|
|
36
|
+
DK_BLUE = 4,
|
|
37
|
+
LT_BLUE = 5,
|
|
38
|
+
DK_RED = 6,
|
|
39
|
+
CYAN = 7,
|
|
40
|
+
MED_RED = 8,
|
|
41
|
+
LT_RED = 9,
|
|
42
|
+
DK_YELLOW = 10,
|
|
43
|
+
LT_YELLOW = 11,
|
|
44
|
+
DK_GREEN = 12,
|
|
45
|
+
MAGENTA = 13,
|
|
46
|
+
GREY = 14,
|
|
47
|
+
WHITE = 15,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// TMS9918 Palette – RGBA bytes (transparent rendered as opaque black)
|
|
51
|
+
const TMS_PALETTE: ReadonlyArray<readonly [number, number, number, number]> = [
|
|
52
|
+
[0x00, 0x00, 0x00, 0xFF], // 0 Transparent (opaque black on display)
|
|
53
|
+
[0x00, 0x00, 0x00, 0xFF], // 1 Black
|
|
54
|
+
[0x21, 0xC9, 0x42, 0xFF], // 2 Medium Green
|
|
55
|
+
[0x5E, 0xDC, 0x78, 0xFF], // 3 Light Green
|
|
56
|
+
[0x54, 0x55, 0xED, 0xFF], // 4 Dark Blue
|
|
57
|
+
[0x7D, 0x75, 0xFC, 0xFF], // 5 Light Blue
|
|
58
|
+
[0xD3, 0x52, 0x4D, 0xFF], // 6 Dark Red
|
|
59
|
+
[0x43, 0xEB, 0xF6, 0xFF], // 7 Cyan
|
|
60
|
+
[0xFD, 0x55, 0x54, 0xFF], // 8 Medium Red
|
|
61
|
+
[0xFF, 0x79, 0x78, 0xFF], // 9 Light Red
|
|
62
|
+
[0xD3, 0xC1, 0x53, 0xFF], // 10 Dark Yellow
|
|
63
|
+
[0xE5, 0xCE, 0x80, 0xFF], // 11 Light Yellow
|
|
64
|
+
[0x21, 0xB0, 0x3C, 0xFF], // 12 Dark Green
|
|
65
|
+
[0xC9, 0x5B, 0xBA, 0xFF], // 13 Magenta
|
|
66
|
+
[0xCC, 0xCC, 0xCC, 0xFF], // 14 Grey
|
|
67
|
+
[0xFF, 0xFF, 0xFF, 0xFF], // 15 White
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
// VRAM
|
|
71
|
+
const VRAM_SIZE = 1 << 14 // 16KB
|
|
72
|
+
const VRAM_MASK = VRAM_SIZE - 1 // 0x3FFF
|
|
73
|
+
|
|
74
|
+
// Active display resolution
|
|
75
|
+
const TMS_PIXELS_X = 256
|
|
76
|
+
const TMS_PIXELS_Y = 192
|
|
77
|
+
|
|
78
|
+
// Output buffer resolution
|
|
79
|
+
const DISPLAY_WIDTH = 320
|
|
80
|
+
const DISPLAY_HEIGHT = 240
|
|
81
|
+
|
|
82
|
+
// Tile / character layout
|
|
83
|
+
const GRAPHICS_NUM_COLS = 32
|
|
84
|
+
const GRAPHICS_CHAR_WIDTH = 8
|
|
85
|
+
const TEXT_NUM_COLS = 40
|
|
86
|
+
const TEXT_CHAR_WIDTH = 6
|
|
87
|
+
const TEXT_PADDING_PX = 8
|
|
88
|
+
|
|
89
|
+
// Pattern table
|
|
90
|
+
const PATTERN_BYTES = 8
|
|
91
|
+
const GFXI_COLOR_GROUP_SIZE = 8
|
|
92
|
+
|
|
93
|
+
// Sprites
|
|
94
|
+
const MAX_SPRITES = 32
|
|
95
|
+
const SPRITE_ATTR_Y = 0
|
|
96
|
+
const SPRITE_ATTR_X = 1
|
|
97
|
+
const SPRITE_ATTR_NAME = 2
|
|
98
|
+
const SPRITE_ATTR_COLOR = 3
|
|
99
|
+
const SPRITE_ATTR_BYTES = 4
|
|
100
|
+
const LAST_SPRITE_YPOS = 0xD0
|
|
101
|
+
const MAX_SCANLINE_SPRITES = 4
|
|
102
|
+
|
|
103
|
+
// Status register flags
|
|
104
|
+
const STATUS_INT = 0x80
|
|
105
|
+
const STATUS_5S = 0x40
|
|
106
|
+
const STATUS_COL = 0x20
|
|
107
|
+
|
|
108
|
+
// Register 0 bits
|
|
109
|
+
const TMS_R0_MODE_GRAPHICS_II = 0x02
|
|
110
|
+
|
|
111
|
+
// Register 1 bits
|
|
112
|
+
const TMS_R1_DISP_ACTIVE = 0x40
|
|
113
|
+
const TMS_R1_INT_ENABLE = 0x20
|
|
114
|
+
const TMS_R1_MODE_MULTICOLOR = 0x08
|
|
115
|
+
const TMS_R1_MODE_TEXT = 0x10
|
|
116
|
+
const TMS_R1_SPRITE_16 = 0x02
|
|
117
|
+
const TMS_R1_SPRITE_MAG2 = 0x01
|
|
118
|
+
|
|
119
|
+
// Register indices
|
|
120
|
+
const TMS_REG_0 = 0
|
|
121
|
+
const TMS_REG_1 = 1
|
|
122
|
+
const TMS_REG_NAME_TABLE = 2
|
|
123
|
+
const TMS_REG_COLOR_TABLE = 3
|
|
124
|
+
const TMS_REG_PATTERN_TABLE = 4
|
|
125
|
+
const TMS_REG_SPRITE_ATTR_TABLE = 5
|
|
126
|
+
const TMS_REG_SPRITE_PATT_TABLE = 6
|
|
127
|
+
const TMS_REG_FG_BG_COLOR = 7
|
|
128
|
+
const TMS_NUM_REGISTERS = 8
|
|
129
|
+
|
|
130
|
+
// Timing (NTSC)
|
|
131
|
+
const TOTAL_SCANLINES = 262
|
|
132
|
+
const FRAMES_PER_SECOND = 60
|
|
133
|
+
const CYCLES_PER_TICK = 128 // Must match Machine.ts ioTickInterval
|
|
134
|
+
|
|
135
|
+
// Border offsets (centering 256x192 in 320x240)
|
|
136
|
+
const BORDER_X = (DISPLAY_WIDTH - TMS_PIXELS_X) / 2 // 32
|
|
137
|
+
const BORDER_Y = (DISPLAY_HEIGHT - TMS_PIXELS_Y) / 2 // 24
|
|
138
|
+
|
|
139
|
+
export class VideoCard implements IO {
|
|
140
|
+
|
|
141
|
+
raiseIRQ = () => {}
|
|
142
|
+
raiseNMI = () => {}
|
|
143
|
+
|
|
144
|
+
// ---- VDP internal state ----
|
|
145
|
+
|
|
146
|
+
/** Eight write-only registers */
|
|
147
|
+
private registers = new Uint8Array(TMS_NUM_REGISTERS)
|
|
148
|
+
|
|
149
|
+
/** Status register (read-only from CPU side) */
|
|
150
|
+
private status: number = 0
|
|
151
|
+
|
|
152
|
+
/** Current VRAM address for CPU access (auto-increments) */
|
|
153
|
+
private currentAddress: number = 0
|
|
154
|
+
|
|
155
|
+
/** Address / register write stage (0 or 1) */
|
|
156
|
+
private regWriteStage: number = 0
|
|
157
|
+
|
|
158
|
+
/** Holds first stage byte written to the control port */
|
|
159
|
+
private regWriteStage0Value: number = 0
|
|
160
|
+
|
|
161
|
+
/** Read-ahead buffer for VRAM reads */
|
|
162
|
+
private readAheadBuffer: number = 0
|
|
163
|
+
|
|
164
|
+
/** Current display mode (derived from registers) */
|
|
165
|
+
private mode: TmsMode = TmsMode.GRAPHICS_I
|
|
166
|
+
|
|
167
|
+
/** 16 KB Video RAM */
|
|
168
|
+
private vram = new Uint8Array(VRAM_SIZE)
|
|
169
|
+
|
|
170
|
+
/** Per-pixel sprite collision mask for the current scanline */
|
|
171
|
+
private rowSpriteBits = new Uint8Array(TMS_PIXELS_X)
|
|
172
|
+
|
|
173
|
+
/** Temporary scanline pixel buffer (color palette indices) */
|
|
174
|
+
private scanlinePixels = new Uint8Array(TMS_PIXELS_X)
|
|
175
|
+
|
|
176
|
+
/** 320 × 240 RGBA output buffer for SDL rendering */
|
|
177
|
+
buffer: Buffer = Buffer.alloc(DISPLAY_WIDTH * DISPLAY_HEIGHT * 4)
|
|
178
|
+
|
|
179
|
+
/** Cycle accumulator for scanline timing */
|
|
180
|
+
private cycleAccumulator: number = 0
|
|
181
|
+
|
|
182
|
+
/** Current scanline being processed (0 – 261) */
|
|
183
|
+
private currentScanline: number = 0
|
|
184
|
+
|
|
185
|
+
// ================================================================
|
|
186
|
+
// IO Interface
|
|
187
|
+
// ================================================================
|
|
188
|
+
|
|
189
|
+
read(address: number): number {
|
|
190
|
+
if (address & 1) {
|
|
191
|
+
return this.readStatus()
|
|
192
|
+
}
|
|
193
|
+
return this.readData()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
write(address: number, data: number): void {
|
|
197
|
+
if (address & 1) {
|
|
198
|
+
this.writeAddr(data)
|
|
199
|
+
} else {
|
|
200
|
+
this.writeData(data)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
tick(frequency: number): void {
|
|
205
|
+
const cyclesPerFrame = frequency / FRAMES_PER_SECOND
|
|
206
|
+
const cyclesPerScanline = cyclesPerFrame / TOTAL_SCANLINES
|
|
207
|
+
|
|
208
|
+
this.cycleAccumulator += CYCLES_PER_TICK
|
|
209
|
+
|
|
210
|
+
while (this.cycleAccumulator >= cyclesPerScanline) {
|
|
211
|
+
this.cycleAccumulator -= cyclesPerScanline
|
|
212
|
+
this.processScanline()
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
reset(_coldStart: boolean): void {
|
|
217
|
+
this.regWriteStage0Value = 0
|
|
218
|
+
this.currentAddress = 0
|
|
219
|
+
this.regWriteStage = 0
|
|
220
|
+
this.status = 0
|
|
221
|
+
this.readAheadBuffer = 0
|
|
222
|
+
this.registers.fill(0)
|
|
223
|
+
this.cycleAccumulator = 0
|
|
224
|
+
this.currentScanline = 0
|
|
225
|
+
this.updateMode()
|
|
226
|
+
// VRAM intentionally left in unknown state (matches C reference)
|
|
227
|
+
this.fillBackground()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ================================================================
|
|
231
|
+
// VDP Data / Control Ports
|
|
232
|
+
// ================================================================
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Write to the control (address / register) port.
|
|
236
|
+
* Two-stage write:
|
|
237
|
+
* Stage 0 – latches the low byte (address LSB or register value)
|
|
238
|
+
* Stage 1 – interprets the high byte:
|
|
239
|
+
* bit 7 set → register write (bits 0-2 = register index)
|
|
240
|
+
* bit 7 clear → address set (bit 6: 0 = read, 1 = write)
|
|
241
|
+
*/
|
|
242
|
+
private writeAddr(data: number): void {
|
|
243
|
+
if (this.regWriteStage === 0) {
|
|
244
|
+
this.regWriteStage0Value = data
|
|
245
|
+
this.regWriteStage = 1
|
|
246
|
+
} else {
|
|
247
|
+
if (data & 0x80) {
|
|
248
|
+
// Register write
|
|
249
|
+
this.registers[data & 0x07] = this.regWriteStage0Value
|
|
250
|
+
this.updateMode()
|
|
251
|
+
} else {
|
|
252
|
+
// Address set
|
|
253
|
+
this.currentAddress = this.regWriteStage0Value | ((data & 0x3F) << 8)
|
|
254
|
+
if ((data & 0x40) === 0) {
|
|
255
|
+
// Read mode – pre-fetch byte and auto-increment
|
|
256
|
+
this.readAheadBuffer = this.vram[this.currentAddress & VRAM_MASK]
|
|
257
|
+
this.currentAddress++
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
this.regWriteStage = 0
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Write data to VRAM at the current address (auto-increments) */
|
|
265
|
+
private writeData(data: number): void {
|
|
266
|
+
this.regWriteStage = 0
|
|
267
|
+
this.readAheadBuffer = data
|
|
268
|
+
this.vram[this.currentAddress & VRAM_MASK] = data
|
|
269
|
+
this.currentAddress++
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Read the status register.
|
|
274
|
+
* Clears the status flags and resets the write stage.
|
|
275
|
+
*/
|
|
276
|
+
private readStatus(): number {
|
|
277
|
+
const tmp = this.status
|
|
278
|
+
this.status = 0
|
|
279
|
+
this.regWriteStage = 0
|
|
280
|
+
return tmp
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Read data from VRAM via the read-ahead buffer (auto-increments) */
|
|
284
|
+
private readData(): number {
|
|
285
|
+
this.regWriteStage = 0
|
|
286
|
+
const value = this.readAheadBuffer
|
|
287
|
+
this.readAheadBuffer = this.vram[this.currentAddress & VRAM_MASK]
|
|
288
|
+
this.currentAddress++
|
|
289
|
+
return value
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ================================================================
|
|
293
|
+
// Mode Detection
|
|
294
|
+
// ================================================================
|
|
295
|
+
|
|
296
|
+
private updateMode(): void {
|
|
297
|
+
if (this.registers[TMS_REG_0] & TMS_R0_MODE_GRAPHICS_II) {
|
|
298
|
+
this.mode = TmsMode.GRAPHICS_II
|
|
299
|
+
} else {
|
|
300
|
+
const bits = (this.registers[TMS_REG_1] & (TMS_R1_MODE_MULTICOLOR | TMS_R1_MODE_TEXT)) >> 3
|
|
301
|
+
switch (bits) {
|
|
302
|
+
case 1: this.mode = TmsMode.MULTICOLOR; break
|
|
303
|
+
case 2: this.mode = TmsMode.TEXT; break
|
|
304
|
+
default: this.mode = TmsMode.GRAPHICS_I; break
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ================================================================
|
|
310
|
+
// Table Address Helpers
|
|
311
|
+
// ================================================================
|
|
312
|
+
|
|
313
|
+
private nameTableAddr(): number {
|
|
314
|
+
return (this.registers[TMS_REG_NAME_TABLE] & 0x0F) << 10
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private colorTableAddr(): number {
|
|
318
|
+
const mask = this.mode === TmsMode.GRAPHICS_II ? 0x80 : 0xFF
|
|
319
|
+
return (this.registers[TMS_REG_COLOR_TABLE] & mask) << 6
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private patternTableAddr(): number {
|
|
323
|
+
const mask = this.mode === TmsMode.GRAPHICS_II ? 0x04 : 0x07
|
|
324
|
+
return (this.registers[TMS_REG_PATTERN_TABLE] & mask) << 11
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private spriteAttrTableAddr(): number {
|
|
328
|
+
return (this.registers[TMS_REG_SPRITE_ATTR_TABLE] & 0x7F) << 7
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private spritePatternTableAddr(): number {
|
|
332
|
+
return (this.registers[TMS_REG_SPRITE_PATT_TABLE] & 0x07) << 11
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ================================================================
|
|
336
|
+
// Color Helpers
|
|
337
|
+
// ================================================================
|
|
338
|
+
|
|
339
|
+
/** Backdrop / border color (low nibble of register 7) */
|
|
340
|
+
private mainBgColor(): number {
|
|
341
|
+
return this.registers[TMS_REG_FG_BG_COLOR] & 0x0F
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Text-mode foreground (high nibble of register 7, transparent → backdrop) */
|
|
345
|
+
private mainFgColor(): number {
|
|
346
|
+
const c = this.registers[TMS_REG_FG_BG_COLOR] >> 4
|
|
347
|
+
return c === TmsColor.TRANSPARENT ? this.mainBgColor() : c
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Foreground from a color byte (high nibble, transparent → backdrop) */
|
|
351
|
+
private fgColor(colorByte: number): number {
|
|
352
|
+
const c = colorByte >> 4
|
|
353
|
+
return c === TmsColor.TRANSPARENT ? this.mainBgColor() : c
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Background from a color byte (low nibble, transparent → backdrop) */
|
|
357
|
+
private bgColor(colorByte: number): number {
|
|
358
|
+
const c = colorByte & 0x0F
|
|
359
|
+
return c === TmsColor.TRANSPARENT ? this.mainBgColor() : c
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ================================================================
|
|
363
|
+
// Sprite Helpers
|
|
364
|
+
// ================================================================
|
|
365
|
+
|
|
366
|
+
private spriteSize(): number {
|
|
367
|
+
return this.registers[TMS_REG_1] & TMS_R1_SPRITE_16 ? 16 : 8
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private spriteMag(): boolean {
|
|
371
|
+
return !!(this.registers[TMS_REG_1] & TMS_R1_SPRITE_MAG2)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private displayEnabled(): boolean {
|
|
375
|
+
return !!(this.registers[TMS_REG_1] & TMS_R1_DISP_ACTIVE)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ================================================================
|
|
379
|
+
// Timing / Scanline Processing
|
|
380
|
+
// ================================================================
|
|
381
|
+
|
|
382
|
+
private processScanline(): void {
|
|
383
|
+
if (this.currentScanline === 0) {
|
|
384
|
+
this.fillBackground()
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (this.currentScanline < TMS_PIXELS_Y) {
|
|
388
|
+
this.renderScanline(this.currentScanline)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
this.currentScanline++
|
|
392
|
+
if (this.currentScanline >= TOTAL_SCANLINES) {
|
|
393
|
+
this.currentScanline = 0
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ================================================================
|
|
398
|
+
// Scanline Rendering
|
|
399
|
+
// ================================================================
|
|
400
|
+
|
|
401
|
+
private renderScanline(y: number): void {
|
|
402
|
+
const pixels = this.scanlinePixels
|
|
403
|
+
|
|
404
|
+
if (!this.displayEnabled() || y >= TMS_PIXELS_Y) {
|
|
405
|
+
pixels.fill(this.mainBgColor())
|
|
406
|
+
} else {
|
|
407
|
+
switch (this.mode) {
|
|
408
|
+
case TmsMode.GRAPHICS_I:
|
|
409
|
+
this.graphicsIScanLine(y, pixels)
|
|
410
|
+
break
|
|
411
|
+
case TmsMode.GRAPHICS_II:
|
|
412
|
+
this.graphicsIIScanLine(y, pixels)
|
|
413
|
+
break
|
|
414
|
+
case TmsMode.TEXT:
|
|
415
|
+
this.textScanLine(y, pixels)
|
|
416
|
+
break
|
|
417
|
+
case TmsMode.MULTICOLOR:
|
|
418
|
+
this.multicolorScanLine(y, pixels)
|
|
419
|
+
break
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Set interrupt flag at end of active display
|
|
424
|
+
if (y === TMS_PIXELS_Y - 1 && (this.registers[TMS_REG_1] & TMS_R1_INT_ENABLE)) {
|
|
425
|
+
this.status |= STATUS_INT
|
|
426
|
+
this.raiseIRQ()
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
this.writeScanlineToBuffer(y, pixels)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ---- Graphics I ----
|
|
433
|
+
|
|
434
|
+
private graphicsIScanLine(y: number, pixels: Uint8Array): void {
|
|
435
|
+
const tileY = y >> 3
|
|
436
|
+
const pattRow = y & 0x07
|
|
437
|
+
const rowNamesAddr = this.nameTableAddr() + tileY * GRAPHICS_NUM_COLS
|
|
438
|
+
const patternBase = this.patternTableAddr()
|
|
439
|
+
const colorBase = this.colorTableAddr()
|
|
440
|
+
|
|
441
|
+
for (let tileX = 0; tileX < GRAPHICS_NUM_COLS; tileX++) {
|
|
442
|
+
const pattIdx = this.vram[(rowNamesAddr + tileX) & VRAM_MASK]
|
|
443
|
+
let pattByte = this.vram[(patternBase + pattIdx * PATTERN_BYTES + pattRow) & VRAM_MASK]
|
|
444
|
+
const colorByte = this.vram[(colorBase + (pattIdx >>> 3)) & VRAM_MASK]
|
|
445
|
+
|
|
446
|
+
const fg = this.fgColor(colorByte)
|
|
447
|
+
const bg = this.bgColor(colorByte)
|
|
448
|
+
|
|
449
|
+
const base = tileX * GRAPHICS_CHAR_WIDTH
|
|
450
|
+
for (let bit = 0; bit < GRAPHICS_CHAR_WIDTH; bit++) {
|
|
451
|
+
pixels[base + bit] = (pattByte & 0x80) ? fg : bg
|
|
452
|
+
pattByte = (pattByte << 1) & 0xFF
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
this.outputSprites(y, pixels)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ---- Graphics II ----
|
|
460
|
+
|
|
461
|
+
private graphicsIIScanLine(y: number, pixels: Uint8Array): void {
|
|
462
|
+
const tileY = y >> 3
|
|
463
|
+
const pattRow = y & 0x07
|
|
464
|
+
const rowNamesAddr = this.nameTableAddr() + tileY * GRAPHICS_NUM_COLS
|
|
465
|
+
|
|
466
|
+
const nameMask = ((this.registers[TMS_REG_COLOR_TABLE] & 0x7F) << 3) | 0x07
|
|
467
|
+
|
|
468
|
+
const pageThird = ((tileY & 0x18) >> 3)
|
|
469
|
+
& (this.registers[TMS_REG_PATTERN_TABLE] & 0x03)
|
|
470
|
+
const pageOffset = pageThird << 11
|
|
471
|
+
|
|
472
|
+
const patternBase = this.patternTableAddr() + pageOffset
|
|
473
|
+
const colorBase = this.colorTableAddr()
|
|
474
|
+
+ (pageOffset & ((this.registers[TMS_REG_COLOR_TABLE] & 0x60) << 6))
|
|
475
|
+
|
|
476
|
+
for (let tileX = 0; tileX < GRAPHICS_NUM_COLS; tileX++) {
|
|
477
|
+
const pattIdx = this.vram[(rowNamesAddr + tileX) & VRAM_MASK] & nameMask
|
|
478
|
+
const pattRowOffset = pattIdx * PATTERN_BYTES + pattRow
|
|
479
|
+
const pattByte = this.vram[(patternBase + pattRowOffset) & VRAM_MASK]
|
|
480
|
+
const colorByte = this.vram[(colorBase + pattRowOffset) & VRAM_MASK]
|
|
481
|
+
|
|
482
|
+
const fg = this.fgColor(colorByte)
|
|
483
|
+
const bg = this.bgColor(colorByte)
|
|
484
|
+
|
|
485
|
+
const base = tileX * GRAPHICS_CHAR_WIDTH
|
|
486
|
+
for (let bit = 0; bit < GRAPHICS_CHAR_WIDTH; bit++) {
|
|
487
|
+
pixels[base + bit] = ((pattByte << bit) & 0x80) ? fg : bg
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
this.outputSprites(y, pixels)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ---- Text ----
|
|
495
|
+
|
|
496
|
+
private textScanLine(y: number, pixels: Uint8Array): void {
|
|
497
|
+
const tileY = y >> 3
|
|
498
|
+
const pattRow = y & 0x07
|
|
499
|
+
const rowNamesAddr = this.nameTableAddr() + tileY * TEXT_NUM_COLS
|
|
500
|
+
const patternBase = this.patternTableAddr()
|
|
501
|
+
|
|
502
|
+
const bg = this.mainBgColor()
|
|
503
|
+
const fg = this.mainFgColor()
|
|
504
|
+
|
|
505
|
+
// Left and right padding
|
|
506
|
+
for (let i = 0; i < TEXT_PADDING_PX; i++) {
|
|
507
|
+
pixels[i] = bg
|
|
508
|
+
pixels[TMS_PIXELS_X - TEXT_PADDING_PX + i] = bg
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
for (let tileX = 0; tileX < TEXT_NUM_COLS; tileX++) {
|
|
512
|
+
const pattIdx = this.vram[(rowNamesAddr + tileX) & VRAM_MASK]
|
|
513
|
+
const pattByte = this.vram[(patternBase + pattIdx * PATTERN_BYTES + pattRow) & VRAM_MASK]
|
|
514
|
+
|
|
515
|
+
for (let bit = 0; bit < TEXT_CHAR_WIDTH; bit++) {
|
|
516
|
+
pixels[TEXT_PADDING_PX + tileX * TEXT_CHAR_WIDTH + bit] =
|
|
517
|
+
((pattByte << bit) & 0x80) ? fg : bg
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// No sprites in Text mode
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ---- Multicolor ----
|
|
524
|
+
|
|
525
|
+
private multicolorScanLine(y: number, pixels: Uint8Array): void {
|
|
526
|
+
const tileY = y >> 3
|
|
527
|
+
const pattRow = (Math.floor(y / 4) & 0x01) + (tileY & 0x03) * 2
|
|
528
|
+
const namesAddr = this.nameTableAddr() + tileY * GRAPHICS_NUM_COLS
|
|
529
|
+
const patternBase = this.patternTableAddr()
|
|
530
|
+
|
|
531
|
+
for (let tileX = 0; tileX < GRAPHICS_NUM_COLS; tileX++) {
|
|
532
|
+
const pattIdx = this.vram[(namesAddr + tileX) & VRAM_MASK]
|
|
533
|
+
const colorByte = this.vram[(patternBase + pattIdx * PATTERN_BYTES + pattRow) & VRAM_MASK]
|
|
534
|
+
|
|
535
|
+
const fg = this.fgColor(colorByte)
|
|
536
|
+
const bg = this.bgColor(colorByte)
|
|
537
|
+
|
|
538
|
+
const base = tileX * 8
|
|
539
|
+
for (let i = 0; i < 4; i++) pixels[base + i] = fg
|
|
540
|
+
for (let i = 4; i < 8; i++) pixels[base + i] = bg
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
this.outputSprites(y, pixels)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ================================================================
|
|
547
|
+
// Sprite Rendering
|
|
548
|
+
// ================================================================
|
|
549
|
+
|
|
550
|
+
private outputSprites(y: number, pixels: Uint8Array): void {
|
|
551
|
+
const mag = this.spriteMag()
|
|
552
|
+
const sprite16 = this.spriteSize() === 16
|
|
553
|
+
const sprSize = this.spriteSize()
|
|
554
|
+
const spriteSizePx = sprSize * (mag ? 2 : 1)
|
|
555
|
+
const attrTableAddr = this.spriteAttrTableAddr()
|
|
556
|
+
const pattTableAddr = this.spritePatternTableAddr()
|
|
557
|
+
|
|
558
|
+
let spritesShown = 0
|
|
559
|
+
|
|
560
|
+
// Clear status at start of frame (matches C reference)
|
|
561
|
+
if (y === 0) {
|
|
562
|
+
this.status = 0
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
for (let spriteIdx = 0; spriteIdx < MAX_SPRITES; spriteIdx++) {
|
|
566
|
+
const attrBase = attrTableAddr + spriteIdx * SPRITE_ATTR_BYTES
|
|
567
|
+
let yPos: number = this.vram[(attrBase + SPRITE_ATTR_Y) & VRAM_MASK]
|
|
568
|
+
|
|
569
|
+
// Stop processing at sentinel value
|
|
570
|
+
if (yPos === LAST_SPRITE_YPOS) {
|
|
571
|
+
if ((this.status & STATUS_5S) === 0) {
|
|
572
|
+
this.status |= spriteIdx
|
|
573
|
+
}
|
|
574
|
+
break
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Handle wrap-around for sprites above the top of the screen
|
|
578
|
+
if (yPos > 0xE0) {
|
|
579
|
+
yPos -= 256
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// First visible row is yPos + 1
|
|
583
|
+
yPos += 1
|
|
584
|
+
|
|
585
|
+
let pattRow = y - yPos
|
|
586
|
+
if (mag) {
|
|
587
|
+
pattRow >>= 1
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Skip sprite if not visible on this scanline
|
|
591
|
+
if (pattRow < 0 || pattRow >= sprSize) {
|
|
592
|
+
continue
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Clear collision mask on first visible sprite of this scanline
|
|
596
|
+
if (spritesShown === 0) {
|
|
597
|
+
this.rowSpriteBits.fill(0)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const spriteColor = this.vram[(attrBase + SPRITE_ATTR_COLOR) & VRAM_MASK] & 0x0F
|
|
601
|
+
|
|
602
|
+
// Check scanline sprite limit
|
|
603
|
+
spritesShown++
|
|
604
|
+
if (spritesShown > MAX_SCANLINE_SPRITES) {
|
|
605
|
+
if ((this.status & STATUS_5S) === 0) {
|
|
606
|
+
this.status |= STATUS_5S | spriteIdx
|
|
607
|
+
}
|
|
608
|
+
break
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Sprite pattern data
|
|
612
|
+
const pattIdx = this.vram[(attrBase + SPRITE_ATTR_NAME) & VRAM_MASK]
|
|
613
|
+
const pattOffset = pattTableAddr + pattIdx * PATTERN_BYTES + pattRow
|
|
614
|
+
|
|
615
|
+
// Early clock shifts sprite 32 pixels left
|
|
616
|
+
const earlyClockBit = this.vram[(attrBase + SPRITE_ATTR_COLOR) & VRAM_MASK] & 0x80
|
|
617
|
+
const earlyClockOffset = earlyClockBit ? -32 : 0
|
|
618
|
+
const xPos = this.vram[(attrBase + SPRITE_ATTR_X) & VRAM_MASK] + earlyClockOffset
|
|
619
|
+
|
|
620
|
+
let pattByte = this.vram[pattOffset & VRAM_MASK]
|
|
621
|
+
let screenBit = 0
|
|
622
|
+
let pattBit = 0
|
|
623
|
+
|
|
624
|
+
const endXPos = Math.min(xPos + spriteSizePx, TMS_PIXELS_X)
|
|
625
|
+
|
|
626
|
+
for (let screenX = xPos; screenX < endXPos; screenX++, screenBit++) {
|
|
627
|
+
if (screenX >= 0) {
|
|
628
|
+
// Check high bit of pattern byte
|
|
629
|
+
if (pattByte & 0x80) {
|
|
630
|
+
// Write pixel if sprite is non-transparent and no higher-priority non-transparent sprite already wrote here
|
|
631
|
+
if (spriteColor !== TmsColor.TRANSPARENT && this.rowSpriteBits[screenX] < 2) {
|
|
632
|
+
pixels[screenX] = spriteColor
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Collision detection
|
|
636
|
+
if (this.rowSpriteBits[screenX]) {
|
|
637
|
+
this.status |= STATUS_COL
|
|
638
|
+
} else {
|
|
639
|
+
this.rowSpriteBits[screenX] = spriteColor + 1
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Advance pattern bit (every pixel, or every other pixel if magnified)
|
|
645
|
+
if (!mag || (screenBit & 0x01)) {
|
|
646
|
+
pattByte = (pattByte << 1) & 0xFF
|
|
647
|
+
pattBit++
|
|
648
|
+
if (pattBit === GRAPHICS_CHAR_WIDTH && sprite16) {
|
|
649
|
+
// Switch from left half (A/B) to right half (C/D) of 16×16 sprite
|
|
650
|
+
pattBit = 0
|
|
651
|
+
pattByte = this.vram[(pattOffset + PATTERN_BYTES * 2) & VRAM_MASK]
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ================================================================
|
|
659
|
+
// Buffer Management
|
|
660
|
+
// ================================================================
|
|
661
|
+
|
|
662
|
+
/** Fill entire output buffer with the current backdrop color */
|
|
663
|
+
private fillBackground(): void {
|
|
664
|
+
const bgIdx = this.mainBgColor()
|
|
665
|
+
const [r, g, b, a] = TMS_PALETTE[bgIdx]
|
|
666
|
+
for (let i = 0; i < this.buffer.length; i += 4) {
|
|
667
|
+
this.buffer[i] = r
|
|
668
|
+
this.buffer[i + 1] = g
|
|
669
|
+
this.buffer[i + 2] = b
|
|
670
|
+
this.buffer[i + 3] = a
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** Write a rendered scanline into the output buffer at the correct position */
|
|
675
|
+
private writeScanlineToBuffer(y: number, pixels: Uint8Array): void {
|
|
676
|
+
const bufferY = y + BORDER_Y
|
|
677
|
+
if (bufferY < 0 || bufferY >= DISPLAY_HEIGHT) return
|
|
678
|
+
|
|
679
|
+
const rowOffset = bufferY * DISPLAY_WIDTH * 4
|
|
680
|
+
for (let x = 0; x < TMS_PIXELS_X; x++) {
|
|
681
|
+
const offset = rowOffset + (BORDER_X + x) * 4
|
|
682
|
+
const [r, g, b, a] = TMS_PALETTE[pixels[x] & 0x0F]
|
|
683
|
+
this.buffer[offset] = r
|
|
684
|
+
this.buffer[offset + 1] = g
|
|
685
|
+
this.buffer[offset + 2] = b
|
|
686
|
+
this.buffer[offset + 3] = a
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ================================================================
|
|
691
|
+
// Public Accessors (testing / debugging)
|
|
692
|
+
// ================================================================
|
|
693
|
+
|
|
694
|
+
/** Read a VDP register value */
|
|
695
|
+
getRegister(reg: number): number {
|
|
696
|
+
return this.registers[reg & 0x07]
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** Write a VDP register value directly (bypasses control-port staging) */
|
|
700
|
+
setRegister(reg: number, value: number): void {
|
|
701
|
+
this.registers[reg & 0x07] = value
|
|
702
|
+
this.updateMode()
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** Read a VRAM byte directly (does not affect read-ahead buffer) */
|
|
706
|
+
getVramByte(addr: number): number {
|
|
707
|
+
return this.vram[addr & VRAM_MASK]
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Write a VRAM byte directly (does not affect address pointer) */
|
|
711
|
+
setVramByte(addr: number, value: number): void {
|
|
712
|
+
this.vram[addr & VRAM_MASK] = value
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** Peek at the status register without clearing it */
|
|
716
|
+
getStatus(): number {
|
|
717
|
+
return this.status
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** Get the current display mode */
|
|
721
|
+
getMode(): TmsMode {
|
|
722
|
+
return this.mode
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** Get the display-enabled state */
|
|
726
|
+
isDisplayEnabled(): boolean {
|
|
727
|
+
return this.displayEnabled()
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
}
|