mirror of
https://github.com/libretro/fixGB.git
synced 2025-02-20 01:42:18 +00:00
more apu work and added gbs music support
This commit is contained in:
parent
be6fefddd4
commit
096f760361
@ -3,6 +3,7 @@ This is still quite early in development so it cant do all that much, its essent
|
||||
If you want to check it out for some reason I do include a windows binary in the "Releases" tab, if you want to compile it go check out the "build" files.
|
||||
You will need freeglut as well as openal-soft to compile the project, it should run on most systems since it is fairly generic C code.
|
||||
Right now most standard GB titles using MBC1, 3 and 5 should work just fine and also save into standard .sav files.
|
||||
You can also listen to .gbs files by dragging them in, changing tracks works by pressing left/right.
|
||||
|
||||
Controls right now are keyboard only and do the following:
|
||||
Y/Z is A
|
||||
|
65
apu.c
65
apu.c
@ -52,9 +52,11 @@ static envelope_t p1Env, p2Env, noiseEnv;
|
||||
typedef struct _sweep_t {
|
||||
bool enabled;
|
||||
bool negative;
|
||||
bool inNegative;
|
||||
uint8_t period;
|
||||
uint8_t divider;
|
||||
uint8_t shift;
|
||||
uint16_t pfreq;
|
||||
} sweep_t;
|
||||
|
||||
static sweep_t p1Sweep;
|
||||
@ -133,6 +135,7 @@ void apuInit()
|
||||
p1dacenable = false; p2dacenable = false;
|
||||
wavdacenable = false; noisedacenable = false;
|
||||
noiseMode1 = false;
|
||||
soundEnabled = true;
|
||||
}
|
||||
|
||||
extern uint32_t cpu_oam_dma;
|
||||
@ -283,30 +286,36 @@ void doEnvelopeLogic(envelope_t *env)
|
||||
env->vol--;
|
||||
}
|
||||
}
|
||||
env->divider = env->period;
|
||||
//period 0 is actually period 8!
|
||||
env->divider = (env->period-1)&7;
|
||||
}
|
||||
else
|
||||
env->divider--;
|
||||
//too slow on its own?
|
||||
//env->envelope = (env->constant ? env->vol : env->decay);
|
||||
}
|
||||
|
||||
void sweepUpdateFreq(sweep_t *sw, uint16_t *freq)
|
||||
void sweepUpdateFreq(sweep_t *sw, uint16_t *freq, bool update)
|
||||
{
|
||||
if(!sw->enabled)
|
||||
return;
|
||||
//printf("%i\n", *freq);
|
||||
uint16_t inFreq = *freq;
|
||||
uint16_t inFreq = sw->pfreq;
|
||||
uint16_t shiftVal = (inFreq >> sw->shift);
|
||||
//if(sw->shift > 0)
|
||||
|
||||
if(sw->negative)
|
||||
{
|
||||
if(sw->negative)
|
||||
inFreq -= shiftVal;
|
||||
else
|
||||
inFreq += shiftVal;
|
||||
sw->inNegative = true;
|
||||
inFreq -= shiftVal;
|
||||
}
|
||||
else
|
||||
inFreq += shiftVal;
|
||||
|
||||
if(inFreq <= 0x7FF)
|
||||
{
|
||||
if(sw->enabled && sw->shift && sw->period)
|
||||
if(sw->enabled && sw->shift && sw->period && update)
|
||||
{
|
||||
*freq = inFreq;
|
||||
sw->pfreq = inFreq;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -319,11 +328,12 @@ void doSweepLogic(sweep_t *sw, uint16_t *freq)
|
||||
{
|
||||
if(sw->divider == 0)
|
||||
{
|
||||
//printf("Divider 0\n");
|
||||
if(sw->period)
|
||||
{
|
||||
sweepUpdateFreq(sw, freq);
|
||||
sweepUpdateFreq(sw, freq, true);
|
||||
//gameboy checks a SECOND time after updating...
|
||||
uint16_t inFreq = *freq;
|
||||
uint16_t inFreq = sw->pfreq;
|
||||
uint16_t shiftVal = (inFreq >> sw->shift);
|
||||
if(sw->negative)
|
||||
inFreq -= shiftVal;
|
||||
@ -335,7 +345,8 @@ void doSweepLogic(sweep_t *sw, uint16_t *freq)
|
||||
p1enable = false;
|
||||
}
|
||||
}
|
||||
sw->divider = sw->period;
|
||||
//period 0 is actually period 8!
|
||||
sw->divider = (sw->period-1)&7;
|
||||
}
|
||||
else
|
||||
sw->divider--;
|
||||
@ -442,12 +453,8 @@ void apuSet8(uint8_t reg, uint8_t val)
|
||||
p1Sweep.shift = val&7;
|
||||
p1Sweep.period = (val>>4)&7;
|
||||
p1Sweep.negative = ((val&0x8) != 0);
|
||||
//enabled by trigger
|
||||
if(p1Sweep.enabled)
|
||||
{
|
||||
p1Sweep.divider = 0;
|
||||
sweepUpdateFreq(&p1Sweep, &freq1);
|
||||
}
|
||||
if(p1Sweep.inNegative && !p1Sweep.negative)
|
||||
p1enable = false;
|
||||
}
|
||||
else if(reg == 0x11)
|
||||
{
|
||||
@ -462,8 +469,6 @@ void apuSet8(uint8_t reg, uint8_t val)
|
||||
if(!p1dacenable)
|
||||
p1enable = false;
|
||||
p1Env.period = val&7;
|
||||
//if(p1Env.period==0)
|
||||
// p1Env.period=8;
|
||||
p1Env.divider = p1Env.period;
|
||||
}
|
||||
else if(reg == 0x13)
|
||||
@ -483,15 +488,17 @@ void apuSet8(uint8_t reg, uint8_t val)
|
||||
p1Cycle = 0;
|
||||
//trigger used to enable/disable sweep
|
||||
if(p1Sweep.period || p1Sweep.shift)
|
||||
{
|
||||
|
||||
p1Sweep.divider = 0;
|
||||
p1Sweep.enabled = true;
|
||||
}
|
||||
else
|
||||
p1Sweep.enabled = false;
|
||||
//trigger also resets divider, neg mode and frequency
|
||||
p1Sweep.inNegative = false;
|
||||
p1Sweep.pfreq = freq1;
|
||||
//period 0 is actually period 8!
|
||||
p1Sweep.divider = (p1Sweep.period-1)&7;
|
||||
//if sweep shift>0, pre-calc frequency
|
||||
if(p1Sweep.shift)
|
||||
sweepUpdateFreq(&p1Sweep, &freq1);
|
||||
sweepUpdateFreq(&p1Sweep, &freq1, false);
|
||||
}
|
||||
//printf("P1 new freq %04x\n", freq1);
|
||||
}
|
||||
@ -508,8 +515,6 @@ void apuSet8(uint8_t reg, uint8_t val)
|
||||
if(!p2dacenable)
|
||||
p2enable = false;
|
||||
p2Env.period = val&7;
|
||||
//if(p2Env.period==0)
|
||||
// p2Env.period=8;
|
||||
p2Env.divider = p2Env.period;
|
||||
}
|
||||
else if(reg == 0x18)
|
||||
@ -588,8 +593,6 @@ void apuSet8(uint8_t reg, uint8_t val)
|
||||
if(!noisedacenable)
|
||||
noiseenable = false;
|
||||
noiseEnv.period=val&7;
|
||||
//if(noiseEnv.period==0)
|
||||
// noiseEnv.period=8;
|
||||
noiseEnv.divider = noiseEnv.period;
|
||||
}
|
||||
else if(reg == 0x22)
|
||||
|
72
cpu.c
72
cpu.c
@ -9,6 +9,8 @@
|
||||
#include <stdbool.h>
|
||||
#include <inttypes.h>
|
||||
#include "cpu.h"
|
||||
#include "apu.h"
|
||||
#include "ppu.h"
|
||||
#include "mem.h"
|
||||
#include "input.h"
|
||||
|
||||
@ -17,6 +19,13 @@
|
||||
#define P_FLAG_N (1<<6)
|
||||
#define P_FLAG_Z (1<<7)
|
||||
|
||||
//from main.c
|
||||
extern bool gbEmuGBSPlayback;
|
||||
extern uint16_t gbsLoadAddr;
|
||||
extern uint16_t gbsInitAddr;
|
||||
extern uint16_t gbsPlayAddr;
|
||||
extern uint16_t gbsSP;
|
||||
|
||||
void cpuSetupActionArr();
|
||||
|
||||
static uint16_t sp, pc, cpuTmp16;
|
||||
@ -984,6 +993,7 @@ bool firstIrq = false, secondIrq = false;
|
||||
|
||||
bool cpuHandleIrqUpdates()
|
||||
{
|
||||
if(gbEmuGBSPlayback) return false;
|
||||
if(!irqEnable) return false;
|
||||
uint8_t irqList = (memGetCurIrqList());
|
||||
if(irqList & 1)
|
||||
@ -1039,6 +1049,12 @@ void cpuGetInstruction()
|
||||
cpu_arr_pos = 0;
|
||||
return;
|
||||
}
|
||||
if(gbEmuGBSPlayback && pc == 0x8765)
|
||||
{
|
||||
cpu_action_arr = cpu_nop_arr;
|
||||
cpu_arr_pos = 0;
|
||||
return;
|
||||
}
|
||||
if(cpuHaltLoop)
|
||||
{
|
||||
//happens when IME=0
|
||||
@ -1377,42 +1393,42 @@ bool cpuCycle()
|
||||
case CPU_SP_WRITE8_PCL_DEC_PC_FROM_00:
|
||||
sp--;
|
||||
memSet8(sp, pc&0xFF);
|
||||
pc = 0x00;
|
||||
pc = 0x00+gbsLoadAddr;
|
||||
break;
|
||||
case CPU_SP_WRITE8_PCL_DEC_PC_FROM_08:
|
||||
sp--;
|
||||
memSet8(sp, pc&0xFF);
|
||||
pc = 0x08;
|
||||
pc = 0x08+gbsLoadAddr;
|
||||
break;
|
||||
case CPU_SP_WRITE8_PCL_DEC_PC_FROM_10:
|
||||
sp--;
|
||||
memSet8(sp, pc&0xFF);
|
||||
pc = 0x10;
|
||||
pc = 0x10+gbsLoadAddr;
|
||||
break;
|
||||
case CPU_SP_WRITE8_PCL_DEC_PC_FROM_18:
|
||||
sp--;
|
||||
memSet8(sp, pc&0xFF);
|
||||
pc = 0x18;
|
||||
pc = 0x18+gbsLoadAddr;
|
||||
break;
|
||||
case CPU_SP_WRITE8_PCL_DEC_PC_FROM_20:
|
||||
sp--;
|
||||
memSet8(sp, pc&0xFF);
|
||||
pc = 0x20;
|
||||
pc = 0x20+gbsLoadAddr;
|
||||
break;
|
||||
case CPU_SP_WRITE8_PCL_DEC_PC_FROM_28:
|
||||
sp--;
|
||||
memSet8(sp, pc&0xFF);
|
||||
pc = 0x28;
|
||||
pc = 0x28+gbsLoadAddr;
|
||||
break;
|
||||
case CPU_SP_WRITE8_PCL_DEC_PC_FROM_30:
|
||||
sp--;
|
||||
memSet8(sp, pc&0xFF);
|
||||
pc = 0x30;
|
||||
pc = 0x30+gbsLoadAddr;
|
||||
break;
|
||||
case CPU_SP_WRITE8_PCL_DEC_PC_FROM_38:
|
||||
sp--;
|
||||
memSet8(sp, pc&0xFF);
|
||||
pc = 0x38;
|
||||
pc = 0x38+gbsLoadAddr;
|
||||
break;
|
||||
case CPU_SP_WRITE8_PCL_DEC_PC_FROM_40:
|
||||
sp--;
|
||||
@ -1566,3 +1582,43 @@ uint16_t cpuCurPC()
|
||||
{
|
||||
return pc;
|
||||
}
|
||||
|
||||
void cpuPlayGBS()
|
||||
{
|
||||
//push back detect pc
|
||||
sp--;
|
||||
memSet8(sp, 0x87);
|
||||
sp--;
|
||||
memSet8(sp, 0x65);
|
||||
//jump to play
|
||||
pc = gbsPlayAddr;
|
||||
cpu_action_arr = cpu_nop_arr;
|
||||
cpu_arr_pos = 0;
|
||||
//printf("Playback Start at %04x\n", pc);
|
||||
}
|
||||
extern uint8_t gbsTMA, gbsTAC;
|
||||
void cpuLoadGBS(uint8_t song)
|
||||
{
|
||||
//full reset
|
||||
cpuInit();
|
||||
ppuInit();
|
||||
apuInit();
|
||||
inputInit();
|
||||
memInit(false,false);
|
||||
memSet8(0xFF06,gbsTMA);
|
||||
memSet8(0xFF07,gbsTAC);
|
||||
//set requested sp
|
||||
sp = gbsSP;
|
||||
//push back detect pc
|
||||
sp--;
|
||||
memSet8(sp, 0x87);
|
||||
sp--;
|
||||
memSet8(sp, 0x65);
|
||||
//set song and init routine
|
||||
a = song;
|
||||
pc = gbsInitAddr;
|
||||
//start getting instructions
|
||||
cpu_action_arr = cpu_nop_arr;
|
||||
cpu_arr_pos = 0;
|
||||
//printf("Init Start at %04x\n", pc);
|
||||
}
|
||||
|
2
cpu.h
2
cpu.h
@ -11,5 +11,7 @@
|
||||
void cpuInit();
|
||||
bool cpuCycle();
|
||||
uint16_t cpuCurPC();
|
||||
void cpuLoadGBS(uint8_t song);
|
||||
void cpuPlayGBS();
|
||||
|
||||
#endif
|
||||
|
160
main.c
160
main.c
@ -26,7 +26,7 @@
|
||||
#define DEBUG_KEY 0
|
||||
#define DEBUG_LOAD_INFO 1
|
||||
|
||||
static const char *VERSION_STRING = "fixGB Alpha v0.1";
|
||||
static const char *VERSION_STRING = "fixGB Alpha v0.2";
|
||||
|
||||
static void gbEmuDisplayFrame(void);
|
||||
static void gbEmuMainLoop(void);
|
||||
@ -43,11 +43,15 @@ char *emuSaveName = NULL;
|
||||
uint8_t *textureImage = NULL;
|
||||
bool nesPause = false;
|
||||
bool ppuDebugPauseFrame = false;
|
||||
bool doOverscan = true;
|
||||
bool gbEmuGBSPlayback = false;
|
||||
bool gbsTimerMode = false;
|
||||
uint16_t gbsLoadAddr = 0;
|
||||
uint16_t gbsInitAddr = 0;
|
||||
uint16_t gbsPlayAddr = 0;
|
||||
uint16_t gbsSP = 0;
|
||||
uint8_t gbsTracksTotal = 0, gbsTMA = 0, gbsTAC = 0;
|
||||
|
||||
static bool inPause = false;
|
||||
static bool inOverscanToggle = false;
|
||||
static bool inResize = false;
|
||||
|
||||
#if WINDOWS_BUILD
|
||||
@ -74,14 +78,66 @@ static const uint32_t visibleImg = VISIBLE_DOTS*VISIBLE_LINES*4;
|
||||
static uint8_t scaleFactor = 3;
|
||||
static uint32_t mainLoopRuns;
|
||||
static uint16_t mainLoopPos;
|
||||
static uint8_t cpuTimer;
|
||||
//from input.c
|
||||
extern uint8_t inValReads[8];
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
puts(VERSION_STRING);
|
||||
if(argc >= 2 && (strstr(argv[1],".gb") != NULL || strstr(argv[1],".GB") != NULL
|
||||
|| strstr(argv[1],".gbc") != NULL || strstr(argv[1],".GBC") != NULL))
|
||||
if(argc >= 2 && (strstr(argv[1],".gbs") != NULL || strstr(argv[1],".GBS") != NULL))
|
||||
{
|
||||
FILE *gbF = fopen(argv[1],"rb");
|
||||
if(!gbF) return EXIT_SUCCESS;
|
||||
fseek(gbF,0,SEEK_END);
|
||||
size_t fsize = ftell(gbF);
|
||||
rewind(gbF);
|
||||
uint8_t *tmpROM = malloc(fsize);
|
||||
fread(tmpROM,1,fsize,gbF);
|
||||
fclose(gbF);
|
||||
gbsTracksTotal = tmpROM[4];
|
||||
gbsLoadAddr = (tmpROM[6])|(tmpROM[7]<<8);
|
||||
gbsInitAddr = (tmpROM[8])|(tmpROM[9]<<8);
|
||||
gbsPlayAddr = (tmpROM[0xA])|(tmpROM[0xB]<<8);
|
||||
gbsSP = (tmpROM[0xC])|(tmpROM[0xD]<<8);
|
||||
//should give more than enough room for everything
|
||||
uint32_t totalROMsize = (fsize-0x70+gbsLoadAddr+0x7FFF)&(~0x7FFF);
|
||||
emuGBROM = malloc(totalROMsize);
|
||||
memset(emuGBROM,0xFF,totalROMsize);
|
||||
memcpy(emuGBROM+gbsLoadAddr,tmpROM+0x70,fsize-0x70);
|
||||
memInit(true,true);
|
||||
gbsTMA = tmpROM[0xE];
|
||||
gbsTAC = tmpROM[0xF];
|
||||
if(gbsTAC&4)
|
||||
{
|
||||
printf("Play Timing: Timer\n");
|
||||
gbsTimerMode = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("Play Timing: VSync\n");
|
||||
gbsTimerMode = false;
|
||||
}
|
||||
if(gbsTAC&0x80)
|
||||
{
|
||||
printf("CPU: CGB Speed\n");
|
||||
cpuTimer = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("CPU: DMG Speed\n");
|
||||
cpuTimer = 4;
|
||||
}
|
||||
if(tmpROM[0x10] != 0)
|
||||
printf("Game: %.32s\n",(char*)(tmpROM+0x10));
|
||||
free(tmpROM);
|
||||
apuInitBufs();
|
||||
//does all inits for us
|
||||
memStartGBS();
|
||||
gbEmuGBSPlayback = true;
|
||||
}
|
||||
else if(argc >= 2 && (strstr(argv[1],".gbc") != NULL || strstr(argv[1],".GBC") != NULL
|
||||
|| strstr(argv[1],".gb") != NULL || strstr(argv[1],".GB") != NULL))
|
||||
{
|
||||
FILE *gbF = fopen(argv[1],"rb");
|
||||
if(!gbF) return EXIT_SUCCESS;
|
||||
@ -108,12 +164,14 @@ int main(int argc, char** argv)
|
||||
memcpy(emuSaveName,argv[1],strlen(argv[1])+1);
|
||||
memcpy(emuSaveName+strlen(argv[1])-2,"sav",4);
|
||||
}
|
||||
if(!memInit())
|
||||
if(!memInit(true,false))
|
||||
{
|
||||
free(emuGBROM);
|
||||
printf("Exit...\n");
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
//DMG Mode
|
||||
cpuTimer = 4;
|
||||
apuInitBufs();
|
||||
cpuInit();
|
||||
ppuInit();
|
||||
@ -125,26 +183,6 @@ int main(int argc, char** argv)
|
||||
//printf("PRG: 0x%x bytes PRG RAM: 0x%x bytes CHR: 0x%x bytes\n", prgROMsize, emuPrgRAMsize, chrROMsize);
|
||||
#endif
|
||||
}
|
||||
/*else if(argc >= 2 && (strstr(argv[1],".gbs") != NULL || strstr(argv[1],".GBS") != NULL))
|
||||
{
|
||||
FILE *gbF = fopen(argv[1],"rb");
|
||||
if(!gbF) return EXIT_SUCCESS;
|
||||
fseek(gbF,0,SEEK_END);
|
||||
size_t fsize = ftell(gbF);
|
||||
rewind(gbF);
|
||||
emuGBROM = malloc(fsize);
|
||||
fread(emuGBROM,1,fsize,gbF);
|
||||
fclose(gbF);
|
||||
emuPrgRAMsize = 0x2000;
|
||||
emuPrgRAM = malloc(emuPrgRAMsize);
|
||||
if(!mapperInitGBS(emuGBROM, fsize, emuPrgRAM, emuPrgRAMsize))
|
||||
{
|
||||
printf("GBS init failed!\n");
|
||||
free(emuGBROM);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
gbEmuGBSPlayback = true;
|
||||
}*/
|
||||
if(emuGBROM == NULL)
|
||||
return EXIT_SUCCESS;
|
||||
#if WINDOWS_BUILD
|
||||
@ -229,6 +267,7 @@ bool emuSkipFrame = false;
|
||||
//static bool emuApuDoCycle = false;
|
||||
|
||||
static uint16_t mainClock = 1;
|
||||
static uint16_t cpuClock = 1;
|
||||
static uint16_t memClock = 1;
|
||||
//static uint16_t vrc7Clock = 1;
|
||||
|
||||
@ -244,6 +283,18 @@ static void gbEmuMainLoop(void)
|
||||
audioSleep();
|
||||
return;
|
||||
}
|
||||
if(cpuClock == cpuTimer)
|
||||
{
|
||||
//main CPU clock
|
||||
if(!cpuCycle())
|
||||
{
|
||||
//memDumpMainMem();
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
cpuClock = 1;
|
||||
}
|
||||
else
|
||||
cpuClock++;
|
||||
if(mainClock == 4)
|
||||
{
|
||||
if(!apuCycle())
|
||||
@ -254,12 +305,6 @@ static void gbEmuMainLoop(void)
|
||||
audioSleep();
|
||||
return;
|
||||
}
|
||||
//main CPU clock
|
||||
if(!cpuCycle())
|
||||
{
|
||||
//memDumpMainMem();
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
if(memClock == 4)
|
||||
{
|
||||
memClockTimers();
|
||||
@ -280,26 +325,31 @@ static void gbEmuMainLoop(void)
|
||||
apuClockTimers();
|
||||
if(!ppuCycle())
|
||||
exit(EXIT_SUCCESS);
|
||||
if(!gbEmuGBSPlayback && ppuDrawDone())
|
||||
if(ppuDrawDone())
|
||||
{
|
||||
//printf("%i\n",mCycles);
|
||||
//mCycles = 0;
|
||||
emuRenderFrame = true;
|
||||
#if (WINDOWS_BUILD && DEBUG_HZ)
|
||||
emuTimesCalled++;
|
||||
DWORD end = GetTickCount();
|
||||
emuTotalElapsed += end - emuFrameStart;
|
||||
if(emuTotalElapsed >= 1000)
|
||||
if(!gbEmuGBSPlayback)
|
||||
{
|
||||
printf("\r%iHz ", emuTimesCalled);
|
||||
emuTimesCalled = 0;
|
||||
emuTotalElapsed = 0;
|
||||
//printf("%i\n",mCycles);
|
||||
//mCycles = 0;
|
||||
emuRenderFrame = true;
|
||||
#if (WINDOWS_BUILD && DEBUG_HZ)
|
||||
emuTimesCalled++;
|
||||
DWORD end = GetTickCount();
|
||||
emuTotalElapsed += end - emuFrameStart;
|
||||
if(emuTotalElapsed >= 1000)
|
||||
{
|
||||
printf("\r%iHz ", emuTimesCalled);
|
||||
emuTimesCalled = 0;
|
||||
emuTotalElapsed = 0;
|
||||
}
|
||||
emuFrameStart = end;
|
||||
#endif
|
||||
glutPostRedisplay();
|
||||
if(ppuDebugPauseFrame)
|
||||
nesPause = true;
|
||||
}
|
||||
emuFrameStart = end;
|
||||
#endif
|
||||
glutPostRedisplay();
|
||||
if(ppuDebugPauseFrame)
|
||||
nesPause = true;
|
||||
else if(!gbsTimerMode)
|
||||
cpuPlayGBS();
|
||||
}
|
||||
}
|
||||
while(mainLoopPos--);
|
||||
@ -437,14 +487,6 @@ static void gbEmuHandleKeyDown(unsigned char key, int x, int y)
|
||||
glutReshapeWindow(VISIBLE_DOTS*9, VISIBLE_LINES*9);
|
||||
}
|
||||
break;
|
||||
case 'o':
|
||||
case 'O':
|
||||
if(!inOverscanToggle)
|
||||
{
|
||||
inOverscanToggle = true;
|
||||
doOverscan ^= true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -498,10 +540,6 @@ static void gbEmuHandleKeyUp(unsigned char key, int x, int y)
|
||||
case '7': case '8': case '9':
|
||||
inResize = false;
|
||||
break;
|
||||
case 'o':
|
||||
case 'O':
|
||||
inOverscanToggle = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
9
mbc.c
9
mbc.c
@ -27,6 +27,7 @@ static void noSet8(uint16_t addr, uint8_t val);
|
||||
static void mbc1Set8(uint16_t addr, uint8_t val);
|
||||
static void mbc3Set8(uint16_t addr, uint8_t val);
|
||||
static void mbc5Set8(uint16_t addr, uint8_t val);
|
||||
static void gbsSet8(uint16_t addr, uint8_t val);
|
||||
|
||||
void mbcInit(uint8_t type)
|
||||
{
|
||||
@ -36,6 +37,8 @@ void mbcInit(uint8_t type)
|
||||
mbcSet8 = mbc3Set8;
|
||||
else if(type == MBC_TYPE_5)
|
||||
mbcSet8 = mbc5Set8;
|
||||
else if(type == MBC_TYPE_GBS)
|
||||
mbcSet8 = gbsSet8;
|
||||
else
|
||||
mbcSet8 = noSet8;
|
||||
}
|
||||
@ -142,3 +145,9 @@ static void mbc5Set8(uint16_t addr, uint8_t val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void gbsSet8(uint16_t addr, uint8_t val)
|
||||
{
|
||||
if(addr >= 0x2000 && addr < 0x3000)
|
||||
cBank = val;
|
||||
}
|
||||
|
1
mbc.h
1
mbc.h
@ -17,6 +17,7 @@ enum {
|
||||
MBC_TYPE_5,
|
||||
MBC_TYPE_6,
|
||||
MBC_TYPE_7,
|
||||
MBC_TYPE_GBS,
|
||||
};
|
||||
|
||||
typedef void (*set8FuncT)(uint16_t, uint8_t);
|
||||
|
213
mem.c
213
mem.c
@ -17,6 +17,7 @@
|
||||
static uint8_t Ext_Mem[0x20000];
|
||||
static uint8_t Main_Mem[0x2000];
|
||||
static uint8_t High_Mem[0x80];
|
||||
static uint8_t gbs_prevValReads[8];
|
||||
static uint8_t memLastVal;
|
||||
static uint8_t irqEnableReg;
|
||||
static uint8_t irqFlagsReg;
|
||||
@ -154,88 +155,99 @@ static void memSetExtVal()
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool memInit()
|
||||
static uint8_t curGBS = 0;
|
||||
extern uint8_t gbsTracksTotal;
|
||||
bool memInit(bool romcheck, bool gbs)
|
||||
{
|
||||
switch(emuGBROM[0x147])
|
||||
if(romcheck)
|
||||
{
|
||||
case 0x00:
|
||||
printf("ROM Only\n");
|
||||
mbcInit(MBC_TYPE_NONE);
|
||||
bankUsed = false;
|
||||
extMemUsed = false;
|
||||
break;
|
||||
case 0x01:
|
||||
printf("ROM Only (MBC1)\n");
|
||||
mbcInit(MBC_TYPE_1);
|
||||
memSetBankVal();
|
||||
extMemUsed = false;
|
||||
break;
|
||||
case 0x02:
|
||||
printf("ROM and RAM (without save) (MBC1)\n");
|
||||
mbcInit(MBC_TYPE_1);
|
||||
memSetBankVal();
|
||||
memSetExtVal();
|
||||
break;
|
||||
case 0x03:
|
||||
printf("ROM and RAM (with save) (MBC1)\n");
|
||||
mbcInit(MBC_TYPE_1);
|
||||
memSetBankVal();
|
||||
memSetExtVal();
|
||||
memLoadSave();
|
||||
break;
|
||||
case 0x11:
|
||||
printf("ROM Only (MBC3)\n");
|
||||
mbcInit(MBC_TYPE_3);
|
||||
memSetBankVal();
|
||||
extMemUsed = false;
|
||||
break;
|
||||
case 0x12:
|
||||
printf("ROM and RAM (without save) (MBC3)\n");
|
||||
mbcInit(MBC_TYPE_3);
|
||||
memSetBankVal();
|
||||
memSetExtVal();
|
||||
break;
|
||||
case 0x13:
|
||||
printf("ROM and RAM (with save) (MBC3)\n");
|
||||
mbcInit(MBC_TYPE_3);
|
||||
memSetBankVal();
|
||||
memSetExtVal();
|
||||
memLoadSave();
|
||||
break;
|
||||
case 0x19:
|
||||
case 0x1C:
|
||||
printf("ROM Only (MBC5)\n");
|
||||
mbcInit(MBC_TYPE_5);
|
||||
memSetBankVal();
|
||||
extMemUsed = false;
|
||||
break;
|
||||
case 0x1A:
|
||||
case 0x1D:
|
||||
printf("ROM and RAM (without save) (MBC5)\n");
|
||||
mbcInit(MBC_TYPE_5);
|
||||
memSetBankVal();
|
||||
memSetExtVal();
|
||||
break;
|
||||
case 0x1B:
|
||||
case 0x1E:
|
||||
printf("ROM and RAM (with save) (MBC5)\n");
|
||||
mbcInit(MBC_TYPE_5);
|
||||
memSetBankVal();
|
||||
memSetExtVal();
|
||||
memLoadSave();
|
||||
break;
|
||||
default:
|
||||
printf("Unsupported Type %02x!\n", emuGBROM[0x147]);
|
||||
return false;
|
||||
}
|
||||
extMemUsed = (emuGBROM[0x149] > 0);
|
||||
if(extMemUsed)
|
||||
{
|
||||
if(emuGBROM[0x149] == 1)
|
||||
extMask = 0x7FF;
|
||||
if(gbs)
|
||||
{
|
||||
printf("GBS Mode\n");
|
||||
mbcInit(MBC_TYPE_GBS);
|
||||
bankUsed = true;
|
||||
extMemUsed = true;
|
||||
printf("8KB RAM allowed\n");
|
||||
extTotalSize = 0x2000;
|
||||
extTotalMask = 0x1FFF;
|
||||
extMask = 1;
|
||||
memset(gbs_prevValReads,0,8);
|
||||
}
|
||||
else
|
||||
extMask = 0x1FFF;
|
||||
{
|
||||
switch(emuGBROM[0x147])
|
||||
{
|
||||
case 0x00:
|
||||
printf("ROM Only\n");
|
||||
mbcInit(MBC_TYPE_NONE);
|
||||
bankUsed = false;
|
||||
extMemUsed = false;
|
||||
break;
|
||||
case 0x01:
|
||||
printf("ROM Only (MBC1)\n");
|
||||
mbcInit(MBC_TYPE_1);
|
||||
memSetBankVal();
|
||||
extMemUsed = false;
|
||||
break;
|
||||
case 0x02:
|
||||
printf("ROM and RAM (without save) (MBC1)\n");
|
||||
mbcInit(MBC_TYPE_1);
|
||||
memSetBankVal();
|
||||
memSetExtVal();
|
||||
break;
|
||||
case 0x03:
|
||||
printf("ROM and RAM (with save) (MBC1)\n");
|
||||
mbcInit(MBC_TYPE_1);
|
||||
memSetBankVal();
|
||||
memSetExtVal();
|
||||
memLoadSave();
|
||||
break;
|
||||
case 0x11:
|
||||
printf("ROM Only (MBC3)\n");
|
||||
mbcInit(MBC_TYPE_3);
|
||||
memSetBankVal();
|
||||
extMemUsed = false;
|
||||
break;
|
||||
case 0x12:
|
||||
printf("ROM and RAM (without save) (MBC3)\n");
|
||||
mbcInit(MBC_TYPE_3);
|
||||
memSetBankVal();
|
||||
memSetExtVal();
|
||||
break;
|
||||
case 0x13:
|
||||
printf("ROM and RAM (with save) (MBC3)\n");
|
||||
mbcInit(MBC_TYPE_3);
|
||||
memSetBankVal();
|
||||
memSetExtVal();
|
||||
memLoadSave();
|
||||
break;
|
||||
case 0x19:
|
||||
case 0x1C:
|
||||
printf("ROM Only (MBC5)\n");
|
||||
mbcInit(MBC_TYPE_5);
|
||||
memSetBankVal();
|
||||
extMemUsed = false;
|
||||
break;
|
||||
case 0x1A:
|
||||
case 0x1D:
|
||||
printf("ROM and RAM (without save) (MBC5)\n");
|
||||
mbcInit(MBC_TYPE_5);
|
||||
memSetBankVal();
|
||||
memSetExtVal();
|
||||
break;
|
||||
case 0x1B:
|
||||
case 0x1E:
|
||||
printf("ROM and RAM (with save) (MBC5)\n");
|
||||
mbcInit(MBC_TYPE_5);
|
||||
memSetBankVal();
|
||||
memSetExtVal();
|
||||
memLoadSave();
|
||||
break;
|
||||
default:
|
||||
printf("Unsupported Type %02x!\n", emuGBROM[0x147]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
memset(Main_Mem,0,0x2000);
|
||||
memset(High_Mem,0,0x80);
|
||||
@ -253,6 +265,13 @@ bool memInit()
|
||||
return true;
|
||||
}
|
||||
|
||||
void memStartGBS()
|
||||
{
|
||||
curGBS = 1;
|
||||
printf("Track %i/%i ", curGBS, gbsTracksTotal);
|
||||
cpuLoadGBS(curGBS-1);
|
||||
}
|
||||
|
||||
uint8_t memGet8(uint16_t addr)
|
||||
{
|
||||
uint8_t val = memLastVal;
|
||||
@ -428,9 +447,40 @@ void memSaveGame()
|
||||
}
|
||||
}
|
||||
|
||||
extern bool gbEmuGBSPlayback;
|
||||
extern bool gbsTimerMode;
|
||||
extern uint8_t inValReads[8];
|
||||
|
||||
//clocked at 262144 Hz
|
||||
void memClockTimers()
|
||||
{
|
||||
if(gbEmuGBSPlayback)
|
||||
{
|
||||
if(inValReads[BUTTON_RIGHT] && !gbs_prevValReads[BUTTON_RIGHT])
|
||||
{
|
||||
gbs_prevValReads[BUTTON_RIGHT] = inValReads[BUTTON_RIGHT];
|
||||
curGBS++;
|
||||
if(curGBS > gbsTracksTotal)
|
||||
curGBS = 1;
|
||||
printf("\rTrack %i/%i ", curGBS, gbsTracksTotal);
|
||||
cpuLoadGBS(curGBS-1);
|
||||
}
|
||||
else if(!inValReads[BUTTON_RIGHT])
|
||||
gbs_prevValReads[BUTTON_RIGHT] = 0;
|
||||
|
||||
if(inValReads[BUTTON_LEFT] && !gbs_prevValReads[BUTTON_LEFT])
|
||||
{
|
||||
gbs_prevValReads[BUTTON_LEFT] = inValReads[BUTTON_LEFT];
|
||||
curGBS--;
|
||||
if(curGBS < 1)
|
||||
curGBS = gbsTracksTotal;
|
||||
printf("\rTrack %i/%i ", curGBS, gbsTracksTotal);
|
||||
cpuLoadGBS(curGBS-1);
|
||||
}
|
||||
else if(!inValReads[BUTTON_LEFT])
|
||||
gbs_prevValReads[BUTTON_LEFT] = 0;
|
||||
}
|
||||
|
||||
//clocked at 16384 Hz (262144 / 16 = 16384)
|
||||
if(divRegClock == 16)
|
||||
{
|
||||
@ -451,7 +501,10 @@ void memClockTimers()
|
||||
{
|
||||
//printf("Timer interrupt\n");
|
||||
timerRegVal = timerResetVal;
|
||||
irqFlagsReg |= 4;
|
||||
if(!gbEmuGBSPlayback)
|
||||
irqFlagsReg |= 4;
|
||||
else if(gbsTimerMode)
|
||||
cpuPlayGBS();
|
||||
}
|
||||
timerRegClock = 1;
|
||||
}
|
||||
|
3
mem.h
3
mem.h
@ -8,7 +8,8 @@
|
||||
#ifndef _mem_h_
|
||||
#define _mem_h_
|
||||
|
||||
bool memInit();
|
||||
bool memInit(bool romcheck, bool gbs);
|
||||
void memStartGBS();
|
||||
uint8_t memGet8(uint16_t addr);
|
||||
void memSet8(uint16_t addr, uint8_t val);
|
||||
void memSet16(uint16_t addr, uint16_t val);
|
||||
|
186
ppu.c
186
ppu.c
@ -35,6 +35,8 @@
|
||||
#define PPU_SPRITE_FLIP_Y (1<<6)
|
||||
#define PPU_SPRITE_PRIO (1<<7)
|
||||
|
||||
static uint8_t ppuDoSprites(uint8_t color, uint8_t tCol);
|
||||
|
||||
extern uint8_t *textureImage;
|
||||
|
||||
static uint32_t ppuClock;
|
||||
@ -183,76 +185,7 @@ bool ppuCycle()
|
||||
tCol = (~(PPU_Reg[7]>>(color*2)))&3;
|
||||
}
|
||||
if(PPU_Reg[0]&PPU_SPRITE_ENABLE)
|
||||
{
|
||||
uint8_t i;
|
||||
uint8_t cSpriteAnd = (PPU_Reg[0] & PPU_SPRITE_8_16) ? 15 : 7;
|
||||
for(i = 0; i < ppuOAM2pos; i++)
|
||||
{
|
||||
uint8_t OAMcXpos = PPU_OAM2[(i<<2)+1];
|
||||
if(OAMcXpos >= 168)
|
||||
continue;
|
||||
int16_t cmpPos = ((int16_t)OAMcXpos)-8;
|
||||
if(cmpPos <= ppuDots && (cmpPos+8) > ppuDots)
|
||||
{
|
||||
uint8_t cSpriteByte3 = PPU_OAM2[(i<<2)+3];
|
||||
uint8_t tVal = PPU_OAM2[(i<<2)+2];
|
||||
uint16_t tPos = tVal*16;
|
||||
|
||||
uint8_t OAMcYpos = PPU_OAM2[(i<<2)];
|
||||
uint8_t cmpYPos = OAMcYpos-16;
|
||||
uint8_t cSpriteY = (PPU_Reg[4] - cmpYPos)&cSpriteAnd;
|
||||
uint8_t cSpriteAdd = 0; //used to select which 8 by 16 tile
|
||||
if(cSpriteY > 7) //8 by 16 select
|
||||
{
|
||||
cSpriteAdd = 16;
|
||||
cSpriteY &= 7;
|
||||
}
|
||||
if(cSpriteByte3 & PPU_SPRITE_FLIP_Y)
|
||||
{
|
||||
cSpriteY ^= 7;
|
||||
if(PPU_Reg[0] & PPU_SPRITE_8_16)
|
||||
cSpriteAdd ^= 16; //8 by 16 select
|
||||
}
|
||||
tPos+=(cSpriteY)*2;
|
||||
|
||||
ChrRegA = PPU_VRAM[(tPos+cSpriteAdd)&0x1FFF];
|
||||
ChrRegB = PPU_VRAM[(tPos+cSpriteAdd+1)&0x1FFF];
|
||||
|
||||
uint8_t cSpriteX = (ppuDots - OAMcXpos)&7;
|
||||
if(cSpriteByte3 & PPU_SPRITE_FLIP_X)
|
||||
cSpriteX ^= 7;
|
||||
uint8_t sprCol = 0;
|
||||
if(ChrRegA & (0x80>>cSpriteX))
|
||||
sprCol |= 1;
|
||||
if(ChrRegB & (0x80>>cSpriteX))
|
||||
sprCol |= 2;
|
||||
|
||||
//done looking at sprites, we have to
|
||||
//always return the first one we find
|
||||
if(sprCol != 0)
|
||||
{
|
||||
//sprite has highest priority, return sprite
|
||||
if((cSpriteByte3 & PPU_SPRITE_PRIO) == 0)
|
||||
{
|
||||
if(cSpriteByte3 & PPU_SPRITE_PAL)
|
||||
tCol = (~(PPU_Reg[9]>>(sprCol*2)))&3;
|
||||
else
|
||||
tCol = (~(PPU_Reg[8]>>(sprCol*2)))&3;
|
||||
break;
|
||||
} //sprite has low priority and BG is not 0, return BG
|
||||
else if((color&3) != 0)
|
||||
break;
|
||||
//background is 0 so return sprite
|
||||
if(cSpriteByte3 & PPU_SPRITE_PAL)
|
||||
tCol = (~(PPU_Reg[9]>>(sprCol*2)))&3;
|
||||
else
|
||||
tCol = (~(PPU_Reg[8]>>(sprCol*2)))&3;
|
||||
break;
|
||||
}
|
||||
//Sprite is 0, keep looking for sprites
|
||||
}
|
||||
}
|
||||
}
|
||||
tCol = ppuDoSprites(color, tCol);
|
||||
uint8_t draw = (tCol == 0) ? 0 : (tCol == 1) ? 0x55 : (tCol == 2) ? 0xAA : 0xFF;
|
||||
{
|
||||
size_t drawPos = (ppuDots*4)+(PPU_Reg[4]*160*4);
|
||||
@ -319,9 +252,19 @@ uint8_t ppuGet8(uint16_t addr)
|
||||
{
|
||||
uint8_t val = 0;
|
||||
if(addr >= 0x8000 && addr < 0xA000)
|
||||
val = PPU_VRAM[addr&0x1FFF];
|
||||
{
|
||||
if(!(PPU_Reg[0] & PPU_ENABLE) || (ppuMode != 3))
|
||||
val = PPU_VRAM[addr&0x1FFF];
|
||||
else
|
||||
val = 0xFF;
|
||||
}
|
||||
else if(addr >= 0xFE00 && addr < 0xFEA0)
|
||||
val = PPU_OAM[addr&0xFF];
|
||||
{
|
||||
if(!(PPU_Reg[0] & PPU_ENABLE) || (ppuMode == 0) || (ppuMode == 1))
|
||||
val = PPU_OAM[addr&0xFF];
|
||||
else
|
||||
val = 0xFF;
|
||||
}
|
||||
else if(addr >= 0xFF40 && addr < 0xFF4C)
|
||||
{
|
||||
if(addr == 0xFF41)
|
||||
@ -342,9 +285,15 @@ uint8_t ppuGet8(uint16_t addr)
|
||||
void ppuSet8(uint16_t addr, uint8_t val)
|
||||
{
|
||||
if(addr >= 0x8000 && addr < 0xA000)
|
||||
PPU_VRAM[addr&0x1FFF] = val;
|
||||
{
|
||||
if(!(PPU_Reg[0] & PPU_ENABLE) || (ppuMode != 3))
|
||||
PPU_VRAM[addr&0x1FFF] = val;
|
||||
}
|
||||
else if(addr >= 0xFE00 && addr < 0xFEA0)
|
||||
PPU_OAM[addr&0xFF] = val;
|
||||
{
|
||||
if(!(PPU_Reg[0] & PPU_ENABLE) || (ppuMode == 0) || (ppuMode == 1))
|
||||
PPU_OAM[addr&0xFF] = val;
|
||||
}
|
||||
else if(addr >= 0xFF40 && addr < 0xFF4C)
|
||||
{
|
||||
if(addr == 0xFF46) //OAM DMA
|
||||
@ -362,7 +311,12 @@ void ppuSet8(uint16_t addr, uint8_t val)
|
||||
PPU_Reg[addr&0xF] = (val&(~7));
|
||||
else //other R/W regs
|
||||
PPU_Reg[addr&0xF] = val;
|
||||
//if(addr == 0xFF40)
|
||||
if(addr == 0xFF40 && !(val&PPU_ENABLE))
|
||||
{
|
||||
PPU_Reg[4] = 0;
|
||||
ppuClock = 0;
|
||||
ppuMode = 2;
|
||||
}
|
||||
// printf("ppuSet8(%04x, %02x)\n",addr,val);
|
||||
}
|
||||
}
|
||||
@ -405,3 +359,85 @@ bool ppuInVBlank()
|
||||
ppuVBlankTriggered = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
static uint8_t ppuDoSprites(uint8_t color, uint8_t tCol)
|
||||
{
|
||||
uint8_t i;
|
||||
uint8_t cSpriteAnd = (PPU_Reg[0] & PPU_SPRITE_8_16) ? 15 : 7;
|
||||
uint8_t cPrioSpriteX = 0xFF;
|
||||
uint8_t ChrRegA = 0, ChrRegB = 0;
|
||||
for(i = 0; i < ppuOAM2pos; i++)
|
||||
{
|
||||
uint8_t OAMcXpos = PPU_OAM2[(i<<2)+1];
|
||||
if(OAMcXpos >= 168)
|
||||
continue;
|
||||
int16_t cmpPos = ((int16_t)OAMcXpos)-8;
|
||||
if(cmpPos <= ppuDots && (cmpPos+8) > ppuDots)
|
||||
{
|
||||
uint8_t cSpriteByte3 = PPU_OAM2[(i<<2)+3];
|
||||
uint8_t tVal = PPU_OAM2[(i<<2)+2];
|
||||
uint16_t tPos = tVal*16;
|
||||
|
||||
uint8_t OAMcYpos = PPU_OAM2[(i<<2)];
|
||||
uint8_t cmpYPos = OAMcYpos-16;
|
||||
uint8_t cSpriteY = (PPU_Reg[4] - cmpYPos)&cSpriteAnd;
|
||||
uint8_t cSpriteAdd = 0; //used to select which 8 by 16 tile
|
||||
if(cSpriteY > 7) //8 by 16 select
|
||||
{
|
||||
cSpriteAdd = 16;
|
||||
cSpriteY &= 7;
|
||||
}
|
||||
if(cSpriteByte3 & PPU_SPRITE_FLIP_Y)
|
||||
{
|
||||
cSpriteY ^= 7;
|
||||
if(PPU_Reg[0] & PPU_SPRITE_8_16)
|
||||
cSpriteAdd ^= 16; //8 by 16 select
|
||||
}
|
||||
tPos+=(cSpriteY)*2;
|
||||
|
||||
ChrRegA = PPU_VRAM[(tPos+cSpriteAdd)&0x1FFF];
|
||||
ChrRegB = PPU_VRAM[(tPos+cSpriteAdd+1)&0x1FFF];
|
||||
|
||||
uint8_t cSpriteX = (ppuDots - OAMcXpos)&7;
|
||||
if(cSpriteByte3 & PPU_SPRITE_FLIP_X)
|
||||
cSpriteX ^= 7;
|
||||
uint8_t sprCol = 0;
|
||||
if(ChrRegA & (0x80>>cSpriteX))
|
||||
sprCol |= 1;
|
||||
if(ChrRegB & (0x80>>cSpriteX))
|
||||
sprCol |= 2;
|
||||
|
||||
//found possible candidate to display
|
||||
if(sprCol != 0)
|
||||
{
|
||||
//there already was a sprite set with lower X
|
||||
if(cPrioSpriteX < OAMcXpos)
|
||||
continue;
|
||||
//sprite has highest priority, return sprite
|
||||
if((cSpriteByte3 & PPU_SPRITE_PRIO) == 0)
|
||||
{
|
||||
//sprite so far has highest prio so set color
|
||||
if(cSpriteByte3 & PPU_SPRITE_PAL)
|
||||
tCol = (~(PPU_Reg[9]>>(sprCol*2)))&3;
|
||||
else
|
||||
tCol = (~(PPU_Reg[8]>>(sprCol*2)))&3;
|
||||
//keep looking if there is a lower X
|
||||
cPrioSpriteX = OAMcXpos;
|
||||
continue;
|
||||
} //sprite has low priority and BG is not 0, keep BG for now
|
||||
else if((color&3) != 0)
|
||||
continue;
|
||||
//background is 0 so set color
|
||||
if(cSpriteByte3 & PPU_SPRITE_PAL)
|
||||
tCol = (~(PPU_Reg[9]>>(sprCol*2)))&3;
|
||||
else
|
||||
tCol = (~(PPU_Reg[8]>>(sprCol*2)))&3;
|
||||
//keep looking if there is a lower X
|
||||
cPrioSpriteX = OAMcXpos;
|
||||
continue;
|
||||
}
|
||||
//Sprite is 0, keep looking for sprites
|
||||
}
|
||||
}
|
||||
return tCol;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user