Split event handling and "video" state to separate class.
Signed-off-by: mariofutire@gmail.com <pi@raspberrypi>
This commit is contained in:
parent
ddea5b139e
commit
aac2c5139a
4 changed files with 267 additions and 181 deletions
|
@ -3,6 +3,7 @@ include(FindPkgConfig)
|
||||||
add_executable(sa2
|
add_executable(sa2
|
||||||
main.cpp
|
main.cpp
|
||||||
bitmaps.cpp
|
bitmaps.cpp
|
||||||
|
emulator.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package(Boost REQUIRED
|
find_package(Boost REQUIRED
|
||||||
|
|
233
source/frontends/sa2/emulator.cpp
Normal file
233
source/frontends/sa2/emulator.cpp
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
#include "frontends/sa2/emulator.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "linux/data.h"
|
||||||
|
#include "linux/paddle.h"
|
||||||
|
|
||||||
|
#include "StdAfx.h"
|
||||||
|
#include "Common.h"
|
||||||
|
#include "CardManager.h"
|
||||||
|
#include "Applewin.h"
|
||||||
|
#include "Disk.h"
|
||||||
|
#include "CPU.h"
|
||||||
|
#include "Frame.h"
|
||||||
|
#include "Video.h"
|
||||||
|
#include "NTSC.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
SDL_Rect refreshTexture(const std::shared_ptr<SDL_Texture> & tex)
|
||||||
|
{
|
||||||
|
uint8_t * data;
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
int sx, sy;
|
||||||
|
int sw, sh;
|
||||||
|
|
||||||
|
getScreenData(data, width, height, sx, sy, sw, sh);
|
||||||
|
|
||||||
|
void * pixels;
|
||||||
|
int pitch;
|
||||||
|
SDL_LockTexture(tex.get(), nullptr, &pixels, &pitch);
|
||||||
|
|
||||||
|
memcpy(pixels, data, width * height * 4);
|
||||||
|
|
||||||
|
SDL_UnlockTexture(tex.get());
|
||||||
|
|
||||||
|
SDL_Rect srect;
|
||||||
|
srect.x = sx;
|
||||||
|
srect.y = sy;
|
||||||
|
srect.w = sw;
|
||||||
|
srect.h = sh;
|
||||||
|
|
||||||
|
return srect;
|
||||||
|
}
|
||||||
|
|
||||||
|
void renderScreen(const std::shared_ptr<SDL_Renderer> & ren, const std::shared_ptr<SDL_Texture> & tex, const SDL_Rect & srect)
|
||||||
|
{
|
||||||
|
SDL_RenderCopyEx(ren.get(), tex.get(), &srect, nullptr, 0.0, nullptr, SDL_FLIP_VERTICAL);
|
||||||
|
SDL_RenderPresent(ren.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
void cycleVideoType(const std::shared_ptr<SDL_Window> & win)
|
||||||
|
{
|
||||||
|
g_eVideoType++;
|
||||||
|
if (g_eVideoType >= NUM_VIDEO_MODES)
|
||||||
|
g_eVideoType = 0;
|
||||||
|
|
||||||
|
SetWindowTitle();
|
||||||
|
SDL_SetWindowTitle(win.get(), g_pAppTitle.c_str());
|
||||||
|
|
||||||
|
Config_Save_Video();
|
||||||
|
VideoReinitialize();
|
||||||
|
VideoRedrawScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
void cycle50ScanLines(const std::shared_ptr<SDL_Window> & win)
|
||||||
|
{
|
||||||
|
VideoStyle_e videoStyle = GetVideoStyle();
|
||||||
|
videoStyle = VideoStyle_e(videoStyle ^ VS_HALF_SCANLINES);
|
||||||
|
|
||||||
|
SetVideoStyle(videoStyle);
|
||||||
|
|
||||||
|
SetWindowTitle();
|
||||||
|
SDL_SetWindowTitle(win.get(), g_pAppTitle.c_str());
|
||||||
|
|
||||||
|
Config_Save_Video();
|
||||||
|
VideoReinitialize();
|
||||||
|
VideoRedrawScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
int getFPS()
|
||||||
|
{
|
||||||
|
SDL_DisplayMode current;
|
||||||
|
|
||||||
|
int should_be_zero = SDL_GetCurrentDisplayMode(0, ¤t);
|
||||||
|
|
||||||
|
if (should_be_zero)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return current.refresh_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Emulator::Emulator(
|
||||||
|
const std::shared_ptr<SDL_Window> & window,
|
||||||
|
const std::shared_ptr<SDL_Renderer> & renderer,
|
||||||
|
const std::shared_ptr<SDL_Texture> & texture
|
||||||
|
)
|
||||||
|
: myWindow(window)
|
||||||
|
, myRenderer(renderer)
|
||||||
|
, myTexture(texture)
|
||||||
|
, myFPS(getFPS())
|
||||||
|
, myMultiplier(1)
|
||||||
|
, myFullscreen(false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void Emulator::executeOneFrame()
|
||||||
|
{
|
||||||
|
const DWORD uCyclesToExecute = int(g_fCurrentCLK6502 / myFPS);
|
||||||
|
const UINT dwClksPerFrame = NTSC_GetCyclesPerFrame();
|
||||||
|
const bool bVideoUpdate = true;
|
||||||
|
const DWORD uActualCyclesExecuted = CpuExecute(uCyclesToExecute, bVideoUpdate);
|
||||||
|
g_dwCyclesThisFrame += uActualCyclesExecuted;
|
||||||
|
|
||||||
|
g_CardMgr.GetDisk2CardMgr().UpdateDriveState(uActualCyclesExecuted);
|
||||||
|
|
||||||
|
// SDL2 seems to synch with screen refresh rate so we do not need to worry about timers
|
||||||
|
const SDL_Rect srect = refreshTexture(myTexture);
|
||||||
|
renderScreen(myRenderer, myTexture, srect);
|
||||||
|
|
||||||
|
g_dwCyclesThisFrame = (g_dwCyclesThisFrame + g_dwCyclesThisFrame) % dwClksPerFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Emulator::processEvents(bool & quit)
|
||||||
|
{
|
||||||
|
SDL_Event e;
|
||||||
|
while (SDL_PollEvent(&e) != 0)
|
||||||
|
{
|
||||||
|
switch (e.type)
|
||||||
|
{
|
||||||
|
case SDL_QUIT:
|
||||||
|
{
|
||||||
|
quit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SDL_KEYDOWN:
|
||||||
|
{
|
||||||
|
processKeyDown(e, quit);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SDL_KEYUP:
|
||||||
|
{
|
||||||
|
processKeyUp(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Emulator::processKeyDown(const SDL_Event & e, bool & quit)
|
||||||
|
{
|
||||||
|
if (!e.key.repeat)
|
||||||
|
{
|
||||||
|
switch (e.key.keysym.scancode)
|
||||||
|
{
|
||||||
|
case SDL_SCANCODE_F9:
|
||||||
|
{
|
||||||
|
cycleVideoType(myWindow);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SDL_SCANCODE_F6:
|
||||||
|
{
|
||||||
|
if ((e.key.keysym.mod & KMOD_CTRL) && (e.key.keysym.mod & KMOD_SHIFT))
|
||||||
|
{
|
||||||
|
cycle50ScanLines(myWindow);
|
||||||
|
}
|
||||||
|
else if (e.key.keysym.mod & KMOD_CTRL)
|
||||||
|
{
|
||||||
|
myMultiplier = myMultiplier == 1 ? 2 : 1;
|
||||||
|
const int sw = GetFrameBufferBorderlessWidth();
|
||||||
|
const int sh = GetFrameBufferBorderlessHeight();
|
||||||
|
SDL_SetWindowSize(myWindow.get(), sw * myMultiplier, sh * myMultiplier);
|
||||||
|
}
|
||||||
|
else if (!(e.key.keysym.mod & KMOD_SHIFT))
|
||||||
|
{
|
||||||
|
myFullscreen = !myFullscreen;
|
||||||
|
SDL_SetWindowFullscreen(myWindow.get(), myFullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SDL_SCANCODE_F5:
|
||||||
|
{
|
||||||
|
if (g_CardMgr.QuerySlot(SLOT6) == CT_Disk2)
|
||||||
|
{
|
||||||
|
dynamic_cast<Disk2InterfaceCard*>(g_CardMgr.GetObj(SLOT6))->DriveSwap();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SDL_SCANCODE_F2:
|
||||||
|
{
|
||||||
|
quit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SDL_SCANCODE_LALT:
|
||||||
|
{
|
||||||
|
Paddle::setButtonPressed(Paddle::ourOpenApple);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SDL_SCANCODE_RALT:
|
||||||
|
{
|
||||||
|
Paddle::setButtonPressed(Paddle::ourSolidApple);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::cerr << "KEY DOWN: " << e.key.keysym.scancode << "," << e.key.keysym.sym << "," << e.key.keysym.mod << "," << bool(e.key.repeat) << std::endl;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Emulator::processKeyUp(const SDL_Event & e)
|
||||||
|
{
|
||||||
|
switch (e.key.keysym.scancode)
|
||||||
|
{
|
||||||
|
case SDL_SCANCODE_LALT:
|
||||||
|
{
|
||||||
|
Paddle::setButtonReleased(Paddle::ourOpenApple);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SDL_SCANCODE_RALT:
|
||||||
|
{
|
||||||
|
Paddle::setButtonReleased(Paddle::ourSolidApple);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::cerr << "KEY UP: " << e.key.keysym.scancode << "," << e.key.keysym.sym << "," << e.key.keysym.mod << "," << bool(e.key.repeat) << std::endl;
|
||||||
|
}
|
29
source/frontends/sa2/emulator.h
Normal file
29
source/frontends/sa2/emulator.h
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
class Emulator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Emulator(
|
||||||
|
const std::shared_ptr<SDL_Window> & window,
|
||||||
|
const std::shared_ptr<SDL_Renderer> & renderer,
|
||||||
|
const std::shared_ptr<SDL_Texture> & texture
|
||||||
|
);
|
||||||
|
|
||||||
|
void executeOneFrame();
|
||||||
|
void processEvents(bool & quit);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void processKeyDown(const SDL_Event & e, bool & quit);
|
||||||
|
void processKeyUp(const SDL_Event & e);
|
||||||
|
|
||||||
|
const std::shared_ptr<SDL_Window> myWindow;
|
||||||
|
const std::shared_ptr<SDL_Renderer> myRenderer;
|
||||||
|
const std::shared_ptr<SDL_Texture> myTexture;
|
||||||
|
const int myFPS;
|
||||||
|
|
||||||
|
int myMultiplier;
|
||||||
|
bool myFullscreen;
|
||||||
|
};
|
|
@ -10,6 +10,7 @@
|
||||||
#include "frontends/common2/configuration.h"
|
#include "frontends/common2/configuration.h"
|
||||||
#include "frontends/common2/utils.h"
|
#include "frontends/common2/utils.h"
|
||||||
#include "frontends/common2/programoptions.h"
|
#include "frontends/common2/programoptions.h"
|
||||||
|
#include "frontends/sa2/emulator.h"
|
||||||
|
|
||||||
#include "StdAfx.h"
|
#include "StdAfx.h"
|
||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
|
@ -106,82 +107,6 @@ namespace
|
||||||
LogDone();
|
LogDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_Rect refreshTexture(const std::shared_ptr<SDL_Texture> & tex)
|
|
||||||
{
|
|
||||||
uint8_t * data;
|
|
||||||
int width;
|
|
||||||
int height;
|
|
||||||
int sx, sy;
|
|
||||||
int sw, sh;
|
|
||||||
|
|
||||||
getScreenData(data, width, height, sx, sy, sw, sh);
|
|
||||||
|
|
||||||
void * pixels;
|
|
||||||
int pitch;
|
|
||||||
SDL_LockTexture(tex.get(), nullptr, &pixels, &pitch);
|
|
||||||
|
|
||||||
memcpy(pixels, data, width * height * 4);
|
|
||||||
|
|
||||||
SDL_UnlockTexture(tex.get());
|
|
||||||
|
|
||||||
SDL_Rect srect;
|
|
||||||
srect.x = sx;
|
|
||||||
srect.y = sy;
|
|
||||||
srect.w = sw;
|
|
||||||
srect.h = sh;
|
|
||||||
|
|
||||||
return srect;
|
|
||||||
}
|
|
||||||
|
|
||||||
void renderScreen(const std::shared_ptr<SDL_Renderer> & ren, const std::shared_ptr<SDL_Texture> & tex, const SDL_Rect & srect)
|
|
||||||
{
|
|
||||||
SDL_RenderCopyEx(ren.get(), tex.get(), &srect, nullptr, 0.0, nullptr, SDL_FLIP_VERTICAL);
|
|
||||||
SDL_RenderPresent(ren.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
void cycleVideoType(const std::shared_ptr<SDL_Window> & win)
|
|
||||||
{
|
|
||||||
g_eVideoType++;
|
|
||||||
if (g_eVideoType >= NUM_VIDEO_MODES)
|
|
||||||
g_eVideoType = 0;
|
|
||||||
|
|
||||||
SetWindowTitle();
|
|
||||||
SDL_SetWindowTitle(win.get(), g_pAppTitle.c_str());
|
|
||||||
|
|
||||||
Config_Save_Video();
|
|
||||||
VideoReinitialize();
|
|
||||||
VideoRedrawScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
void cycle50ScanLines(const std::shared_ptr<SDL_Window> & win)
|
|
||||||
{
|
|
||||||
VideoStyle_e videoStyle = GetVideoStyle();
|
|
||||||
videoStyle = VideoStyle_e(videoStyle ^ VS_HALF_SCANLINES);
|
|
||||||
|
|
||||||
SetVideoStyle(videoStyle);
|
|
||||||
|
|
||||||
SetWindowTitle();
|
|
||||||
SDL_SetWindowTitle(win.get(), g_pAppTitle.c_str());
|
|
||||||
|
|
||||||
Config_Save_Video();
|
|
||||||
VideoReinitialize();
|
|
||||||
VideoRedrawScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
int getFPS()
|
|
||||||
{
|
|
||||||
SDL_DisplayMode current;
|
|
||||||
|
|
||||||
int should_be_zero = SDL_GetCurrentDisplayMode(0, ¤t);
|
|
||||||
|
|
||||||
if (should_be_zero)
|
|
||||||
{
|
|
||||||
throw std::runtime_error(SDL_GetError());
|
|
||||||
}
|
|
||||||
|
|
||||||
return current.refresh_rate;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int MessageBox(HWND, const char * text, const char * caption, UINT type)
|
int MessageBox(HWND, const char * text, const char * caption, UINT type)
|
||||||
|
@ -248,9 +173,6 @@ void run_sdl(int argc, const char * argv [])
|
||||||
const int sw = GetFrameBufferBorderlessWidth();
|
const int sw = GetFrameBufferBorderlessWidth();
|
||||||
const int sh = GetFrameBufferBorderlessHeight();
|
const int sh = GetFrameBufferBorderlessHeight();
|
||||||
|
|
||||||
int multiplier = 1;
|
|
||||||
bool fullscreen = false;
|
|
||||||
|
|
||||||
std::shared_ptr<SDL_Window> win(SDL_CreateWindow(g_pAppTitle.c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, sw, sh, SDL_WINDOW_SHOWN), SDL_DestroyWindow);
|
std::shared_ptr<SDL_Window> win(SDL_CreateWindow(g_pAppTitle.c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, sw, sh, SDL_WINDOW_SHOWN), SDL_DestroyWindow);
|
||||||
if (!win)
|
if (!win)
|
||||||
{
|
{
|
||||||
|
@ -268,112 +190,13 @@ void run_sdl(int argc, const char * argv [])
|
||||||
const Uint32 format = SDL_PIXELFORMAT_BGRA32;
|
const Uint32 format = SDL_PIXELFORMAT_BGRA32;
|
||||||
std::shared_ptr<SDL_Texture> tex(SDL_CreateTexture(ren.get(), format, SDL_TEXTUREACCESS_STREAMING, width, height), SDL_DestroyTexture);
|
std::shared_ptr<SDL_Texture> tex(SDL_CreateTexture(ren.get(), format, SDL_TEXTUREACCESS_STREAMING, width, height), SDL_DestroyTexture);
|
||||||
|
|
||||||
const int fps = getFPS();
|
Emulator emulator(win, ren, tex);
|
||||||
const DWORD uCyclesToExecute = int(g_fCurrentCLK6502 / fps);
|
|
||||||
const UINT dwClksPerFrame = NTSC_GetCyclesPerFrame();
|
|
||||||
|
|
||||||
bool quit = false;
|
bool quit = false;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
SDL_Event e;
|
emulator.processEvents(quit);
|
||||||
while (SDL_PollEvent(&e) != 0)
|
emulator.executeOneFrame();
|
||||||
{
|
|
||||||
switch (e.type)
|
|
||||||
{
|
|
||||||
case SDL_QUIT:
|
|
||||||
{
|
|
||||||
quit = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SDL_KEYDOWN:
|
|
||||||
{
|
|
||||||
if (!e.key.repeat)
|
|
||||||
{
|
|
||||||
switch (e.key.keysym.scancode)
|
|
||||||
{
|
|
||||||
case SDL_SCANCODE_F9:
|
|
||||||
{
|
|
||||||
cycleVideoType(win);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SDL_SCANCODE_F6:
|
|
||||||
{
|
|
||||||
if ((e.key.keysym.mod & KMOD_CTRL) && (e.key.keysym.mod & KMOD_SHIFT))
|
|
||||||
{
|
|
||||||
cycle50ScanLines(win);
|
|
||||||
}
|
|
||||||
else if (e.key.keysym.mod & KMOD_CTRL)
|
|
||||||
{
|
|
||||||
multiplier = multiplier == 1 ? 2 : 1;
|
|
||||||
SDL_SetWindowSize(win.get(), sw * multiplier, sh * multiplier);
|
|
||||||
}
|
|
||||||
else if (!(e.key.keysym.mod & KMOD_SHIFT))
|
|
||||||
{
|
|
||||||
fullscreen = !fullscreen;
|
|
||||||
SDL_SetWindowFullscreen(win.get(), fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SDL_SCANCODE_F5:
|
|
||||||
{
|
|
||||||
if (g_CardMgr.QuerySlot(SLOT6) == CT_Disk2)
|
|
||||||
{
|
|
||||||
dynamic_cast<Disk2InterfaceCard*>(g_CardMgr.GetObj(SLOT6))->DriveSwap();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SDL_SCANCODE_F2:
|
|
||||||
{
|
|
||||||
quit = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SDL_SCANCODE_LALT:
|
|
||||||
{
|
|
||||||
Paddle::setButtonPressed(Paddle::ourOpenApple);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SDL_SCANCODE_RALT:
|
|
||||||
{
|
|
||||||
Paddle::setButtonPressed(Paddle::ourSolidApple);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::cerr << "KEY DOWN: " << e.key.keysym.scancode << "," << e.key.keysym.sym << "," << e.key.keysym.mod << "," << bool(e.key.repeat) << std::endl;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SDL_KEYUP:
|
|
||||||
{
|
|
||||||
switch (e.key.keysym.scancode)
|
|
||||||
{
|
|
||||||
case SDL_SCANCODE_LALT:
|
|
||||||
{
|
|
||||||
Paddle::setButtonReleased(Paddle::ourOpenApple);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SDL_SCANCODE_RALT:
|
|
||||||
{
|
|
||||||
Paddle::setButtonReleased(Paddle::ourSolidApple);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::cerr << "KEY UP: " << e.key.keysym.scancode << "," << e.key.keysym.sym << "," << e.key.keysym.mod << "," << bool(e.key.repeat) << std::endl;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool bVideoUpdate = true;
|
|
||||||
const DWORD uActualCyclesExecuted = CpuExecute(uCyclesToExecute, bVideoUpdate);
|
|
||||||
g_dwCyclesThisFrame += uActualCyclesExecuted;
|
|
||||||
|
|
||||||
g_CardMgr.GetDisk2CardMgr().UpdateDriveState(uActualCyclesExecuted);
|
|
||||||
|
|
||||||
// SDL2 seems to synch with screen refresh rate so we do not need to worry about timers
|
|
||||||
const SDL_Rect srect = refreshTexture(tex);
|
|
||||||
renderScreen(ren, tex, srect);
|
|
||||||
|
|
||||||
g_dwCyclesThisFrame = (g_dwCyclesThisFrame + g_dwCyclesThisFrame) % dwClksPerFrame;
|
|
||||||
} while (!quit);
|
} while (!quit);
|
||||||
|
|
||||||
stopEmulator();
|
stopEmulator();
|
||||||
|
|
Loading…
Add table
Reference in a new issue