/***************************************************************************** * * File: OsmoseCore.cpp * * Project: Osmose emulator. * * Description: This file contains Osmose main loop, handling keyboard, SDL * event, and hardware emulation. * * Author: Vedder Bruno * Date: 23/01/2005, 14h13 * * URL: http://bcz.emu-france.com/ *****************************************************************************/ #include "OsmoseCore.h" #define TIME_LIMITE 7*60 SDL_sem *timer; // Semaphore used for FPS synchronisation. SN76489 *p; // These methods are not from OsmoseCore unsigned int timer_callback(unsigned int i, void *p); void sndCallback(void *ud, unsigned char *s, int len); /*--------------------------------------------------------------------*/ /* This method is the OsmoseCore constructor. */ /*--------------------------------------------------------------------*/ OsmoseCore::OsmoseCore(const char *rom_f) { quit = false; nmi = false; sound_shot_toggle = false; frame_skip_increment = .70; frame_skip_counter =0.0; screenshotNbr = 0; tileshotNbr = 0; soundshotNbr = 0; rom_filename = rom_f; gain = 0.00f; // Setup default parameters. if (emu_opt.default_config == true) { oc = new OsmoseConfiguration(); } else { oc = new OsmoseConfiguration(emu_opt.ini_file); } tw = new TextWriter(); mem = new MemoryMapper(rom_filename, oc->user_home_folder.c_str()); env = new SmsEnvironment(); v = new VDP(opt.ntsc); // Instanciate ntsc or pal VDP. p = new SN76489(); p->setGain((unsigned char) (gain * 255)); iom = new IOMapper(*v, *p); switch (opt.inputType) { case DEFAULT_PAD: input= new PadInputDevice(iom, oc); break; case PADDLE: input= new PaddleInputDevice(iom, oc); break; default: input= new PadInputDevice(iom, oc); break; } cout << "Using as sms input device: " << input->getInputDeviceName() << endl; cpu = new Z80(*env); env->setIOMapper(iom); env->setMemoryMapper(mem); env->setVDP(v); env->setCPU(cpu); wavW = NULL; pt = new PrecisionTimer(); game_name = mem->getROMName(); vf = new NullVideoFilter(); // Timer quiet, then calibrate for 3 seconds. pt->setVerbose(false); pt->calibrate(3); pt->setMeasureMode(CUMULATIVE); pt->setCumulativeBufferSize(16); #ifdef BUILT_IN_DEBUGGER dbg = new SmsDebugger(); dasm = new Z80Dasm(*env); dbg->setMemoryMapper(mem); dbg->setEnvironment(env); dbg->setVDP(v); dbg->setIOMapper(iom); dbg->setDasm(dasm); dbg->setCPU(cpu); old_cycles = 0; #endif // Instanciate the right VideoFilter. switch (emu_opt.videoFilterType) { case NULL_FILTER: vf = new NullVideoFilter(); break; case TV_FILTER: vf = new TvVideoFilter(); break; case MONOCHROM_FILTER: vf = new MonochromVideoFilter(); break; case SCALE2X_FILTER: vf = new Scale2xVideoFilter(); break; case BILINEAR_FILTER: vf = new BilinearVideoFilter(); break; default: vf = new NullVideoFilter(); break; } setupSDLVideo(vf); if (emu_opt.sound == true) { setupSDLAudio(); } timer = SDL_CreateSemaphore(0); t_id = SDL_AddTimer(DELAY_BETWEEN_FRAME, timer_callback, NULL); } /*--------------------------------------------------------------------*/ /* This method handles SDL keyboard events. */ /*--------------------------------------------------------------------*/ void OsmoseCore::handleSDLKeyboardEvent(SDL_Event e) { int k = e.key.keysym.sym; if(e.type == SDL_KEYDOWN) { if ( k == oc->PAUSE_KEY) { nmi = true; return; } if ( k == oc->TILESHOT_KEY) { captureTiles(v); return; } if ( k == oc->SCREENSHOT_KEY) { captureScreen(); return; } if ( k == SDLK_KP_MINUS) { char buffer[64]; if (gain>= 0.1f) { gain -= 0.1f; } else { gain = 0.0; } sprintf (buffer,"gain set to +%.2f%%", (gain*100.0f)); tw->addText(buffer,120); p->setGain((unsigned char) (gain*255) ); return; } if ( k == SDLK_KP_PLUS) { char buffer[64]; if (gain >= 0.9f) { gain = 1.0f; } else { gain += 0.1f; } sprintf (buffer,"gain set to +%.2f%%", (gain*100.0f)); tw->addText(buffer,120); p->setGain((unsigned char) (gain*255) ); return; } #ifdef BUILT_IN_DEBUGGER if ( k == oc->DEBUGGER_KEY) { dbg->beginSession(); return; } #endif } else // Key release event. { if ( k == oc->QUIT_KEY) { quit = true; return; } if (k == oc->SOUNDSHOT_KEY) { if (emu_opt.sound == true) { if (sound_shot_toggle == true) { // We were recording sound, so stop it now. tw->addText("stopping sound recording.", 120); wavW->close(); delete wavW; // To avoid memory leak. sound_shot_toggle = false; } else { char snd_shot_filename[256]; // We weren't recording sound, record it now, so create WaveWriter. #ifdef __USE_UNIX98 sprintf(snd_shot_filename,"%s/.osmose/snd/%s(%d).wav",oc->user_home_folder.c_str(), game_name.c_str(), soundshotNbr); #else sprintf(snd_shot_filename,"%s(%d).wav", game_name.c_str(), soundshotNbr); #endif wavW = new WaveWriter( snd_shot_filename ); sound_shot_toggle = true; soundshotNbr++; tw->addText("starting sound recording!", 120); } } return; } } // We had handle general emulation key, now call specific sms handler. input->handleDeviceChange(e); } /*--------------------------------------------------------------------*/ /* This method is the main emulation loop. */ /* Note about frame variable: */ /* This variable is the total number of frame (displayed or not !) */ /* emulated by Osmose. This value is use for speed synchronisation at */ /* 60 Fps. That's why the value is incremented even if the frame isn't*/ /* rendered. */ /*--------------------------------------------------------------------*/ void OsmoseCore::run() { bool drawline = true; bool snd_started = false; bool played = false; float snd_update = 0; short snd; int scanline_number; unsigned int frame = 0; unsigned int skipped_frame = 0 ; unsigned int over_cycles = 0; tw->addText(__OSMOSE_VERSION__,120); if (opt.ntsc == true) { scanline_number = 262; // NTSC. } else { scanline_number = 313; // PAL / SECAM } cout << "Starting emulation." << endl; cpu->reset(); while(!quit) { #ifndef BUILT_IN_DEBUGGER if (frame % 3 == 0) { SDL_SemWait(timer); } #endif /* Handle SDL Events */ while( SDL_PollEvent( &event ) ) { switch(event.type) { case SDL_MOUSEBUTTONUP: handleSDLKeyboardEvent(event); break; case SDL_MOUSEBUTTONDOWN: handleSDLKeyboardEvent(event); break; case SDL_MOUSEMOTION: handleSDLKeyboardEvent(event); break; case SDL_KEYDOWN: handleSDLKeyboardEvent(event); break; case SDL_KEYUP: handleSDLKeyboardEvent(event); break; case SDL_QUIT: quit = true; break; } } if (nmi == true) { cpu->nmi(); nmi = false; } // Here we decide if we should draw the frame, depending on frame skipping. frame_skip_counter += frame_skip_increment; if (frame_skip_counter >= 1.0) { frame_skip_counter -= 1.0; drawline = true; } else { drawline = false; } // Start measuring frame rendering. if (drawline == true) { pt->start(); } for (v->line=0; v->lineline++) { #ifdef BUILT_IN_DEBUGGER while (cpu->getCycles()enter(); if (exec_f== true) { cpu->step(); } } cpu->setCycles(0); #else over_cycles = cpu->run(CPU_CYCLES_PER_LINE - over_cycles); //cout << over_cycles << endl; if (emu_opt.sound == true) { snd_update+=(float)SND_TOGGLE; // Needed to call update_sound_buffer 368/frame played = p->update_sound_buffer(&snd); if (sound_shot_toggle == true && played==true) { wavW->writeData(snd); } if (snd_update > 1.0) { played = p->update_sound_buffer(&snd); if (played == true) { snd_update = snd_update - (float)1.0; } if (sound_shot_toggle == true && played==true) { wavW->writeData(snd); } } } #endif v->update(buffer, drawline); if (v->sms_irq == true) { if (cpu->interrupt(0xFF)) { v->sms_irq = false; } } } // For i = 0 to scanline_number tw->update(buffer, drawline); if (snd_started == false && emu_opt.sound == true) { // Start playing only if sound buffer is full. // This avoid playing silence on emulator startup. if (p->bufferFull() ) { snd_started = true; SDL_PauseAudio(0); // start playing ! } } if (drawline == true) { bool r; // Apply Filter, then update screen. vf->Filter(buffer, screen); SDL_UpdateRect(screen, 0, 0, 0, 0); // Stop measuring frame rendering. // Here we are calculating auto frame_skip. // Compute average render time of 16 rendered frames. // compute instant fps. if >= 60.0 ok, no frame skip needed. // Else set frame skip to get instant fps - 5% frame per seconds. r = pt->stop(); if (r == true) { double val, tfps; char caption[128]; val = pt->getDuration(); // Average duration of 16 (rendered) frames. tfps = (1.0/ val); // Theorical FPS, based on average render. if (emu_opt.display_fps == true) { if (tfps >= 60.0f) { sprintf (caption,"fps>=60.0 (%2.1f)", tfps); } else { sprintf (caption,"fps = %2.1f", tfps); } SDL_WM_SetCaption(caption,__OSMOSE_VERSION__); /* Window title, Iconified widows title */ } if (tfps >= 60.0) { frame_skip_increment = 1.0; } else { frame_skip_increment = (tfps * 0.95/60.0); } } } else { skipped_frame++; } frame++; // Trackball, Paddle need to be updated in time. input->updateDevice(); // For time limited execution (compat. test) if ( (emu_opt.time_limited == true) && (frame >= TIME_LIMITE)) { quit = true; } // To avoid overflow of cycles_ that cause cpu to halt. cpu->setCycles(0); } // While (!quit) save_bbr(); #ifndef BUILT_IN_DEBUGGER cout << "Leaving emulation..." << endl; cout << "Total frames :" << dec << frame << endl; cout << "Rendered frames :" << dec << (frame - skipped_frame) << endl; cout << "Skipped frames :" << dec << skipped_frame<< endl; cout << "Emulation time :" << (float)(SDL_GetTicks()/1000.0) << " sec." << endl; cout << "Aver. frame rate:" << (float)((frame - skipped_frame)/(float)(SDL_GetTicks()/1000)) << " fps." << endl; #endif // We may be recording sound we leaving emulation. If it's // The case, close sound_shot file. if (sound_shot_toggle == true && emu_opt.sound == true) { wavW->close(); } SDL_CloseAudio(); SDL_Quit(); } /*--------------------------------------------------------------------*/ /* This method will save as bitmap, vdp graphics tiles. */ /* First, a 128x224 SDL_Surface is allocated. */ /* Then tiles are drawn there. */ /* A screenshot is taken */ /* The Surface is freed. */ /* Filename is tiles_rip_ + game_name.bmp. */ /*--------------------------------------------------------------------*/ void OsmoseCore::captureTiles(VDP *v) { int status; char sName[256]; unsigned short map_p = 0; SDL_Surface *tiles; // Allocate new software surface. tiles = SDL_CreateRGBSurface(SDL_SWSURFACE, 128,224,16,0xF800,0x7E0,0x1f,0x0); if (tiles == NULL) { cerr << "Couldn't get 128x224 surface: %s" << endl << SDL_GetError() << endl;; cerr << "Tiles are not saved." << endl << SDL_GetError() << endl;; } // Draw tiles there. for (int o=0; o<28;o++) for (int i=0; i<16;i++) { int tile = map_p; displayTiles(tiles, v, tile, i<<3, o<<3); map_p++; } SDL_UpdateRect(screen, 0, 0, 0, 0); // Save it ! #ifdef __USE_UNIX98 sprintf(sName,"%s/.osmose/tiles/gfx_%s(%d).bmp", oc->user_home_folder.c_str(),game_name.c_str(), tileshotNbr); #else sprintf(sName,"gfx_%s(%d).bmp", game_name.c_str(), tileshotNbr); #endif tileshotNbr++; SDL_LockSurface(tiles); status = SDL_SaveBMP(tiles, sName); SDL_UnlockSurface(screen); if(status == 0) { tw->addText("gfx have been saved.", 120); } else { tw->addText("fail to save gfx!", 120); } SDL_FreeSurface(tiles); } /*--------------------------------------------------------------------*/ /* This method draws a tile n, at position x,y, assuming that the */ /* Surface is 128 pixels wide. */ /*--------------------------------------------------------------------*/ void OsmoseCore::displayTiles(SDL_Surface *s, VDP *vd, int tile, int x, int y) { unsigned short *ptr; unsigned char col_index, p0, p1, p2, p3; unsigned int ti, c; ti = tile<<5; ptr = (unsigned short *)s->pixels + ((y<<7)+x ); for(int o=0; o<8;o++) { c = (o<<2) + ti; p0 = vd->VRAM[c++]; p1 = vd->VRAM[c++]; p2 = vd->VRAM[c++]; p3 = vd->VRAM[c++]; for (int i=0; i<8;i++) { col_index = (p0 >>7) | (p1 >> 7)<<1 | (p2 >> 7)<<2 | (p3 >> 7)<<3; *(unsigned short *)ptr = vd->colors[col_index]; ptr++; p0<<=1; p1<<=1; p2<<=1; p3<<=1; } ptr += 120; // Complement to next line, based on 256 pixel width. } } /*--------------------------------------------------------------------*/ /* This method is used to synchronise emulator at good FPS. Each */ /* DELAY_BETWEEN_FRAME milliseconds, the semaphore is freed. Then, the*/ /* main loop can continue it's execution. Note that DELAY_.. is 50ms. */ /* This is done to avoid problems due to timer granularity. Timer<20ms*/ /* aren't very accurate. So instead of drawing frame, waiting 16ms for*/ /* three times, we draw 3 frames, and wait 50ms. 50ms is large enough */ /* to get good synchronisation. */ /*--------------------------------------------------------------------*/ unsigned int timer_callback(unsigned int i, void *p) { #ifndef BUILT_IN_DEBUGGER SDL_SemPost(timer); #endif return DELAY_BETWEEN_FRAME; } /*--------------------------------------------------------------------*/ /* This method setup our default sound. */ /*--------------------------------------------------------------------*/ void OsmoseCore::setupAudioFormat() { format.freq = 22050; format.format = AUDIO_S16LSB; format.channels = 1; format.samples = SAMPLE_SIZE; format.callback = sndCallback; format.userdata = NULL; } /*--------------------------------------------------------------------*/ /* This method is called by SDL sound system, to fill the sound buffer*/ /* s is the place to put sound data, len is length of buffer in bytes.*/ /*--------------------------------------------------------------------*/ void sndCallback(void *ud, unsigned char *s, int len) { p->setWave(s, len); } /*--------------------------------------------------------------------*/ /* This method setup SDL video system. */ /*--------------------------------------------------------------------*/ void OsmoseCore::setupSDLVideo(VideoFilter *v) { /* Initialize SDL */ if ( SDL_Init(SDL_INIT_NOPARACHUTE | SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO) < 0 ) { cerr << "Couldn't initialize SDL: "<< endl << SDL_GetError() << endl; exit(1); } cout << "Active VideoFilter is: " << v->getFilterName() << endl; SDL_WM_SetCaption(__OSMOSE_VERSION__,__OSMOSE_VERSION__); /* Window title, Iconified widows title */ int w = v->getFinalOutputWidth(); int h = v->getFinalOutputHeight(); SDL_ShowCursor(SDL_DISABLE); if (emu_opt.fullscreen_flag == false) { if ( (screen=SDL_SetVideoMode(w, h,16, SDL_SWSURFACE)) == NULL ) { cerr << "Couldn't set video mode: %s" << endl << SDL_GetError() << endl;; exit(2); } } else { if ( (screen=SDL_SetVideoMode(w, h,16, SDL_FULLSCREEN)) == NULL ) { cerr << "Couldn't set video mode: %s" << endl << SDL_GetError() << endl;; exit(2); } SDL_ShowCursor(SDL_DISABLE); } // Allocate our 256*192 16bits buffer. buffer = SDL_CreateRGBSurface(SDL_SWSURFACE, 256,192,16,0xF800,0x7E0,0x1f,0x0); if (buffer == NULL) { cerr << "Couldn't get 256x192x16 surface: %s" << endl << SDL_GetError() << endl;; exit (1); } cout << "Video surfaces successfully allocated." << endl; } /*--------------------------------------------------------------------*/ /* This method setup SDL audio system. */ /*--------------------------------------------------------------------*/ void OsmoseCore::setupSDLAudio() { setupAudioFormat(); int r = SDL_OpenAudio(&format, NULL); if (r >= 0) { cout << "Audio device successfully opened." << endl; } else { cerr << "Couldn't open audio device:" << endl << SDL_GetError() ; cerr << "Activating -nosound option." << endl; cerr << "Disabling -snd_shot option." << endl; emu_opt.sound = false; } } /*--------------------------------------------------------------------*/ /* This method takes a screenshot of the game. The filename is */ /* game_name +x .bmp , where x is the number of taken screenshot, */ /* which is incremented every time captureScreen() is called. */ /*--------------------------------------------------------------------*/ void OsmoseCore::captureScreen() { int status; char sName[256]; #ifdef __USE_UNIX98 sprintf(sName,"%s/.osmose/screen/%s(%d).bmp", oc->user_home_folder.c_str(), game_name.c_str(), screenshotNbr); #else sprintf(sName,"%s(%d).bmp", game_name.c_str(), screenshotNbr); #endif screenshotNbr++; SDL_LockSurface(screen); status = SDL_SaveBMP(screen, sName); SDL_UnlockSurface(screen); if(status == 0) { tw->addText("screenshot saved.", 120); } else { tw->addText("fail to save screenshot!", 120); } } /*--------------------------------------------------------------------*/ /* This method generates save state file. Here is the format: */ /* File Offset - Data type. */ /* 0000-0003 unsigned char[4] marker "OESS" */ /* 0004-0007 unsigned char[4] 0 + version that create savestate. */ /* 0008-0019 unsigned char[18] z80_8bits_registers. */ /* 001A-0021 unsigned short[4] z80_16bits_registers. */ /*--------------------------------------------------------------------*/ bool OsmoseCore::saveSaveState(int slot) { bool ret = true; ofstream file("savestate.bin", ios::out | ios::binary | ios::ate); if (file.is_open() == false ) { cerr << "Unable to create save state." << endl; ret = false;; } // Write osmose marker. file << "OESS"; // Write osmose version. file << (unsigned char) 0 <<(unsigned char) MAJOR << (unsigned char) MIDDLE << (unsigned char) MINOR; // Write CPU 8 bits registers. file << (unsigned char)cpu->A << (unsigned char)cpu->B << (unsigned char)cpu->C << (unsigned char)cpu->D<< (unsigned char)cpu->E<< (unsigned char)cpu->F<< (unsigned char)cpu->H<< (unsigned char)cpu->L; file << (unsigned char)cpu->A1 << (unsigned char)cpu->B1 << (unsigned char)cpu->C1 << (unsigned char)cpu->D1<< (unsigned char)cpu->E1<< (unsigned char)cpu->F1<< (unsigned char)cpu->H1<< (unsigned char)cpu->L1; file << (unsigned char)cpu->I << (unsigned char)cpu->R; // Write CPU 16 bits registers. file << (unsigned short)cpu->PC << (unsigned short)cpu->IX;// << (unsigned short)cpu->IY<<(unsigned short)cpu->SP; file.close(); return ret; } /*--------------------------------------------------------------------*/ /* This method saves Battery Backed Memory if needed. */ /*--------------------------------------------------------------------*/ void OsmoseCore::save_bbr() { char full_name[256]; #ifdef __USE_UNIX98 sprintf(full_name,"%s/.osmose/bbr/%s.bbr",oc->user_home_folder.c_str(), game_name.c_str()); #else sprintf(full_name,"%s.bbr", game_name.c_str()); #endif mem->save_battery_backed_memory( string(full_name) ); }