• Updated 2023-07-12: Hello, Guest! Welcome back, and be sure to check out this follow-up post about our outage a week or so ago.

VetteHack

Toni_

Well-known member
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.

Code:
// 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();
}
 

ravuya

Active member
Thanks for doing this and especially for the quality write-up. I look forward to learning from it!

 

Byrd

Well-known member
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.

 

BadGoldEagle

Well-known member
 @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.

 
Last edited by a moderator:

joel

New member
Hey - I'm new here and not sure if this thread is closed. I was a developer for Vette way back when. This is really cool - I got 1.0.1 up and running with this and Basilisk. I left before the Color QD bug showed up but would love to dig back in and fix it. I'm retired now and would like to purchase the rights to the game, maybe open source it. I think Hasbro may own it now. More on all this later...
 

cheesestraws

Well-known member
Hey @joel and welcome! Threads here never really close, they just kind of peter out. Somewhat like the hotel california, you know. Looking forward to hearing more :).
 

Renegade

Well-known member
Hey - I'm new here and not sure if this thread is closed. I was a developer for Vette way back when. This is really cool - I got 1.0.1 up and running with this and Basilisk. I left before the Color QD bug showed up but would love to dig back in and fix it. I'm retired now and would like to purchase the rights to the game, maybe open source it. I think Hasbro may own it now. More on all this later...
Very happy to count one of Vette's developers among us !
Vette was the first game I bought (Xmas 1991), a week after my Mac Classic... And my first encounter with a system error too :LOL:
 
Top