Files
gameboy/gbscreen.cpp

488 lines
15 KiB
C++

#include "gbscreen.h"
#include <cstring>
#include "sm83.h"
#include "mem.h"
#include "audio_viewer.h"
//#include "zx_ula.h"
#include <SDL2/SDL.h>
#include "ui_window.h"
#include "debug.h"
#include "ui.h"
namespace gbscreen
{
uint32_t palette[4] = {
//0xFFFFFF, 0xFF0000, 0x00FF00, 0x0000FF
0xFFFFFF, 0xAAAAAA, 0x555555, 0x000000
};
SDL_Window *win = nullptr;
SDL_Renderer *ren = nullptr;
SDL_Texture *tex = nullptr;
SDL_Texture *uitex = nullptr;
uint32_t t_states_total = 70224;
uint32_t t_states_per_scanline = 456;
uint32_t vsync_lines = 10;
uint8_t zoom = 2;
bool fullscreen = false;
bool full_refresh = true;
int fullscreen_scale = 1;
SDL_Rect dest_rect;
//uint32_t time=0;
uint32_t t_screen = 0;
uint8_t gb_pixels[160*144];
uint8_t *ptr_pixel = gb_pixels;
uint16_t dots_in_scanline = 0;
uint8_t line_buffer[160];
bool eventHandler(SDL_Event *e)
{
if (e->type==SDL_WINDOWEVENT) {
if (e->window.event==SDL_WINDOWEVENT_CLOSE) {
return false;
} else if ((e->window.event==SDL_WINDOWEVENT_SHOWN) || (e->window.event==SDL_WINDOWEVENT_EXPOSED)) {
redraw();
}
}
if (!debug::debugging()) {
if (debug::paused()) {
if (e->type == SDL_KEYDOWN) {
if (e->key.keysym.scancode==SDL_SCANCODE_ESCAPE) {
const uint8_t dt = sm83::step();
debug::cont();
gbscreen::refresh(dt);
}
}
} else {
if (e->type == SDL_KEYDOWN) {
if (e->key.keysym.scancode==SDL_SCANCODE_ESCAPE) {
debug::pause();
gbscreen::redraw();
} else if (e->key.keysym.scancode==SDL_SCANCODE_F1) {
gbscreen::decZoom();
} else if (e->key.keysym.scancode==SDL_SCANCODE_F2) {
gbscreen::incZoom();
} else if (e->key.keysym.scancode==SDL_SCANCODE_F3) {
gbscreen::toggleFullscreen();
} else if (e->key.keysym.scancode==SDL_SCANCODE_F6) {
//zx_tape::play();
} else if (e->key.keysym.scancode==SDL_SCANCODE_F7) {
//zx_tape::rewind();
}
}
}
}
if (e->type == SDL_MOUSEMOTION) {
SDL_ShowCursor(true);
}
return true;
}
void reinit()
{
if (win) ui::window::unregisterWindow(SDL_GetWindowID(win));
if (tex) SDL_DestroyTexture(tex);
if (uitex) SDL_DestroyTexture(uitex);
if (ren) SDL_DestroyRenderer(ren);
if (win) SDL_DestroyWindow(win);
const int z = fullscreen ? 1 : zoom;
win = SDL_CreateWindow("Gameboy Screen", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 160*z, 144*z, fullscreen?SDL_WINDOW_FULLSCREEN_DESKTOP:SDL_WINDOW_SHOWN);
ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED);
tex = SDL_CreateTexture(ren, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 160, 144);
uitex = ui::createtexture(ren);
ui::window::registerWindow(SDL_GetWindowID(win), eventHandler);
if (fullscreen)
{
int w, h;
SDL_GetWindowSize(win, &w, &h);
fullscreen_scale = h/144;
dest_rect.w = 160 * fullscreen_scale;
dest_rect.h = 144 * fullscreen_scale;
dest_rect.x = (w - dest_rect.w)/2;
dest_rect.y = (h - dest_rect.h)/2;
}
else
{
dest_rect.x = dest_rect.y = 0;
dest_rect.w = 160 * zoom;
dest_rect.h = 144 * zoom;
}
focus();
}
void init(int mode)
{
reinit();
}
void focus()
{
if (win)
{
SDL_RaiseWindow(win);
redraw();
}
}
void fill_line_buffer_bkg(uint8_t LY)
{
const uint8_t LCDC = mem::readMem(0xff40);
if ((LCDC & 0x1) == 0) {
for (int i=0; i<160; ++i) line_buffer[i]=0;
return;
}
const uint8_t SCY = mem::readMem(0xff42);
const uint8_t SCX = mem::readMem(0xff43);
const uint8_t BGP = mem::readMem(0xff47);
const uint16_t ty = uint8_t(SCY+LY) >> 3;
const uint8_t ly = uint8_t(SCY+LY) & 0x7;
uint16_t tx = SCX >> 3;
uint8_t ox = SCX & 0x7;
uint16_t base_tilemap_address = LCDC&0x8 ? 0x9c00 : 0x9800;
int pi = 0;
while(true) {
uint16_t tilemap_address = base_tilemap_address + tx + (ty<<5);
uint16_t tile = mem::readMem(tilemap_address);
uint16_t base_tile_address = 0x8000;
if ( ((LCDC&0x10)==0) && (tile<128) ) base_tile_address = 0x9000;
uint16_t tile_address = base_tile_address + (tile<<4) + (ly*2);
uint8_t a = mem::readMem(tile_address);
uint8_t b = mem::readMem(tile_address+1);
for (int i=0; i<8; ++i) {
if (ox==0) {
uint8_t index = (a&0x80 ? 1 : 0) + (b&0x80 ? 2 : 0 );
line_buffer[pi++] = (BGP >> (index*2)) & 0x3;
} else {
ox--;
}
a=a<<1; b=b<<1;
if (pi==160) return;
}
tx = (tx+1)&0x1f;
}
}
void fill_line_buffer_win(uint8_t LY)
{
const uint8_t LCDC = mem::readMem(0xff40);
if ((LCDC & 0x21) != 0x21) return;
const uint8_t WY = mem::readMem(0xff4a);
if (LY<WY) return;
const uint8_t WX = mem::readMem(0xff4b);
const uint8_t BGP = mem::readMem(0xff47);
const uint16_t ty = uint8_t(LY-WY) >> 3;
const uint8_t ly = uint8_t(LY-WY) & 0x7;
uint8_t ox = WX<7 ? 7-WX : 0;
uint16_t tx = 0;
uint16_t base_tilemap_address = LCDC&0x40 ? 0x9c00 : 0x9800;
int pi = WX<7 ? 0 : WX-7;
while(true) {
uint16_t tilemap_address = base_tilemap_address + tx + (ty<<5);
uint16_t tile = mem::readMem(tilemap_address);
uint16_t base_tile_address = 0x8000;
if ( ((LCDC&0x10)==0) && (tile<128) ) base_tile_address = 0x9000;
uint16_t tile_address = base_tile_address + (tile<<4) + (ly*2);
uint8_t a = mem::readMem(tile_address);
uint8_t b = mem::readMem(tile_address+1);
for (int i=0; i<8; ++i) {
if (ox==0) {
uint8_t index = (a&0x80 ? 1 : 0) + (b&0x80 ? 2 : 0 );
line_buffer[pi++] = (BGP >> (index*2)) & 0x3;
} else {
ox--;
}
a=a<<1; b=b<<1;
if (pi==160) return;
}
tx = (tx+1)&0x1f;
}
}
struct oam_entry_t
{
uint8_t y, x, tile, attr;
};
oam_entry_t *oam = nullptr;
void fill_line_buffer_obj(uint8_t LY)
{
const uint8_t LCDC = mem::readMem(0xff40);
const uint8_t OBP[2] = { mem::readMem(0xff48), mem::readMem(0xff49) };
if ((LCDC & 0x2) == 0) return;
oam = (oam_entry_t*)mem::rawPtr(0xfe00);
const uint8_t height = (LCDC & 0x4) ? 16 : 8;
uint8_t obj_list[10] = { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 };
int num_obj_found=0;
int obj=0;
// Revisem els 40 posibles sprites o fins a trobar 10...
while (obj<40 && num_obj_found<10) {
// Si el sprite està tocant la linea actual l'afegim a la llista
if ( (LY+16 >= (oam[obj].y)) && (LY+16 < (oam[obj].y+height)) ) {
obj_list[num_obj_found]=obj;
num_obj_found++;
}
obj++;
}
// Pintem els sprites en el buffer de sprites
uint8_t pixels[160];
uint8_t x_pos[160];
for (int i=0;i<160;++i) { pixels[i] = 255; x_pos[i] = 255; }
obj=0;
while (obj_list[obj] != 255) {
oam_entry_t *o = &oam[obj_list[obj]];
const uint8_t ly = uint8_t(LY-o->y) & 0xf;
uint16_t tile = height==8 ? o->tile : o->tile & 0xFE; // si es dos tiles de alt, el primer sempre comença en numero parell
uint8_t yflip = o->attr&0x40 ? (height-1)-ly : ly; // està invertit verticalment?
uint16_t tile_address = 0x8000 + (tile<<4) + (yflip*2);
uint8_t a = mem::readMem(tile_address);
uint8_t b = mem::readMem(tile_address+1);
for (int i=0; i<8; ++i) { // Per a cada pixel de la linea del tile...
if (o->x+i>=168) break; // Si ja estem fora de la pantalla per la dreta, eixim del bucle
if (o->x+i>=8) { // Si està dins de la pantalla...
if (x_pos[o->x+i-8]>o->x) { // Si te una x menor que la que tenía
//uint8_t xflip = o->attr&0x20 ? 8 : 0; // està invertit horitzontalment?
const uint8_t ppos = 1 << ( o->attr&0x20 ? i : 7-i);
const uint8_t val = (a&ppos ? 1 : 0) + (b&ppos ? 2 : 0 ); // agafem el pixel que toca
if (val) { // Si el pixel no es transparent...
const uint8_t color = (OBP[(o->attr>>4)&1] >> (val*2)) & 0x3;
pixels[o->x+i-8] = color | o->attr&0x80;; // el pintem al buffer, amb el flag de prioritat respecte al BKG
x_pos[o->x+i-8] = o->x; // I apuntem la seua x per a comparar després
}
}
}
}
obj++;
}
// Per últim, volquem els pixels que toque al buffer de linea
for (int i=0; i<160; ++i) {
if (pixels[i]!=255) { // si el pixel no es transparent...
if ( !(pixels[i]&0x80) || (line_buffer[i]==0) ) { // Si te prioritat o el color de fondo es 0...
line_buffer[i] = pixels[i]&0x03; // pintem el pixel (llevant el flag de prioritat)
}
}
}
}
void refresh(const uint32_t dt, const bool full)
{
const uint8_t LCDC = mem::readMem(0xff40);
if ((LCDC&0x80)==0) return;
uint8_t STAT = mem::readMem(0xff41);
uint8_t LY = mem::readMem(0xff44);
const uint8_t LYC = mem::readMem(0xff45);
for (int i=0;i<dt;++i)
{
// Açò va volcant els pixels del line_buffer en pantalla
if ( (STAT&0x3)==3) {
uint16_t current_pixel = dots_in_scanline-80;
if (current_pixel<160) {
//*(ptr_pixel++) = line_buffer[current_pixel];
gb_pixels[current_pixel+LY*160] = line_buffer[current_pixel];
}
}
// gestió de en quin dot i linea estem, i tot el que ha de passar
uint8_t interrupts = 0x00;
dots_in_scanline++;
if ( (dots_in_scanline==80) && (LY<144) )
{
STAT = (STAT & 0xFC) | 0x3; // Set mode 3
}
else if ( (dots_in_scanline==252) && (LY<144) )
{
STAT = (STAT & 0xFC); // Set mode 0
if (STAT&0x08) interrupts |= INTERRUPT_LCD;
}
else if (dots_in_scanline==456)
{
dots_in_scanline = 0;
LY++;
if (LY==144)
{
STAT = (STAT & 0xFC) | 0x01; // Set mode 1
//mem::writeMem(0xff41, STAT);
//mem::writeMem(0xff44, LY);
interrupts |= INTERRUPT_VBLANK;
if (STAT&0x10) interrupts |= INTERRUPT_LCD;
}
else
{
if (LY<144)
{
STAT = (STAT & 0xFC) | 0x02; // Set mode 2
if (STAT&0x20) interrupts |= INTERRUPT_LCD;
fill_line_buffer_bkg(LY);
fill_line_buffer_win(LY);
fill_line_buffer_obj(LY);
}
else if (LY==154)
{
LY=0;
}
}
if (LY==LYC)
{
STAT = (STAT & 0xFB) | 0x04;
if (STAT&0x40) interrupts |= INTERRUPT_LCD;
}
else
{
STAT = (STAT & 0xFB);
}
}
if (interrupts)
{
mem::writeMem(0xff41, STAT);
mem::writeMem(0xff44, LY);
sm83::interrupt(interrupts);
}
t_screen++;
if (t_screen>=t_states_total)
{
t_screen=0;
ptr_pixel = gb_pixels;
redraw();
//if (!full) sm83::interrupt(INTERRUPT_VBLANK);
}
}
mem::writeMem(0xff41, STAT);
mem::writeMem(0xff44, LY);
}
void fullrefresh()
{
uint32_t tmp = t_screen;
t_screen = 0;
//uint8_t * tmp_ptr = ptr_pixel;
//ptr_pixel = gb_pixels;
refresh(t_states_total, true);
//ptr_pixel = tmp_ptr;
t_screen = tmp;
}
void debugrefresh(const uint32_t dt)
{
/*if (full_refresh) fullrefresh(); else*/ refresh(dt);
redraw();
}
void redraw(const bool present)
{
//if (zx_tape::getplaying() && zx_tape::getOption(ZXTAPE_OPTION_FAST_LOAD)) return;
ui::setrenderer(ren, uitex);
Uint32* pixels;
int pitch;
SDL_LockTexture(tex, NULL, (void**)&pixels, &pitch);
for (int i=0; i<160*144;++i) *(pixels++) = palette[gb_pixels[i]];
SDL_UnlockTexture(tex);
if (fullscreen)
{
SDL_SetRenderDrawColor(ren, 0, 0, 0, 255);
SDL_RenderClear(ren);
}
// Pintem la textura a pantalla
SDL_RenderCopy(ren, tex, NULL, &dest_rect);
if (present)
SDL_RenderPresent(ren);
else
{
SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(ren, 0, 0, 0, 128);
SDL_Rect rect {0,0,160*zoom,144*zoom};
if (fullscreen) SDL_GetWindowSize(win, &rect.w, &rect.h);
SDL_RenderFillRect(ren, &rect);
}
audio_viewer::refresh();
}
void present()
{
SDL_RenderPresent(ren);
}
void setTitle(const char* title)
{
char tmp[256];
strcpy(tmp, "Gameboy Screen");
strcat(tmp, title);
SDL_SetWindowTitle(win, tmp);
}
void setZoom(const int value)
{
if (value < 1) return;
SDL_DisplayMode dm;
SDL_GetCurrentDisplayMode(0, &dm);
if (160*value > dm.w) return;
if (144*value > dm.h) return;
zoom = value;
reinit();
}
void incZoom()
{
setZoom(zoom+1);
}
void decZoom()
{
setZoom(zoom-1);
}
void toggleFullscreen()
{
fullscreen = !fullscreen;
reinit();
}
const bool getFullscreen()
{
return fullscreen;
}
void toggleFullRefresh()
{
full_refresh = !full_refresh;
}
const bool getFullRefresh()
{
return full_refresh;
}
SDL_Renderer *getrenderer()
{
return ren;
}
}