Emulate the entire tilegen chip in a GLSL shader. (This is now possible with opengl 3+). The tilegen drawing was emulated on the CPU, but was one of the most expensive functions in the emulator according to a profiler. On a modern GPU it's pretty much free, because a GPU is a massive SIMD monster.

Tilegen shaders are mapped to uniforms, and the vram and palette are mapped to two textures.

TODO rip out the redundant code in the tilegen class. We don't need to pre-calculate palettes anymore. etc

The tilegen code supports has a start/end line so we can emulate as many lines as we want in a chunk, which will come in later as some games update the tilegen immediately after the ping_pong bit has flipped ~ 66% of the frame.

The scud rolling start tilegen bug is probably actually a bug in the original h/w implementation, that ends up looking correct on original h/w but not for us. Need hardware testing to confirm what it's actually doing.
This commit is contained in:
Ian Curtis 2023-09-23 15:27:04 +01:00
parent 015e8e9212
commit c6ea81d996
7 changed files with 1008 additions and 765 deletions

73
Src/Graphics/FBO.cpp Normal file
View file

@ -0,0 +1,73 @@
#include "FBO.h"
FBO::FBO() :
m_frameBufferID(0),
m_textureID(0)
{
}
bool FBO::Create(int width, int height)
{
CreateTexture(width, height);
glGenFramebuffers(1, &m_frameBufferID);
glBindFramebuffer(GL_FRAMEBUFFER, m_frameBufferID);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_textureID, 0);
auto frameBufferStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
glBindFramebuffer(GL_FRAMEBUFFER, 0); //created FBO now disable it
return frameBufferStatus == GL_FRAMEBUFFER_COMPLETE;
}
void FBO::Destroy()
{
if (m_frameBufferID) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &m_frameBufferID);
}
if (m_textureID) {
glDeleteTextures(1, &m_textureID);
}
m_frameBufferID = 0;
m_textureID = 0;
}
void FBO::BindTexture()
{
glBindTexture(GL_TEXTURE_2D, m_textureID);
}
void FBO::Set()
{
glBindFramebuffer(GL_FRAMEBUFFER, m_frameBufferID);
}
void FBO::Disable()
{
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
GLuint FBO::GetFBOID()
{
return m_frameBufferID;
}
GLuint FBO::GetTextureID()
{
return m_textureID;
}
void FBO::CreateTexture(int width, int height)
{
glGenTextures (1, &m_textureID);
glBindTexture (GL_TEXTURE_2D, m_textureID);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
}

28
Src/Graphics/FBO.h Normal file
View file

@ -0,0 +1,28 @@
#ifndef _FBO_H_
#define _FBO_H_
#include <GL/glew.h>
class FBO
{
public:
FBO();
bool Create(int width, int height);
void Destroy();
void BindTexture();
void Set();
void Disable();
GLuint GetFBOID();
GLuint GetTextureID();
private:
void CreateTexture(int width, int height);
GLuint m_frameBufferID;
GLuint m_textureID;
};
#endif

File diff suppressed because it is too large Load diff

View file

@ -19,11 +19,11 @@
** with Supermodel. If not, see <http://www.gnu.org/licenses/>.
**/
/*
* Render2D.h
*
* Header file defining the CRender2D class: OpenGL tile generator graphics.
*/
/*
* Render2D.h
*
* Header file defining the CRender2D class: OpenGL tile generator graphics.
*/
#ifndef INCLUDED_RENDER2D_H
#define INCLUDED_RENDER2D_H
@ -31,181 +31,184 @@
#include <GL/glew.h>
#include "Util/NewConfig.h"
#include "New3D/GLSLShader.h"
#include "FBO.h"
/*
* CRender2D:
*
* Tile generator graphics engine. This must be constructed and initialized
* before being attached to any objects that want to make use of it. Apart from
* the constructor, all members assume that a global GL device
* context is available and that GL functions may be called.
*/
/*
* CRender2D:
*
* Tile generator graphics engine. This must be constructed and initialized
* before being attached to any objects that want to make use of it. Apart from
* the constructor, all members assume that a global GL device
* context is available and that GL functions may be called.
*/
class CRender2D
{
public:
/*
* BeginFrame(void):
*
* Prepare to render a new frame. Must be called once per frame prior to
* drawing anything.
*/
void BeginFrame(void);
/*
* BeginFrame(void):
*
* Prepare to render a new frame. Must be called once per frame prior to
* drawing anything.
*/
void BeginFrame(void);
/*
* PreRenderFrame(void):
*
* Draws the all top layers (above 3D graphics) and bottom layers (below 3D
* graphics) but does not yet display them. May send data to the GPU.
*/
void PreRenderFrame(void);
/*
* PreRenderFrame(void):
*
* Draws the all top layers (above 3D graphics) and bottom layers (below 3D
* graphics) but does not yet display them. May send data to the GPU.
*/
void PreRenderFrame(void);
/*
* RenderFrameBottom(void):
*
* Overwrites the color buffer with bottom surface that was pre-rendered by
* the last call to PreRenderFrame().
*/
void RenderFrameBottom(void);
/*
* RenderFrameBottom(void):
*
* Overwrites the color buffer with bottom surface that was pre-rendered by
* the last call to PreRenderFrame().
*/
void RenderFrameBottom(void);
/*
* RenderFrameTop(void):
*
* Draws the top surface (if it exists) that was pre-rendered by the last
* call to PreRenderFrame(). Previously drawn graphics layers will be visible
* through transparent regions.
*/
void RenderFrameTop(void);
/*
* RenderFrameTop(void):
*
* Draws the top surface (if it exists) that was pre-rendered by the last
* call to PreRenderFrame(). Previously drawn graphics layers will be visible
* through transparent regions.
*/
void RenderFrameTop(void);
/*
* EndFrame(void):
*
* Signals the end of rendering for this frame. Must be called last during
* the frame.
*/
void EndFrame(void);
/*
* EndFrame(void):
*
* Signals the end of rendering for this frame. Must be called last during
* the frame.
*/
void EndFrame(void);
/*
* WriteVRAM(addr, data):
*
* Indicates what will be written next to the tile generator's RAM. The
* VRAM address must not have yet been updated, to allow the renderer to
* check for changes. Data is accepted in the same form as the tile
* generator: the MSB is what was written to addr+3. This function is
* intended to facilitate on-the-fly decoding of tiles and palette data.
*
* Parameters:
* addr Address in tile generator RAM. Caller must ensure it is
* clamped to the range 0x000000 to 0x11FFFF because this
* function does not.
* data The data to write.
*/
void WriteVRAM(unsigned addr, uint32_t data);
/*
* WriteVRAM(addr, data):
*
* Indicates what will be written next to the tile generator's RAM. The
* VRAM address must not have yet been updated, to allow the renderer to
* check for changes. Data is accepted in the same form as the tile
* generator: the MSB is what was written to addr+3. This function is
* intended to facilitate on-the-fly decoding of tiles and palette data.
*
* Parameters:
* addr Address in tile generator RAM. Caller must ensure it is
* clamped to the range 0x000000 to 0x11FFFF because this
* function does not.
* data The data to write.
*/
void WriteVRAM(unsigned addr, uint32_t data);
/*
* AttachRegisters(regPtr):
*
* Attaches tile generator registers. This must be done prior to any
* rendering otherwise the program may crash with an access violation.
*
* Parameters:
* regPtr Pointer to the base of the tile generator registers. There
* are assumed to be 64 in all.
*/
void AttachRegisters(const uint32_t *regPtr);
/*
* AttachRegisters(regPtr):
*
* Attaches tile generator registers. This must be done prior to any
* rendering otherwise the program may crash with an access violation.
*
* Parameters:
* regPtr Pointer to the base of the tile generator registers. There
* are assumed to be 64 in all.
*/
void AttachRegisters(const uint32_t* regPtr);
/*
* AttachPalette(palPtr):
*
* Attaches tile generator palettes. This must be done prior to any
* rendering.
*
* Parameters:
* palPtr Pointer to two palettes. The first is for layers A/A' and
* the second is for B/B'.
*/
void AttachPalette(const uint32_t *palPtr[2]);
/*
* AttachPalette(palPtr):
*
* Attaches tile generator palettes. This must be done prior to any
* rendering.
*
* Parameters:
* palPtr Pointer to two palettes. The first is for layers A/A' and
* the second is for B/B'.
*/
void AttachPalette(const uint32_t* palPtr[2]);
/*
* AttachVRAM(vramPtr):
*
* Attaches tile generator RAM. This must be done prior to any rendering
* otherwise the program may crash with an access violation.
*
* Parameters:
* vramPtr Pointer to the base of the tile generator RAM (0x120000
* bytes). VRAM is assumed to be in little endian format.
*/
void AttachVRAM(const uint8_t *vramPtr);
/*
* AttachVRAM(vramPtr):
*
* Attaches tile generator RAM. This must be done prior to any rendering
* otherwise the program may crash with an access violation.
*
* Parameters:
* vramPtr Pointer to the base of the tile generator RAM (0x120000
* bytes). VRAM is assumed to be in little endian format.
*/
void AttachVRAM(const uint8_t* vramPtr);
/*
* Init(xOffset, yOffset, xRes, yRes, totalXRes, totalYRes);
*
* One-time initialization of the context. Must be called before any other
* members (meaning it should be called even before being attached to any
* other objects that want to use it).
*
* Parameters:
* xOffset X offset of the viewable area within OpenGL display
* surface, in pixels.
* yOffset Y offset.
* xRes Horizontal resolution of the viewable area.
* yRes Vertical resolution.
* totalXRes Horizontal resolution of the complete display area.
* totalYRes Vertical resolution.
*
* Returns:
* OKAY is successful, otherwise FAILED if a non-recoverable error
* occurred. Prints own error messages.
*/
bool Init(unsigned xOffset, unsigned yOffset, unsigned xRes, unsigned yRes, unsigned totalXRes, unsigned totalYRes);
/*
* Init(xOffset, yOffset, xRes, yRes, totalXRes, totalYRes);
*
* One-time initialization of the context. Must be called before any other
* members (meaning it should be called even before being attached to any
* other objects that want to use it).
*
* Parameters:
* xOffset X offset of the viewable area within OpenGL display
* surface, in pixels.
* yOffset Y offset.
* xRes Horizontal resolution of the viewable area.
* yRes Vertical resolution.
* totalXRes Horizontal resolution of the complete display area.
* totalYRes Vertical resolution.
*
* Returns:
* OKAY is successful, otherwise FAILED if a non-recoverable error
* occurred. Prints own error messages.
*/
bool Init(unsigned xOffset, unsigned yOffset, unsigned xRes, unsigned yRes, unsigned totalXRes, unsigned totalYRes);
/*
* CRender2D(config):
* ~CRender2D(void):
*
* Constructor and destructor.
*
* Parameters:
* config Run-time configuration.
*/
CRender2D(const Util::Config::Node &config);
~CRender2D(void);
/*
* CRender2D(config):
* ~CRender2D(void):
*
* Constructor and destructor.
*
* Parameters:
* config Run-time configuration.
*/
CRender2D(const Util::Config::Node& config);
~CRender2D(void);
private:
// Private member functions
std::pair<bool, bool> DrawTilemaps(uint32_t *destBottom, uint32_t *destTop);
void DisplaySurface(int surface);
void Setup2D(bool isBottom);
// Run-time configuration
const Util::Config::Node &m_config;
bool IsEnabled (int layerNumber);
bool Above3D (int layerNumber);
void Setup2D (bool isBottom);
void DrawSurface (GLuint textureID);
// Data received from tile generator device object
const uint32_t *m_vram;
const uint32_t *m_palette[2]; // palettes for A/A' and B/B'
const uint32_t *m_regs;
float LineToPercentStart (int lineNumber); // vertical line numbers are from 0-383
float LineToPercentEnd (int lineNumber); // vertical line numbers are from 0-383
// OpenGL data
GLuint m_texID[2]; // IDs for the 2 layer textures (top and bottom)
unsigned m_xPixels = 496; // display surface resolution
unsigned m_yPixels = 384; // ...
unsigned m_xOffset = 0; // offset
unsigned m_yOffset = 0;
unsigned m_totalXPixels; // total display surface resolution
unsigned m_totalYPixels;
unsigned m_correction = 0;
// Run-time configuration
const Util::Config::Node& m_config;
GLuint m_vao;
GLSLShader m_shader;
// Data received from tile generator device object
const uint32_t* m_vram;
const uint32_t* m_palette[2]; // palettes for A/A' and B/B'
const uint32_t* m_regs;
// PreRenderFrame() tracks which surfaces exist in current frame
std::pair<bool, bool> m_surfaces_present = std::pair<bool, bool>(false, false);
// OpenGL data
unsigned m_xPixels = 496; // display surface resolution
unsigned m_yPixels = 384; // ...
unsigned m_xOffset = 0; // offset
unsigned m_yOffset = 0;
unsigned m_totalXPixels = 0; // total display surface resolution
unsigned m_totalYPixels = 0;
unsigned m_correction = 0;
GLuint m_vao;
GLSLShader m_shader;
GLSLShader m_shaderTileGen;
GLuint m_vramTexID = 0;
GLuint m_paletteTexID = 0;
FBO m_fboBottom;
FBO m_fboTop;
// Buffers
uint8_t *m_memoryPool = 0; // all memory is allocated here
uint32_t *m_topSurface = 0; // 512x384x32bpp pixel surface for top layers
uint32_t *m_bottomSurface = 0; // bottom layers
};

View file

@ -71,4 +71,255 @@ static const char s_fragmentShaderSource[] = R"glsl(
)glsl";
// Vertex shader
static const char s_vertexShaderTileGen[] = R"glsl(
#version 410 core
uniform float lineStart; // defined as a % of the viewport height in the range 0-1. So 0 is top line, 0.5 is line 192 etc
uniform float lineEnd;
void main(void)
{
const float v1 = -1.0;
const float v2 = 1.0;
vec4 vertices[] = vec4[]( vec4(-1.0, v1, 0.0, 1.0),
vec4(-1.0, v2, 0.0, 1.0),
vec4( 1.0, v1, 0.0, 1.0),
vec4( 1.0, v2, 0.0, 1.0));
float top = ((v2 - v1) * lineStart) + v1;
float bottom = ((v2 - v1) * lineEnd ) + v1;
vertices[0].y = top;
vertices[2].y = top;
vertices[1].y = bottom;
vertices[3].y = bottom;
gl_Position = vertices[gl_VertexID % 4];
}
)glsl";
// Fragment shader
static const char s_fragmentShaderTileGen[] = R"glsl(
#version 410 core
//layout(origin_upper_left) in vec4 gl_FragCoord;
// inputs
uniform usampler2D vram; // texture 512x512
uniform usampler2D palette; // texture 128x256 - actual dimensions dont matter too much but we have to stay in the limits of max tex width/height, so can't have 1 giant 1d array
uniform uint regs[32];
uniform int layerNumber;
// outputs
out vec4 fragColor;
ivec2 GetVRamCoords(int offset)
{
return ivec2(offset % 512, offset / 512);
}
ivec2 GetPaletteCoords(int offset)
{
return ivec2(offset % 128, offset / 128);
}
uint GetLineMask(int layerNum, int yCoord)
{
uint shift = (layerNum<2) ? 16u : 0u; // need to check this, we could be endian swapped so could be wrong
uint maskPolarity = ((layerNum & 1) > 0) ? 0xFFFFu : 0x0000u;
int index = (0xF7000 / 4) + yCoord;
ivec2 coords = GetVRamCoords(index);
uint mask = ((texelFetch(vram,coords,0).r >> shift) & 0xFFFFu) ^ maskPolarity;
return mask;
}
bool GetPixelMask(int layerNum, int xCoord, int yCoord)
{
uint lineMask = GetLineMask(layerNum, yCoord);
uint maskTest = 1 << (15-(xCoord/32));
return (lineMask & maskTest) != 0;
}
int GetLineScrollValue(int layerNum, int yCoord)
{
int index = ((0xF6000 + (layerNum * 0x400)) / 4) + (yCoord / 2);
int shift = (yCoord % 2) * 16; // double check this
ivec2 coords = GetVRamCoords(index);
return int((texelFetch(vram,coords,0).r >> shift) & 0xFFFFu);
}
int GetTileNumber(int xCoord, int yCoord, int xScroll, int yScroll)
{
int xIndex = ((xCoord + xScroll) / 8) & 0x3F;
int yIndex = ((yCoord + yScroll) / 8) & 0x3F;
return (yIndex*64) + xIndex;
}
int GetTileData(int layerNum, int tileNumber)
{
int addressBase = (0xF8000 + (layerNum * 0x2000)) / 4;
int offset = tileNumber / 2; // two tiles per 32bit word
int shift = (1 - (tileNumber % 2)) * 16; // triple check this
ivec2 coords = GetVRamCoords(addressBase+offset);
uint data = (texelFetch(vram,coords,0).r >> shift) & 0xFFFFu;
return int(data);
}
int GetVFine(int yCoord, int yScroll)
{
return (yCoord + yScroll) & 7;
}
int GetHFine(int xCoord, int xScroll)
{
return (xCoord + xScroll) & 7;
}
// register data
bool LineScrollMode (int layerNum) { return (regs[0x60/4 + layerNum] & 0x8000) != 0; }
int GetHorizontalScroll(int layerNum) { return int(regs[0x60 / 4 + layerNum] &0x3FFu); }
int GetVerticalScroll (int layerNum) { return int((regs[0x60/4 + layerNum] >> 16) & 0x1FFu); }
int LayerPriority () { return int((regs[0x20/4] >> 8) & 0xFu); }
bool LayerIs4Bit (int layerNum) { return (regs[0x20/4] & (1 << (12 + layerNum))) != 0; }
bool LayerEnabled (int layerNum) { return (regs[0x60/4 + layerNum] & 0x80000000) != 0; }
bool LayerSelected (int layerNum) { return (LayerPriority() & (1 << layerNum)) == 0; }
float Int8ToFloat(uint c)
{
if((c & 0x80u) > 0u) { // this is a bit harder in GLSL. Top bit means negative number, we extend to make 32bit
return float(int(c | 0xFFFFFF00u)) / 128.0;
}
else {
return float(c) / 127.0;
}
}
vec4 AddColourOffset(int layerNum, vec4 colour)
{
uint offsetReg = regs[(0x40/4) + layerNum/2];
vec4 c;
c.b = Int8ToFloat((offsetReg >>16) & 0xFFu);
c.g = Int8ToFloat((offsetReg >> 8) & 0xFFu);
c.r = Int8ToFloat((offsetReg >> 0) & 0xFFu);
c.a = 0.0;
colour += c;
return clamp(colour,0.0,1.0); // clamp is probably not needed since will get clamped on render target
}
vec4 Int16ColourToVec4(uint colour)
{
uint alpha = (colour>>15); // top bit is alpha. 1 means clear, 0 opaque
alpha = ~alpha; // invert
alpha = alpha & 0x1u; // mask bit
vec4 c;
c.r = float((colour >> 0 ) & 0x1F) / 31.0;
c.g = float((colour >> 5 ) & 0x1F) / 31.0;
c.b = float((colour >> 10) & 0x1F) / 31.0;
c.a = float(alpha) / 1.0;
c.rgb *= c.a; // multiply by alpha value, this will push transparent to black, no branch needed
return c;
}
vec4 GetColour(int layerNum, int paletteOffset)
{
ivec2 coords = GetPaletteCoords(paletteOffset);
uint colour = texelFetch(palette,coords,0).r;
vec4 col = Int16ColourToVec4(colour); // each colour is only 16bits, but occupies 32bits
return AddColourOffset(layerNum,col); // apply colour offsets from registers
}
vec4 Draw4Bit(int layerNum, int tileData, int hFine, int vFine)
{
// Tile pattern offset: each tile occupies 32 bytes when using 4-bit pixels (offset of tile pattern within VRAM)
int patternOffset = ((tileData & 0x3FFF) << 1) | ((tileData >> 15) & 1);
patternOffset *= 32;
patternOffset /= 4;
// Upper color bits; the lower 4 bits come from the tile pattern
int paletteIndex = tileData & 0x7FF0;
ivec2 coords = GetVRamCoords(patternOffset+vFine);
uint pattern = texelFetch(vram,coords,0).r;
pattern = (pattern >> ((7-hFine)*4)) & 0xFu; // get the pattern for our horizontal value
return GetColour(layerNum, paletteIndex | int(pattern));
}
vec4 Draw8Bit(int layerNum, int tileData, int hFine, int vFine)
{
// Tile pattern offset: each tile occupies 64 bytes when using 8-bit pixels
int patternOffset = tileData & 0x3FFF;
patternOffset *= 64;
patternOffset /= 4;
// Upper color bits
int paletteIndex = tileData & 0x7F00;
// each read is 4 pixels
int offset = hFine / 4;
ivec2 coords = GetVRamCoords(patternOffset+(vFine*2)+offset); // 8-bit pixels, each line is two words
uint pattern = texelFetch(vram,coords,0).r;
pattern = (pattern >> ((3-(hFine%4))*8)) & 0xFFu; // shift out the bits we want for this pixel
return GetColour(layerNum, paletteIndex | int(pattern));
}
void main()
{
ivec2 pos = ivec2(gl_FragCoord.xy);
int scrollX;
if(LineScrollMode(layerNumber)) {
scrollX = GetLineScrollValue(layerNumber, pos.y);
}
else {
scrollX = GetHorizontalScroll(layerNumber);
}
int scrollY = GetVerticalScroll(layerNumber);
int tileNumber = GetTileNumber(pos.x,pos.y,scrollX,scrollY);
int hFine = GetHFine(pos.x,scrollX);
int vFine = GetVFine(pos.y,scrollY);
bool pixelMask = GetPixelMask(layerNumber,pos.x,pos.y);
if(pixelMask==true) {
int tileData = GetTileData(layerNumber,tileNumber);
if(LayerIs4Bit(layerNumber)) {
fragColor = Draw4Bit(layerNumber,tileData,hFine,vFine);
}
else {
fragColor = Draw8Bit(layerNumber,tileData,hFine,vFine);
}
}
else {
fragColor = vec4(0.0);
}
}
)glsl";
#endif // INCLUDED_SHADERS2D_H

View file

@ -306,6 +306,7 @@ xcopy /D /Y "$(ProjectDir)..\Assets\*" "$(TargetDir)Assets"</Command>
<ClCompile Include="..\Src\Debugger\SupermodelDebugger.cpp" />
<ClCompile Include="..\Src\Debugger\Watch.cpp" />
<ClCompile Include="..\Src\GameLoader.cpp" />
<ClCompile Include="..\Src\Graphics\FBO.cpp" />
<ClCompile Include="..\Src\Graphics\Legacy3D\Error.cpp" />
<ClCompile Include="..\Src\Graphics\Legacy3D\Legacy3D.cpp" />
<ClCompile Include="..\Src\Graphics\Legacy3D\Models.cpp" />
@ -478,6 +479,7 @@ xcopy /D /Y "$(ProjectDir)..\Assets\*" "$(TargetDir)Assets"</Command>
<ClInclude Include="..\Src\Debugger\SupermodelDebugger.h" />
<ClInclude Include="..\Src\Debugger\Watch.h" />
<ClInclude Include="..\Src\GameLoader.h" />
<ClInclude Include="..\Src\Graphics\FBO.h" />
<ClInclude Include="..\Src\Graphics\IRender3D.h" />
<ClInclude Include="..\Src\Graphics\Legacy3D\Legacy3D.h" />
<ClInclude Include="..\Src\Graphics\Legacy3D\Shaders3D.h" />

View file

@ -467,6 +467,9 @@
<ClCompile Include="..\Src\OSD\SDL\Crosshair.cpp">
<Filter>Source Files\OSD\SDL</Filter>
</ClCompile>
<ClCompile Include="..\Src\Graphics\FBO.cpp">
<Filter>Source Files\Graphics</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<MASM Include="..\Src\CPU\68K\Turbo68K\Turbo68K.asm">
@ -847,6 +850,9 @@
<ClInclude Include="..\Src\OSD\SDL\Crosshair.h">
<Filter>Header Files\OSD\SDL</Filter>
</ClInclude>
<ClInclude Include="..\Src\Graphics\FBO.h">
<Filter>Header Files\Graphics</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<CustomBuild Include="..\Src\Debugger\ReadMe.txt">