libgambatte: switch the system bus read to use a much safer (100%?) deterministic peek. also implement core side stuff for scanline-based callback
This commit is contained in:
parent
788591ba77
commit
ac1f9a90a1
|
@ -145,25 +145,10 @@ namespace BizHawk.Emulation.Consoles.GB
|
||||||
tracecb = null;
|
tracecb = null;
|
||||||
LibGambatte.gambatte_settracecallback(GambatteState, tracecb);
|
LibGambatte.gambatte_settracecallback(GambatteState, tracecb);
|
||||||
|
|
||||||
// todo: have the gambatte core actually call this at an appropriate time
|
|
||||||
if (scanlinecallback != null)
|
|
||||||
{
|
|
||||||
IntPtr vram = IntPtr.Zero;
|
|
||||||
IntPtr bgpal = IntPtr.Zero;
|
|
||||||
IntPtr sppal = IntPtr.Zero;
|
|
||||||
IntPtr oam = IntPtr.Zero;
|
|
||||||
int unused = 0;
|
|
||||||
if (!LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.vram, ref vram, ref unused)
|
|
||||||
|| !LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.bgpal, ref bgpal, ref unused)
|
|
||||||
|| !LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.sppal, ref sppal, ref unused)
|
|
||||||
|| !LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.oam, ref oam, ref unused))
|
|
||||||
throw new Exception();
|
|
||||||
|
|
||||||
scanlinecallback(vram, IsCGBMode(), LibGambatte.gambatte_cpuread(GambatteState, 0xff40), bgpal, sppal, oam);
|
|
||||||
}
|
|
||||||
|
|
||||||
LibGambatte.gambatte_runfor(GambatteState, VideoBuffer, 160, soundbuff, ref nsamp);
|
LibGambatte.gambatte_runfor(GambatteState, VideoBuffer, 160, soundbuff, ref nsamp);
|
||||||
|
|
||||||
|
Console.WriteLine("===");
|
||||||
|
|
||||||
// upload any modified data to the memory domains
|
// upload any modified data to the memory domains
|
||||||
|
|
||||||
foreach (var r in MemoryRefreshers)
|
foreach (var r in MemoryRefreshers)
|
||||||
|
@ -593,18 +578,36 @@ namespace BizHawk.Emulation.Consoles.GB
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region ppudebug
|
#region ppudebug
|
||||||
|
public bool GetGPUMemoryAreas(out IntPtr vram, out IntPtr bgpal, out IntPtr sppal, out IntPtr oam)
|
||||||
|
{
|
||||||
|
IntPtr _vram = IntPtr.Zero;
|
||||||
|
IntPtr _bgpal = IntPtr.Zero;
|
||||||
|
IntPtr _sppal = IntPtr.Zero;
|
||||||
|
IntPtr _oam = IntPtr.Zero;
|
||||||
|
int unused = 0;
|
||||||
|
if (!LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.vram, ref _vram, ref unused)
|
||||||
|
|| !LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.bgpal, ref _bgpal, ref unused)
|
||||||
|
|| !LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.sppal, ref _sppal, ref unused)
|
||||||
|
|| !LibGambatte.gambatte_getmemoryarea(GambatteState, LibGambatte.MemoryAreas.oam, ref _oam, ref unused))
|
||||||
|
{
|
||||||
|
vram = IntPtr.Zero;
|
||||||
|
bgpal = IntPtr.Zero;
|
||||||
|
sppal = IntPtr.Zero;
|
||||||
|
oam = IntPtr.Zero;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
vram = _vram;
|
||||||
|
bgpal = _bgpal;
|
||||||
|
sppal = _sppal;
|
||||||
|
oam = _oam;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="vram"></param>
|
/// <param name="lcdc">current value of register $ff40 (LCDC)</param>
|
||||||
/// <param name="cgb"></param>
|
public delegate void ScanlineCallback(int lcdc);
|
||||||
/// <param name="lcdc"></param>
|
|
||||||
/// <param name="bgpal"></param>
|
|
||||||
/// <param name="sppal"></param>
|
|
||||||
/// <param name="oam"></param>
|
|
||||||
public delegate void ScanlineCallback(IntPtr vram, bool cgb, int lcdc, IntPtr bgpal, IntPtr sppal, IntPtr oam);
|
|
||||||
|
|
||||||
ScanlineCallback scanlinecallback;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// set up callback
|
/// set up callback
|
||||||
|
@ -613,14 +616,25 @@ namespace BizHawk.Emulation.Consoles.GB
|
||||||
/// <param name="line">scanline</param>
|
/// <param name="line">scanline</param>
|
||||||
public void SetScanlineCallback(ScanlineCallback callback, int line)
|
public void SetScanlineCallback(ScanlineCallback callback, int line)
|
||||||
{
|
{
|
||||||
|
if (GambatteState == IntPtr.Zero)
|
||||||
|
// not sure how this is being reached. tried the debugger...
|
||||||
|
return;
|
||||||
if (callback == null)
|
if (callback == null)
|
||||||
this.scanlinecallback = null;
|
scanlinecb = null;
|
||||||
else if (line < 0 || line > 153)
|
else if (line < 0 || line > 153)
|
||||||
throw new ArgumentOutOfRangeException("line must be in [0, 153]");
|
throw new ArgumentOutOfRangeException("line must be in [0, 153]");
|
||||||
else
|
else
|
||||||
this.scanlinecallback = callback;
|
scanlinecb = delegate()
|
||||||
|
{
|
||||||
|
callback(LibGambatte.gambatte_cpuread(GambatteState, 0xff40));
|
||||||
|
//callback(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
LibGambatte.gambatte_setscanlinecallback(GambatteState, scanlinecb, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LibGambatte.ScanlineCallback scanlinecb;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
|
@ -167,13 +167,30 @@ namespace BizHawk.Emulation.Consoles.GB
|
||||||
[DllImport("libgambatte.dll", CallingConvention = CallingConvention.Cdecl)]
|
[DllImport("libgambatte.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||||
public static extern void gambatte_settracecallback(IntPtr core, TraceCallback callback);
|
public static extern void gambatte_settracecallback(IntPtr core, TraceCallback callback);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// type of the scanline callback
|
||||||
|
/// </summary>
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
public delegate void ScanlineCallback();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// set a callback to occur when ly reaches a particular scanline (so at the beginning of the scanline).
|
||||||
|
/// when the LCD is active, typically 145 will be the first callback after the beginning of frame advance,
|
||||||
|
/// and 144 will be the last callback right before frame advance returns
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="core">opaque state pointer</param>
|
||||||
|
/// <param name="callback">null to clear</param>
|
||||||
|
/// <param name="sl">0-153 inclusive</param>
|
||||||
|
[DllImport("libgambatte.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
public static extern void gambatte_setscanlinecallback(IntPtr core, ScanlineCallback callback, int sl);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the directory used for storing save data. The default is the same directory as the ROM Image file.
|
/// Sets the directory used for storing save data. The default is the same directory as the ROM Image file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="core">opaque state pointer</param>
|
/// <param name="core">opaque state pointer</param>
|
||||||
/// <param name="sdir"></param>
|
/// <param name="sdir"></param>
|
||||||
[DllImport("libgambatte.dll", CallingConvention = CallingConvention.Cdecl)]
|
//[DllImport("libgambatte.dll", CallingConvention = CallingConvention.Cdecl)]
|
||||||
public static extern void gambatte_setsavedir(IntPtr core, string sdir);
|
//public static extern void gambatte_setsavedir(IntPtr core, string sdir);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns true if the currently loaded ROM image is treated as having CGB support.
|
/// Returns true if the currently loaded ROM image is treated as having CGB support.
|
||||||
|
|
|
@ -13,6 +13,15 @@ namespace BizHawk.MultiClient.GBtools
|
||||||
{
|
{
|
||||||
Emulation.Consoles.GB.Gameboy gb;
|
Emulation.Consoles.GB.Gameboy gb;
|
||||||
|
|
||||||
|
// gambatte doesn't modify these memory locations unless you reconstruct, so we can store
|
||||||
|
IntPtr vram;
|
||||||
|
IntPtr bgpal;
|
||||||
|
IntPtr sppal;
|
||||||
|
IntPtr oam;
|
||||||
|
|
||||||
|
bool cgb; // set once at start
|
||||||
|
int lcdc; // set at each callback
|
||||||
|
|
||||||
public GBGPUView()
|
public GBGPUView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
@ -30,7 +39,16 @@ namespace BizHawk.MultiClient.GBtools
|
||||||
if (Global.Emulator is Emulation.Consoles.GB.Gameboy)
|
if (Global.Emulator is Emulation.Consoles.GB.Gameboy)
|
||||||
{
|
{
|
||||||
gb = Global.Emulator as Emulation.Consoles.GB.Gameboy;
|
gb = Global.Emulator as Emulation.Consoles.GB.Gameboy;
|
||||||
if (gb.IsCGBMode())
|
cgb = gb.IsCGBMode();
|
||||||
|
lcdc = 0;
|
||||||
|
if (!gb.GetGPUMemoryAreas(out vram, out bgpal, out sppal, out oam))
|
||||||
|
{
|
||||||
|
gb = null;
|
||||||
|
if (Visible)
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cgb)
|
||||||
label4.Enabled = true;
|
label4.Enabled = true;
|
||||||
else
|
else
|
||||||
label4.Enabled = false;
|
label4.Enabled = false;
|
||||||
|
@ -302,8 +320,9 @@ namespace BizHawk.MultiClient.GBtools
|
||||||
b.UnlockBits(lockdata);
|
b.UnlockBits(lockdata);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanlineCallback(IntPtr vram, bool cgb, int lcdc, IntPtr bgpal, IntPtr sppal, IntPtr oam)
|
void ScanlineCallback(int lcdc)
|
||||||
{
|
{
|
||||||
|
this.lcdc = lcdc;
|
||||||
// set alpha on all pixels
|
// set alpha on all pixels
|
||||||
unsafe
|
unsafe
|
||||||
{
|
{
|
||||||
|
|
Binary file not shown.
|
@ -86,6 +86,7 @@ public:
|
||||||
void setReadCallback(void (*callback)(unsigned));
|
void setReadCallback(void (*callback)(unsigned));
|
||||||
void setWriteCallback(void (*callback)(unsigned));
|
void setWriteCallback(void (*callback)(unsigned));
|
||||||
void setTraceCallback(void (*callback)(void *));
|
void setTraceCallback(void (*callback)(void *));
|
||||||
|
void setScanlineCallback(void (*callback)(), int sl);
|
||||||
|
|
||||||
/** Sets the directory used for storing save data. The default is the same directory as the ROM Image file. */
|
/** Sets the directory used for storing save data. The default is the same directory as the ROM Image file. */
|
||||||
void setSaveDir(const std::string &sdir);
|
void setSaveDir(const std::string &sdir);
|
||||||
|
|
|
@ -83,6 +83,12 @@ __declspec(dllexport) void gambatte_settracecallback(void *core, void (*callback
|
||||||
g->setTraceCallback(callback);
|
g->setTraceCallback(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
__declspec(dllexport) void gambatte_setscanlinecallback(void *core, void (*callback)(), int sl)
|
||||||
|
{
|
||||||
|
GB *g = (GB *) core;
|
||||||
|
g->setScanlineCallback(callback, sl);
|
||||||
|
}
|
||||||
|
|
||||||
__declspec(dllexport) void gambatte_setsavedir(void *core, const char *sdir)
|
__declspec(dllexport) void gambatte_setsavedir(void *core, const char *sdir)
|
||||||
{
|
{
|
||||||
GB *g = (GB *) core;
|
GB *g = (GB *) core;
|
||||||
|
|
|
@ -24,6 +24,8 @@ extern "C"
|
||||||
|
|
||||||
__declspec(dllexport) void gambatte_settracecallback(void *core, void (*callback)(void *));
|
__declspec(dllexport) void gambatte_settracecallback(void *core, void (*callback)(void *));
|
||||||
|
|
||||||
|
__declspec(dllexport) void gambatte_setscanlinecallback(void *core, void (*callback)(), int sl);
|
||||||
|
|
||||||
__declspec(dllexport) void gambatte_setsavedir(void *core, const char *sdir);
|
__declspec(dllexport) void gambatte_setsavedir(void *core, const char *sdir);
|
||||||
|
|
||||||
__declspec(dllexport) int gambatte_iscgb(void *core);
|
__declspec(dllexport) int gambatte_iscgb(void *core);
|
||||||
|
|
|
@ -79,6 +79,10 @@ public:
|
||||||
tracecallback = callback;
|
tracecallback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setScanlineCallback(void (*callback)(), int sl) {
|
||||||
|
memory.setScanlineCallback(callback, sl);
|
||||||
|
}
|
||||||
|
|
||||||
void setSaveDir(const std::string &sdir) {
|
void setSaveDir(const std::string &sdir) {
|
||||||
memory.setSaveDir(sdir);
|
memory.setSaveDir(sdir);
|
||||||
}
|
}
|
||||||
|
@ -110,7 +114,8 @@ public:
|
||||||
void setGameGenie(const std::string &codes) { memory.setGameGenie(codes); }
|
void setGameGenie(const std::string &codes) { memory.setGameGenie(codes); }
|
||||||
void setGameShark(const std::string &codes) { memory.setGameShark(codes); }
|
void setGameShark(const std::string &codes) { memory.setGameShark(codes); }
|
||||||
|
|
||||||
unsigned char ExternalRead(unsigned short addr) { return memory.read(addr, cycleCounter_); }
|
//unsigned char ExternalRead(unsigned short addr) { return memory.read(addr, cycleCounter_); }
|
||||||
|
unsigned char ExternalRead(unsigned short addr) { return memory.peek(addr); }
|
||||||
void ExternalWrite(unsigned short addr, unsigned char val) { memory.write(addr, val, cycleCounter_); }
|
void ExternalWrite(unsigned short addr, unsigned char val) { memory.write(addr, val, cycleCounter_); }
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -107,6 +107,10 @@ void GB::setTraceCallback(void (*callback)(void *)) {
|
||||||
p_->cpu.setTraceCallback(callback);
|
p_->cpu.setTraceCallback(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GB::setScanlineCallback(void (*callback)(), int sl) {
|
||||||
|
p_->cpu.setScanlineCallback(callback, sl);
|
||||||
|
}
|
||||||
|
|
||||||
void GB::setSaveDir(const std::string &sdir) {
|
void GB::setSaveDir(const std::string &sdir) {
|
||||||
p_->cpu.setSaveDir(sdir);
|
p_->cpu.setSaveDir(sdir);
|
||||||
}
|
}
|
||||||
|
|
|
@ -570,6 +570,32 @@ unsigned Memory::nontrivial_read(const unsigned P, const unsigned long cycleCoun
|
||||||
return ioamhram[P - 0xFE00];
|
return ioamhram[P - 0xFE00];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsigned Memory::nontrivial_peek(const unsigned P) {
|
||||||
|
if (P < 0xC000) {
|
||||||
|
if (P < 0x8000)
|
||||||
|
return cart.romdata(P >> 14)[P];
|
||||||
|
|
||||||
|
if (P < 0xA000) {
|
||||||
|
return cart.vrambankptr()[P];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cart.rsrambankptr())
|
||||||
|
return cart.rsrambankptr()[P];
|
||||||
|
|
||||||
|
return cart.rtcRead(); // verified side-effect free
|
||||||
|
}
|
||||||
|
if (P < 0xFE00)
|
||||||
|
return cart.wramdata(P >> 12 & 1)[P & 0xFFF];
|
||||||
|
if (P >= 0xFF00 && P < 0xFF80)
|
||||||
|
return nontrivial_ff_peek(P);
|
||||||
|
return ioamhram[P - 0xFE00];
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned Memory::nontrivial_ff_peek(const unsigned P) {
|
||||||
|
// some regs may be somewhat wrong with this
|
||||||
|
return ioamhram[P - 0xFE00];
|
||||||
|
}
|
||||||
|
|
||||||
void Memory::nontrivial_ff_write(const unsigned P, unsigned data, const unsigned long cycleCounter) {
|
void Memory::nontrivial_ff_write(const unsigned P, unsigned data, const unsigned long cycleCounter) {
|
||||||
if (lastOamDmaUpdate != DISABLED_TIME)
|
if (lastOamDmaUpdate != DISABLED_TIME)
|
||||||
updateOamDma(cycleCounter);
|
updateOamDma(cycleCounter);
|
||||||
|
|
|
@ -66,6 +66,9 @@ class Memory {
|
||||||
void nontrivial_ff_write(unsigned P, unsigned data, unsigned long cycleCounter);
|
void nontrivial_ff_write(unsigned P, unsigned data, unsigned long cycleCounter);
|
||||||
void nontrivial_write(unsigned P, unsigned data, unsigned long cycleCounter);
|
void nontrivial_write(unsigned P, unsigned data, unsigned long cycleCounter);
|
||||||
|
|
||||||
|
unsigned nontrivial_peek(unsigned P);
|
||||||
|
unsigned nontrivial_ff_peek(unsigned P);
|
||||||
|
|
||||||
void updateSerial(unsigned long cc);
|
void updateSerial(unsigned long cc);
|
||||||
void updateTimaIrq(unsigned long cc);
|
void updateTimaIrq(unsigned long cc);
|
||||||
void updateIrqs(unsigned long cc);
|
void updateIrqs(unsigned long cc);
|
||||||
|
@ -119,6 +122,10 @@ public:
|
||||||
return cart.rmem(P >> 12) ? cart.rmem(P >> 12)[P] : nontrivial_read(P, cycleCounter);
|
return cart.rmem(P >> 12) ? cart.rmem(P >> 12)[P] : nontrivial_read(P, cycleCounter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsigned peek(const unsigned P) {
|
||||||
|
return cart.rmem(P >> 12) ? cart.rmem(P >> 12)[P] : nontrivial_peek(P);
|
||||||
|
}
|
||||||
|
|
||||||
void write(const unsigned P, const unsigned data, const unsigned long cycleCounter) {
|
void write(const unsigned P, const unsigned data, const unsigned long cycleCounter) {
|
||||||
if (cart.wmem(P >> 12)) {
|
if (cart.wmem(P >> 12)) {
|
||||||
cart.wmem(P >> 12)[P] = data;
|
cart.wmem(P >> 12)[P] = data;
|
||||||
|
@ -152,6 +159,10 @@ public:
|
||||||
this->writeCallback = callback;
|
this->writeCallback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setScanlineCallback(void (*callback)(), int sl) {
|
||||||
|
display.setScanlineCallback(callback, sl);
|
||||||
|
}
|
||||||
|
|
||||||
void setEndtime(unsigned long cc, unsigned long inc);
|
void setEndtime(unsigned long cc, unsigned long inc);
|
||||||
|
|
||||||
void setSoundBuffer(uint_least32_t *const buf) { sound.setBuffer(buf); }
|
void setSoundBuffer(uint_least32_t *const buf) { sound.setBuffer(buf); }
|
||||||
|
|
|
@ -71,7 +71,9 @@ LCD::LCD(const unsigned char *const oamram, const unsigned char *const vram, con
|
||||||
eventTimes_(memEventRequester),
|
eventTimes_(memEventRequester),
|
||||||
statReg(0),
|
statReg(0),
|
||||||
m2IrqStatReg_(0),
|
m2IrqStatReg_(0),
|
||||||
m1IrqStatReg_(0)
|
m1IrqStatReg_(0),
|
||||||
|
scanlinecallback(0),
|
||||||
|
scanlinecallbacksl(0)
|
||||||
{
|
{
|
||||||
std::memset( bgpData, 0, sizeof bgpData);
|
std::memset( bgpData, 0, sizeof bgpData);
|
||||||
std::memset(objpData, 0, sizeof objpData);
|
std::memset(objpData, 0, sizeof objpData);
|
||||||
|
@ -772,6 +774,8 @@ inline void LCD::event() {
|
||||||
case LY_COUNT:
|
case LY_COUNT:
|
||||||
ppu.doLyCountEvent();
|
ppu.doLyCountEvent();
|
||||||
eventTimes_.set<LY_COUNT>(ppu.lyCounter().time());
|
eventTimes_.set<LY_COUNT>(ppu.lyCounter().time());
|
||||||
|
if (scanlinecallback && ppu.lyCounter().ly() == scanlinecallbacksl)
|
||||||
|
scanlinecallback();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,6 +150,9 @@ class LCD {
|
||||||
void doCgbBgColorChange(unsigned index, unsigned data, unsigned long cycleCounter);
|
void doCgbBgColorChange(unsigned index, unsigned data, unsigned long cycleCounter);
|
||||||
void doCgbSpColorChange(unsigned index, unsigned data, unsigned long cycleCounter);
|
void doCgbSpColorChange(unsigned index, unsigned data, unsigned long cycleCounter);
|
||||||
|
|
||||||
|
void (*scanlinecallback)();
|
||||||
|
int scanlinecallbacksl;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
LCD(const unsigned char *oamram, const unsigned char *vram_in, VideoInterruptRequester memEventRequester);
|
LCD(const unsigned char *oamram, const unsigned char *vram_in, VideoInterruptRequester memEventRequester);
|
||||||
void reset(const unsigned char *oamram, const unsigned char *vram, bool cgb);
|
void reset(const unsigned char *oamram, const unsigned char *vram, bool cgb);
|
||||||
|
@ -253,6 +256,8 @@ public:
|
||||||
|
|
||||||
unsigned long *bgPalette() { return ppu.bgPalette(); }
|
unsigned long *bgPalette() { return ppu.bgPalette(); }
|
||||||
unsigned long *spPalette() { return ppu.spPalette(); }
|
unsigned long *spPalette() { return ppu.spPalette(); }
|
||||||
|
|
||||||
|
void setScanlineCallback(void (*callback)(), int sl) { scanlinecallback = callback; scanlinecallbacksl = sl; }
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue