338 lines
9.7 KiB
C++
338 lines
9.7 KiB
C++
|
#include "hqn_gui_controller.h"
|
||
|
#include "hqn_util.h"
|
||
|
#include <algorithm>
|
||
|
#include <cstring>
|
||
|
#include <SDL.h>
|
||
|
|
||
|
#define DEFAULT_WIDTH 256
|
||
|
#define DEFAULT_HEIGHT 240
|
||
|
|
||
|
namespace hqn
|
||
|
{
|
||
|
|
||
|
const char *DEFAULT_WINDOW_TITLE = "HeadlessQuickNES";
|
||
|
|
||
|
|
||
|
const SDL_Rect NES_BLIT_RECT = { 0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT };
|
||
|
|
||
|
// Determine the letterboxing required to display something on screen.
|
||
|
// The original code is basically magic.
|
||
|
// aspectW/H and windowW/H are the aspect ratio and window size, they are
|
||
|
// inputs. The view* parameters are the outputs and correspond to the
|
||
|
// calculated area in the window where the view should be.
|
||
|
void determineLetterbox(double aspectW, double aspectH, double windowW,
|
||
|
double windowH, int &viewX, int &viewY, int &viewW, int &viewH)
|
||
|
{
|
||
|
double scale = std::min(windowW / aspectW, windowH / aspectH);
|
||
|
viewW = (int)(aspectW * scale);
|
||
|
viewH = (int)(aspectH * scale);
|
||
|
viewX = (int)(windowW - viewW) / 2;
|
||
|
viewY = (int)(windowH - viewH) / 2;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
-- Returns: scale, scissorX, scissorY, scissorW, scissorH, offsetX, offsetY
|
||
|
function xl.calculateViewport(sizes, winW, winH, scaleInterval, screenFlex )
|
||
|
local gameW,gameH = sizes.w, sizes.h
|
||
|
local screenW,screenH = winW + screenFlex, winH + screenFlex
|
||
|
local scale = math.min(screenW / gameW, screenH / gameH)
|
||
|
scale = math.floor(scale * scaleInterval) / scaleInterval
|
||
|
local scissorW, scissorH = gameW * scale, gameH * scale
|
||
|
local scissorX, scissorY = (winW - scissorW) / 2, (winH - scissorH) / 2
|
||
|
local offsetX, offsetY = scissorX / scale, scissorY / scale
|
||
|
return scale, scissorX, scissorY, scissorW, scissorH, offsetX, offsetY
|
||
|
end
|
||
|
*/
|
||
|
|
||
|
GUIController::GUIController(HQNState &state)
|
||
|
:m_state(state)
|
||
|
{
|
||
|
m_tex = nullptr;
|
||
|
m_texOverlay = nullptr;
|
||
|
m_renderer = nullptr;
|
||
|
m_window = nullptr;
|
||
|
m_overlay = nullptr;
|
||
|
m_closeOp = CLOSE_QUIT;
|
||
|
m_isFullscreen = false;
|
||
|
}
|
||
|
|
||
|
GUIController::~GUIController()
|
||
|
{
|
||
|
if (m_tex)
|
||
|
SDL_DestroyTexture(m_tex);
|
||
|
if (m_texOverlay)
|
||
|
SDL_DestroyTexture(m_texOverlay);
|
||
|
if (m_renderer)
|
||
|
SDL_DestroyRenderer(m_renderer);
|
||
|
if (m_window)
|
||
|
SDL_DestroyWindow(m_window);
|
||
|
m_state.setListener(nullptr);
|
||
|
}
|
||
|
|
||
|
GUIController *GUIController::create(HQNState &state)
|
||
|
{
|
||
|
GUIController *self = new GUIController(state);
|
||
|
if (!self->init())
|
||
|
{
|
||
|
delete self;
|
||
|
return nullptr;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return self;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool GUIController::init()
|
||
|
{
|
||
|
// create the window
|
||
|
if (!(m_window = SDL_CreateWindow(DEFAULT_WINDOW_TITLE,
|
||
|
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, DEFAULT_WIDTH,
|
||
|
DEFAULT_HEIGHT, 0)))
|
||
|
return false;
|
||
|
if (!(m_renderer = SDL_CreateRenderer(m_window, -1, SDL_RENDERER_ACCELERATED)))
|
||
|
return false;
|
||
|
if (!(m_tex = SDL_CreateTexture(m_renderer, SDL_PIXELFORMAT_ARGB8888,
|
||
|
SDL_TEXTUREACCESS_STREAMING, 256, 256)))
|
||
|
return false;
|
||
|
// Set the clear color now rather than later
|
||
|
SDL_SetRenderDrawColor(m_renderer, 0, 0, 0, 255);
|
||
|
// Default the scale to 1
|
||
|
if (!setScale(1))
|
||
|
return false;
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
bool GUIController::setScale(int scale)
|
||
|
{
|
||
|
if (scale < 1 || scale > 5)
|
||
|
return false;
|
||
|
if (m_isFullscreen)
|
||
|
setFullscreen(false, false); // reset to windowed and don't bother changing the overlay
|
||
|
int winW = DEFAULT_WIDTH * scale;
|
||
|
int winH = DEFAULT_HEIGHT * scale;
|
||
|
|
||
|
// Change the window size
|
||
|
SDL_SetWindowSize(m_window, winW, winH);
|
||
|
|
||
|
if (!onResize(winW, winH, true))
|
||
|
return false;
|
||
|
|
||
|
// update the overlay destination
|
||
|
m_overlayDest = { 0, 0, winW, winH };
|
||
|
m_nesDest = { 0, 0, winW, winH };
|
||
|
|
||
|
// Update internal scale variable
|
||
|
m_scale = scale;
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
int GUIController::getScale() const
|
||
|
{ return m_scale; }
|
||
|
|
||
|
void GUIController::setFullscreen(bool full, bool adjustOverlay)
|
||
|
{
|
||
|
m_isFullscreen = full;
|
||
|
SDL_SetWindowFullscreen(m_window, full ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
|
||
|
int w, h;
|
||
|
SDL_GetWindowSize(m_window, &w, &h);
|
||
|
onResize((size_t)w, (size_t)h, adjustOverlay);
|
||
|
}
|
||
|
|
||
|
bool GUIController::isFullscreen() const
|
||
|
{
|
||
|
return m_isFullscreen;
|
||
|
}
|
||
|
|
||
|
bool GUIController::onResize(size_t w, size_t h, bool adjustOverlay)
|
||
|
{
|
||
|
// first calculate letterboxing
|
||
|
int viewX, viewY, viewW, viewH;
|
||
|
determineLetterbox(DEFAULT_WIDTH, DEFAULT_HEIGHT, w, h,
|
||
|
viewX, viewY, viewW, viewH);
|
||
|
// make sure images are put in the right places
|
||
|
m_overlayDest = { viewX, viewY, viewW, viewH };
|
||
|
m_nesDest = { viewX, viewY, viewW, viewH };
|
||
|
if (adjustOverlay && !resizeOverlay(viewW, viewH))
|
||
|
return false;
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
bool GUIController::resizeOverlay(size_t w, size_t h)
|
||
|
{
|
||
|
|
||
|
|
||
|
// destroy the overlay and corresponding texture
|
||
|
if (m_overlay)
|
||
|
{
|
||
|
// first check if the overlay is already the correct size
|
||
|
if (m_overlay->getWidth() == (int)w
|
||
|
&& m_overlay->getHeight() == (int)h)
|
||
|
return true;
|
||
|
else
|
||
|
delete m_overlay;
|
||
|
}
|
||
|
if (m_texOverlay)
|
||
|
SDL_DestroyTexture(m_texOverlay);
|
||
|
// Now re-create them
|
||
|
m_overlay = new Surface(w, h);
|
||
|
if (!(m_texOverlay = SDL_CreateTexture(m_renderer, SDL_PIXELFORMAT_ARGB8888,
|
||
|
SDL_TEXTUREACCESS_STREAMING, w, h)))
|
||
|
return false;
|
||
|
SDL_SetTextureBlendMode(m_texOverlay, SDL_BLENDMODE_BLEND);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
void GUIController::update(bool readNES)
|
||
|
{
|
||
|
void *nesPixels = nullptr;
|
||
|
int pitch = 0;
|
||
|
|
||
|
if (SDL_LockTexture(m_tex, nullptr, &nesPixels, &pitch) < 0) return;
|
||
|
|
||
|
m_state.blit((int32_t*)nesPixels, HQNState::NES_VIDEO_PALETTE, 0, 0, 0, 0);
|
||
|
SDL_UnlockTexture(m_tex);
|
||
|
|
||
|
// render to screen
|
||
|
SDL_RenderClear(m_renderer);
|
||
|
SDL_RenderCopy(m_renderer, m_tex, &NES_BLIT_RECT, &m_nesDest);
|
||
|
SDL_RenderPresent(m_renderer);
|
||
|
// Process any outstanding events
|
||
|
processEvents();
|
||
|
}
|
||
|
|
||
|
void GUIController::update_blit(int32_t* blit, SDL_Surface* base, SDL_Surface* button_a, SDL_Surface* button_b, SDL_Surface* button_select, SDL_Surface* button_start, SDL_Surface* button_up, SDL_Surface* button_down, SDL_Surface* button_left, SDL_Surface* button_right)
|
||
|
{
|
||
|
void *nesPixels = nullptr;
|
||
|
int pitch = 0;
|
||
|
|
||
|
if (SDL_LockTexture(m_tex, nullptr, &nesPixels, &pitch) < 0) return;
|
||
|
|
||
|
memcpy(nesPixels, blit, sizeof(int32_t) * BLIT_SIZE);
|
||
|
SDL_UnlockTexture(m_tex);
|
||
|
|
||
|
const SDL_Rect OVERLAY_BLIT_RECT_SRC = { 0, 0, 169, 53 };
|
||
|
const SDL_Rect OVERLAY_BLIT_RECT_DST = { 343, 425, 169, 53 };
|
||
|
|
||
|
// render to screen
|
||
|
SDL_RenderClear(m_renderer);
|
||
|
SDL_RenderCopy(m_renderer, m_tex, &NES_BLIT_RECT, &m_nesDest);
|
||
|
|
||
|
auto overlayTex = SDL_CreateTextureFromSurface(m_renderer, base);
|
||
|
SDL_RenderCopy(m_renderer, overlayTex, &OVERLAY_BLIT_RECT_SRC, &OVERLAY_BLIT_RECT_DST);
|
||
|
SDL_DestroyTexture(overlayTex);
|
||
|
|
||
|
if (button_a != NULL)
|
||
|
{
|
||
|
auto overlayTex = SDL_CreateTextureFromSurface(m_renderer, button_a);
|
||
|
SDL_RenderCopy(m_renderer, overlayTex, &OVERLAY_BLIT_RECT_SRC, &OVERLAY_BLIT_RECT_DST);
|
||
|
SDL_DestroyTexture(overlayTex);
|
||
|
}
|
||
|
|
||
|
if (button_b != NULL)
|
||
|
{
|
||
|
auto overlayTex = SDL_CreateTextureFromSurface(m_renderer, button_b);
|
||
|
SDL_RenderCopy(m_renderer, overlayTex, &OVERLAY_BLIT_RECT_SRC, &OVERLAY_BLIT_RECT_DST);
|
||
|
SDL_DestroyTexture(overlayTex);
|
||
|
}
|
||
|
|
||
|
if (button_select != NULL)
|
||
|
{
|
||
|
auto overlayTex = SDL_CreateTextureFromSurface(m_renderer, button_select);
|
||
|
SDL_RenderCopy(m_renderer, overlayTex, &OVERLAY_BLIT_RECT_SRC, &OVERLAY_BLIT_RECT_DST);
|
||
|
SDL_DestroyTexture(overlayTex);
|
||
|
}
|
||
|
|
||
|
if (button_start != NULL)
|
||
|
{
|
||
|
auto overlayTex = SDL_CreateTextureFromSurface(m_renderer, button_start);
|
||
|
SDL_RenderCopy(m_renderer, overlayTex, &OVERLAY_BLIT_RECT_SRC, &OVERLAY_BLIT_RECT_DST);
|
||
|
SDL_DestroyTexture(overlayTex);
|
||
|
}
|
||
|
|
||
|
if (button_up != NULL)
|
||
|
{
|
||
|
auto overlayTex = SDL_CreateTextureFromSurface(m_renderer, button_up);
|
||
|
SDL_RenderCopy(m_renderer, overlayTex, &OVERLAY_BLIT_RECT_SRC, &OVERLAY_BLIT_RECT_DST);
|
||
|
SDL_DestroyTexture(overlayTex);
|
||
|
}
|
||
|
|
||
|
if (button_down != NULL)
|
||
|
{
|
||
|
auto overlayTex = SDL_CreateTextureFromSurface(m_renderer, button_down);
|
||
|
SDL_RenderCopy(m_renderer, overlayTex, &OVERLAY_BLIT_RECT_SRC, &OVERLAY_BLIT_RECT_DST);
|
||
|
SDL_DestroyTexture(overlayTex);
|
||
|
}
|
||
|
|
||
|
if (button_left != NULL)
|
||
|
{
|
||
|
auto overlayTex = SDL_CreateTextureFromSurface(m_renderer, button_left);
|
||
|
SDL_RenderCopy(m_renderer, overlayTex, &OVERLAY_BLIT_RECT_SRC, &OVERLAY_BLIT_RECT_DST);
|
||
|
SDL_DestroyTexture(overlayTex);
|
||
|
}
|
||
|
|
||
|
if (button_right != NULL)
|
||
|
{
|
||
|
auto overlayTex = SDL_CreateTextureFromSurface(m_renderer, button_right);
|
||
|
SDL_RenderCopy(m_renderer, overlayTex, &OVERLAY_BLIT_RECT_SRC, &OVERLAY_BLIT_RECT_DST);
|
||
|
SDL_DestroyTexture(overlayTex);
|
||
|
}
|
||
|
|
||
|
SDL_RenderPresent(m_renderer);
|
||
|
|
||
|
// Process any outstanding events
|
||
|
processEvents();
|
||
|
}
|
||
|
|
||
|
void GUIController::onAdvanceFrame(HQNState *state)
|
||
|
{
|
||
|
update(true);
|
||
|
}
|
||
|
|
||
|
void GUIController::setTitle(const char *title)
|
||
|
{
|
||
|
SDL_SetWindowTitle(m_window, title);
|
||
|
}
|
||
|
|
||
|
void GUIController::setCloseOperation(CloseOperation closeop)
|
||
|
{
|
||
|
m_closeOp = closeop;
|
||
|
}
|
||
|
|
||
|
GUIController::CloseOperation GUIController::getCloseOperation() const
|
||
|
{
|
||
|
return m_closeOp;
|
||
|
}
|
||
|
|
||
|
void GUIController::processEvents()
|
||
|
{
|
||
|
bool quit = false;
|
||
|
SDL_Event event;
|
||
|
while (SDL_PollEvent(&event))
|
||
|
{
|
||
|
switch(event.type)
|
||
|
{
|
||
|
case SDL_QUIT:
|
||
|
quit = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (quit)
|
||
|
{
|
||
|
if (m_closeOp & CLOSE_QUIT)
|
||
|
{
|
||
|
exit(0);
|
||
|
}
|
||
|
if (m_closeOp & CLOSE_DELETE)
|
||
|
{
|
||
|
m_state.setListener(nullptr);
|
||
|
delete this;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void GUIController::onLoadROM(HQNState *state, const char *filename) {} // unimportant
|
||
|
void GUIController::onLoadState(HQNState *state) {} // also unimportant
|
||
|
|
||
|
}
|