Compare commits

...
7 Commits
Author SHA1 Message Date
Bobby Dilley 2dc358acda Test 2026-08-12 14:26:06 +01:00
dkeruza 0af606d845 Update Patreon link in README.md 2026-05-05 00:17:15 -04:00
Bobby Dilley 88e1628382 Merge branch 'bobby/outrun-chihiro' 2026-02-24 21:59:38 +00:00
Bobby Dilley 6918990746 Update changelog 2026-02-24 21:59:35 +00:00
Bobby Dilley 87dccf51a4 Undo changes to resolution 2026-02-24 21:51:47 +00:00
Bobby Dilley 81d9185227 Fix outrun 4:3 in a different way@ 2026-02-24 21:51:47 +00:00
Bobby Dilley 510d13398e Try to render like chihiro in 640x480 2026-02-24 21:51:47 +00:00
11 changed files with 207 additions and 41 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ For detailed information on configuration and controls, please see the following
- [**🚀 General Guide**](docs/guide.md): Information on supported games, known issues, and general configuration. - [**🚀 General Guide**](docs/guide.md): Information on supported games, known issues, and general configuration.
- [**🎮 Controls Guide**](docs/CONTROLS.md): A detailed guide on how to customize your controls using the `controls.ini` file. - [**🎮 Controls Guide**](docs/CONTROLS.md): A detailed guide on how to customize your controls using the `controls.ini` file.
If you'd like to support the development work of this emulator, see early development builds or get support from the authors please consider [becoming a patreon here](https://www.patreon.com/LindberghLoader). If you'd like to support the development work of this emulator, see early development builds or get support from the authors please consider [becoming a patreon here](https://www.patreon.com/c/LinuxLoader).
If you need any help please ask the community in the [arcade community discord](https://arcade.community). Please only submit issues if they are bugs with the software, ask in the arcade community discord if you're not sure if it's a bug or you're not setting something up properly! If you need any help please ask the community in the [arcade community discord](https://arcade.community). Please only submit issues if they are bugs with the software, ask in the arcade community discord if you're not sure if it's a bug or you're not setting something up properly!
+6 -1
View File
@@ -3,6 +3,11 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [2.1.4]
### Added
- Force Outrun to render properly in 4:3 when resolution is set to 640x480
to match the chihiro version of the game for use on CRT cabinets.
## [2.1.3] ## [2.1.3]
### Added ### Added
- MJ4 Rev. A, MJ4 Rev. F and MJ4 Evolution Rev. B support. - MJ4 Rev. A, MJ4 Rev. F and MJ4 Evolution Rev. B support.
@@ -176,4 +181,4 @@ All notable changes to this project will be documented in this file.
- Dynamic resolution patching allowing most games to scale properly in HD - Dynamic resolution patching allowing most games to scale properly in HD
- Original cabinet support with pass through of JVS and FFB - Original cabinet support with pass through of JVS and FFB
- Network play - Network play
- Both simple X11 and advanced evdev input system allowing for ready-to-play keyboard and mouse support or mapping of any controller - Both simple X11 and advanced evdev input system allowing for ready-to-play keyboard and mouse support or mapping of any controller
Submodule scripts/appimage/build-deps/FAudio added at e9e9d9478f
Submodule scripts/appimage/build-deps/SDL added at 129627068f
Submodule scripts/appimage/build-deps/SDL_image added at d516bc507f
Submodule scripts/appimage/build-deps/SDL_ttf added at 053bbc8951
+19 -8
View File
@@ -83,7 +83,6 @@ int initBaseboard()
} }
initJVSSerial(jvsFileDescriptor); initJVSSerial(jvsFileDescriptor);
startJVSFrameThread(&jvsFileDescriptor);
} }
strcpy(serialString, SERIAL_STRING); strcpy(serialString, SERIAL_STRING);
@@ -209,16 +208,16 @@ int baseboardIoctl(int fd, unsigned long request, void *data)
jvsCommand.destAddress = _data[3]; jvsCommand.destAddress = _data[3];
jvsCommand.destSize = _data[4]; jvsCommand.destSize = _data[4];
memcpy(inputBuffer, &sharedMemory[jvsCommand.srcAddress], jvsCommand.srcSize); memcpy(inputBuffer, &sharedMemory[jvsCommand.srcAddress], jvsCommand.srcSize);
if (getConfig()->emulateJVS) if (getConfig()->emulateJVS)
{ {
processPacket(&jvsPacketSize); processPacket(&jvsPacketSize);
} }
else if (jvsFileDescriptor >= 0) else if (jvsFileDescriptor >= 0)
{ {
write(jvsFileDescriptor, inputBuffer, jvsCommand.srcSize);
for (int i = 0; i < jvsCommand.srcSize; i++) for (int i = 0; i < jvsCommand.srcSize; i++)
{ {
write(jvsFileDescriptor, &inputBuffer[i], 1);
if (inputBuffer[i] == 0xF0) if (inputBuffer[i] == 0xF0)
{ {
setSenseLine(3); setSenseLine(3);
@@ -228,6 +227,7 @@ int baseboardIoctl(int fd, unsigned long request, void *data)
setSenseLine(1); setSenseLine(1);
} }
} }
} }
} }
break; break;
@@ -278,12 +278,23 @@ int baseboardIoctl(int fd, unsigned long request, void *data)
} }
else if (jvsFileDescriptor >= 0) else if (jvsFileDescriptor >= 0)
{ {
JVSFrame frame = readJVSFrameFromThread(); if(getSenseLine() == 3)
memcpy(&sharedMemory[jvsCommand.destAddress], frame.buffer, frame.size); {
_data[2] = jvsCommand.destAddress; char errorMessage[5] = {0xE0, 0x00, 0x02, 0x01, 0x03};
_data[3] = frame.size; memcpy(&sharedMemory[jvsCommand.destAddress], errorMessage, 5);
_data[1] = frame.ready; _data[2] = jvsCommand.destAddress;
_data[3] = 5;
_data[1] = 1;
} else {
JVSFrame frame = readJVSFrameFromThread(jvsFileDescriptor);
memcpy(&sharedMemory[jvsCommand.destAddress], frame.buffer, frame.size);
_data[2] = jvsCommand.destAddress;
_data[3] = frame.size;
_data[1] = frame.ready;
}
} }
} }
break; break;
+45 -8
View File
@@ -22,6 +22,8 @@ extern SDL_Window *sdlWindow;
int blitWidth = 0; int blitWidth = 0;
int blitHeight = 0; int blitHeight = 0;
int gameIsOutrunChihiroMode = 0;
int fboInitialized = false; int fboInitialized = false;
GLuint fboId = 0; GLuint fboId = 0;
GLuint fboTextureId = 0; GLuint fboTextureId = 0;
@@ -36,6 +38,30 @@ void initBlitting()
blitSetWidthandHeightSize(); blitSetWidthandHeightSize();
blitInitializeFbo(); blitInitializeFbo();
dest.gameScale = 1.0f; dest.gameScale = 1.0f;
if(gGrp == GROUP_OUTRUN && getConfig()->width == 640)
gameIsOutrunChihiroMode = 1;
}
/**
* If the game is outrun in 640x480 mode we force the game to write to out temp framebuffer directly
* to avoid the framebuffer of the window being too small and the edges getting clipped off. So we
* hook glBindFramebuffer and if the game tries to bind the default framebuffer, we bind our FBO instead and
* set the viewport to 800x480 so the game renders to our FBO. Then in blitStretch we copy from our FBO to the
* screen with the correct aspect ratio.
*/
void glBindFramebufferEXT(GLenum target, GLuint framebuffer) {
void (*_glBindFramebufferEXT)(GLenum target, GLuint framebuffer) = dlsym(RTLD_NEXT, "glBindFramebufferEXT");
if(target == GL_FRAMEBUFFER_EXT && framebuffer == 0 && fboInitialized && gameIsOutrunChihiroMode)
{
glBindFramebuffer(GL_FRAMEBUFFER, fboId);
glViewport(0, 0, 800, 480);
glClear(GL_COLOR_BUFFER_BIT);
return;
}
_glBindFramebufferEXT(target, framebuffer);
} }
void blitSetWidthandHeightSize() void blitSetWidthandHeightSize()
@@ -121,14 +147,18 @@ void blitStretch()
if (fboInitialized && fboId > 0 && fboTextureId > 0 && sdlWindow) if (fboInitialized && fboId > 0 && fboTextureId > 0 && sdlWindow)
{ {
SDL_GetWindowSizeInPixels(sdlWindow, &drawableW, &drawableH); SDL_GetWindowSizeInPixels(sdlWindow, &drawableW, &drawableH);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fboId);
glBlitFramebuffer(0, 0, blitWidth, blitHeight, 0, 0, blitWidth, blitHeight, GL_COLOR_BUFFER_BIT, GL_NEAREST);
float gameAspect = (float)blitWidth / (float)blitHeight;
float windowAspect = (float)drawableW / (float)drawableH; float windowAspect = (float)drawableW / (float)drawableH;
float gameAspect = 4.0 / 3.0;
if(!gameIsOutrunChihiroMode)
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fboId);
glBlitFramebuffer(0, 0, blitWidth, blitHeight, 0, 0, blitWidth, blitHeight, GL_COLOR_BUFFER_BIT, GL_NEAREST);
gameAspect = (float)blitWidth / (float)blitHeight;
}
dest.W = drawableW; dest.W = drawableW;
dest.H = drawableH; dest.H = drawableH;
@@ -142,7 +172,7 @@ void blitStretch()
dest.H = (GLsizei)(drawableW / gameAspect); dest.H = (GLsizei)(drawableW / gameAspect);
dest.Y = (drawableH - dest.H) / 2; dest.Y = (drawableH - dest.H) / 2;
} }
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT);
@@ -151,7 +181,14 @@ void blitStretch()
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glReadBuffer(GL_COLOR_ATTACHMENT0); glReadBuffer(GL_COLOR_ATTACHMENT0);
glBlitFramebuffer(0, 0, blitWidth, blitHeight, dest.X, dest.Y, dest.X + dest.W, dest.Y + dest.H, GL_COLOR_BUFFER_BIT, GL_LINEAR); if(gameIsOutrunChihiroMode)
{
glBlitFramebuffer(80, 0, blitWidth - 80, blitHeight, dest.X, dest.Y, dest.X + dest.W, dest.Y + dest.H, GL_COLOR_BUFFER_BIT, GL_LINEAR);
}
else
{
glBlitFramebuffer(0, 0, blitWidth, blitHeight, dest.X, dest.Y, dest.X + dest.W, dest.Y + dest.H, GL_COLOR_BUFFER_BIT, GL_LINEAR);
}
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
} }
+121 -19
View File
@@ -13,6 +13,14 @@
#define TIMEOUT_SELECT 200 #define TIMEOUT_SELECT 200
#define CTS_ON_RETRY 100 #define CTS_ON_RETRY 100
#define JVS_MAX_PACKET_SIZE 255
#define SYNC 0xE0
#define ESCAPE 0xD0
#define BROADCAST 0xFF
#define BUS_MASTER 0x00
#define DEVICE_ADDR_START 0x01
// Used to read JVS frame in a non-blocking way // Used to read JVS frame in a non-blocking way
JVSFrame jvsFrameBuffer; JVSFrame jvsFrameBuffer;
pthread_mutex_t jvsBuffer_lock = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t jvsBuffer_lock = PTHREAD_MUTEX_INITIALIZER;
@@ -95,7 +103,7 @@ int initJVSSerial(int fd)
// Block until either VMIN characters have been received or VTIME **after first character** has been received // Block until either VMIN characters have been received or VTIME **after first character** has been received
options.c_cc[VMIN] = 0; options.c_cc[VMIN] = 0;
options.c_cc[VTIME] = 1; options.c_cc[VTIME] = 2; // 200ms
// options.c_cflag |= CREAD; // Turn on READ, let ctrl lines work // options.c_cflag |= CREAD; // Turn on READ, let ctrl lines work
tcsetattr(fd, TCSANOW, &options); tcsetattr(fd, TCSANOW, &options);
@@ -224,6 +232,8 @@ int startJVSFrameThread(int *fd)
{ {
printf("SERIAL: starting thread.\n"); printf("SERIAL: starting thread.\n");
return 0;
// Clean shared JVS frame buffer // Clean shared JVS frame buffer
jvsFrameBuffer.ready = 0; jvsFrameBuffer.ready = 0;
jvsFrameBuffer.size = 0; jvsFrameBuffer.size = 0;
@@ -240,30 +250,122 @@ int startJVSFrameThread(int *fd)
return 0; return 0;
} }
typedef struct
{
unsigned char destination;
unsigned char length;
unsigned char data[JVS_MAX_PACKET_SIZE];
} JVSPacket;
/** /**
* Return a JVSFrame structure with empty or full data, no in between * Return a JVSFrame structure with empty or full data, no in between
* @return * @return
*/ */
JVSFrame readJVSFrameFromThread() JVSFrame readJVSFrameFromThread(int fd)
{ {
JVSFrame frame;
// Lock while reading/writing to shared frame
pthread_mutex_lock(&jvsBuffer_lock);
// Check if we have a valid frame JVSFrame frame;
if (jvsFrameBuffer.ready == 1) JVSPacket packet = {0};
{
frame = jvsFrameBuffer; frame.size = 0;
// It has been read, we disable this frame frame.ready = 1;
jvsFrameBuffer.ready = 0;
} int bytesAvailable = 0, escape = 0, phase = 0, index = 0, dataIndex = 0, finished = 0;
else unsigned char checksum = 0x00;
{ int timeout = 3;
frame.ready = 0;
frame.size = 0; unsigned char inputBuffer[JVS_MAX_PACKET_SIZE];
memset(frame.buffer, 0, JVSBUFFER_SIZE);
}
pthread_mutex_unlock(&jvsBuffer_lock); while (!finished)
{
int bytesRead = read(fd, inputBuffer + bytesAvailable, JVS_MAX_PACKET_SIZE - bytesAvailable);
if(bytesRead > 0) {
timeout = 3;
} else {
timeout = timeout - 1;
}
if(timeout == 0) {
return frame;
}
if (bytesRead < 0)
return frame;
bytesAvailable += bytesRead;
while ((index < bytesAvailable) && !finished)
{
/* If we encounter a SYNC start again */
if (!escape && (inputBuffer[index] == SYNC))
{
phase = 0;
dataIndex = 0;
packet.data[dataIndex++] = inputBuffer[index];
index++;
continue;
}
/* If we encounter an ESCAPE byte escape the next byte */
if (!escape && inputBuffer[index] == ESCAPE)
{
escape = 1;
index++;
continue;
}
/* Escape next byte by adding 1 to it */
if (escape)
{
inputBuffer[index]++;
escape = 0;
}
/* Deal with the main bulk of the data */
switch (phase)
{
case 0: // If we have not yet got the address
packet.data[dataIndex++] = inputBuffer[index];
packet.destination = inputBuffer[index];
checksum = packet.destination & 0xFF;
phase++;
break;
case 1: // If we have not yet got the length
packet.length = inputBuffer[index];
packet.data[dataIndex++] = inputBuffer[index];
checksum = (checksum + packet.length) & 0xFF;
phase++;
break;
case 2: // If there is still data to read
if (dataIndex == (packet.length + 2))
{
if (checksum != inputBuffer[index]) {
printf("CHEKCSUM ERROR\n");
return frame; // Checksum error, return empty frame
}
packet.data[dataIndex++] = checksum;
finished = 1;
break;
}
packet.data[dataIndex++] = inputBuffer[index];
checksum = (checksum + inputBuffer[index]) & 0xFF;
break;
default:
return frame;
}
index++;
}
}
frame.size = dataIndex;
memcpy(frame.buffer, packet.data, dataIndex);
frame.ready = 1;
return frame; return frame;
} }
+1 -1
View File
@@ -11,7 +11,7 @@ int getDCD(int fd);
int getDSR(int fd); int getDSR(int fd);
int getCTS(int fd); int getCTS(int fd);
JVSFrame readJVSFrameFromThread(); JVSFrame readJVSFrameFromThread(int fd);
int startJVSFrameThread(int * fd); int startJVSFrameThread(int * fd);
void * readJVSFrameThread(void * arg); void * readJVSFrameThread(void * arg);
+10 -3
View File
@@ -130,11 +130,18 @@ void startSDL(int *argcp, char **argv)
strcat(sdlGameTitle, getGameName()); strcat(sdlGameTitle, getGameName());
int windowWidth = gWidth;
if(gGrp == GROUP_OUTRUN && getConfig()->width == 640)
{
gWidth = 800;
}
// Hacky way to make AxA games render the characters properly // Hacky way to make AxA games render the characters properly
if (gId == QUIZ_AXA || gId == QUIZ_AXA_LIVE) if (gId == QUIZ_AXA || gId == QUIZ_AXA_LIVE)
sdlWindow = SDL_CreateWindow(sdlGameTitle, 1024, 768, windowFlags); sdlWindow = SDL_CreateWindow(sdlGameTitle, 1024, 768, windowFlags);
else else
sdlWindow = SDL_CreateWindow(sdlGameTitle, gWidth, gHeight, windowFlags); sdlWindow = SDL_CreateWindow(sdlGameTitle, windowWidth, gHeight, windowFlags);
if (!sdlWindow) if (!sdlWindow)
{ {
@@ -172,11 +179,11 @@ void startSDL(int *argcp, char **argv)
// If games are any of the LGJ or Primeval Hunt, we prevent the window to resize. // If games are any of the LGJ or Primeval Hunt, we prevent the window to resize.
if ((gGrp == GROUP_LGJ || gId == PRIMEVAL_HUNT) && !getConfig()->fullscreen) if ((gGrp == GROUP_LGJ || gId == PRIMEVAL_HUNT) && !getConfig()->fullscreen)
SDL_SetWindowMaximumSize(sdlWindow, gWidth, gHeight); SDL_SetWindowMaximumSize(sdlWindow, windowWidth, gHeight);
else if (gGrp != GROUP_LGJ) else if (gGrp != GROUP_LGJ)
SDL_SetWindowMaximumSize(sdlWindow, displayMode->w, displayMode->h); SDL_SetWindowMaximumSize(sdlWindow, displayMode->w, displayMode->h);
SDL_SetWindowMinimumSize(sdlWindow, gWidth, gHeight); SDL_SetWindowMinimumSize(sdlWindow, windowWidth, gHeight);
// Hacky way to make AxA games render the characters properly // Hacky way to make AxA games render the characters properly
if (gId == QUIZ_AXA || gId == QUIZ_AXA_LIVE) if (gId == QUIZ_AXA || gId == QUIZ_AXA_LIVE)