Toni_ Posted August 17, 2020 Report Share Posted August 17, 2020 Hi, I just wanted to share with you guys a crazy little hack for Color VETTE! I made during the past couple weekends. Basically, it should fix the graphic corruption bug the game has on System 7.1, and seems to work at least on my Mac OS 8.1 in Basilisk II, but I haven't had time/chance to test on much other systems. I have written detailed information about the bug, and have the download link (including the source code) here - Note that the download link is the end of the page: https://mace.software/vettehack/ (Not strictly related to the MACE project, I just don't have other hosting services available right now, so I added a "Side Projects" section on the page for this kind of purposes ) If you have Color VETTE!, and are suffering from the graphics glitching, please try it out and let me know if it works on your systems (the code is VERY hacky and mostly UNTESTED, so crashes may happen! USE AT YOUR OWN RISK!) (Note that you still need to have 16-color mode supported to run Vette! in color, so I think Power Macs won't be able to run it, as to my understanding they don't go below 256 colors) Thanks! Ps. I've also included the source below, so you can check it out if you're interested, to see what kind of crazy things the hack does. The source file download on my page will include complete CWPro 2.1 project & resources. // VETTEHACK 1.0.0 // =============== // // This extension should fix the graphics corruption issue // in Color VETTE! on System 7.1 and later. // // DISCLAIMER: This code is VERY HACKY! That is, you should // consider it as "How to NOT write system patches" example :-) // // Please see accompanying READ ME FIRST!!! file for more information // // Last modified: // 2020-08-17 by Toni Lääveri // // Changelist: // 2020-08-17 • First release of prototype source #include "ShowInitIcon.h" #include "Types.h" #include "Windows.h" #include "Memory.h" #include "Resources.h" #include "Sound.h" #include "OSUtils.h" #include "SetupA4.h" #include "A4Stuff.h" #include "LowMem.h" #include "Processes.h" #include "Quickdraw.h" #include "QDOffscreen.h" #include "Gestalt.h" // Static globals (referenced through A4) ProcPtr gOldLoadSeg; ProcPtr gOldQDExtensions; Ptr gTmpReturnAddress; GWorldPtr* gOffscreenGWorldPtr; // Forward declarations for the functions short NumToolboxTraps(void); TrapType GetTrapType (int theTrap); Boolean CheckTrapAvailable (int theTrap); static pascal void CheckGWorldRowBytes(); static asm void PatchedQDExtensions(); static asm void CheckNewGWorld(); static asm void PatchedLoadSeg(); static pascal void CheckVettePatch(SInt16 segment); void main(void); short NumToolboxTraps(void) { if (NGetTrapAddress(0xA86E, ToolTrap) == NGetTrapAddress(0xaa6e, ToolTrap)) return 0x200; else return 0x400; } TrapType GetTrapType (int theTrap) { if (theTrap & 0x800) return ToolTrap; else return OSTrap; } Boolean CheckTrapAvailable (int theTrap) { TrapType tType; tType = GetTrapType(theTrap); if (tType == ToolTrap) { theTrap = (theTrap & 0x7ff); if (theTrap >= NumToolboxTraps()) theTrap = _Unimplemented;//0xA89F } return ( NGetTrapAddress(theTrap, tType) != NGetTrapAddress(_Unimplemented, ToolTrap)); } // Just a debug routine to convert numbers to hexadecimal strings static void Hex2Str(UInt32 value, StringPtr str) { const char* hexchars = "0123456789ABCDEF"; int i; for (i = 0; i < 8; i++) str[8 - i] = hexchars[(value >> (i * 4)) & 0xF]; str[0] = 8; } // Check if the allocated GWorld needs "fixing" :-P static pascal void CheckGWorldRowBytes() { PixMapHandle pm; SInt16 expectedRowBytes, width; //GrafPtr savePort; //Str255 str; //UInt32 finalTicks; // The Vette expects rowBytes to be calculated using this formula. // Basically, rowbytes aligned up to nearest 32-bit boundary, PLUS // a 32-bit slop (later version of QuickDraw uses 128-bit alignment // AND 128-bit slop, which causes the rowBytes to be wildly different, // which in turn causes the graphic corruption). pm = (**gOffscreenGWorldPtr).portPixMap; width = (**pm).bounds.right - (**pm).bounds.left; expectedRowBytes = (((width * (**pm).pixelSize >> 3) + 3) & ~3) + 4; /* // This is just hacky "hack" o'hack debug code to print // some addresses in the FrontWindow for debugging. Did not // find NMI button in Basilisk II, so just used this to do the // dirty work... GetPort(&savePort); SetPort(FrontWindow()); TextFont(0); TextSize(12); TextMode(0); TextFace(0); Hex2Str((UInt32)expectedRowBytes, str); MoveTo(20,20); DrawString(str); Hex2Str((UInt32)((**pm).rowBytes & 0x3FFF), str); MoveTo(20,40); DrawString(str); Delay(60,&finalTicks); SetPort(savePort); */ // REALLY dirty HACK! If rowBytes does not match expected, we just // bravely replace rowBytes with "correct" value. We avoid // reallocation of pixmap data buffer, because the allocated // buffers are larger than what they need to be, and will not // overflow. In this case, it wil work and just leave unused data // at the end. The role of the slop at end of each pixmap row is // a bit unclear, but at least in most of Vette's cases the alignment // (4 bytes instead of 16) should not be a problem, because the rects // used are already aligned on that boundary. In worst case, it might // cause performance issues, BUT that is exactly why this patch is ONLY // made for Vette's process, and no one else, to avoid blowing other // stuff up accidentally. if (((**pm).rowBytes & 0x3FFF) != expectedRowBytes) { (**pm).rowBytes = (expectedRowBytes & 0x3FFF) | ((**pm).rowBytes & 0xC000); } } // This function is invoked after NewGWorld call has been made, as // it was injected as return address by PatchedQDExtensions. // It will call CheckGWorldRowBytes to analyze and hack the GWorld data static asm void CheckNewGWorld() { // Reserve space for return address CLR.L -(A7) // Three locals, for saving D0, A4 and result code LINK a6,#-8 // Stack state at this point // +8 DWORD - Return value (QDErr) // +4 DWORD - Space reserved for return address // A6 +0 DWORD - Old A6 from LINK // -4 DWORD - Space reserved for saving D0 // -8 DWORD - Space reserved for saving A4 // Save the D0 state from caller MOVE.L d0,-4(a6) JSR SetUpA4 // D0 contains old A4, save in local variable MOVE.L d0,-8(a6) // Put original caller's return address in the reserved space MOVE.L gTmpReturnAddress,4(a6) // Save some extra registers which C funtion may trash MOVEM.L d1-d2/a0-a1,-(a7) // Analyze and hack the GWorld data JSR CheckGWorldRowBytes // Restore the extra registers MOVEM.L (a7)+,d1-d2/a0-a1 // Restore A4 MOVE.L -8(a6),d0 MOVE.L d0,a4 // Restore D0 MOVE.L -4(a6),d0 // Unlink A6 UNLK a6 RTS } // Patch for QuickDraw's QDExtensions trap. Basically, if // NewGWorld is called, this code injects address of // CheckNewGWorld routine to be executed after the NewGWorld // call, which will analyze and hack the GWorld structure // if needed to make Vette work static asm void PatchedQDExtensions() { // Reserve space for qdextensions jump CLR.L -(A7) // Three locals, for saving D0, A0 and A4 LINK a6,#-12 // Stack state at this point (for NewGWorld selector) // +34 WORD - Space for the return value (QDErr) // +30 DWORD - GWorldPtr* offscreenGWorld // +28 WORD - short PixelDepth // +24 DWORD - const Rect* boundsRect // +20 DWORD - CTableHandle cTable // +16 DWORD - GDHandle aGDevice // +12 DWORD - GWorldFlags flags // +8 DWORD - Return address of the caller (will be replaced with CheckNewGWorld) // +4 DWORD - Space reserved for gOldQDExtensions // A6 +0 DWORD - Old A6 from LINK // -4 DWORD - Space reserved for saving D0 // -8 DWORD - Space reserved for saving A4 // -12 DWORD - Space reserved for saving A0 MOVE.L a0,-12(a6) // Save the D0 state from caller MOVE.L d0,-4(a6) JSR SetUpA4 // D0 contains old A4, save in local variable MOVE.L d0,-8(a6) MOVE.L gOldQDExtensions,4(a6) // We saved original D0 value at -4, which contains the // selector number passed to QDExtensions. Technically, // the correct selector for NewGWorld should be 0x00160000, // where 0x0016 indicates the size of arguments passed into // the call as bytes, and lower 16 bits are the selector // itself (zero), but Vette uses what is probably older // call convention where the upper word is not set, so we // only compare the lower part (as the QDExtensions handler // does) CLR.L d0 CMP.L -4(a6),d0 // For any other selectors, just pass control to NewGWorld as // usually, and return to caller after that BNE.S __skipTweakReturnAddress // This *IS* NewGWorld call, so first save off the caller's // return address (we need that after doing the check) MOVE.L 8(a6),gTmpReturnAddress // And replace return address with the CheckNewGWorld, to // analyze and "hack" the GWorld LEA CheckNewGWorld,a0 MOVE.L a0,8(a6) // Get the GWorldPtr* pointer off the stack, because NewGWorld // call will remove the arguments from stack MOVE.L 8+4+4+4+4+4+2(a6),gOffscreenGWorldPtr __skipTweakReturnAddress: // Restore A4 MOVE.L -8(a6),d0 MOVE.L d0,a4 // Restore D0 MOVE.L -4(a6),d0 MOVE.L -12(a6),a0 UNLK a6 RTS } // Check if the QuickDraw patch should be installed for this invocation of // LoadSeg trap. Basically, check if current process is Vette, and if the // patch has already been installed for this process. pascal void CheckVettePatch(SInt16 segment) { #pragma unused(segment) ProcPtr curQDExtensions; ProcessSerialNumber psn; ProcessInfoRec info; // Check if the QuickDraw patch already installed, and exit if it is curQDExtensions = NGetTrapAddress(_QDExtensions, ToolTrap); if (curQDExtensions == (ProcPtr)PatchedQDExtensions) return; // Ensure process manager is installed. We need it to detect if Vette is // running if (NGetTrapAddress(_OSDispatch, ToolTrap) == NGetTrapAddress(_Unimplemented, ToolTrap)) return; // Check if current process is Vette ('VETT' application creator) if (noErr == GetCurrentProcess(&psn)) { info.processInfoLength = sizeof(info); info.processName = nil; info.processAppSpec = nil; if (noErr == GetProcessInformation(&psn,&info)) { if (info.processSignature == 'VETT') { // Yep, install the patch gOldQDExtensions = curQDExtensions; NSetTrapAddress((UniversalProcPtr)&PatchedQDExtensions, _QDExtensions, ToolTrap); } } } } // LoadSeg trap patch. It will just setup A4 for accessing global variables, // and call CheckVettePatch to check if the actual QuickDraw patch should be // activated. static asm void PatchedLoadSeg() { // Reserve space for address to gOldLoadSeg, for RTS CLR.L -(A7) // Two locals for saving D0 and A4 LINK a6,#-8 // Stack state at this point: // +12 WORD - Segment number (argument for LoadSeg) // +8 DWORD - Return address of the caller // +4 DWORD - Space reserved for gOldLoadSeg // A6 +0 DWORD - Old A6 from LINK // -4 DWORD - Space reserved for saving D0 // -8 DWORD - Space reserved for saving A4 // Save the D0 state from caller MOVE.L d0,-4(a6) JSR SetUpA4 // D0 contains old A4 from SetupA4, save in local variable MOVE.L d0,-8(a6) // Need to save some extra registers too, because some glue code // trashes at least D1 (NGetTrapAddress) MOVEM.L d1/a0-a1,-(a7) // Push segment number as pascal argument on stack MOVE.W 12(a6),-(a7) // The magic; Will check if Vette running & patch QuickDraw JSR CheckVettePatch // Restore the extra registers MOVEM.L (a7)+,d1/a0-a1 // Move actual LoadSeg address from globals, to the space // reserved in stack for RTS MOVE.L gOldLoadSeg,4(a6) // Restore A4 MOVE.L -8(a6),d0 MOVE.L d0,a4 // Restore D0 MOVE.L -4(a6),d0 // Unlink A6 & jump to real LoadSeg through RTS (all registers preserved) UNLK a6 RTS } void main(void) { Handle theINIT; long response; Boolean canInstall = false; EnterCodeResource(); PrepareCallback(); // Only activate this INIT on System 7.1 and later if (CheckTrapAvailable(_Gestalt) && CheckTrapAvailable(_QDExtensions) && ((Gestalt(gestaltSystemVersion, &response) == noErr) && (response >= 0x710))) { // Draw the fancy icon to let people know we're active ShowInitIcon(128,true); // Get handle for this code resource theINIT = GetResource('INIT', 0); if (theINIT) { // Turn resource into regular handle, and make sure // it will stay in the system heap DetachResource(theINIT); HNoPurge(theINIT); HLock(theINIT); // Save old LoadSeg for chaining, and set the new trap handler gOldLoadSeg = (ProcPtr)NGetTrapAddress(_LoadSeg, ToolTrap); NSetTrapAddress((UniversalProcPtr)&PatchedLoadSeg, _LoadSeg, ToolTrap); } } else { // Cannot install; show the INIT icon, and draw X to cross // it over with ShowInitIcon(128,false); ShowInitIcon(129,true); } ExitCodeResource(); } Quote Link to post Share on other sites
ravuya Posted August 17, 2020 Report Share Posted August 17, 2020 Thanks for doing this and especially for the quality write-up. I look forward to learning from it! Quote Link to post Share on other sites
Byrd Posted August 17, 2020 Report Share Posted August 17, 2020 Great! VETTE! continues to be one of my favourite Mac games, I'll try it out on a machine in the next week or so. Quote Link to post Share on other sites
nickpunt Posted August 18, 2020 Report Share Posted August 18, 2020 Love this game, thanks for fixing this issue! Quote Link to post Share on other sites
BadGoldEagle Posted November 9, 2020 Report Share Posted November 9, 2020 (edited) @Toni_ It took me a while to test it (my Quadra being dead and all) but I can report that it works 100% correctly on my Q950 running 8.1. Vette! is my favorite game. Thanks for fixing this bug! Now if only it would run at double the resolution... Also do you think there'd be a way to fix the bisected singer in the intro? https://youtu.be/NtDgTz2peXw?t=74 Kuranov probably used an emulator but this bug also occurs on real hardware. Edited November 9, 2020 by BadGoldEagle Quote Link to post Share on other sites
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.