[ChannelFHawk] Approximate the clocking of video circuitry rather than generating the framebuffer from RAM at the end of a frame

This commit is contained in:
Asnivor 2024-09-06 15:44:46 +01:00
parent 404f28fa64
commit 789a6266fc
5 changed files with 173 additions and 88 deletions

View File

@ -12,7 +12,7 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
public bool DeterministicEmulation { get; set; }
public int ClockPerFrame;
public int CpuClocksPerFrame;
public int FrameClock;
private void CalcClock()
@ -23,6 +23,15 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
int pixelClocksPerFrame;
if (Region == DisplayType.NTSC)
{
HTotal = 256;
HBlankOff = 8;
HBlankOn = 212;
VTotal = 264;
VBlankOff = 16;
VBlankOn = 248;
ScanlineRepeats = 4;
PixelWidth = 2;
// NTSC CPU speed is NTSC Colorburst / 2
const double NTSC_COLORBURST = 4500000 * 227.5 / 286;
cpuFreq = NTSC_COLORBURST / 2;
@ -37,6 +46,15 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
}
else
{
HTotal = 256;
HBlankOff = 8;
HBlankOn = 212;
VTotal = 312;
VBlankOff = 20;
VBlankOn = 310;
ScanlineRepeats = 5;
PixelWidth = 2;
if (version == ConsoleVersion.ChannelF)
{
// PAL CPU speed is 2MHz
@ -77,7 +95,9 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
}
var c = cpuFreq * pixelClocksPerFrame / pixelClock;
ClockPerFrame = (int) c;
CpuClocksPerFrame = (int) c;
PixelClocksPerCpuClock = pixelClock / cpuFreq;
PixelClocksPerFrame = pixelClocksPerFrame;
SetupAudio();
}
@ -98,9 +118,10 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
PollInput();
while (FrameClock++ < ClockPerFrame)
while (FrameClock++ < CpuClocksPerFrame)
{
CPU.ExecuteOne();
ClockVideo();
}
FrameClock = 0;

View File

@ -29,7 +29,7 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
{
_samplesPerFrame = (int)(SAMPLE_RATE * VsyncDenominator / VsyncNumerator);
// TODO: more precise audio clocking
_cyclesPerSample = ClockPerFrame / (double)_samplesPerFrame;
_cyclesPerSample = CpuClocksPerFrame / (double)_samplesPerFrame;
_sampleBuffer = new short[_samplesPerFrame];
_filteredSampleBuffer = new double[_samplesPerFrame];
_toneBuffer = new int[_samplesPerFrame];

View File

@ -8,13 +8,13 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
{
ser.BeginSection("ChannelF");
ser.Sync(nameof(VRAM), ref VRAM, false);
ser.Sync(nameof(latch_colour), ref latch_colour);
ser.Sync(nameof(latch_x), ref latch_x);
ser.Sync(nameof(latch_y), ref latch_y);
ser.Sync(nameof(_latch_colour), ref _latch_colour);
ser.Sync(nameof(_latch_x), ref _latch_x);
ser.Sync(nameof(_latch_y), ref _latch_y);
ser.Sync(nameof(_pixelClocksRemaining), ref _pixelClocksRemaining);
ser.Sync(nameof(FrameClock), ref FrameClock);
ser.Sync(nameof(_frame), ref _frame);
ser.Sync(nameof(ClockPerFrame), ref ClockPerFrame);
ser.Sync(nameof(_isLag), ref _isLag);
ser.Sync(nameof(_lagCount), ref _lagCount);

View File

@ -1,5 +1,6 @@
using BizHawk.Common;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores.Nintendo.NES;
namespace BizHawk.Emulation.Cores.Consoles.ChannelF
{
@ -7,9 +8,10 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
{
/// <summary>
/// 128x64 pixels - 8192x2bits (2 KB)
/// For the purposes of this core we will use 8192 bytes and just &amp; 0x03
/// For the purposes of this core we will use 8192 bytes and just mask 0x03
/// (Also adding an additional 10 rows to the RAM buffer so that it's more aligned with the actual display)
/// </summary>
public byte[] VRAM = new byte[(128 * 64)];
public byte[] VRAM = new byte[128 * (64 + 10)];
public static readonly int[] FPalette =
@ -23,8 +25,7 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
Colors.ARGB(0x4B, 0x3F, 0xF3), // Blue
Colors.ARGB(0xE0, 0xE0, 0xE0), // Gray
Colors.ARGB(0x91, 0xFF, 0xA6), // BGreen
Colors.ARGB(0xCE, 0xD0, 0xFF), // BBlue
Colors.ARGB(0xCE, 0xD0, 0xFF), // BBlue
};
public static readonly int[] CMap =
@ -35,21 +36,147 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
6, 4, 2, 3,
};
private int latch_colour = 2; //2;
private int latch_x;
private int latch_y;
private int scanlineRepeats;
private int _latch_colour = 2;
private int _latch_x;
private int _latch_y;
private int[] frameBuffer;
private int[] outputBuffer;
private int[] videoBuffer;
private double _pixelClockCounter;
private double _pixelClocksRemaining;
private int ScanlineRepeats;
private int PixelWidth;
private int HTotal;
private int HBlankOff;
private int HBlankOn;
private int VTotal;
private int VBlankOff;
private int VBlankOn;
private double PixelClocksPerCpuClock;
private double PixelClocksPerFrame;
public void SetupVideo()
{
scanlineRepeats = Region == DisplayType.NTSC ? 4 : 5;
frameBuffer = new int[128 * 64];
outputBuffer = new int[128 * 2 * 64 * scanlineRepeats];
outputBuffer = new int[(HBlankOn - HBlankOff) * (VBlankOn - VBlankOff)];
videoBuffer = new int[HTotal * VTotal];
}
/// <summary>
/// Called after every CPU clock
/// </summary>
private void ClockVideo()
{
while (_pixelClocksRemaining > 1)
{
var currScanline = (int)(_pixelClockCounter / HTotal);
var currPixelInLine = (int)(_pixelClockCounter % HTotal);
var currRowInVram = currScanline / ScanlineRepeats;
var currColInVram = currPixelInLine / PixelWidth;
if (currScanline < VBlankOff || currScanline >= VBlankOn)
{
// vertical flyback
}
else if (currPixelInLine < HBlankOff || currPixelInLine >= HBlankOn)
{
// horizontal flyback
}
else
{
// active display
var p1 = (VRAM[(currRowInVram * 0x80) + 125]) & 0x03;
var p2 = (VRAM[(currRowInVram * 0x80) + 126]) & 0x03;
var pOffset = ((p2 & 0x02) | (p1 >> 1)) << 2;
var colourIndex = pOffset + (VRAM[currColInVram | (currRowInVram << 7)] & 0x03);
videoBuffer[(currScanline * HTotal) + currPixelInLine] = FPalette[CMap[colourIndex]];
}
_pixelClockCounter++;
_pixelClocksRemaining -= 1;
}
_pixelClocksRemaining += PixelClocksPerCpuClock;
_pixelClockCounter %= PixelClocksPerFrame;
}
private int HDisplayable => HBlankOn - HBlankOff;
private int VDisplayable => VBlankOn - VBlankOff;
private double HPixelAspectModifier => Region == DisplayType.NTSC ? 1.75 : 1.95; // This is only here because the aspect ratio is off between regions. It could maybe be negated by trimming the number of scanlines on PAL??
private int[] ClampBuffer(int[] buffer, int originalWidth, int originalHeight, int trimLeft, int trimTop, int trimRight, int trimBottom)
{
int newWidth = originalWidth - trimLeft - trimRight;
int newHeight = originalHeight - trimTop - trimBottom;
int[] newBuffer = new int[newWidth * newHeight];
for (int y = 0; y < newHeight; y++)
{
for (int x = 0; x < newWidth; x++)
{
int originalIndex = (y + trimTop) * originalWidth + (x + trimLeft);
int newIndex = y * newWidth + x;
newBuffer[newIndex] = buffer[originalIndex];
}
}
return newBuffer;
}
private static double GetHorizontalModifier(int bufferWidth, int bufferHeight, double targetAspectRatio)
{
// Calculate the current aspect ratio
double currentAspectRatio = (double)bufferWidth / bufferHeight;
// Calculate the horizontal modifier needed to achieve the target aspect ratio
double horizontalModifier = targetAspectRatio / currentAspectRatio;
return horizontalModifier;
}
private static double GetVerticalModifier(int bufferWidth, int bufferHeight, double targetAspectRatio)
{
// Calculate the current aspect ratio
double currentAspectRatio = (double)bufferWidth / bufferHeight;
// Calculate the vertical modifier needed to achieve the target aspect ratio
double verticalModifier = currentAspectRatio / targetAspectRatio;
return verticalModifier;
}
public int VirtualWidth => HDisplayable * 2;
public int VirtualHeight => (int)(VDisplayable * GetVerticalModifier(HDisplayable, VDisplayable, 4.0/3.0)) * 2;
public int BufferWidth => HDisplayable;
public int BufferHeight => VDisplayable;
public int BackgroundColor => Colors.ARGB(0xFF, 0xFF, 0xFF);
public int VsyncNumerator { get; private set; }
public int VsyncDenominator { get; private set; }
public int[] GetVideoBuffer()
{
// https://channelf.se/veswiki/index.php?title=VRAM
// 'The emulator MESS uses a fixed 102x58 resolution starting at (4,4) but the safe area for a real system is about 95x58 pixels'
// 'Note that the pixel aspect is a lot closer to widescreen (16:9) than standard definition (4:3). On a TV from the 70's or 80's pixels are rectangular, standing up. In widescreen mode they are close to perfect squares'
// https://channelf.se/veswiki/index.php?title=Resolution
// 'Even though PAL televisions system has more lines vertically, the Channel F displays about the same as on the original NTSC video system'
//
// Right now we are just trimming based on the HBLANK and VBLANK values (we might need to go further like the other emulators)
// VirtualWidth is being used to force the aspect ratio into 4:3
// On real hardware it looks like this (so we are close): https://www.youtube.com/watch?v=ZvQA9tiEIuQ
return ClampBuffer(videoBuffer, HTotal, VTotal, HBlankOff, VBlankOff, HTotal - HBlankOn, VTotal - VBlankOn);
}
public DisplayType Region => region == RegionType.NTSC ? DisplayType.NTSC : DisplayType.PAL;
/*
private void BuildFrameFromRAM()
{
for (int r = 0; r < 64; r++)
@ -67,69 +194,6 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
}
}
}
private void ExpandFrame()
{
int initialWidth = 128;
int initialHeight = 64;
for (int lines = 0; lines < initialHeight; lines++)
{
for (int i = 0; i < scanlineRepeats; i++)
{
for (int x = 0; x < initialWidth; x++)
{
for (int j = 0; j < 2; j++)
{
outputBuffer[(lines * scanlineRepeats + i) * initialWidth * 2 + x * 2 + j] = FPalette[frameBuffer[lines * initialWidth + x]];
}
}
}
}
}
public int lBorder => 8;
public int rBorder => 52;
public int tBorder => 8;
public int bBorder => 4;
public int VirtualWidth => (int)((BufferWidth - lBorder - rBorder) * 2.2);
public int VirtualHeight => Region == DisplayType.NTSC ? BufferHeight - tBorder - bBorder : (int)((BufferHeight - tBorder - bBorder) * 0.8);
public int BufferWidth => 256 - lBorder - rBorder;
public int BufferHeight => (64 * scanlineRepeats) - tBorder - bBorder;
public int BackgroundColor => Colors.ARGB(0xFF, 0xFF, 0xFF);
public int VsyncNumerator { get; private set; }
public int VsyncDenominator { get; private set; }
public int[] TrimOutputBuffer(int[] buff, int leftTrim, int topTrim, int rightTrim, int bottomTrim)
{
int initialWidth = 128 * 2;
int initialHeight = 64 * scanlineRepeats;
int newWidth = initialWidth - leftTrim - rightTrim;
int newHeight = initialHeight - topTrim - bottomTrim;
int[] trimmedBuffer = new int[newWidth * newHeight];
for (int y = 0; y < newHeight; y++)
{
for (int x = 0; x < newWidth; x++)
{
trimmedBuffer[y * newWidth + x] = buff[(y + topTrim) * initialWidth + (x + leftTrim)];
}
}
return trimmedBuffer;
}
public int[] GetVideoBuffer()
{
BuildFrameFromRAM();
ExpandFrame();
return TrimOutputBuffer(outputBuffer, lBorder, tBorder, rBorder, bBorder);
}
public DisplayType Region => region == RegionType.NTSC ? DisplayType.NTSC : DisplayType.PAL;
*/
}
}

View File

@ -107,24 +107,24 @@ namespace BizHawk.Emulation.Cores.Consoles.ChannelF
// WRT pulse
// pulse clocks the 74195 parallel access shift register which feeds inputs of 2 NAND gates
// writing data to both sets of even and odd VRAM chips (based on the row and column addresses latched into the 7493 ICs)
VRAM[((latch_y) * 0x80) + latch_x] = (byte)latch_colour;
VRAM[((_latch_y) * 0x80) + _latch_x] = (byte)_latch_colour;
}
break;
case 1:
OutputLatch[addr] = value;
latch_colour = ((value ^ 0xFF) >> 6) & 0x03;
_latch_colour = ((value ^ 0xFF) >> 6) & 0x03;
break;
case 4:
OutputLatch[addr] = value;
latch_x = (value | 0x80) ^ 0xFF;
_latch_x = (value | 0x80) ^ 0xFF;
break;
case 5:
OutputLatch[addr] = value;
latch_y = (value | 0xC0) ^ 0xFF;
_latch_y = (value | 0xC0) ^ 0xFF;
var audio = ((value) >> 6) & 0x03;
if (audio != _tone)
{