AppleWin/source/Video.cpp

1248 lines
39 KiB
C++

/*
AppleWin : An Apple //e emulator for Windows
Copyright (C) 1994-1996, Michael O'Brien
Copyright (C) 1999-2001, Oliver Schmidt
Copyright (C) 2002-2005, Tom Charlesworth
Copyright (C) 2006-2010, Tom Charlesworth, Michael Pohoreski, Nick Westgate
AppleWin is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
AppleWin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with AppleWin; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/* Description: Emulation of video modes
*
* Author: Various
*/
#include "StdAfx.h"
#include "Applewin.h"
#include "CPU.h"
#include "Frame.h"
#include "Keyboard.h"
#include "Memory.h"
#include "Registry.h"
#include "Video.h"
#include "NTSC.h"
#include "../resource/resource.h"
#include "Configuration/PropertySheet.h"
#include "YamlHelper.h"
#define SW_80COL (g_uVideoMode & VF_80COL)
#define SW_DHIRES (g_uVideoMode & VF_DHIRES)
#define SW_HIRES (g_uVideoMode & VF_HIRES)
#define SW_80STORE (g_uVideoMode & VF_80STORE)
#define SW_MIXED (g_uVideoMode & VF_MIXED)
#define SW_PAGE2 (g_uVideoMode & VF_PAGE2)
#define SW_TEXT (g_uVideoMode & VF_TEXT)
// Globals (Public)
uint8_t *g_pFramebufferbits = NULL; // last drawn frame
int g_nAltCharSetOffset = 0; // alternate character set
// Globals (Private)
// video scanner constants
int const kHBurstClock = 53; // clock when Color Burst starts
int const kHBurstClocks = 4; // clocks per Color Burst duration
int const kHClock0State = 0x18; // H[543210] = 011000
int const kHClocks = 65; // clocks per horizontal scan (including HBL)
int const kHPEClock = 40; // clock when HPE (horizontal preset enable) goes low
int const kHPresetClock = 41; // clock when H state presets
int const kHSyncClock = 49; // clock when HSync starts
int const kHSyncClocks = 4; // clocks per HSync duration
int const kNTSCScanLines = 262; // total scan lines including VBL (NTSC)
int const kNTSCVSyncLine = 224; // line when VSync starts (NTSC)
int const kPALScanLines = 312; // total scan lines including VBL (PAL)
int const kPALVSyncLine = 264; // line when VSync starts (PAL)
int const kVLine0State = 0x100; // V[543210CBA] = 100000000
int const kVPresetLine = 256; // line when V state presets
int const kVSyncLines = 4; // lines per VSync duration
int const kVDisplayableScanLines = 192; // max displayable scanlines
static COLORREF customcolors[256]; // MONOCHROME is last custom color
static HBITMAP g_hDeviceBitmap;
static HDC g_hDeviceDC;
static LPBITMAPINFO g_pFramebufferinfo = NULL;
HBITMAP g_hLogoBitmap;
const int MAX_SOURCE_Y = 512;
static LPBYTE g_aSourceStartofLine[ MAX_SOURCE_Y ];
COLORREF g_nMonochromeRGB = RGB(0xC0,0xC0,0xC0);
uint32_t g_uVideoMode = VF_TEXT; // Current Video Mode (this is the last set one as it may change mid-scan line!)
DWORD g_eVideoType = VT_COLOR_TV;
DWORD g_uHalfScanLines = 1; // drop 50% scan lines for a more authentic look
static const bool bVideoScannerNTSC = true; // NTSC video scanning (or PAL)
//-------------------------------------
// NOTE: KEEP IN SYNC: VideoType_e g_aVideoChoices g_apVideoModeDesc
TCHAR g_aVideoChoices[] =
TEXT("Monochrome (Custom)\0")
TEXT("Color Monitor\0")
TEXT("B&W TV\0")
TEXT("Color TV\0")
TEXT("Monochrome (Amber)\0")
TEXT("Monochrome (Green)\0")
TEXT("Monochrome (White)\0")
;
// NOTE: KEEP IN SYNC: VideoType_e g_aVideoChoices g_apVideoModeDesc
// The window title will be set to this.
char *g_apVideoModeDesc[ NUM_VIDEO_MODES ] =
{
"Monochrome Monitor (Custom)"
, "Color Monitor"
, "B&W TV"
, "Color TV"
, "Amber Monitor"
, "Green Monitor"
, "White Monitor"
};
// Prototypes (Private) _____________________________________________
bool g_bDisplayPrintScreenFileName = false;
bool g_bShowPrintScreenWarningDialog = true;
void Util_MakeScreenShotFileName( char *pFinalFileName_ );
bool Util_TestScreenShotFileName( const char *pFileName );
void Video_SaveScreenShot( const char *pScreenShotFileName, const VideoScreenShot_e ScreenShotType );
void Video_MakeScreenShot( FILE *pFile, const VideoScreenShot_e ScreenShotType );
void videoCreateDIBSection();
//===========================================================================
void VideoInitialize ()
{
// RESET THE VIDEO MODE SWITCHES AND THE CHARACTER SET OFFSET
VideoResetState();
// LOAD THE LOGO
g_hLogoBitmap = LoadBitmap( g_hInstance, MAKEINTRESOURCE(IDB_APPLEWIN) );
// CREATE A BITMAPINFO STRUCTURE FOR THE FRAME BUFFER
g_pFramebufferinfo = (LPBITMAPINFO)VirtualAlloc(
NULL,
sizeof(BITMAPINFOHEADER) + 256*sizeof(RGBQUAD),
MEM_COMMIT,
PAGE_READWRITE);
ZeroMemory(g_pFramebufferinfo,sizeof(BITMAPINFOHEADER)+256*sizeof(RGBQUAD));
g_pFramebufferinfo->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
g_pFramebufferinfo->bmiHeader.biWidth = GetFrameBufferWidth();
g_pFramebufferinfo->bmiHeader.biHeight = GetFrameBufferHeight();
g_pFramebufferinfo->bmiHeader.biPlanes = 1;
g_pFramebufferinfo->bmiHeader.biBitCount = 32;
g_pFramebufferinfo->bmiHeader.biCompression = BI_RGB;
g_pFramebufferinfo->bmiHeader.biClrUsed = 0;
videoCreateDIBSection();
}
//===========================================================================
//
// ----- ALL GLOBALLY ACCESSIBLE FUNCTIONS ARE BELOW THIS LINE -----
//
//===========================================================================
void VideoBenchmark () {
Sleep(500);
// PREPARE TWO DIFFERENT FRAME BUFFERS, EACH OF WHICH HAVE HALF OF THE
// BYTES SET TO 0x14 AND THE OTHER HALF SET TO 0xAA
int loop;
LPDWORD mem32 = (LPDWORD)mem;
for (loop = 4096; loop < 6144; loop++)
*(mem32+loop) = ((loop & 1) ^ ((loop & 0x40) >> 6)) ? 0x14141414
: 0xAAAAAAAA;
for (loop = 6144; loop < 8192; loop++)
*(mem32+loop) = ((loop & 1) ^ ((loop & 0x40) >> 6)) ? 0xAAAAAAAA
: 0x14141414;
// SEE HOW MANY TEXT FRAMES PER SECOND WE CAN PRODUCE WITH NOTHING ELSE
// GOING ON, CHANGING HALF OF THE BYTES IN THE VIDEO BUFFER EACH FRAME TO
// SIMULATE THE ACTIVITY OF AN AVERAGE GAME
DWORD totaltextfps = 0;
g_uVideoMode = VF_TEXT;
FillMemory(mem+0x400,0x400,0x14);
VideoRedrawScreen();
DWORD milliseconds = GetTickCount();
while (GetTickCount() == milliseconds) ;
milliseconds = GetTickCount();
DWORD cycle = 0;
do {
if (cycle & 1)
FillMemory(mem+0x400,0x400,0x14);
else
CopyMemory(mem+0x400,mem+((cycle & 2) ? 0x4000 : 0x6000),0x400);
VideoRefreshScreen();
if (cycle++ >= 3)
cycle = 0;
totaltextfps++;
} while (GetTickCount() - milliseconds < 1000);
// SEE HOW MANY HIRES FRAMES PER SECOND WE CAN PRODUCE WITH NOTHING ELSE
// GOING ON, CHANGING HALF OF THE BYTES IN THE VIDEO BUFFER EACH FRAME TO
// SIMULATE THE ACTIVITY OF AN AVERAGE GAME
DWORD totalhiresfps = 0;
g_uVideoMode = VF_HIRES;
FillMemory(mem+0x2000,0x2000,0x14);
VideoRedrawScreen();
milliseconds = GetTickCount();
while (GetTickCount() == milliseconds) ;
milliseconds = GetTickCount();
cycle = 0;
do {
if (cycle & 1)
FillMemory(mem+0x2000,0x2000,0x14);
else
CopyMemory(mem+0x2000,mem+((cycle & 2) ? 0x4000 : 0x6000),0x2000);
VideoRefreshScreen();
if (cycle++ >= 3)
cycle = 0;
totalhiresfps++;
} while (GetTickCount() - milliseconds < 1000);
// DETERMINE HOW MANY 65C02 CLOCK CYCLES WE CAN EMULATE PER SECOND WITH
// NOTHING ELSE GOING ON
CpuSetupBenchmark();
DWORD totalmhz10 = 0;
milliseconds = GetTickCount();
while (GetTickCount() == milliseconds) ;
milliseconds = GetTickCount();
cycle = 0;
do {
CpuExecute(100000, true);
totalmhz10++;
} while (GetTickCount() - milliseconds < 1000);
// IF THE PROGRAM COUNTER IS NOT IN THE EXPECTED RANGE AT THE END OF THE
// CPU BENCHMARK, REPORT AN ERROR AND OPTIONALLY TRACK IT DOWN
if ((regs.pc < 0x300) || (regs.pc > 0x400))
if (MessageBox(g_hFrameWindow,
TEXT("The emulator has detected a problem while running ")
TEXT("the CPU benchmark. Would you like to gather more ")
TEXT("information?"),
TEXT("Benchmarks"),
MB_ICONQUESTION | MB_YESNO | MB_SETFOREGROUND) == IDYES) {
BOOL error = 0;
WORD lastpc = 0x300;
int loop = 0;
while ((loop < 10000) && !error) {
CpuSetupBenchmark();
CpuExecute(loop, true);
if ((regs.pc < 0x300) || (regs.pc > 0x400))
error = 1;
else {
lastpc = regs.pc;
++loop;
}
}
if (error) {
TCHAR outstr[256];
wsprintf(outstr,
TEXT("The emulator experienced an error %u clock cycles ")
TEXT("into the CPU benchmark. Prior to the error, the ")
TEXT("program counter was at $%04X. After the error, it ")
TEXT("had jumped to $%04X."),
(unsigned)loop,
(unsigned)lastpc,
(unsigned)regs.pc);
MessageBox(g_hFrameWindow,
outstr,
TEXT("Benchmarks"),
MB_ICONINFORMATION | MB_SETFOREGROUND);
}
else
MessageBox(g_hFrameWindow,
TEXT("The emulator was unable to locate the exact ")
TEXT("point of the error. This probably means that ")
TEXT("the problem is external to the emulator, ")
TEXT("happening asynchronously, such as a problem in ")
TEXT("a timer interrupt handler."),
TEXT("Benchmarks"),
MB_ICONINFORMATION | MB_SETFOREGROUND);
}
// DO A REALISTIC TEST OF HOW MANY FRAMES PER SECOND WE CAN PRODUCE
// WITH FULL EMULATION OF THE CPU, JOYSTICK, AND DISK HAPPENING AT
// THE SAME TIME
DWORD realisticfps = 0;
FillMemory(mem+0x2000,0x2000,0xAA);
VideoRedrawScreen();
milliseconds = GetTickCount();
while (GetTickCount() == milliseconds) ;
milliseconds = GetTickCount();
cycle = 0;
do {
if (realisticfps < 10) {
int cycles = 100000;
while (cycles > 0) {
DWORD executedcycles = CpuExecute(103, true);
cycles -= executedcycles;
DiskUpdateDriveState(executedcycles);
JoyUpdateButtonLatch(executedcycles);
}
}
if (cycle & 1)
FillMemory(mem+0x2000,0x2000,0xAA);
else
CopyMemory(mem+0x2000,mem+((cycle & 2) ? 0x4000 : 0x6000),0x2000);
VideoRedrawScreen();
if (cycle++ >= 3)
cycle = 0;
realisticfps++;
} while (GetTickCount() - milliseconds < 1000);
// DISPLAY THE RESULTS
VideoDisplayLogo();
TCHAR outstr[256];
wsprintf(outstr,
TEXT("Pure Video FPS:\t%u hires, %u text\n")
TEXT("Pure CPU MHz:\t%u.%u%s\n\n")
TEXT("EXPECTED AVERAGE VIDEO GAME\n")
TEXT("PERFORMANCE: %u FPS"),
(unsigned)totalhiresfps,
(unsigned)totaltextfps,
(unsigned)(totalmhz10/10),
(unsigned)(totalmhz10 % 10),
(LPCTSTR)(IS_APPLE2 ? TEXT(" (6502)") : TEXT("")),
(unsigned)realisticfps);
MessageBox(g_hFrameWindow,
outstr,
TEXT("Benchmarks"),
MB_ICONINFORMATION | MB_SETFOREGROUND);
}
//===========================================================================
BYTE VideoCheckMode (WORD, WORD address, BYTE, BYTE, ULONG uExecutedCycles)
{
address &= 0xFF;
if (address == 0x7F)
return MemReadFloatingBus(SW_DHIRES != 0, uExecutedCycles);
else {
BOOL result = 0;
switch (address) {
case 0x1A: result = SW_TEXT; break;
case 0x1B: result = SW_MIXED; break;
case 0x1D: result = SW_HIRES; break;
case 0x1E: result = g_nAltCharSetOffset; break;
case 0x1F: result = SW_80COL; break;
case 0x7F: result = SW_DHIRES; break;
}
return KeybGetKeycode() | (result ? 0x80 : 0);
}
}
//===========================================================================
BYTE VideoCheckVbl ( ULONG uExecutedCycles )
{
bool bVblBar = VideoGetVblBar(uExecutedCycles);
BYTE r = KeybGetKeycode();
return (r & ~0x80) | (bVblBar ? 0x80 : 0);
}
// This is called from PageConfig
//===========================================================================
void VideoChooseMonochromeColor ()
{
CHOOSECOLOR cc;
ZeroMemory(&cc,sizeof(CHOOSECOLOR));
cc.lStructSize = sizeof(CHOOSECOLOR);
cc.hwndOwner = g_hFrameWindow;
cc.rgbResult = g_nMonochromeRGB;
cc.lpCustColors = customcolors + 1;
cc.Flags = CC_RGBINIT | CC_SOLIDCOLOR;
if (ChooseColor(&cc))
{
g_nMonochromeRGB = cc.rgbResult;
VideoReinitialize();
if ((g_nAppMode != MODE_LOGO) && (g_nAppMode != MODE_DEBUG))
{
VideoRedrawScreen();
}
Config_Save_Video();
}
}
//===========================================================================
void VideoDestroy () {
// DESTROY BUFFERS
VirtualFree(g_pFramebufferinfo,0,MEM_RELEASE);
g_pFramebufferinfo = NULL;
// DESTROY FRAME BUFFER
DeleteDC(g_hDeviceDC);
DeleteObject(g_hDeviceBitmap);
g_hDeviceDC = (HDC)0;
g_hDeviceBitmap = (HBITMAP)0;
// DESTROY LOGO
if (g_hLogoBitmap) {
DeleteObject(g_hLogoBitmap);
g_hLogoBitmap = (HBITMAP)0;
}
}
//===========================================================================
static void VideoDrawLogoBitmap(HDC hDstDC, int xoff, int yoff, int srcw, int srch, int scale)
{
HDC hSrcDC = CreateCompatibleDC( hDstDC );
SelectObject( hSrcDC, g_hLogoBitmap );
StretchBlt(
hDstDC, // hdcDest
xoff, yoff, // nXDest, nYDest
scale * srcw, scale * srch, // nWidth, nHeight
hSrcDC, // hdcSrc
0, 0, // nXSrc, nYSrc
srcw, srch,
SRCCOPY // dwRop
);
DeleteObject( hSrcDC );
}
//===========================================================================
void VideoDisplayLogo ()
{
int nLogoX = 0, nLogoY = 0;
int scale = GetViewportScale();
HDC hFrameDC = FrameGetDC();
// DRAW THE LOGO
SelectObject(hFrameDC, GetStockObject(NULL_PEN));
if (g_hLogoBitmap)
{
BITMAP bm;
if (GetObject(g_hLogoBitmap, sizeof(bm), &bm))
{
nLogoX = (g_nViewportCX - scale*bm.bmWidth )/2;
nLogoY = (g_nViewportCY - scale*bm.bmHeight)/2;
if( IsFullScreen() )
{
nLogoX += GetFullScreenOffsetX();
nLogoY += GetFullScreenOffsetY();
}
VideoDrawLogoBitmap( hFrameDC, nLogoX, nLogoY, bm.bmWidth, bm.bmHeight, scale );
}
}
// DRAW THE VERSION NUMBER
TCHAR sFontName[] = TEXT("Arial");
HFONT font = CreateFont(-20,0,0,0,FW_NORMAL,0,0,0,ANSI_CHARSET,
OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,
VARIABLE_PITCH | 4 | FF_SWISS,
sFontName );
SelectObject(hFrameDC,font);
SetTextAlign(hFrameDC,TA_RIGHT | TA_TOP);
SetBkMode(hFrameDC,TRANSPARENT);
char szVersion[ 64 ] = "";
sprintf( szVersion, "Version %s", VERSIONSTRING );
int xoff = GetFullScreenOffsetX(), yoff = GetFullScreenOffsetY();
#define DRAWVERSION(x,y,c) \
SetTextColor(hFrameDC,c); \
TextOut(hFrameDC, \
scale*540+x+xoff,scale*358+y+yoff, \
szVersion, \
strlen(szVersion));
if (GetDeviceCaps(hFrameDC,PLANES) * GetDeviceCaps(hFrameDC,BITSPIXEL) <= 4) {
DRAWVERSION( 2, 2,RGB(0x00,0x00,0x00));
DRAWVERSION( 1, 1,RGB(0x00,0x00,0x00));
DRAWVERSION( 0, 0,RGB(0xFF,0x00,0xFF));
} else {
DRAWVERSION( 1, 1,PALETTERGB(0x30,0x30,0x70));
DRAWVERSION(-1,-1,PALETTERGB(0xC0,0x70,0xE0));
DRAWVERSION( 0, 0,PALETTERGB(0x70,0x30,0xE0));
}
#if _DEBUG
sprintf( szVersion, "DEBUG" );
DRAWVERSION( 2, -358*scale,RGB(0x00,0x00,0x00));
DRAWVERSION( 1, -357*scale,RGB(0x00,0x00,0x00));
DRAWVERSION( 0, -356*scale,RGB(0xFF,0x00,0xFF));
#endif
#undef DRAWVERSION
DeleteObject(font);
}
//===========================================================================
void VideoRedrawScreenDuringFullSpeed(DWORD dwCyclesThisFrame, bool bInit /*=false*/)
{
static DWORD dwFullSpeedStartTime = 0;
// static bool bValid = false;
if (bInit)
{
// Just entered full-speed mode
// bValid = false;
dwFullSpeedStartTime = GetTickCount();
return;
}
DWORD dwFullSpeedDuration = GetTickCount() - dwFullSpeedStartTime;
if (dwFullSpeedDuration <= 16) // Only update after every realtime ~17ms of *continuous* full-speed
return;
dwFullSpeedStartTime += dwFullSpeedDuration;
//
#if 0
static BYTE text_main[1024*2] = {0}; // page1 & 2
static BYTE text_aux[1024*2] = {0}; // page1 & 2
static BYTE hgr_main[8192*2] = {0}; // page1 & 2
static BYTE hgr_aux[8192*2] = {0}; // page1 & 2
bool bRedraw = true; // Always redraw for bValid==false (ie. just entered full-speed mode)
if (bValid)
{
if ((g_uVideoMode&(VF_DHIRES|VF_HIRES|VF_TEXT|VF_MIXED)) == VF_HIRES)
{
// HIRES (not MIXED) - eg. AZTEC.DSK
if ((g_uVideoMode&VF_PAGE2) == 0)
bRedraw = memcmp(&hgr_main[0x0000], MemGetMainPtr(0x2000), 8192) != 0;
else
bRedraw = memcmp(&hgr_main[0x2000], MemGetMainPtr(0x4000), 8192) != 0;
}
else
{
bRedraw =
(memcmp(text_main, MemGetMainPtr(0x400), sizeof(text_main)) != 0) ||
(memcmp(text_aux, MemGetAuxPtr(0x400), sizeof(text_aux)) != 0) ||
(memcmp(hgr_main, MemGetMainPtr(0x2000), sizeof(hgr_main)) != 0) ||
(memcmp(hgr_aux, MemGetAuxPtr(0x2000), sizeof(hgr_aux)) != 0);
}
}
if (bRedraw)
VideoRedrawScreenAfterFullSpeed(dwCyclesThisFrame);
// Copy all video memory (+ screen holes)
memcpy(text_main, MemGetMainPtr(0x400), sizeof(text_main));
memcpy(text_aux, MemGetAuxPtr(0x400), sizeof(text_aux));
memcpy(hgr_main, MemGetMainPtr(0x2000), sizeof(hgr_main));
memcpy(hgr_aux, MemGetAuxPtr(0x2000), sizeof(hgr_aux));
bValid = true;
#else
VideoRedrawScreenAfterFullSpeed(dwCyclesThisFrame);
#endif
}
//===========================================================================
void VideoRedrawScreenAfterFullSpeed(DWORD dwCyclesThisFrame)
{
if (bVideoScannerNTSC)
{
NTSC_VideoClockResync(dwCyclesThisFrame);
}
else // PAL
{
_ASSERT(0);
g_nVideoClockVert = (uint16_t) (dwCyclesThisFrame / kHClocks) % kPALScanLines;
g_nVideoClockHorz = (uint16_t) (dwCyclesThisFrame % kHClocks);
}
VideoRedrawScreen(); // Better (no flicker) than using: NTSC_VideoReinitialize() or VideoReinitialize()
}
//===========================================================================
void VideoRedrawScreen (void)
{
// NB. Can't rely on g_uVideoMode being non-zero (ie. so it can double up as a flag) since 'GR,PAGE1,non-mixed' mode == 0x00.
VideoRefreshScreen( g_uVideoMode, true );
}
//===========================================================================
// NB. Can get "big" 1000+ms times: these occur during disk loading when the emulator is at full-speed.
//#define DEBUG_REFRESH_TIMINGS
#if defined(_DEBUG) && defined(DEBUG_REFRESH_TIMINGS)
static void DebugRefresh(char uDebugFlag)
{
static DWORD uLastRefreshTime = 0;
const DWORD dwEmuTime_ms = CpuGetEmulationTime_ms();
const DWORD uTimeBetweenRefreshes = uLastRefreshTime ? dwEmuTime_ms - uLastRefreshTime : 0;
uLastRefreshTime = dwEmuTime_ms;
if (!uTimeBetweenRefreshes)
return; // 1st time in func
char szStr[100];
sprintf(szStr, "Time between refreshes = %d ms %c\n", uTimeBetweenRefreshes, (uDebugFlag==0)?' ':uDebugFlag);
OutputDebugString(szStr);
}
#endif
// TC: Hacky-fix for GH#341 - better to draw to the correct position in the framebuffer to start with! (in NTSC.cpp)
static void VideoFrameBufferAdjust(int& xSrc, int& ySrc, bool bInvertY=false)
{
int dx=0, dy=0;
if (g_eVideoType == VT_MONO_TV || g_eVideoType == VT_COLOR_TV)
{
// Adjust the src locations for the NTSC video modes
dx = 2;
dy = -1;
}
else if (g_eVideoType == VT_COLOR_MONITOR)
{
//if ((g_uVideoMode & VF_TEXT) == 0) // NB. Not sufficient, eg. ANSI STORY...
if ( NTSC_GetColorBurst() == true ) // ANSI STORY (end credits): split DGR/TEXT80/DGR on scanline
dx = 2;
}
if (bInvertY)
dy =- dy;
xSrc += dx;
ySrc += dy;
}
void VideoRefreshScreen ( uint32_t uRedrawWholeScreenVideoMode /* =0*/, bool bRedrawWholeScreen /* =false*/ )
{
#if defined(_DEBUG) && defined(DEBUG_REFRESH_TIMINGS)
DebugRefresh(0);
#endif
if (bRedrawWholeScreen || g_nAppMode == MODE_PAUSED)
{
// uVideoModeForWholeScreen set if:
// . MODE_DEBUG : always
// . MODE_RUNNING : called from VideoRedrawScreen(), eg. during full-speed
if (bRedrawWholeScreen)
NTSC_SetVideoMode( uRedrawWholeScreenVideoMode );
NTSC_VideoRedrawWholeScreen();
}
HDC hFrameDC = FrameGetDC();
if (hFrameDC)
{
int xSrc = GetFrameBufferBorderWidth();
int ySrc = GetFrameBufferBorderHeight();
VideoFrameBufferAdjust(xSrc, ySrc); // TC: Hacky-fix for GH#341
int xdest = IsFullScreen() ? GetFullScreenOffsetX() : 0;
int ydest = IsFullScreen() ? GetFullScreenOffsetY() : 0;
int wdest = g_nViewportCX;
int hdest = g_nViewportCY;
SetStretchBltMode(hFrameDC, COLORONCOLOR);
StretchBlt(
hFrameDC,
xdest, ydest,
wdest, hdest,
g_hDeviceDC,
xSrc, ySrc,
GetFrameBufferBorderlessWidth(), GetFrameBufferBorderlessHeight(),
SRCCOPY);
}
GdiFlush();
}
//===========================================================================
void VideoReinitialize ()
{
NTSC_VideoReinitialize( g_dwCyclesThisFrame );
NTSC_VideoInitAppleType();
NTSC_SetVideoStyle();
NTSC_SetVideoTextMode( g_uVideoMode & VF_80COL ? 80 : 40 );
NTSC_SetVideoMode( g_uVideoMode ); // Pre-condition: g_nVideoClockHorz (derived from g_dwCyclesThisFrame)
}
//===========================================================================
void VideoResetState ()
{
g_nAltCharSetOffset = 0;
g_uVideoMode = VF_TEXT;
NTSC_SetVideoTextMode( 40 );
NTSC_SetVideoMode( g_uVideoMode );
}
//===========================================================================
BYTE VideoSetMode (WORD, WORD address, BYTE write, BYTE, ULONG uExecutedCycles)
{
address &= 0xFF;
switch (address)
{
case 0x00: g_uVideoMode &= ~VF_80STORE; break;
case 0x01: g_uVideoMode |= VF_80STORE; break;
case 0x0C: if (!IS_APPLE2){g_uVideoMode &= ~VF_80COL; NTSC_SetVideoTextMode(40);}; break;
case 0x0D: if (!IS_APPLE2){g_uVideoMode |= VF_80COL; NTSC_SetVideoTextMode(80);}; break;
case 0x0E: if (!IS_APPLE2) g_nAltCharSetOffset = 0; break; // Alternate char set off
case 0x0F: if (!IS_APPLE2) g_nAltCharSetOffset = 256; break; // Alternate char set on
case 0x50: g_uVideoMode &= ~VF_TEXT; break;
case 0x51: g_uVideoMode |= VF_TEXT; break;
case 0x52: g_uVideoMode &= ~VF_MIXED; break;
case 0x53: g_uVideoMode |= VF_MIXED; break;
case 0x54: g_uVideoMode &= ~VF_PAGE2; break;
case 0x55: g_uVideoMode |= VF_PAGE2; break;
case 0x56: g_uVideoMode &= ~VF_HIRES; break;
case 0x57: g_uVideoMode |= VF_HIRES; break;
case 0x5E: if (!IS_APPLE2) g_uVideoMode |= VF_DHIRES; break;
case 0x5F: if (!IS_APPLE2) g_uVideoMode &= ~VF_DHIRES; break;
}
NTSC_SetVideoMode( g_uVideoMode );
return MemReadFloatingBus(uExecutedCycles);
}
//===========================================================================
bool VideoGetSW80COL(void)
{
return SW_80COL ? true : false;
}
bool VideoGetSWDHIRES(void)
{
return SW_DHIRES ? true : false;
}
bool VideoGetSWHIRES(void)
{
return SW_HIRES ? true : false;
}
bool VideoGetSW80STORE(void)
{
return SW_80STORE ? true : false;
}
bool VideoGetSWMIXED(void)
{
return SW_MIXED ? true : false;
}
bool VideoGetSWPAGE2(void)
{
return SW_PAGE2 ? true : false;
}
bool VideoGetSWTEXT(void)
{
return SW_TEXT ? true : false;
}
bool VideoGetSWAltCharSet(void)
{
return g_nAltCharSetOffset != 0;
}
//===========================================================================
void VideoSetSnapshot_v1(const UINT AltCharSet, const UINT VideoMode)
{
g_nAltCharSetOffset = !AltCharSet ? 0 : 256;
g_uVideoMode = VideoMode;
g_dwCyclesThisFrame = 0;
}
//
#define SS_YAML_KEY_ALTCHARSET "Alt Char Set"
#define SS_YAML_KEY_VIDEOMODE "Video Mode"
#define SS_YAML_KEY_CYCLESTHISFRAME "Cycles This Frame"
static std::string VideoGetSnapshotStructName(void)
{
static const std::string name("Video");
return name;
}
void VideoSaveSnapshot(YamlSaveHelper& yamlSaveHelper)
{
YamlSaveHelper::Label state(yamlSaveHelper, "%s:\n", VideoGetSnapshotStructName().c_str());
yamlSaveHelper.SaveBool(SS_YAML_KEY_ALTCHARSET, g_nAltCharSetOffset ? true : false);
yamlSaveHelper.SaveHexUint32(SS_YAML_KEY_VIDEOMODE, g_uVideoMode);
yamlSaveHelper.SaveUint(SS_YAML_KEY_CYCLESTHISFRAME, g_dwCyclesThisFrame);
}
void VideoLoadSnapshot(YamlLoadHelper& yamlLoadHelper)
{
if (!yamlLoadHelper.GetSubMap(VideoGetSnapshotStructName()))
return;
g_nAltCharSetOffset = yamlLoadHelper.LoadBool(SS_YAML_KEY_ALTCHARSET) ? 256 : 0;
g_uVideoMode = yamlLoadHelper.LoadUint(SS_YAML_KEY_VIDEOMODE);
g_dwCyclesThisFrame = yamlLoadHelper.LoadUint(SS_YAML_KEY_CYCLESTHISFRAME);
yamlLoadHelper.PopMap();
}
//===========================================================================
//
// References to Jim Sather's books are given as eg:
// UTAIIe:5-7,P3 (Understanding the Apple IIe, chapter 5, page 7, Paragraph 3)
//
WORD VideoGetScannerAddress(bool* pbVblBar_OUT, const DWORD uExecutedCycles)
{
// get video scanner position
//
int nCycles = CpuGetCyclesThisVideoFrame(uExecutedCycles);
// machine state switches
//
int nHires = (SW_HIRES && !SW_TEXT) ? 1 : 0;
int nPage2 = SW_PAGE2 ? 1 : 0;
int n80Store = SW_80STORE ? 1 : 0;
// calculate video parameters according to display standard
//
int nScanLines = bVideoScannerNTSC ? kNTSCScanLines : kPALScanLines;
int nVSyncLine = bVideoScannerNTSC ? kNTSCVSyncLine : kPALVSyncLine;
int nScanCycles = nScanLines * kHClocks;
nCycles %= nScanCycles;
// calculate horizontal scanning state
//
int nHClock = (nCycles + kHPEClock) % kHClocks; // which horizontal scanning clock
int nHState = kHClock0State + nHClock; // H state bits
if (nHClock >= kHPresetClock) // check for horizontal preset
{
nHState -= 1; // correct for state preset (two 0 states)
}
int h_0 = (nHState >> 0) & 1; // get horizontal state bits
int h_1 = (nHState >> 1) & 1;
int h_2 = (nHState >> 2) & 1;
int h_3 = (nHState >> 3) & 1;
int h_4 = (nHState >> 4) & 1;
int h_5 = (nHState >> 5) & 1;
// calculate vertical scanning state
//
int nVLine = nCycles / kHClocks; // which vertical scanning line
int nVState = kVLine0State + nVLine; // V state bits
if ((nVLine >= kVPresetLine)) // check for previous vertical state preset
{
nVState -= nScanLines; // compensate for preset
}
int v_A = (nVState >> 0) & 1; // get vertical state bits
int v_B = (nVState >> 1) & 1;
int v_C = (nVState >> 2) & 1;
int v_0 = (nVState >> 3) & 1;
int v_1 = (nVState >> 4) & 1;
int v_2 = (nVState >> 5) & 1;
int v_3 = (nVState >> 6) & 1;
int v_4 = (nVState >> 7) & 1;
int v_5 = (nVState >> 8) & 1;
// calculate scanning memory address
//
if (nHires && SW_MIXED && v_4 && v_2) // HIRES TIME signal (UTAIIe:5-7,P3)
{
nHires = 0; // address is in text memory for mixed hires
}
int nAddend0 = 0x0D; // 1 1 0 1
int nAddend1 = (h_5 << 2) | (h_4 << 1) | (h_3 << 0);
int nAddend2 = (v_4 << 3) | (v_3 << 2) | (v_4 << 1) | (v_3 << 0);
int nSum = (nAddend0 + nAddend1 + nAddend2) & 0x0F; // SUM (UTAIIe:5-9)
int nAddress = 0; // build address from video scanner equations (UTAIIe:5-8,T5.1)
nAddress |= h_0 << 0; // a0
nAddress |= h_1 << 1; // a1
nAddress |= h_2 << 2; // a2
nAddress |= nSum << 3; // a3 - a6
nAddress |= v_0 << 7; // a7
nAddress |= v_1 << 8; // a8
nAddress |= v_2 << 9; // a9
int p2a = !(nPage2 && !n80Store);
int p2b = nPage2 && !n80Store;
if (nHires) // hires?
{
// Y: insert hires-only address bits
//
nAddress |= v_A << 10; // a10
nAddress |= v_B << 11; // a11
nAddress |= v_C << 12; // a12
nAddress |= p2a << 13; // a13
nAddress |= p2b << 14; // a14
}
else
{
// N: insert text-only address bits
//
nAddress |= p2a << 10; // a10
nAddress |= p2b << 11; // a11
// Apple ][ (not //e) and HBL?
//
if (IS_APPLE2 && // Apple II only (UTAIIe:I-4,#5)
!h_5 && (!h_4 || !h_3)) // HBL (UTAIIe:8-10,F8.5)
{
nAddress |= 1 << 12; // Y: a12 (add $1000 to address!)
}
}
// update VBL' state
//
if (pbVblBar_OUT != NULL)
{
*pbVblBar_OUT = !v_4 || !v_3; // VBL' = (v_4 & v_3)' (UTAIIe:5-10,#3)
}
return static_cast<WORD>(nAddress);
}
//===========================================================================
bool VideoGetVblBar(const DWORD uExecutedCycles)
{
// get video scanner position
int nCycles = CpuGetCyclesThisVideoFrame(uExecutedCycles);
// calculate video parameters according to display standard
const int kScanLines = bVideoScannerNTSC ? kNTSCScanLines : kPALScanLines;
const int kScanCycles = kScanLines * kHClocks;
nCycles %= kScanCycles;
// VBL'
return nCycles < kVDisplayableScanLines * kHClocks;
}
//===========================================================================
#define SCREENSHOT_BMP 1
#define SCREENSHOT_TGA 0
static int g_nLastScreenShot = 0;
const int nMaxScreenShot = 999999999;
static char *g_pLastDiskImageName = NULL;
//===========================================================================
void Video_ResetScreenshotCounter( char *pImageName )
{
g_nLastScreenShot = 0;
g_pLastDiskImageName = pImageName;
}
//===========================================================================
void Util_MakeScreenShotFileName( char *pFinalFileName_ )
{
char sPrefixScreenShotFileName[ 256 ] = "AppleWin_ScreenShot";
// TODO: g_sScreenshotDir
char *pPrefixFileName = g_pLastDiskImageName ? g_pLastDiskImageName : sPrefixScreenShotFileName;
#if SCREENSHOT_BMP
sprintf( pFinalFileName_, "%s_%09d.bmp", pPrefixFileName, g_nLastScreenShot );
#endif
#if SCREENSHOT_TGA
sprintf( pFinalFileName_, "%s%09d.tga", pPrefixFileName, g_nLastScreenShot );
#endif
}
// Returns TRUE if file exists, else FALSE
//===========================================================================
bool Util_TestScreenShotFileName( const char *pFileName )
{
bool bFileExists = false;
FILE *pFile = fopen( pFileName, "rt" );
if (pFile)
{
fclose( pFile );
bFileExists = true;
}
return bFileExists;
}
//===========================================================================
void Video_TakeScreenShot( const VideoScreenShot_e ScreenShotType )
{
char sScreenShotFileName[ MAX_PATH ];
// find last screenshot filename so we don't overwrite the existing user ones
bool bExists = true;
while( bExists )
{
if (g_nLastScreenShot > nMaxScreenShot) // Holy Crap! User has maxed the number of screenshots!?
{
sprintf( sScreenShotFileName, "You have more then %d screenshot filenames! They will no longer be saved.\n\nEither move some of your screenshots or increase the maximum in video.cpp\n", nMaxScreenShot );
MessageBox( g_hFrameWindow, sScreenShotFileName, "Warning", MB_OK );
g_nLastScreenShot = 0;
return;
}
Util_MakeScreenShotFileName( sScreenShotFileName );
bExists = Util_TestScreenShotFileName( sScreenShotFileName );
if( !bExists )
{
break;
}
g_nLastScreenShot++;
}
Video_SaveScreenShot( sScreenShotFileName, ScreenShotType );
g_nLastScreenShot++;
}
WinBmpHeader_t g_tBmpHeader;
#if SCREENSHOT_TGA
enum TargaImageType_e
{
TARGA_RGB = 2
};
struct TargaHeader_t
{ // Addr Bytes
u8 nIdBytes ; // 00 01 size of ID field that follows 18 byte header (0 usually)
u8 bHasPalette ; // 01 01
u8 iImageType ; // 02 01 type of image 0=none,1=indexed,2=rgb,3=grey,+8=rle packed
s16 iPaletteFirstColor ; // 03 02
s16 nPaletteColors ; // 05 02
u8 nPaletteBitsPerEntry ; // 07 01 number of bits per palette entry 15,16,24,32
s16 nOriginX ; // 08 02 image x origin
s16 nOriginY ; // 0A 02 image y origin
s16 nWidthPixels ; // 0C 02
s16 nHeightPixels ; // 0E 02
u8 nBitsPerPixel ; // 10 01 image bits per pixel 8,16,24,32
u8 iDescriptor ; // 11 01 image descriptor bits (vh flip bits)
// pixel data...
u8 aPixelData[1] ; // rgb
};
TargaHeader_t g_tTargaHeader;
#endif // SCREENSHOT_TGA
void Video_SetBitmapHeader( WinBmpHeader_t *pBmp, int nWidth, int nHeight, int nBitsPerPixel )
{
#if SCREENSHOT_BMP
pBmp->nCookie[ 0 ] = 'B'; // 0x42
pBmp->nCookie[ 1 ] = 'M'; // 0x4d
pBmp->nSizeFile = 0;
pBmp->nReserved1 = 0;
pBmp->nReserved2 = 0;
#if VIDEO_SCREENSHOT_PALETTE
pBmp->nOffsetData = sizeof(WinBmpHeader_t) + (256 * sizeof(bgra_t));
#else
pBmp->nOffsetData = sizeof(WinBmpHeader_t);
#endif
pBmp->nStructSize = 0x28; // sizeof( WinBmpHeader_t );
pBmp->nWidthPixels = nWidth;
pBmp->nHeightPixels = nHeight;
pBmp->nPlanes = 1;
#if VIDEO_SCREENSHOT_PALETTE
pBmp->nBitsPerPixel = 8;
#else
pBmp->nBitsPerPixel = nBitsPerPixel;
#endif
pBmp->nCompression = BI_RGB; // none
pBmp->nSizeImage = 0;
pBmp->nXPelsPerMeter = 0;
pBmp->nYPelsPerMeter = 0;
#if VIDEO_SCREENSHOT_PALETTE
pBmp->nPaletteColors = 256;
#else
pBmp->nPaletteColors = 0;
#endif
pBmp->nImportantColors = 0;
}
//===========================================================================
static void Video_MakeScreenShot(FILE *pFile, const VideoScreenShot_e ScreenShotType)
{
WinBmpHeader_t *pBmp = &g_tBmpHeader;
Video_SetBitmapHeader(
pBmp,
ScreenShotType == SCREENSHOT_280x192 ? GetFrameBufferBorderlessWidth()/2 : GetFrameBufferBorderlessWidth(),
ScreenShotType == SCREENSHOT_280x192 ? GetFrameBufferBorderlessHeight()/2 : GetFrameBufferBorderlessHeight(),
32
);
// char sText[256];
// sprintf( sText, "sizeof: BITMAPFILEHEADER = %d\n", sizeof(BITMAPFILEHEADER) ); // = 14
// MessageBox( g_hFrameWindow, sText, "Info 1", MB_OK );
// sprintf( sText, "sizeof: BITMAPINFOHEADER = %d\n", sizeof(BITMAPINFOHEADER) ); // = 40
// MessageBox( g_hFrameWindow, sText, "Info 2", MB_OK );
char sIfSizeZeroOrUnknown_BadWinBmpHeaderPackingSize54[ sizeof( WinBmpHeader_t ) == (14 + 40) ];
/**/ sIfSizeZeroOrUnknown_BadWinBmpHeaderPackingSize54[0]=0;
// Write Header
fwrite( pBmp, sizeof( WinBmpHeader_t ), 1, pFile );
uint32_t *pSrc;
#if VIDEO_SCREENSHOT_PALETTE
// Write Palette Data
pSrc = ((uint8_t*)g_pFramebufferinfo) + sizeof(BITMAPINFOHEADER);
int nLen = g_tBmpHeader.nPaletteColors * sizeof(bgra_t); // RGBQUAD
fwrite( pSrc, nLen, 1, pFile );
pSrc += nLen;
#endif
// Write Pixel Data
// No need to use GetDibBits() since we already have http://msdn.microsoft.com/en-us/library/ms532334.aspx
// @reference: "Storing an Image" http://msdn.microsoft.com/en-us/library/ms532340(VS.85).aspx
pSrc = (uint32_t*) g_pFramebufferbits;
int xSrc = GetFrameBufferBorderWidth();
int ySrc = GetFrameBufferBorderHeight();
VideoFrameBufferAdjust(xSrc, ySrc, true); // TC: Hacky-fix for GH#341 & GH#356
// Lines stored in reverse, so invert the y-adjust value
pSrc += xSrc; // Skip left border
pSrc += ySrc * GetFrameBufferWidth(); // Skip top border
if( ScreenShotType == SCREENSHOT_280x192 )
{
pSrc += GetFrameBufferWidth(); // Start on odd scanline (otherwise for 50% scanline mode get an all black image!)
uint32_t aScanLine[ 280 ];
uint32_t *pDst;
// 50% Half Scan Line clears every odd scanline.
// SHIFT+PrintScreen saves only the even rows.
// NOTE: Keep in sync with _Video_RedrawScreen() & Video_MakeScreenShot()
for( UINT y = 0; y < GetFrameBufferBorderlessHeight()/2; y++ )
{
pDst = aScanLine;
for( UINT x = 0; x < GetFrameBufferBorderlessWidth()/2; x++ )
{
*pDst++ = pSrc[1]; // correction for left edge loss of scaled scanline [Bill Buckel, B#18928]
pSrc += 2; // skip odd pixels
}
fwrite( aScanLine, sizeof(uint32_t), GetFrameBufferBorderlessWidth()/2, pFile );
pSrc += GetFrameBufferWidth(); // scan lines doubled - skip odd ones
pSrc += GetFrameBufferBorderWidth()*2; // Skip right border & next line's left border
}
}
else
{
for( UINT y = 0; y < GetFrameBufferBorderlessHeight(); y++ )
{
fwrite( pSrc, sizeof(uint32_t), GetFrameBufferBorderlessWidth(), pFile );
pSrc += GetFrameBufferWidth();
}
}
#endif // SCREENSHOT_BMP
#if SCREENSHOT_TGA
TargaHeader_t *pHeader = &g_tTargaHeader;
memset( (void*)pHeader, 0, sizeof( TargaHeader_t ) );
pHeader->iImageType = TARGA_RGB;
pHeader->nWidthPixels = FRAMEBUFFER_W;
pHeader->nHeightPixels = FRAMEBUFFER_H;
pHeader->nBitsPerPixel = 24;
#endif // SCREENSHOT_TGA
}
//===========================================================================
static void Video_SaveScreenShot( const char *pScreenShotFileName, const VideoScreenShot_e ScreenShotType )
{
FILE *pFile = fopen( pScreenShotFileName, "wb" );
if( pFile )
{
Video_MakeScreenShot( pFile, ScreenShotType );
fclose( pFile );
}
if( g_bDisplayPrintScreenFileName )
{
MessageBox( g_hFrameWindow, pScreenShotFileName, "Screen Captured", MB_OK );
}
}
//===========================================================================
void Config_Load_Video()
{
REGLOAD(TEXT(REGVALUE_VIDEO_MODE ),&g_eVideoType);
REGLOAD(TEXT(REGVALUE_VIDEO_HALF_SCAN_LINES),&g_uHalfScanLines);
REGLOAD(TEXT(REGVALUE_VIDEO_MONO_COLOR ),&g_nMonochromeRGB);
if (g_eVideoType >= NUM_VIDEO_MODES)
g_eVideoType = VT_COLOR_MONITOR;
}
void Config_Save_Video()
{
REGSAVE(TEXT(REGVALUE_VIDEO_MODE ),g_eVideoType);
REGSAVE(TEXT(REGVALUE_VIDEO_HALF_SCAN_LINES),g_uHalfScanLines);
REGSAVE(TEXT(REGVALUE_VIDEO_MONO_COLOR ),g_nMonochromeRGB);
}
// ____________________________________________________________________
//===========================================================================
static void videoCreateDIBSection()
{
// CREATE THE DEVICE CONTEXT
HWND window = GetDesktopWindow();
HDC dc = GetDC(window);
if (g_hDeviceDC)
{
DeleteDC(g_hDeviceDC);
}
g_hDeviceDC = CreateCompatibleDC(dc);
// CREATE THE FRAME BUFFER DIB SECTION
if (g_hDeviceBitmap)
DeleteObject(g_hDeviceBitmap);
g_hDeviceBitmap = CreateDIBSection(
dc,
g_pFramebufferinfo,
DIB_RGB_COLORS,
(LPVOID *)&g_pFramebufferbits,0,0
);
SelectObject(g_hDeviceDC,g_hDeviceBitmap);
// CREATE THE OFFSET TABLE FOR EACH SCAN LINE IN THE FRAME BUFFER
// DRAW THE SOURCE IMAGE INTO THE SOURCE BIT BUFFER
ZeroMemory( g_pFramebufferbits, GetFrameBufferWidth()*GetFrameBufferHeight()*sizeof(bgra_t) );
NTSC_VideoInit( g_pFramebufferbits );
}