1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//! The assembled console: CPU + system bus, with a frame-stepping loop.
use crate::bus::SystemBus;
use crate::cartridge::Cartridge;
use crate::io::PadState;
use crate::vdc::{ACTIVE_HEIGHT, ACTIVE_WIDTH, FB_HEIGHT, FB_WIDTH};
use crate::{ACTIVE_CYCLES_PER_SCANLINE, CPU_CYCLES_PER_SCANLINE, SCANLINES_PER_FRAME};
use mos6502::cpu::CPU;
use mos6502::instruction::Huc6280;
/// A fully wired TurboGrafx-16.
pub struct Console {
cpu: CPU<SystemBus, Huc6280>,
/// Cycle accumulator used by [`Console::debug_step`] to pace the VDC.
scanline_accumulator: u64,
}
impl Console {
/// Build a console around a loaded HuCard and run the reset sequence.
#[must_use]
pub fn new(cartridge: Cartridge) -> Self {
let bus = SystemBus::new(cartridge);
let mut console = Self {
cpu: CPU::new(bus, Huc6280),
scanline_accumulator: 0,
};
console.reset();
console
}
/// Perform the CPU reset sequence (loads the reset vector).
pub fn reset(&mut self) {
self.cpu.reset();
}
/// Execute one instruction and keep the bus's MMU shadow in sync.
///
/// Returns the number of CPU cycles the instruction consumed.
fn step_instruction(&mut self) -> u64 {
let before = self.cpu.cycles;
// TAM writes are mirrored to the bus by the core via
// `Bus::set_mapping_register`, so the MMU shadow is already current.
//
// The core advances the cycle count even while halted or waiting
// (WAI/STP/JAM), so the delta is always positive and the scanline
// loop keeps making progress. Use `self.cpu.wait_state()` if we ever
// want to stop the emulation on a stopped CPU.
self.cpu.single_step();
self.cpu.cycles.wrapping_sub(before)
}
/// Run one scanline, interleaving the CPU with the VDC's display and HBlank
/// phases so interrupts land mid-line and the handler's register writes
/// affect the *next* line (as on hardware).
///
/// Returns `true` when this was the final scanline of the frame (the picture
/// is ready to present).
pub fn step_scanline(&mut self) -> bool {
// Draw this line with the registers latched at its start.
self.cpu.memory.vdc.render_current_line();
// Active display: run the CPU up to the start of HBlank.
self.run_cpu_cycles(ACTIVE_CYCLES_PER_SCANLINE);
// HBlank begins: raise raster/vblank interrupts...
self.cpu.memory.vdc.enter_hblank();
// ...then run the CPU through HBlank so the handler sets up the next line.
self.run_cpu_cycles(CPU_CYCLES_PER_SCANLINE - ACTIVE_CYCLES_PER_SCANLINE);
self.cpu.memory.vdc.advance_scanline()
}
/// Run the CPU (and timer) for at least `target` cycles.
fn run_cpu_cycles(&mut self, target: u64) {
let mut spent: u64 = 0;
while spent < target {
let cycles = self.step_instruction();
self.cpu.memory.timer.step(cycles);
spent += cycles;
}
}
/// Run a whole frame (until the VDC wraps to scanline 0).
pub fn run_frame(&mut self) {
for _ in 0..SCANLINES_PER_FRAME {
if self.step_scanline() {
break;
}
}
}
/// Single-step the CPU (advancing the timer, but not the VDC), returning the
/// `(program_counter, opcode)` that was about to execute. Intended for
/// instruction tracing while debugging.
pub fn debug_step(&mut self) -> (u16, u8) {
let pc = self.cpu.registers.program_counter;
let opcode = self.cpu.memory.peek(pc);
let cycles = self.step_instruction();
self.cpu.memory.timer.step(cycles);
// Keep the VDC roughly in step so vblank/raster IRQs still occur.
self.scanline_accumulator += cycles;
while self.scanline_accumulator >= CPU_CYCLES_PER_SCANLINE {
self.scanline_accumulator -= CPU_CYCLES_PER_SCANLINE;
self.cpu.memory.vdc.step_scanline();
}
(pc, opcode)
}
/// Update the controller state (call once per frame from your input layer).
pub fn set_pad(&mut self, pad: PadState) {
self.cpu.memory.io.set_pad(pad);
}
/// Convert the VDC's palette-index framebuffer into a packed `0xAARRGGBB`
/// image through the VCE palette. The returned buffer is [`FB_WIDTH`] ×
/// [`FB_HEIGHT`].
#[must_use]
pub fn render_argb(&self) -> Vec<u32> {
let vdc = &self.cpu.memory.vdc;
let vce = &self.cpu.memory.vce;
vdc.framebuffer
.iter()
.map(|&index| vce.color_argb(index as usize))
.collect()
}
/// The active display size in pixels, `(width, height)`.
#[must_use]
pub const fn active_size(&self) -> (usize, usize) {
(ACTIVE_WIDTH, ACTIVE_HEIGHT)
}
/// Render the active display area as tightly-packed RGBA8 bytes
/// (`ACTIVE_WIDTH * ACTIVE_HEIGHT * 4`), ready to upload as a texture.
#[must_use]
pub fn active_frame_rgba(&self) -> Vec<u8> {
let vdc = &self.cpu.memory.vdc;
let vce = &self.cpu.memory.vce;
let mut out = vec![0u8; ACTIVE_WIDTH * ACTIVE_HEIGHT * 4];
for y in 0..ACTIVE_HEIGHT {
for x in 0..ACTIVE_WIDTH {
let index = vdc.framebuffer[y * FB_WIDTH + x] as usize;
let argb = vce.color_argb(index);
let o = (y * ACTIVE_WIDTH + x) * 4;
out[o] = (argb >> 16) as u8; // R
out[o + 1] = (argb >> 8) as u8; // G
out[o + 2] = argb as u8; // B
out[o + 3] = 0xFF; // A
}
}
out
}
/// Borrow the CPU (registers, cycle count, etc.) for debugging.
#[must_use]
pub const fn cpu(&self) -> &CPU<SystemBus, Huc6280> {
&self.cpu
}
/// Borrow the system bus (VDC, VCE, RAM, ...) for debugging.
#[must_use]
pub const fn bus(&self) -> &SystemBus {
&self.cpu.memory
}
/// Mutably borrow the system bus.
pub fn bus_mut(&mut self) -> &mut SystemBus {
&mut self.cpu.memory
}
/// Framebuffer dimensions, for convenience.
#[must_use]
pub const fn framebuffer_size(&self) -> (usize, usize) {
(FB_WIDTH, FB_HEIGHT)
}
}