[Scummvm-git-logs] scummvm master -> 50674c1a69af97ec6a98cb5296640baf66ca9153

neuromancer noreply at scummvm.org
Tue Jun 9 13:22:12 UTC 2026


This automated email contains information about 7 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
7dc5e27590 SCUMM: RA2: reset music after each mission
34e0d8d555 SCUMM: RA2: initial code to support high resolution mode, disabled by default
b05208d428 SCUMM: RA2: perspective correction in high resolution mode for L2
f00a3eb8de SCUMM: RA2: gameplay corrected for in high resolution mode for L3
9644e84561 SCUMM: RA2: implement damage recovery
74401bdb4d SCUMM: RA2: some features for yoda mode
50674c1a69 SCUMM: RA1: no damage mode


Commit: 7dc5e27590620f4864e3ecb7a257d00aa9ea4b14
    https://github.com/scummvm/scummvm/commit/7dc5e27590620f4864e3ecb7a257d00aa9ea4b14
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-09T15:21:42+02:00

Commit Message:
SCUMM: RA2: reset music after each mission

Changed paths:
    engines/scumm/insane/rebel2/audio.cpp
    engines/scumm/insane/rebel2/levels.cpp
    engines/scumm/insane/rebel2/rebel.h
    engines/scumm/insane/rebel2/runlevels.cpp


diff --git a/engines/scumm/insane/rebel2/audio.cpp b/engines/scumm/insane/rebel2/audio.cpp
index cddd81a69c0..989d93074ef 100644
--- a/engines/scumm/insane/rebel2/audio.cpp
+++ b/engines/scumm/insane/rebel2/audio.cpp
@@ -48,6 +48,14 @@ void InsaneRebel2::terminateAudio() {
 	_audio.terminate();
 }
 
+void InsaneRebel2::resetVideoAudio() {
+	_audio.reset();
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	if (splayer)
+		splayer->resetAudioTracks();
+}
+
 // queueAudioData -- Queue raw PCM data for playback on a track.
 // Creates the queuing stream on first use. RA2 audio is 8-bit unsigned mono.
 void InsaneRebel2::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
diff --git a/engines/scumm/insane/rebel2/levels.cpp b/engines/scumm/insane/rebel2/levels.cpp
index a5d34e8b9fa..a90f85136cb 100644
--- a/engines/scumm/insane/rebel2/levels.cpp
+++ b/engines/scumm/insane/rebel2/levels.cpp
@@ -186,6 +186,7 @@ void InsaneRebel2::playMissionBriefing() {
 // All wrapper functions (FUN_00417168/4171c5/417ab2/417327) add | 8 before calling FUN_0041f4d0.
 void InsaneRebel2::playCinematic(const char *filename) {
 	restoreDamageFlashPalette();
+	resetVideoAudio();
 	_gameplaySectionActive = false;
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;  // No status bar during cinematics
@@ -202,6 +203,7 @@ void InsaneRebel2::playVideoWithText(const char *filename, int textID, int textX
                                      int fadeInFrame, int fadeOutFrame) {
 
 	restoreDamageFlashPalette();
+	resetVideoAudio();
 	_gameplaySectionActive = false;
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;
@@ -274,6 +276,7 @@ void InsaneRebel2::playLevelBegin(int levelId) {
 void InsaneRebel2::playLevelEnd(int levelId) {
 
 	restoreDamageFlashPalette();
+	resetVideoAudio();
 	_gameplaySectionActive = false;
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;  // No status bar during end cinematic
@@ -294,6 +297,7 @@ void InsaneRebel2::playLevelEnd(int levelId) {
 void InsaneRebel2::playLevelRetry(int levelId) {
 
 	restoreDamageFlashPalette();
+	resetVideoAudio();
 	_gameplaySectionActive = false;
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;  // Reset for retry - will be set by IACT opcode 6 if needed
@@ -314,6 +318,7 @@ void InsaneRebel2::playLevelRetry(int levelId) {
 void InsaneRebel2::playLevelGameOver(int levelId) {
 
 	restoreDamageFlashPalette();
+	resetVideoAudio();
 	_gameplaySectionActive = false;
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;  // No status bar during game over cinematic
@@ -381,6 +386,7 @@ void InsaneRebel2::playEndingSequence() {
 void InsaneRebel2::playCreditsSequence() {
 
 	debug("Rebel2: Playing menu credits");
+	resetVideoAudio();
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 	splayer->setCurVideoFlags(0x20);
@@ -721,6 +727,7 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
 void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
 
 	restoreDamageFlashPalette();
+	resetVideoAudio();
 	_gameplaySectionActive = false;
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;  // No status bar during death cinematic
@@ -749,6 +756,7 @@ void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
 void InsaneRebel2::playLevelRetryVariant(int levelId, int phase) {
 
 	restoreDamageFlashPalette();
+	resetVideoAudio();
 	_gameplaySectionActive = false;
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;  // Reset for retry - will be set by IACT opcode 6 if needed
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 9df2a13101b..1ab92ca4a60 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -1312,6 +1312,9 @@ public:
 	// Terminate audio system
 	void terminateAudio();
 
+	// Reset streamed SAN audio at independent video boundaries.
+	void resetVideoAudio();
+
 	// Process audio dispatches - called from SmushPlayer when iMUSE is null
 	// This replaces the iMUSE audio path for RA2
 	void processAudioFrame(int16 feedSize);
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index ede78f398b2..2048677e5ea 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -199,6 +199,8 @@ bool InsaneRebel2::playLevelSegment(const char *filename, uint16 flags, bool rec
 		_gameplaySectionActive = true;
 		enableIOSGamepadController();
 	} else {
+		if (_gameplaySectionActive)
+			resetVideoAudio();
 		_gameplaySectionActive = false;
 		restoreIOSGamepadController();
 	}


Commit: 34e0d8d5556a5927b28a56c469f82905ec32056e
    https://github.com/scummvm/scummvm/commit/34e0d8d5556a5927b28a56c469f82905ec32056e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-09T15:21:42+02:00

Commit Message:
SCUMM: RA2: initial code to support high resolution mode, disabled by default

Changed paths:
    engines/scumm/insane/rebel2/iact.cpp
    engines/scumm/insane/rebel2/levels.cpp
    engines/scumm/insane/rebel2/menu.cpp
    engines/scumm/insane/rebel2/rebel.cpp
    engines/scumm/insane/rebel2/rebel.h
    engines/scumm/insane/rebel2/render.cpp
    engines/scumm/metaengine.cpp
    engines/scumm/scumm.cpp
    engines/scumm/smush/rebel/smush_player_ra2.cpp
    engines/scumm/smush/rebel/smush_player_ra2.h
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 4d484fdb487..6487dd5528b 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -827,8 +827,9 @@ void InsaneRebel2::handleOpcode6Handler7(Common::SeekableReadStream &b, int16 pa
 	// Step 1: Raw mouse input as offset from screen center.
 	// DAT_0047a7e0 = mouseX - 160, DAT_0047a7e2 = mouseY - 100.
 	// Handler 7 applies DAT_0047a7fe to its local vertical input after clamping.
-	const int16 mouseX = _vm->_mouse.x;
-	const int16 mouseY = _vm->_mouse.y;
+	const Common::Point aimPos = getGameplayAimPoint();
+	const int16 mouseX = aimPos.x;
+	const int16 mouseY = aimPos.y;
 	int16 inputX = (int16)(mouseX - 160);  // DAT_0047a7e0
 	int16 inputY = (int16)(mouseY - 100);  // DAT_0047a7e2
 
@@ -1555,8 +1556,8 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 // Handler-specific routing:
 //   Handler 7  (FLY):  FLY NUT sprites via par4 (1, 2, 3, 11)
 //   Handler 8  (POV):  POV NUT sprites via par3 (1, 3, 6, 7) or background via par4=5
-//   Handler 0x26 (turret): Turret HUD NUT via par3 (1-4)
-//   Handler 0x19: Mixed turret mode, similar to 0x26
+//   Handler 0x26 (turret): Turret HUD NUT via par3/par4 (1-4)
+//   Handler 0x19: Speeder bike GRD/HUD resources via par4
 //
 // ScummVM refactor helper for opcode 8 Handler 7 FLY loading, not a separate retail function.
 bool InsaneRebel2::loadOpcode8Handler7FlySprites(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4) {
@@ -1738,11 +1739,14 @@ void InsaneRebel2::loadOpcode8EmbeddedAnim(byte *renderBitmap, Common::SeekableR
 bool InsaneRebel2::handleOpcode8EmbeddedAnim(byte *renderBitmap, byte *animData, int32 animDataSize, int16 par3, int16 par4) {
 	bool handled = false;
 
-	// Handler 0x26/0x19: Turret HUD Overlays.
-	// FUN_00407fcb case 8: par3 1-4 for HUD NUT loading.
-	if (!handled && (_rebelHandler == 0x26 || _rebelHandler == 0x19)) {
-		if (par3 >= 1 && par3 <= 4) {
-			handled = loadTurretHudOverlay(animData, animDataSize, par3);
+	// Handler 0x26: Turret HUD Overlays.
+	// FUN_00407fcb case 8: handler 0x26 uses par4 1-4 for HUD NUT loading.
+	// Some chunks use par3 for the same low/high selector.
+	if (!handled && _rebelHandler == 0x26) {
+		int hudSelector = (par4 >= 1 && par4 <= 4) ? par4 : par3;
+
+		if (hudSelector >= 1 && hudSelector <= 4) {
+			handled = loadTurretHudOverlay(animData, animDataSize, hudSelector);
 		}
 	}
 
@@ -1780,9 +1784,13 @@ bool InsaneRebel2::handleOpcode8EmbeddedAnim(byte *renderBitmap, byte *animData,
 	// Fallback: Embedded SAN HUD overlays.
 	// For other cases, load as embedded SAN frame to HUD overlay slots.
 	if (!handled) {
-		// Skip high-res data (par3 == 2, 4).
-		if (par3 == 2 || par3 == 4) {
-			debug("Rebel2 Opcode 8: Skipping high-res HUD par3=%d", par3);
+		const bool highRes = isHiRes();
+		const bool highResHud = (par3 == 2 || par3 == 4);
+		const bool lowResHud = (par3 == 1 || par3 == 3);
+
+		if ((!highRes && highResHud) || (highRes && lowResHud)) {
+			debug("Rebel2 Opcode 8: Skipping %s HUD par3=%d while running in %s mode",
+				highResHud ? "high-res" : "low-res", par3, highRes ? "high-res" : "low-res");
 			handled = true;
 		} else {
 			// Determine userId: Handler 0x19 uses par3, others use par4.
@@ -2126,45 +2134,48 @@ bool InsaneRebel2::loadHandler7FlySprites(Common::SeekableReadStream &b, int64 r
 	return assigned;
 }
 
-// loadTurretHudOverlay -- Handler 0x26/0x19 turret HUD loading (FUN_00407fcb case 8).
-bool InsaneRebel2::loadTurretHudOverlay(byte *animData, int32 size, int16 par3) {
-	// Handler 0x26/0x19 turret HUD overlay loading - FUN_00407fcb case 8
+// loadTurretHudOverlay -- Handler 0x26 turret HUD loading (FUN_00407fcb case 8).
+bool InsaneRebel2::loadTurretHudOverlay(byte *animData, int32 size, int16 selector) {
+	// Handler 0x26 turret HUD overlay loading - FUN_00407fcb case 8
 	// Resolution-dependent loading:
-	//   par3 == 1: Low-res primary HUD (DAT_0047fe78 / _hudOverlayNut)
-	//   par3 == 2: High-res primary HUD (skip in 320x200 mode)
-	//   par3 == 3: Low-res secondary HUD (DAT_0047fe80 / _hudOverlay2Nut)
-	//   par3 == 4: High-res secondary HUD (skip in 320x200 mode)
+	//   selector == 1: Low-res primary HUD (DAT_0047fe78 / _hudOverlayNut)
+	//   selector == 2: High-res primary HUD (DAT_0047fe78 / _hudOverlayNut)
+	//   selector == 3: Low-res secondary HUD (DAT_0047fe80 / _hudOverlay2Nut)
+	//   selector == 4: High-res secondary HUD (DAT_0047fe80 / _hudOverlay2Nut)
 
 	if (!animData || size <= 0) {
 		return false;
 	}
 
-	// ScummVM runs at 320x200 (low-res), skip high-res data
-	if (par3 == 2 || par3 == 4) {
-		debug("Rebel2 loadTurretHudOverlay: Skipping high-res HUD par3=%d (running in low-res mode)", par3);
-		return true;  // Successfully "handled" by skipping
+	const bool highRes = isHiRes();
+	const int primarySlot = highRes ? 2 : 1;
+	const int secondarySlot = highRes ? 4 : 3;
+
+	if (selector >= 1 && selector <= 4 && selector != primarySlot && selector != secondarySlot) {
+		debug("Rebel2 loadTurretHudOverlay: Skipping %s HUD selector=%d (running in %s mode)",
+			(selector == 2 || selector == 4) ? "high-res" : "low-res", selector,
+			highRes ? "high-res" : "low-res");
+		return true;
 	}
 
-	if (par3 != 1 && par3 != 3) {
+	if (selector != primarySlot && selector != secondarySlot) {
 		return false;  // Not a turret HUD slot
 	}
 
 	NutRenderer *newNut = makeRebel2SpriteFromData(_vm, animData, size);
 	if (!newNut || newNut->getNumChars() <= 0) {
-		debug("Rebel2 loadTurretHudOverlay: NUT load failed for par3=%d", par3);
+		debug("Rebel2 loadTurretHudOverlay: NUT load failed for selector=%d", selector);
 		delete newNut;
 		return false;
 	}
 
-	debug("Rebel2 loadTurretHudOverlay: Loaded turret HUD NUT par3=%d with %d sprites",
-		par3, newNut->getNumChars());
+	debug("Rebel2 loadTurretHudOverlay: Loaded turret HUD NUT selector=%d with %d sprites",
+		selector, newNut->getNumChars());
 
-	if (par3 == 1) {
-		// Low-res primary HUD overlay
+	if (selector == primarySlot) {
 		delete _hudOverlayNut;
 		_hudOverlayNut = newNut;
-	} else {  // par3 == 3
-		// Low-res secondary HUD overlay
+	} else {
 		delete _hudOverlay2Nut;
 		_hudOverlay2Nut = newNut;
 	}
diff --git a/engines/scumm/insane/rebel2/levels.cpp b/engines/scumm/insane/rebel2/levels.cpp
index a90f85136cb..b133a232415 100644
--- a/engines/scumm/insane/rebel2/levels.cpp
+++ b/engines/scumm/insane/rebel2/levels.cpp
@@ -416,10 +416,14 @@ void InsaneRebel2::warpGameplayMouseNow(int x, int y) {
 	if (eventMan)
 		eventMan->purgeMouseEvents();
 
-	_vm->_mouse.x = x;
-	_vm->_mouse.y = y;
-	_vm->_system->warpMouse(_vm->_macScreen ? x * 2 : x,
-		_vm->_macScreen ? y * 2 + 2 * _vm->_macScreenDrawOffset : y);
+	const int scale = isHiRes() ? 2 : 1;
+	const int physicalX = x * scale;
+	const int physicalY = y * scale;
+
+	_vm->_mouse.x = physicalX;
+	_vm->_mouse.y = physicalY;
+	_vm->_system->warpMouse(_vm->_macScreen ? physicalX * 2 : physicalX,
+		_vm->_macScreen ? physicalY * 2 + 2 * _vm->_macScreenDrawOffset : physicalY);
 
 	if (eventMan)
 		eventMan->purgeMouseEvents();
diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index f82fe30354e..400928d1832 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -122,12 +122,11 @@ int InsaneRebel2::processMenuInput() {
 
 	int result = -1;
 
-	// Menu item Y positions (low-res 320x200 mode):
-	// From FUN_0041f5ae: baseY = numItems * -5 + 0x68
-	// With 7 selectable items: 7 * -5 + 104 = 69
-	// Items at Y = 69, 79, 89, 99, 109, 119, 129 with spacing of 10
-	const int baseY = _menuItemCount * -5 + 0x68;
-	const int itemSpacing = 10;
+	const bool highRes = isHiRes();
+	const int baseY = highRes ? (_menuItemCount * -5 + 0x5a) * 2 + 0x1c : _menuItemCount * -5 + 0x68;
+	const int itemSpacing = highRes ? 20 : 10;
+	const int itemHitTop = highRes ? 2 : 1;
+	const int itemHitHeight = highRes ? 18 : 10;
 
 	// Process events from the queue (populated by notifyEvent)
 	while (!_menuEventQueue.empty()) {
@@ -184,7 +183,7 @@ int InsaneRebel2::processMenuInput() {
 			_vm->_mouse.y = event.mouse.y;
 			for (int i = 0; i < _menuItemCount; i++) {
 				int itemY = baseY + i * itemSpacing;
-				if (event.mouse.y >= itemY - 1 && event.mouse.y < itemY + 9) {
+				if (event.mouse.y >= itemY - itemHitTop && event.mouse.y < itemY - itemHitTop + itemHitHeight) {
 					_menuSelection = i;
 					result = _menuSelection;
 					debug("Menu: Item %d selected (mouse)", _menuSelection);
@@ -198,7 +197,8 @@ int InsaneRebel2::processMenuInput() {
 				int mouseY = event.mouse.y;
 				for (int i = 0; i < _menuItemCount; i++) {
 					int itemY = baseY + i * itemSpacing;
-					if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
+					if (mouseY >= itemY - itemHitTop - 3 * (highRes ? 2 : 1) &&
+							mouseY < itemY - itemHitTop + itemHitHeight - 3 * (highRes ? 2 : 1)) {
 						if (i != _menuSelection) {
 							_menuSelection = i;
 							debug(5, "Menu: Hover selection changed to %d (mouseY=%d)", _menuSelection, mouseY);
@@ -258,10 +258,13 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 	//   Item Y:      param_3 * -5 + i * 10 + 0x68
 	//   Box Y:       param_3 * -5 + i * 10 + 0x67  (1px above text)
 
-	const int centerX = width / 2;
-	const int titleY = numItems * -5 + (leftAligned ? 0x56 : 0x51);
-	const int itemBaseY = numItems * -5 + 0x68;
-	const int itemSpacing = 10;
+	const bool highRes = isHiRes();
+	const int centerX = highRes ? 0x140 : width / 2;
+	const int titleY = highRes ?
+		(numItems * -5 + 0x5a) * 2 + (leftAligned ? -8 : -0x12) :
+		numItems * -5 + (leftAligned ? 0x56 : 0x51);
+	const int itemBaseY = highRes ? (numItems * -5 + 0x5a) * 2 + 0x1c : numItems * -5 + 0x68;
+	const int itemSpacing = highRes ? 20 : 10;
 
 	NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
 	NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
@@ -286,7 +289,7 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 	// -------------------------------------------------------------------
 	{
 		int titleWidth = getStringWidth(items[0]);
-		int titleX = leftAligned ? 40 : (centerX - titleWidth / 2);
+		int titleX = leftAligned ? (highRes ? 0x50 : 0x28) : (centerX - titleWidth / 2);
 		drawString(items[0], titleX, titleY);
 	}
 
@@ -300,25 +303,25 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 		const char *text = items[i + 1];
 
 		int textWidth = getStringWidth(text);
-		int textX = leftAligned ? 23 : (centerX - textWidth / 2);
+		int textX = leftAligned ? (highRes ? 0x2e : 0x17) : (centerX - textWidth / 2);
 		drawString(text, textX, itemY);
 
 		// Selection highlight box - FUN_004292d0
 		if (i == selection) {
 			// Width: textWidth + ((DAT_0047a808 < 2) - 1 & 6) + 6 = textWidth + 6
-			int bracketWidth = textWidth + 6;
+			int bracketWidth = textWidth + (highRes ? 12 : 6);
 			// Height: ((DAT_0047a808 < 2) - 1 & 10) + 10 = 10
-			int bracketHeight = 10;
+			int bracketHeight = highRes ? 20 : 10;
 
 			// Flash color: (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10
 			// bit0==0: 8-16=248(0xF8), bit0==1: 0-16=240(0xF0)
 			byte highlightColor = ((_vm->_system->getMillis() / 133) & 1) ? 248 : 240;
 
 			// Box position: Y = itemY - 1 (0x67 vs 0x68)
-			int leftX = leftAligned ? 20 : (centerX - bracketWidth / 2);
+			int leftX = leftAligned ? (highRes ? 0x28 : 0x14) : (centerX - bracketWidth / 2);
 			int rightX = leftX + bracketWidth;
-			int topY = itemY - 1;
-			int bottomY = itemY + bracketHeight - 1;
+			int topY = highRes ? itemY - 2 : itemY - 1;
+			int bottomY = topY + bracketHeight - 1;
 
 			int screenW = _vm->_screenWidth;
 			int screenH = _vm->_screenHeight;
@@ -929,10 +932,14 @@ int InsaneRebel2::processChapterSelectInput() {
 			_vm->_mouse.x = event.mouse.x;
 			_vm->_mouse.y = event.mouse.y;
 			{
-				int baseY = _chapterItemCount * -5 + 0x68;
+				const bool highRes = isHiRes();
+				const int baseY = highRes ? (_chapterItemCount * -5 + 0x5a) * 2 + 0x1c : _chapterItemCount * -5 + 0x68;
+				const int itemSpacing = highRes ? 20 : 10;
+				const int itemHitTop = highRes ? 2 : 1;
+				const int itemHitHeight = highRes ? 18 : 10;
 				for (int i = 0; i < _chapterItemCount; i++) {
-					int itemY = baseY + i * 10;
-					if (event.mouse.y >= itemY - 1 && event.mouse.y < itemY + 9) {
+					int itemY = baseY + i * itemSpacing;
+					if (event.mouse.y >= itemY - itemHitTop && event.mouse.y < itemY - itemHitTop + itemHitHeight) {
 						_chapterSelection = i;
 						_previewOffsetY = _chapterSelection * -50 + 75;
 						updateMenuVirtualKeyboard();
@@ -948,12 +955,17 @@ int InsaneRebel2::processChapterSelectInput() {
 			{
 				// Mouse hover changes highlight (original FUN_0041f5ae mouse mode).
 				// Item Y = numItems * -5 + i * 10 + 0x68
-				int baseY = _chapterItemCount * -5 + 0x68;
+				const bool highRes = isHiRes();
+				const int baseY = highRes ? (_chapterItemCount * -5 + 0x5a) * 2 + 0x1c : _chapterItemCount * -5 + 0x68;
+				const int itemSpacing = highRes ? 20 : 10;
+				const int itemHitTop = highRes ? 2 : 1;
+				const int itemHitHeight = highRes ? 18 : 10;
 				int mouseY = event.mouse.y;
 
 				for (int i = 0; i < _chapterItemCount; i++) {
-					int itemY = baseY + i * 10;
-					if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
+					int itemY = baseY + i * itemSpacing;
+					if (mouseY >= itemY - itemHitTop - 3 * (highRes ? 2 : 1) &&
+							mouseY < itemY - itemHitTop + itemHitHeight - 3 * (highRes ? 2 : 1)) {
 						if (i != _chapterSelection) {
 							_chapterSelection = i;
 							_previewOffsetY = _chapterSelection * -50 + 75;
@@ -1442,8 +1454,11 @@ int InsaneRebel2::processLevelSelectInput() {
 	if (itemCount <= 0)
 		return -1;
 
-	const int itemBaseY = itemCount * -5 + 0x68;
-	const int itemSpacing = 10;
+	const bool highRes = isHiRes();
+	const int itemBaseY = highRes ? (itemCount * -5 + 0x5a) * 2 + 0x1c : itemCount * -5 + 0x68;
+	const int itemSpacing = highRes ? 20 : 10;
+	const int itemHitTop = highRes ? 2 : 1;
+	const int itemHitHeight = highRes ? 18 : 10;
 
 	while (!_menuEventQueue.empty()) {
 		Common::Event event = _menuEventQueue.pop();
@@ -1494,7 +1509,7 @@ int InsaneRebel2::processLevelSelectInput() {
 			_vm->_mouse.y = event.mouse.y;
 			for (int i = 0; i < itemCount; i++) {
 				int itemY = itemBaseY + i * itemSpacing;
-				if (event.mouse.y >= itemY - 1 && event.mouse.y < itemY + 9) {
+				if (event.mouse.y >= itemY - itemHitTop && event.mouse.y < itemY - itemHitTop + itemHitHeight) {
 					selection = i;
 					result = selection;
 					break;
@@ -1507,7 +1522,8 @@ int InsaneRebel2::processLevelSelectInput() {
 			_vm->_mouse.y = event.mouse.y;
 			for (int i = 0; i < itemCount; i++) {
 				int itemY = itemBaseY + i * itemSpacing;
-				if (event.mouse.y >= itemY - 4 && event.mouse.y < itemY + 6) {
+				if (event.mouse.y >= itemY - itemHitTop - 3 * (highRes ? 2 : 1) &&
+						event.mouse.y < itemY - itemHitTop + itemHitHeight - 3 * (highRes ? 2 : 1)) {
 					selection = i;
 					break;
 				}
@@ -1759,52 +1775,54 @@ void InsaneRebel2::drawTopPilotsOverlay(byte *renderBitmap, int pitch, int width
 	if (!splayer)
 		return;
 
+	const int scale = isHiRes() ? 2 : 1;
+
 	// Title centered at X=152, Y=10 (TITLFONT)
-	drawMenuStringCentered(renderBitmap, "^f02Top Pilots", 152, 10);
+	drawMenuStringCentered(renderBitmap, "^f02Top Pilots", 152 * scale, 10 * scale);
 
 	// Column headers at Y=30 (SMALFONT), positioned to match data columns
-	int headerY = 30;
+	int headerY = 30 * scale;
 	int headerColor = 5;
-	drawMenuStringCentered(renderBitmap, "^f01Rank", 43, headerY, headerColor);
-	drawMenuString(renderBitmap, "^f01Name", 88, headerY, headerColor);
-	drawMenuStringCentered(renderBitmap, "^f01Difficulty", 195, headerY, headerColor);
-	drawMenuStringCentered(renderBitmap, "^f01Chapter", 245, headerY, headerColor);
-	drawMenuStringRight(renderBitmap, "^f01Score", 295, headerY, headerColor);
+	drawMenuStringCentered(renderBitmap, "^f01Rank", 43 * scale, headerY, headerColor);
+	drawMenuString(renderBitmap, "^f01Name", 88 * scale, headerY, headerColor);
+	drawMenuStringCentered(renderBitmap, "^f01Difficulty", 195 * scale, headerY, headerColor);
+	drawMenuStringCentered(renderBitmap, "^f01Chapter", 245 * scale, headerY, headerColor);
+	drawMenuStringRight(renderBitmap, "^f01Score", 295 * scale, headerY, headerColor);
 
 	// Animated reveal: show up to _topPilotsFrameCount entries
 	int showCount = MIN(_topPilotsFrameCount, _numRankings);
 
 	for (int row = 0; row < showCount; row++) {
 		const RankingEntry &r = _rankings[row];
-		int rowY = row * 10 + 42;
+		int rowY = (row * 10 + 42) * scale;
 		int color = 244;  // 0xF4
 
 		// Column 1: Rank medals at X=43, centered (font 0 = TALKFONT)
 		Common::String rankStr = getRankString(r.rating);
 		if (!rankStr.empty()) {
 			Common::String rankFmt = Common::String::format("^f00%s", rankStr.c_str());
-			drawMenuStringCentered(renderBitmap, rankFmt.c_str(), 43, rowY, color);
+			drawMenuStringCentered(renderBitmap, rankFmt.c_str(), 43 * scale, rowY, color);
 		}
 
 		// Column 2: Pilot name at X=88, left-aligned (font 1 = SMALFONT)
 		Common::String nameFmt = Common::String::format("^f01%s", r.name);
-		drawMenuString(renderBitmap, nameFmt.c_str(), 88, rowY, color);
+		drawMenuString(renderBitmap, nameFmt.c_str(), 88 * scale, rowY, color);
 
 		// Column 3: Difficulty at X=195, centered - TRS (difficulty + 155)
 		int trsIdx = CLIP((int)r.difficulty, 0, 5) + 155;
 		const char *diffStr = splayer->getString(trsIdx);
 		if (diffStr && diffStr[0]) {
 			Common::String diffFmt = Common::String::format("^f01%s", diffStr);
-			drawMenuStringCentered(renderBitmap, diffFmt.c_str(), 195, rowY, color);
+			drawMenuStringCentered(renderBitmap, diffFmt.c_str(), 195 * scale, rowY, color);
 		}
 
 		// Column 4: Highest chapter at X=245, centered
 		Common::String chFmt = Common::String::format("^f01%d", (int)r.chapter);
-		drawMenuStringCentered(renderBitmap, chFmt.c_str(), 245, rowY, color);
+		drawMenuStringCentered(renderBitmap, chFmt.c_str(), 245 * scale, rowY, color);
 
 		// Column 5: Total score at X=295, right-aligned
 		Common::String scoreFmt = Common::String::format("^f01%ld", (long)r.score);
-		drawMenuStringRight(renderBitmap, scoreFmt.c_str(), 295, rowY, color);
+		drawMenuStringRight(renderBitmap, scoreFmt.c_str(), 295 * scale, rowY, color);
 	}
 
 	_topPilotsFrameCount++;
@@ -1938,10 +1956,14 @@ int InsaneRebel2::processOptionsInput() {
 		if (event.type == Common::EVENT_LBUTTONDOWN) {
 			// Mouse click on items — match drawMenuItems Y positions
 			int mouseY = event.mouse.y;
-			int baseY = _optionsItemCount * -5 + 0x68;
+			const bool highRes = isHiRes();
+			const int baseY = highRes ? (_optionsItemCount * -5 + 0x5a) * 2 + 0x1c : _optionsItemCount * -5 + 0x68;
+			const int itemSpacing = highRes ? 20 : 10;
+			const int itemHitTop = highRes ? 2 : 1;
+			const int itemHitHeight = highRes ? 18 : 10;
 			for (int i = 0; i < _optionsItemCount; i++) {
-				int itemY = baseY + i * 10;
-				if (mouseY >= itemY - 1 && mouseY < itemY + 9) {
+				int itemY = baseY + i * itemSpacing;
+				if (mouseY >= itemY - itemHitTop && mouseY < itemY - itemHitTop + itemHitHeight) {
 					_optionsSelection = i;
 					// Simulate enter for this item
 					Common::Event enterEvent;
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index e356621903c..93c786740df 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -129,15 +129,14 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_smush_bencutNut = nullptr;
 	_smush_bensgoggNut = nullptr;
 
-	// Rebel Assault 2 specific initialization can go here
+	const bool highRes = isHiRes();
 
-	// Rebel Assault 2: Load cockpit sprites NUT which contains crosshairs, explosions, status bar
-	// CPITIMAG.NUT = low-res (320x200), CPITIMHI.NUT = high-res (640x480)
-	// The current renderer runs at 320x200, so use the low-res assets.
-	_smush_iconsNut = new NutRenderer(_vm, "SYSTM/CPITIMAG.NUT");
+	// Rebel Assault 2: Load cockpit sprites NUT which contains crosshairs,
+	// explosions, reticles, and warning cues.
+	_smush_iconsNut = new NutRenderer(_vm, highRes ? "SYSTM/CPITIMHI.NUT" : "SYSTM/CPITIMAG.NUT");
 	_smush_icons2Nut = nullptr;  // Not used for Rebel2
 
-	// Initialize laser texture buffer (DAT_0047fee4) from sprite 5 of CPITIMAG.NUT
+	// Initialize laser texture buffer (DAT_0047fee4) from sprite 5 of CPITIMAG/CPITIMHI.NUT
 	// This is done by FUN_0040BAB0/FUN_0040BB87 in the original with sprite index 5
 	// Sprite 5 is 136x13 pixels - a wide, thin texture perfect for laser beams
 	_laserTexture.pixels = nullptr;
@@ -156,7 +155,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	//   >= 0: Edge highlights enabled, >= 1: high-detail (secondary NUTs, widescreen)
 	// Always use high detail in ScummVM.
 	_rebelDetailMode = 1;
-	_smush_cockpitNut = new NutRenderer(_vm, "SYSTM/DISPFONT.NUT");
+	_smush_cockpitNut = new NutRenderer(_vm, highRes ? "SYSTM/DIHIFONT.NUT" : "SYSTM/DISPFONT.NUT");
 
 	// Load DIHIFONT.NUT for in-video messages/subtitles (Opcode 9)
 	_rebelMsgFont = makeRebel2Font(_vm, "SYSTM/DIHIFONT.NUT");
@@ -167,10 +166,10 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	//   Font 1 (^f01): SMALFONT.NUT - Small font for format code switching
 	//   Font 2 (^f02): TITLFONT.NUT - Title font
 	//   Font 3 (^f03): POVFONT.NUT - POV font
-	_smush_talkfontNut = makeRebel2Font(_vm, "SYSTM/TALKFONT.NUT");
-	_smush_smalfontNut = makeRebel2Font(_vm, "SYSTM/SMALFONT.NUT");
-	_smush_titlefontNut = makeRebel2Font(_vm, "SYSTM/TITLFONT.NUT");
-	_smush_povfontNut = makeRebel2Font(_vm, "SYSTM/POVFONT.NUT");
+	_smush_talkfontNut = makeRebel2Font(_vm, highRes ? "SYSTM/TKHIFONT.NUT" : "SYSTM/TALKFONT.NUT");
+	_smush_smalfontNut = makeRebel2Font(_vm, highRes ? "SYSTM/SMHIFONT.NUT" : "SYSTM/SMALFONT.NUT");
+	_smush_titlefontNut = makeRebel2Font(_vm, highRes ? "SYSTM/TIHIFONT.NUT" : "SYSTM/TITLFONT.NUT");
+	_smush_povfontNut = makeRebel2Font(_vm, highRes ? "SYSTM/POHIFONT.NUT" : "SYSTM/POVFONT.NUT");
 
 	_pauseOverlayActive = false;
 	memset(_savedPausePalette, 0, sizeof(_savedPausePalette));
@@ -190,6 +189,8 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_noDamage = false;
 	_viewX = 0;
 	_viewY = 0;
+	_hiResPresentationViewX = 0;
+	_hiResPresentationViewY = 0;
 
 	// Damage visual effect counters (FUN_420515/420562/420754/42073B)
 	_damageFlashCounter = 0;
@@ -618,6 +619,10 @@ InsaneRebel2::~InsaneRebel2() {
 	}
 }
 
+bool InsaneRebel2::isHiRes() const {
+	return _vm->_screenWidth >= 640 && _vm->_screenHeight >= 400;
+}
+
 void InsaneRebel2::openGameplayMainMenu(SmushPlayer *splayer) {
 	if (!splayer)
 		return;
@@ -676,25 +681,25 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 		if (_gameplayMouseSettleUntil != 0) {
 			const uint32 now = _vm->_system->getMillis();
 			if (now < _gameplayMouseSettleUntil) {
+				const int mouseScale = isHiRes() ? 2 : 1;
 				const int jumpX = event.mouse.x - _vm->_mouse.x;
 				const int jumpY = event.mouse.y - _vm->_mouse.y;
 				const bool largeAbsoluteJump =
-					ABS(jumpX) >= kRA2Handler7MouseSettleJumpThreshold ||
-					ABS(jumpY) >= kRA2Handler7MouseSettleJumpThreshold;
+					ABS(jumpX) >= kRA2Handler7MouseSettleJumpThreshold * mouseScale ||
+					ABS(jumpY) >= kRA2Handler7MouseSettleJumpThreshold * mouseScale;
 				const bool smallRelativeMove =
-					ABS((int)event.relMouse.x) < kRA2Handler7MouseSettleRelativeThreshold &&
-					ABS((int)event.relMouse.y) < kRA2Handler7MouseSettleRelativeThreshold;
+					ABS((int)event.relMouse.x) < kRA2Handler7MouseSettleRelativeThreshold * mouseScale &&
+					ABS((int)event.relMouse.y) < kRA2Handler7MouseSettleRelativeThreshold * mouseScale;
 				const bool nearWindowEdge =
-					event.mouse.x <= kRA2Handler7MouseSettleEdgeMargin ||
-					event.mouse.x >= kRA2GameplayMouseMaxX - kRA2Handler7MouseSettleEdgeMargin ||
-					event.mouse.y <= kRA2Handler7MouseSettleEdgeMargin ||
-					event.mouse.y >= kRA2GameplayMouseMaxY - kRA2Handler7MouseSettleEdgeMargin;
+					event.mouse.x <= kRA2Handler7MouseSettleEdgeMargin * mouseScale ||
+					event.mouse.x >= kRA2GameplayMouseMaxX * mouseScale - kRA2Handler7MouseSettleEdgeMargin * mouseScale ||
+					event.mouse.y <= kRA2Handler7MouseSettleEdgeMargin * mouseScale ||
+					event.mouse.y >= kRA2GameplayMouseMaxY * mouseScale - kRA2Handler7MouseSettleEdgeMargin * mouseScale;
 
 				if (largeAbsoluteJump && smallRelativeMove && nearWindowEdge) {
-					const int recenterX = _vm->_mouse.x;
-					const int recenterY = _vm->_mouse.y;
+					const Common::Point recenter = getGameplayAimPoint();
 					_gameplayMouseSettleUntil = now + kRA2Handler7MouseSettleExtendMs;
-					warpGameplayMouseNow(recenterX, recenterY);
+					warpGameplayMouseNow(recenter.x, recenter.y);
 
 					debugC(DEBUG_INSANE, "Rebel2 H7 mouse settle: suppress pos=(%d,%d) rel=(%d,%d) current=(%d,%d) until=%u",
 						event.mouse.x, event.mouse.y, event.relMouse.x, event.relMouse.y,
@@ -1371,11 +1376,12 @@ void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int
 	char scoreStr[16];
 	Common::sprintf_s(scoreStr, "%07d", _playerScore);
 
-	// Score position from FUN_0041c012 assembly (low-res mode):
-	//   X = ((DAT_0047a808 < 2) - 1 & 0x101) + 0x101 = 0x101 = 257
-	//   Y = ((DAT_0047a808 < 2) - 1 & 4) + 4 = 4 (within status bar)
-	int scoreX = 257 + _viewX;
-	int scoreY = statusBarY + 4 + _viewY;
+	// Score position from FUN_0041c012 assembly:
+	//   X = 0x101 low-res, 0x202 high-res
+	//   Y = 4 low-res, 8 high-res (within status bar)
+	const int statusScale = isHiRes() ? 2 : 1;
+	int scoreX = 257 * statusScale + _viewX;
+	int scoreY = statusBarY + 4 * statusScale + _viewY;
 
 	// Render each digit as a NUT sprite (direct pixel blit with color 0 transparency).
 	// This matches the original's FUN_00434cb0 → FUN_004341a0 text rendering which
@@ -1772,12 +1778,19 @@ Common::Point InsaneRebel2::getGameplayAimPoint() {
 	// Pure getter (queried many times per frame): the aim/reticle follows the virtual
 	// mouse position. Directional controls pan that position incrementally once per frame
 	// via updateGameplayAimFromGamepad(), rather than snapping the reticle to a screen edge.
+	int x = _vm->_mouse.x;
 	int y = _vm->_mouse.y;
+	if (isHiRes()) {
+		x /= 2;
+		y /= 2;
+	}
+	x = CLIP<int>(x, 0, 319);
+	y = CLIP<int>(y, 0, 199);
 	if (_optControlsFlipped) {
 		// Original DAT_0047a7fe reverses only the up/down gameplay axis.
 		y = CLIP<int>(200 - y, 0, 199);
 	}
-	return Common::Point(_vm->_mouse.x, y);
+	return Common::Point(x, y);
 }
 
 // Apply the user's configured analog deadzone so a resting stick reports no
@@ -1837,6 +1850,7 @@ void InsaneRebel2::updateGameplayAimFromGamepad() {
 		}
 
 		if (axisX || axisY || _gamepadAimActive) {
+			const Common::Point aimPos = getGameplayAimPoint();
 			const int centerX = 160;
 			const int centerY = 100;
 			const int absAxisX = ABS(axisX);
@@ -1850,8 +1864,8 @@ void InsaneRebel2::updateGameplayAimFromGamepad() {
 				centerY - curvedY * centerY / 127 :
 				centerY + curvedY * (199 - centerY) / 127;
 			const int maxStep = (axisX || axisY) ? 14 : 10;
-			const int distX = targetX - _vm->_mouse.x;
-			const int distY = targetY - _vm->_mouse.y;
+			const int distX = targetX - aimPos.x;
+			const int distY = targetY - aimPos.y;
 
 			if (distX || distY) {
 				deltaX = CLIP<int>(distX, -maxStep, maxStep);
@@ -1882,8 +1896,10 @@ void InsaneRebel2::updateGameplayAimFromGamepad() {
 	_gamepadAimActive = true;
 
 	// Integrate velocity into the reticle, clamped to the 320x200 play area.
-	_vm->_mouse.x = (int16)CLIP<int>(_vm->_mouse.x + deltaX, 0, 319);
-	_vm->_mouse.y = (int16)CLIP<int>(_vm->_mouse.y + deltaY, 0, 199);
+	Common::Point aimPos = getGameplayAimPoint();
+	const int scale = isHiRes() ? 2 : 1;
+	_vm->_mouse.x = (int16)(CLIP<int>(aimPos.x + deltaX, 0, 319) * scale);
+	_vm->_mouse.y = (int16)(CLIP<int>(aimPos.y + deltaY, 0, 199) * scale);
 }
 
 bool InsaneRebel2::isBitSet(int n) {
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 1ab92ca4a60..f7a5490dbf8 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -538,6 +538,7 @@ public:
 
 	// Get current handler ID (8, 25, 38 etc.) for SMUSH player to query
 	int getHandler() const { return _rebelHandler; }
+	bool isHiRes() const;
 
 	void iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 				  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
@@ -591,7 +592,7 @@ public:
 	void updateGameplayDamageEffects(byte *renderBitmap, int pitch, int width, int height);
 	void checkGameplayPostRenderCollisions(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
 
-	// Draw NUT-based HUD overlays for Handler 0x26/0x19 turret modes
+	// Draw NUT-based HUD overlays for Handler 0x26 turret modes
 	void renderTurretHudOverlays(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
 
 	// Draw embedded SAN HUD overlays from IACT chunks
@@ -642,7 +643,7 @@ public:
 	bool loadHandler7ShotTable(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4);
 
 	// Load turret HUD overlay NUT from ANIM data
-	bool loadTurretHudOverlay(byte *animData, int32 size, int16 par3);
+	bool loadTurretHudOverlay(byte *animData, int32 size, int16 selector);
 
 	// Load Handler 8 ship POV NUT sprites from ANIM data (par4 = sprite type: 1,3,6,7)
 	bool loadHandler8ShipSprites(byte *animData, int32 size, int16 par4);
@@ -888,6 +889,8 @@ public:
 
 	int _viewX;
 	int _viewY;
+	int _hiResPresentationViewX;
+	int _hiResPresentationViewY;
 
 	// ---------------------------------------------------------------------------
 	// Damage Visual Effect System
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 7a84b815bff..50534d256e2 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -24,12 +24,14 @@
 #include "common/util.h"
 
 #include "graphics/cursorman.h"
+#include "graphics/managed_surface.h"
 
 #include "scumm/scumm_v7.h"
 
 #include "scumm/smush/smush_player.h"
 #include "scumm/smush/rebel/codec_ra2.h"
 #include "scumm/smush/rebel/font_rebel2.h"
+#include "scumm/smush/rebel/smush_player_ra2.h"
 
 #include "scumm/insane/rebel2/rebel.h"
 
@@ -184,12 +186,12 @@ void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFram
 	// Render the decoded embedded frame to the video buffer
 	// Skip immediate draw for handlers that render HUD during post-processing:
 	// - Handler 7/8: Ship direction sprites selected based on direction
-	// - Handler 0x26/0x19: Cockpit HUD positioned based on mouse/crosshair
+	// - Handler 0x26: Cockpit HUD positioned based on mouse/crosshair
 	//
 	// Exception: Handler 25 (0x19) background overlays (par4/userId=4, 6, 7) should draw immediately.
 	// These complete the visual scene and are NOT positioned by mouse/crosshair.
 	bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8 ||
-	                          _rebelHandler == 0x26 || _rebelHandler == 0x19);
+	                          _rebelHandler == 0x26);
 
 	// Handler 25 overlays:
 	// - userId 4 (corridor overlay): draw immediately at the current view offset.
@@ -340,9 +342,9 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 					debug("Rebel2: Embedded HUD frame: userId=%d, %dx%d at (%d,%d), codec=%d",
 						userId, width, height, left, top, codec);
 
-					// Skip high-resolution frames - ScummVM runs at 320x200
-					// If frame dimensions exceed low-res screen size, it's high-res data
-					if (width > 400 || height > 250) {
+					// High-resolution HUD frames are used when the RA2 high-res option
+					// selects a 640x400 virtual screen. Keep skipping them in low-res mode.
+					if (!isHiRes() && (width > 400 || height > 250)) {
 						debug("Rebel2: SKIPPING high-res embedded frame: userId=%d, %dx%d (exceeds 400x250)",
 							userId, width, height);
 						stream.seek(nextSubPos);
@@ -1469,6 +1471,14 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 	int16 texH = _laserTexture.height;
 	byte *texPixels = _laserTexture.pixels;
 
+	const bool renderHiRes = isHiRes() && width >= 640 && height >= 400;
+	if (renderHiRes) {
+		gunX = (int16)((gunX - _hiResPresentationViewX) * 2);
+		gunY = (int16)((gunY - _hiResPresentationViewY) * 2);
+		targetX = (int16)((targetX - _hiResPresentationViewX) * 2);
+		targetY = (int16)((targetY - _hiResPresentationViewY) * 2);
+	}
+
 	// FUN_0040BBF6 line 23: sVar7 = (thickness * animFrame * 16) / maxFrames
 	if (maxFrames == 0)
 		maxFrames = 1;
@@ -1493,10 +1503,10 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 
 	// Original callers pass a clip rect for the gameplay viewport (excluding status bar).
 	// This preserves texture phase at the viewport edge and avoids visibly "chopped" beams.
-	int clipLeft = CLIP<int>(_viewX, 0, width - 1);
-	int clipTop = CLIP<int>(_viewY, 0, height - 1);
-	int clipRight = CLIP<int>(_viewX + 319, 0, width - 1);
-	int clipBottom = CLIP<int>(_viewY + 179, 0, height - 1);
+	int clipLeft = renderHiRes ? 0 : CLIP<int>(_viewX, 0, width - 1);
+	int clipTop = renderHiRes ? 0 : CLIP<int>(_viewY, 0, height - 1);
+	int clipRight = renderHiRes ? MIN(width - 1, 639) : CLIP<int>(_viewX + 319, 0, width - 1);
+	int clipBottom = renderHiRes ? MIN(height - 1, 359) : CLIP<int>(_viewY + 179, 0, height - 1);
 	if (clipLeft > clipRight || clipTop > clipBottom)
 		return;
 	int edgeClipLeft = CLIP<int>(clipLeft + 1, 1, width - 2);
@@ -1695,8 +1705,9 @@ void InsaneRebel2::checkCollisionZones(byte *renderBitmap, int pitch, int width,
 	// Calculate aim position in centered coordinates.
 	// Handler 0x26 applies the mouse-mode vertical inversion before DAT_0047a7fe
 	// (FUN_407FCB lines 108-123), so this must not use getGameplayAimPoint().
-	const int rawX = _vm->_mouse.x - 160;
-	const int rawY = _vm->_mouse.y - 100;
+	const Common::Point aimPos = getGameplayAimPoint();
+	const int rawX = aimPos.x - 160;
+	const int rawY = aimPos.y - 100;
 	int16 aimX = (int16)(rawX * 52 / 160);
 	int16 aimY = (int16)((_optControlsFlipped ? -rawY : rawY) * 45 / 100);
 
@@ -2189,6 +2200,75 @@ void renderNutSpriteClipped(byte *dst, int pitch, int dstH,
 	}
 }
 
+static void renderNutSpriteScaledClipped(byte *dst, int pitch, int width, int height,
+		int clipLeft, int clipTop, int clipRight, int clipBottom,
+		int x, int y, NutRenderer *nut, int spriteIdx, bool mirror, int scale, bool transparent231) {
+	if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars() || !dst)
+		return;
+
+	if (scale < 1)
+		scale = 1;
+
+	const int dstW = MIN(width, pitch);
+	if (dstW <= 0 || height <= 0)
+		return;
+
+	if (clipLeft < 0)
+		clipLeft = 0;
+	if (clipTop < 0)
+		clipTop = 0;
+	if (clipRight > dstW)
+		clipRight = dstW;
+	if (clipBottom > height)
+		clipBottom = height;
+	if (clipLeft >= clipRight || clipTop >= clipBottom)
+		return;
+
+	int srcW = nut->getCharWidth(spriteIdx);
+	int srcH = nut->getCharHeight(spriteIdx);
+	const byte *src = nut->getCharData(spriteIdx);
+	if (!src || srcW <= 0 || srcH <= 0)
+		return;
+
+	const int scaledW = srcW * scale;
+	const int scaledH = srcH * scale;
+	int drawLeft = MAX(x, clipLeft);
+	int drawTop = MAX(y, clipTop);
+	int drawRight = MIN(x + scaledW, clipRight);
+	int drawBottom = MIN(y + scaledH, clipBottom);
+	if (drawLeft >= drawRight || drawTop >= drawBottom)
+		return;
+
+	if (!transparent231 && x >= clipLeft && y >= clipTop &&
+			x + scaledW <= clipRight && y + scaledH <= clipBottom) {
+		const Graphics::PixelFormat format = Graphics::PixelFormat::createFormatCLUT8();
+		Graphics::Surface srcSurface;
+		srcSurface.init(srcW, srcH, srcW, const_cast<byte *>(src), format);
+
+		Graphics::ManagedSurface dstSurface;
+		dstSurface.surfacePtr()->init(dstW, height, pitch, dst, format);
+		dstSurface.transBlitFrom(srcSurface, Common::Rect(0, 0, srcW, srcH),
+			Common::Rect(x, y, x + scaledW, y + scaledH), 0, mirror);
+		return;
+	}
+
+	for (int dy = drawTop; dy < drawBottom; dy++) {
+		int srcY = (dy - y) / scale;
+		const byte *srcRow = src + srcY * srcW;
+		byte *dstRow = dst + dy * pitch;
+
+		for (int dx = drawLeft; dx < drawRight; dx++) {
+			int srcX = (dx - x) / scale;
+			if (mirror)
+				srcX = srcW - 1 - srcX;
+
+			byte px = srcRow[srcX];
+			if (px != 0 && (!transparent231 || px != 231))
+				dstRow[dx] = px;
+		}
+	}
+}
+
 // renderNutSpriteMirrored -- NUT sprite with optional horizontal flip (FUN_004236e0).
 void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx, bool mirror) {
 	if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars())
@@ -2249,6 +2329,13 @@ void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int
 
 // updatePostRenderScroll -- Set SmushPlayer scroll offsets for the current frame.
 void InsaneRebel2::updatePostRenderScroll(int width, int height) {
+	if (_rebelHandler == 0) {
+		_viewX = 0;
+		_viewY = 0;
+		_player->setScrollOffset(0, 0);
+		return;
+	}
+
 	if (_rebelHandler == 8) {
 		// Handler 8 follows FUN_00401234/FUN_00401CCF: the camera is applied
 		// through FUN_00424510 before FOBJ decoding, not by scrolling the final
@@ -2259,10 +2346,24 @@ void InsaneRebel2::updatePostRenderScroll(int width, int height) {
 		return;
 	}
 
-	// Rebel Assault 2 uses a buffer larger (424x260) than screen (320x200).
-	// Map mouse X (0-320) to Scroll X (0-104), and Y (0-200) to Scroll Y (0-60).
-	int maxScrollX = width - _vm->_screenWidth;
-	int maxScrollY = height - _vm->_screenHeight;
+	if (_rebelHandler == 25 && !isHiRes()) {
+		// Handler 25's low-res L2 corridor layers are authored for the 320x200
+		// viewport. The backing buffer may be larger to decode unclipped FOBJ
+		// data, but panning the final copy exposes unfilled columns/rows and
+		// breaks the corridor perspective.
+		_viewX = 0;
+		_viewY = 0;
+		_player->setScrollOffset(0, 0);
+		return;
+	}
+
+	// Rebel Assault 2 uses a native 320x200 viewport into buffers that may be
+	// larger (424x260). High-res mode still scrolls in those logical units; the
+	// selected viewport is promoted to 640x400 after native FOBJ decoding.
+	const int viewportWidth = isHiRes() ? 320 : _vm->_screenWidth;
+	const int viewportHeight = isHiRes() ? 200 : _vm->_screenHeight;
+	int maxScrollX = width - viewportWidth;
+	int maxScrollY = height - viewportHeight;
 
 	if (maxScrollX < 0)
 		maxScrollX = 0;
@@ -2271,8 +2372,8 @@ void InsaneRebel2::updatePostRenderScroll(int width, int height) {
 
 	// Simple linear mapping: Center of screen corresponds to center of buffer.
 	Common::Point aimPos = getGameplayAimPoint();
-	_viewX = (aimPos.x * maxScrollX) / _vm->_screenWidth;
-	_viewY = (aimPos.y * maxScrollY) / _vm->_screenHeight;
+	_viewX = (aimPos.x * maxScrollX) / viewportWidth;
+	_viewY = (aimPos.y * maxScrollY) / viewportHeight;
 
 	_player->setScrollOffset(_viewX, _viewY);
 }
@@ -2526,6 +2627,24 @@ void InsaneRebel2::renderGameplayPostFrame(byte *renderBitmap, int pitch, int wi
 	// Original: FUN_00403240 only runs handlers when DAT_0047a814 == 0.
 	processMouse();
 
+	_hiResPresentationViewX = 0;
+	_hiResPresentationViewY = 0;
+	if (isHiRes()) {
+		SmushPlayerRebel2 *ra2Player = static_cast<SmushPlayerRebel2 *>(_player);
+		int nativeViewX = _viewX;
+		int nativeViewY = _viewY;
+		if (ra2Player && ra2Player->ra2PromoteCurrentFrameToHiRes(_viewX, _viewY)) {
+			renderBitmap = _player->_dst;
+			width = _player->_width;
+			height = _player->_height;
+			pitch = width;
+			_hiResPresentationViewX = nativeViewX;
+			_hiResPresentationViewY = nativeViewY;
+			_viewX = 0;
+			_viewY = 0;
+		}
+	}
+
 	// NOTE: Level 2 handler 8's background is restored in procPreRendering before
 	// SMUSH decodes the frame's FOBJ sprites. Handler 25 draws its corridor overlay
 	// from IACT opcode 6 instead. Redrawing either here would overwrite enemies.
@@ -2585,7 +2704,7 @@ void InsaneRebel2::renderGameplayPostFrame(byte *renderBitmap, int pitch, int wi
 	renderHandler25ShipPre(renderBitmap, pitch, width, height);
 	renderHandler25Ship(renderBitmap, pitch, width, height);
 
-	// STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (FUN_004089ab lines 195-226).
+	// STEP 1A: Draw NUT-based HUD overlays for Handler 0x26 (FUN_004089ab lines 195-226).
 	// These are cockpit frame, crosshair, and reticle - drawn ON TOP of laser beams.
 	renderTurretHudOverlays(renderBitmap, pitch, width, height, curFrame);
 
@@ -2641,15 +2760,26 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	updatePostRenderScroll(width, height);
 	updatePostRenderDeath();
 
-	// Use video content coordinates, NOT buffer coordinates
-	const int videoWidth = 320;    // Native video width
-	const int videoHeight = 200;   // Native video height
-	const int statusBarY = 180;    // 0xb4 - status bar starts at Y=180 in video coords
+	// Use video content coordinates, NOT oversized low-res gameplay-buffer coordinates.
+	const int hudScale = isHiRes() ? 2 : getRebel2IndicatorScale(width, height);
+	const int videoWidth = 320 * hudScale;
+	const int videoHeight = 200 * hudScale;
+	const int statusBarY = 180 * hudScale;    // 0xb4 low-res, 0x168 high-res
 
 	// Hide HUD/status bar during intro videos (marked by SmushPlayer video flag 0x20)
 	// The 0x20 flag indicates a non-interactive cutscene/intro sequence OR menu
 	bool introPlaying = ((_player->_curVideoFlags & 0x20) != 0);
 
+	if (isHiRes() && _rebelHandler == 0) {
+		SmushPlayerRebel2 *ra2Player = static_cast<SmushPlayerRebel2 *>(_player);
+		if (ra2Player && ra2Player->ra2PromoteCurrentFrameToHiRes(0, 0)) {
+			renderBitmap = _player->_dst;
+			width = _player->_width;
+			height = _player->_height;
+			pitch = width;
+		}
+	}
+
 	if (handlePostRenderMenuModes(renderBitmap, pitch, width, height, introPlaying))
 		return;
 
@@ -2873,7 +3003,8 @@ void InsaneRebel2::renderTextOverlay(byte *renderBitmap, int pitch, int width, i
 			lines.push_back(cur);
 	}
 
-	int drawY = _textOverlayY;
+	const int textScale = isHiRes() ? 2 : 1;
+	int drawY = _textOverlayY * textScale;
 	int visCount = 0;
 
 	for (uint lineIdx = 0; lineIdx < lines.size() && visCount < displayLen; lineIdx++) {
@@ -2902,7 +3033,7 @@ void InsaneRebel2::renderTextOverlay(byte *renderBitmap, int pitch, int width, i
 		}
 
 		// Draw line centered at textX
-		int drawX = _textOverlayX - lineWidth / 2;
+		int drawX = _textOverlayX * textScale - lineWidth / 2;
 		int lineCharsDrawn = 0;
 		{
 			const char *s = lineStr;
@@ -2954,9 +3085,9 @@ void InsaneRebel2::renderStatusBarBackground(byte *renderBitmap, int pitch, int
 	}
 }
 
-// renderTurretHudOverlays -- NUT-based HUD for Handler 0x26/0x19 (FUN_004089ab).
+// renderTurretHudOverlays -- NUT-based HUD for Handler 0x26 (FUN_004089ab).
 void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
-	// Draw NUT-based HUD overlays for Handler 0x26/0x19 (turret modes)
+	// Draw NUT-based HUD overlays for Handler 0x26 (turret modes)
 	// From FUN_004089ab disassembly (lines 195-226):
 	// - DAT_0047fe78 (_hudOverlayNut): Primary HUD overlay with 6 animation frames
 	// - Position formula (low-res):
@@ -2964,7 +3095,7 @@ void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int wi
 	//   Y = 182 - (mouseOffsetY >> 4) - height - spriteOffsetY
 	// - Animation: spriteIndex = (frameCounter / 2) % 6
 
-	if ((_rebelHandler != 0x26 && _rebelHandler != 0x19) || !_hudOverlayNut || _hudOverlayNut->getNumChars() <= 0)
+	if (_rebelHandler != 0x26 || !_hudOverlayNut || _hudOverlayNut->getNumChars() <= 0)
 		return;
 
 	// Calculate mouse offset (clamped to -127..127)
@@ -2988,26 +3119,34 @@ void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int wi
 		animFrame = (curFrame / 2) % animFrameCount;
 	}
 
-	// Get sprite dimensions
-	int spriteW = _hudOverlayNut->getCharWidth(animFrame);
-	int spriteH = _hudOverlayNut->getCharHeight(animFrame);
+	const int hudScale = isHiRes() ? 2 : getRebel2IndicatorScale(width, height);
 
-	// Position calculation from assembly (low-res mode)
-	int spriteOffsetX = 0;
-	int spriteOffsetY = 0;
-	int hudX = 160 + (mouseOffsetX >> 4) - (spriteW / 2) - spriteOffsetX;
-	int hudY = 182 - (mouseOffsetY >> 4) - spriteH - spriteOffsetY;
+	// FUN_004089ab computes the moving overlay anchor from sprite 0's dimensions
+	// and offsets, then FUN_004236e0 applies each rendered frame's own offsets.
+	const int baseSpriteW = _hudOverlayNut->getCharWidth(0);
+	const int baseSpriteH = _hudOverlayNut->getCharHeight(0);
+	const int baseSpriteXOff = _hudOverlayNut->getCharXOffset(0);
+	const int baseSpriteYOff = _hudOverlayNut->getCharYOffset(0);
+	const int horizontalTerm = (mouseOffsetX * hudScale) >> 4;
+	const int verticalInput = isHiRes() ? mouseOffsetY * 2 - 0x100 : mouseOffsetY - 0x80;
+	const int verticalTerm = verticalInput >> 4;
+	int hudX = 160 * hudScale + horizontalTerm - baseSpriteW / 2 - baseSpriteXOff;
+	int hudY = 182 * hudScale - verticalTerm - baseSpriteH - baseSpriteYOff;
 
 	// Apply view offset for scrolling background
 	hudX += _viewX;
 	hudY += _viewY;
 
 	// Draw base cockpit (sprite 0 always drawn first)
-	renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, 0);
+	renderNutSprite(renderBitmap, pitch, width, height,
+		hudX + baseSpriteXOff, hudY + baseSpriteYOff, _hudOverlayNut, 0);
 
 	// Draw animation overlay frame if not frame 0
 	if (animFrame != 0 && animFrame < numSprites) {
-		renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, animFrame);
+		renderNutSprite(renderBitmap, pitch, width, height,
+			hudX + _hudOverlayNut->getCharXOffset(animFrame),
+			hudY + _hudOverlayNut->getCharYOffset(animFrame),
+			_hudOverlayNut, animFrame);
 	}
 
 	debug(5, "Rebel2 HUD: Drawing NUT overlay frame %d/%d at (%d,%d) mouseOffset=(%d,%d)",
@@ -3017,8 +3156,8 @@ void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int wi
 	if (_hudOverlay2Nut && _hudOverlay2Nut->getNumChars() > 0) {
 		int spr2W = _hudOverlay2Nut->getCharWidth(0);
 		int spr2H = _hudOverlay2Nut->getCharHeight(0);
-		int hud2X = 160 + (mouseOffsetX >> 4) - (spr2W / 2) + _viewX;
-		int hud2Y = 182 - (mouseOffsetY >> 4) - spr2H + _viewY;
+		int hud2X = 160 * hudScale + ((mouseOffsetX * hudScale) >> 4) - (spr2W / 2) + _viewX;
+		int hud2Y = 182 * hudScale - ((mouseOffsetY * hudScale) >> 4) - spr2H + _viewY;
 		renderNutSprite(renderBitmap, pitch, width, height, hud2X, hud2Y, _hudOverlay2Nut, 0);
 	}
 }
@@ -3085,16 +3224,18 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
 		int destX = frame.renderX;
 		int destY = frame.renderY;
 
-		// Handler 0x26/0x19 turret positioning
-		if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && (hudSlot == 1 || hudSlot == 2)) {
-			destX = 160 - frame.width / 2 - frame.renderX;
-			destY = 200 - frame.height - frame.renderY;
+		const int hudScale = isHiRes() ? 2 : getRebel2IndicatorScale(width, height);
+
+		// Handler 0x26 turret positioning
+		if (_rebelHandler == 0x26 && hudSlot >= 1 && hudSlot <= 4) {
+			destX = 160 * hudScale - frame.width / 2 - frame.renderX;
+			destY = 200 * hudScale - frame.height - frame.renderY;
 		}
 
 		// Handler 7 large cockpit frame positioning
 		if (_rebelHandler == 7 && (hudSlot == 1 || hudSlot == 2) && frame.width > 100) {
-			destX = 160 - frame.width / 2 - frame.renderX;
-			destY = 170 - frame.height - frame.renderY;
+			destX = 160 * hudScale - frame.width / 2 - frame.renderX;
+			destY = 170 * hudScale - frame.height - frame.renderY;
 		} else if (_rebelHandler == 7 && destX > 100 && destY > 50) {
 			int16 offsetX = (_shipPosX - 160) / 8;
 			int16 offsetY = (_shipPosY - 100) / 8;
@@ -3127,6 +3268,7 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
 		return;
 
 	int numSprites = _smush_cockpitNut->getNumChars();
+	const int statusScale = isHiRes() ? 2 : getRebel2IndicatorScale(width, height);
 
 	// --- Sprite 1: Status bar background (always drawn first as base layer) ---
 	if (numSprites > 1) {
@@ -3152,13 +3294,16 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
 	//   If shield > 0xAA (170): alert blink with sprite 7
 	if (numSprites > 6) {
 		// Bar width from assembly: param_1 >> 2 (low-res)
-		int damageBarWidth = _playerDamage >> 2;
+		int damageBarWidth = (_playerDamage * statusScale) >> 2;
 
 		const byte *src = _smush_cockpitNut->getCharData(6);
 		int sw = _smush_cockpitNut->getCharWidth(6);
 		int sh = _smush_cockpitNut->getCharHeight(6);
 
-		const int dmgClipX = 63, dmgClipY = 9, dmgClipW = 64, dmgClipH = 6;
+		const int dmgClipX = 63 * statusScale;
+		const int dmgClipY = 9 * statusScale;
+		const int dmgClipW = 64 * statusScale;
+		const int dmgClipH = 6 * statusScale;
 
 		if (src && sw > 0 && sh > 0 && damageBarWidth > 0) {
 			int drawW = MIN(damageBarWidth, MIN(dmgClipW, sw - dmgClipX));
@@ -3223,12 +3368,16 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
 		int livesBarWidth = (_playerLives * 5 - 5) * 2;
 		if (livesBarWidth > 50)
 			livesBarWidth = 50;
+		livesBarWidth *= statusScale;
 
 		const byte *src = _smush_cockpitNut->getCharData(6);
 		int sw = _smush_cockpitNut->getCharWidth(6);
 		int sh = _smush_cockpitNut->getCharHeight(6);
 
-		const int livClipX = 168, livClipY = 7, livClipW = 50, livClipH = 9;
+		const int livClipX = 168 * statusScale;
+		const int livClipY = 7 * statusScale;
+		const int livClipW = 50 * statusScale;
+		const int livClipH = 9 * statusScale;
 
 		if (src && sw > 0 && sh > 0 && livesBarWidth > 0) {
 			int drawW = MIN(livesBarWidth, MIN(livClipW, sw - livClipX));
@@ -3412,6 +3561,8 @@ void InsaneRebel2::renderVehicleShotImpacts(byte *renderBitmap, int pitch, int w
 	if (_rebelHandler != 8)
 		return;
 
+	const bool renderHiRes = isHiRes() && width >= 640 && height >= 400;
+
 	for (int i = 0; i < 7; i++) {
 		VehicleShotImpact &impact = _vehicleShotImpacts[i];
 		if (impact.counter <= 0)
@@ -3419,6 +3570,10 @@ void InsaneRebel2::renderVehicleShotImpacts(byte *renderBitmap, int pitch, int w
 
 		int drawX = impact.x - _shipPosX;
 		int drawY = impact.y - _shipPosY;
+		if (renderHiRes) {
+			drawX = (drawX - _hiResPresentationViewX) * 2;
+			drawY = (drawY - _hiResPresentationViewY) * 2;
+		}
 
 		// Original draws DAT_0047e020 repeatedly based on remaining life, then
 		// DAT_0047e018 once, both using the sampled background-mask sprite index.
@@ -3456,9 +3611,15 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
 		return;
 
 	// CRITICAL: Clip height to 180 (0xb4) + viewport Y to avoid drawing over status bar.
-	// For oversized buffers (e.g., Level 12's 424x260), the status bar is at
-	// Y = 180 + _viewY in buffer coordinates.
-	int renderHeight = MIN(height, 180 + _viewY);
+	// In high-res presentation the low-res GRD sprite is scaled into the promoted
+	// 640x400 frame, so the gameplay clip becomes 360 pixels tall.
+	const bool renderHiRes = isHiRes() && width >= 640 && height >= 400;
+	const int renderScale = renderHiRes ? 2 : 1;
+	const int nativeViewX = renderHiRes ? _hiResPresentationViewX : 0;
+	const int nativeViewY = renderHiRes ? _hiResPresentationViewY : 0;
+	const int nativeBufferViewX = renderHiRes ? nativeViewX : _viewX;
+	const int nativeBufferViewY = renderHiRes ? nativeViewY : _viewY;
+	int renderHeight = renderHiRes ? MIN(height, 180 * renderScale) : MIN(height, 180 + _viewY);
 
 	// Draw _grd001Sprite based on _grdSpriteMode (DAT_00457900)
 	// Each mode has specific conditions from FUN_0041db5e:
@@ -3493,32 +3654,36 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
 		int16 spriteYOffset = _grd001Sprite->getCharYOffset(0);
 
 		// Add viewport offset so sprite follows the visible area.
-		// For 320x200 buffers (Level 2), _viewX/_viewY are 0 — no change.
-		// For oversized buffers (Level 12's 424x260), the viewport scrolls
-		// and sprites must be drawn at the correct position within it.
-		int drawX = _rebelViewOffset2X + spriteXOffset + _viewX;
-		int drawY = _rebelViewOffset2Y + spriteYOffset + _viewY;
+		// Handler 25 stays viewport-locked in low-res mode, so _viewX/_viewY
+		// remain 0 even when the backing buffer is larger than 320x200.
+		// Other oversized-buffer modes scroll and need this compensation.
+		int nativeDrawX = _rebelViewOffset2X + spriteXOffset + nativeBufferViewX;
+		int nativeDrawY = _rebelViewOffset2Y + spriteYOffset + nativeBufferViewY;
+		int drawX = renderHiRes ? (nativeDrawX - nativeViewX) * renderScale : nativeDrawX;
+		int drawY = renderHiRes ? (nativeDrawY - nativeViewY) * renderScale : nativeDrawY;
 
 		// Apply half-width clipping from FUN_41DB5E:
 		// - mode1 uncovered: left half
 		// - mode4 uncovered: right half
 		int clipLeft = 0;
-		int clipRight = width;
+		int clipRight = renderHiRes ? 320 : width;
 		if (useHalfWidth) {
-			const int halfWidth = width / 2;
+			const int halfWidth = clipRight / 2;
 			clipLeft = useRightHalf ? halfWidth : 0;
 			clipRight = clipLeft + halfWidth;
 		}
+		int scaledClipLeft = clipLeft * renderScale;
+		int scaledClipRight = clipRight * renderScale;
 
 		// FUN_41DB5E mode-4 uncovered mutates DAT_00482230/34 (clip region),
 		// not DAT_00457910 (draw X). Keep drawX unchanged and clip only.
-		renderNutSpriteClipped(renderBitmap, pitch, renderHeight,
-			clipLeft, 0, clipRight, renderHeight,
-			drawX, drawY, _grd001Sprite, 0);
+		renderNutSpriteScaledClipped(renderBitmap, pitch, width, renderHeight,
+			scaledClipLeft, 0, scaledClipRight, renderHeight,
+			drawX, drawY, _grd001Sprite, 0, false, renderScale, false);
 
-		debug("Rebel2 Handler25 PRE: GRD001 at (%d,%d) nutOff(%d,%d) viewOff(%d,%d) size(%d,%d) mode=%d dmg=%d halfW=%d rightHalf=%d clip=[%d,%d)",
+		debug("Rebel2 Handler25 PRE: GRD001 at (%d,%d) nutOff(%d,%d) viewOff(%d,%d) size(%d,%d) mode=%d dmg=%d halfW=%d rightHalf=%d clip=[%d,%d) scale=%d",
 			drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y,
-			spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel, useHalfWidth ? 1 : 0, useRightHalf ? 1 : 0, clipLeft, clipRight);
+			spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel, useHalfWidth ? 1 : 0, useRightHalf ? 1 : 0, scaledClipLeft, scaledClipRight, renderScale);
 	}
 }
 
@@ -3534,7 +3699,13 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
 		return;
 
 	// CRITICAL: Clip height to 180 (0xb4) + viewport Y to avoid drawing over status bar.
-	int renderHeight = MIN(height, 180 + _viewY);
+	const bool renderHiRes = isHiRes() && width >= 640 && height >= 400;
+	const int renderScale = renderHiRes ? 2 : 1;
+	const int nativeViewX = renderHiRes ? _hiResPresentationViewX : 0;
+	const int nativeViewY = renderHiRes ? _hiResPresentationViewY : 0;
+	const int nativeBufferViewX = renderHiRes ? nativeViewX : _viewX;
+	const int nativeBufferViewY = renderHiRes ? nativeViewY : _viewY;
+	int renderHeight = renderHiRes ? MIN(height, 180 * renderScale) : MIN(height, 180 + _viewY);
 
 	// _grd002Sprite (GRD002) is always drawn if it exists (from FUN_41DB5E line 230)
 	// The sprite index is calculated based on damage level and aiming position
@@ -3642,26 +3813,30 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
 		int16 spriteXOffset = _grd002Sprite->getCharXOffset(spriteIdx);
 		int16 spriteYOffset = _grd002Sprite->getCharYOffset(spriteIdx);
 
-		int drawX, drawY;
+		int nativeDrawX, nativeDrawY;
 
 		if (shouldMirror) {
 			// Mirrored position: X = DAT_00457910 + (320 - sprite_width - sprite_x_offset)
 			// From assembly lines 240-243
-			drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset) + _viewX;
+			nativeDrawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset) + nativeBufferViewX;
 		} else {
 			// Normal position: X = DAT_00457910 + sprite_internal_x_offset
 			// From assembly line 238
-			drawX = _rebelViewOffset2X + spriteXOffset + _viewX;
+			nativeDrawX = _rebelViewOffset2X + spriteXOffset + nativeBufferViewX;
 		}
 
 		// Y = sprite_internal_y_offset + DAT_00457912
 		// From assembly line 246
-		drawY = spriteYOffset + _rebelViewOffset2Y + _viewY;
+		nativeDrawY = spriteYOffset + _rebelViewOffset2Y + nativeBufferViewY;
+		int drawX = renderHiRes ? (nativeDrawX - nativeViewX) * renderScale : nativeDrawX;
+		int drawY = renderHiRes ? (nativeDrawY - nativeViewY) * renderScale : nativeDrawY;
 
-		renderNutSpriteMirrored(renderBitmap, pitch, width, renderHeight, drawX, drawY, _grd002Sprite, spriteIdx, shouldMirror);
+		renderNutSpriteScaledClipped(renderBitmap, pitch, width, renderHeight,
+			0, 0, width, renderHeight,
+			drawX, drawY, _grd002Sprite, spriteIdx, shouldMirror, renderScale, true);
 
-		debug("Rebel2 Handler25: GRD002 at (%d,%d) nutOffset(%d,%d) viewOffset(%d,%d) size(%d,%d) spriteIdx=%d damage=%d dir=%d mirror=%d",
-			drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y, spriteW, spriteH, spriteIdx, _rebelDamageLevel, _rebelFlightDir, shouldMirror ? 1 : 0);
+		debug("Rebel2 Handler25: GRD002 at (%d,%d) nutOffset(%d,%d) viewOffset(%d,%d) size(%d,%d) spriteIdx=%d damage=%d dir=%d mirror=%d scale=%d",
+			drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y, spriteW, spriteH, spriteIdx, _rebelDamageLevel, _rebelFlightDir, shouldMirror ? 1 : 0, renderScale);
 	}
 }
 
@@ -3738,8 +3913,13 @@ void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width,
 	// so indicators stay aligned with decoded enemy sprites.
 	int fobjOffX = _player ? _player->_fobjOffsetX : 0;
 	int fobjOffY = _player ? _player->_fobjOffsetY : 0;
+	const bool renderHiRes = isHiRes() && width >= 640 && height >= 400;
+	const int indicatorScale = renderHiRes ? 2 : getRebel2IndicatorScale(width, height);
+	const int nativeViewX = renderHiRes ? _hiResPresentationViewX : _viewX;
+	const int nativeViewY = renderHiRes ? _hiResPresentationViewY : _viewY;
+	const int nativeVideoWidth = renderHiRes ? videoWidth / indicatorScale : 320;
 
-	Common::Rect viewRect(_viewX, _viewY, _viewX + videoWidth, _viewY + 200);
+	Common::Rect viewRect(nativeViewX, nativeViewY, nativeViewX + nativeVideoWidth, nativeViewY + 200);
 	const int sizeClamp = dparams.specialDamage; // DAT_0047e0fa in FUN_40A2E0
 
 	for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
@@ -3784,8 +3964,14 @@ void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width,
 		int centerY = r.top + halfH;
 		int iw = _smush_iconsNut->getCharWidth(spriteIndex);
 		int ih = _smush_iconsNut->getCharHeight(spriteIndex);
+		int drawX = centerX * indicatorScale - iw / 2;
+		int drawY = centerY * indicatorScale - ih / 2;
+		if (renderHiRes) {
+			drawX = (centerX - nativeViewX) * indicatorScale - iw / 2;
+			drawY = (centerY - nativeViewY) * indicatorScale - ih / 2;
+		}
 		renderNutSprite(renderBitmap, pitch, width, height,
-			centerX - iw / 2, centerY - ih / 2, _smush_iconsNut, spriteIndex);
+			drawX, drawY, _smush_iconsNut, spriteIndex);
 	}
 }
 
@@ -3858,8 +4044,15 @@ void InsaneRebel2::renderExplosionFrame(byte *renderBitmap, int pitch, int width
 	if (_smush_iconsNut->getNumChars() > spriteIndex) {
 		int ew = _smush_iconsNut->getCharWidth(spriteIndex);
 		int eh = _smush_iconsNut->getCharHeight(spriteIndex);
+		const bool renderHiRes = isHiRes() && width >= 640 && height >= 400;
+		int drawX = screenX;
+		int drawY = screenY;
+		if (renderHiRes) {
+			drawX = (screenX - _hiResPresentationViewX) * 2;
+			drawY = (screenY - _hiResPresentationViewY) * 2;
+		}
 		renderNutSprite(renderBitmap, pitch, width, height,
-			screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
+			drawX - ew / 2, drawY - eh / 2, _smush_iconsNut, spriteIndex);
 	}
 
 	if (advance == kExplosionAdvanceAfterDraw)
@@ -3938,11 +4131,14 @@ void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int widt
 
 		int numChars = _smush_iconsNut->getNumChars();
 		int spriteIndex = 0x15 - _hitCooldown;  // 21 - remaining cooldown
+		const bool renderHiRes = isHiRes() && width >= 640 && height >= 400;
+		const int nativeViewX = renderHiRes ? _hiResPresentationViewX : _viewX;
+		const int nativeViewY = renderHiRes ? _hiResPresentationViewY : _viewY;
 
 		if (spriteIndex >= 0 && spriteIndex < numChars) {
 			// Compute ship screen position (simplified FUN_0041c720 transform)
-			int shipDrawX = (_flyShipScreenX - 0xd4) + _perspectiveX + 160 + _viewX;
-			int shipDrawY = (_flyShipScreenY - 0x82) + _perspectiveY + 100 + _viewY;
+			int shipDrawX = (_flyShipScreenX - 0xd4) + _perspectiveX + 160 + nativeViewX;
+			int shipDrawY = (_flyShipScreenY - 0x82) + _perspectiveY + 100 + nativeViewY;
 
 			// Per-direction offset from ship center.
 			// Original uses lookup tables (DAT_004438da etc.) indexed by
@@ -3968,6 +4164,10 @@ void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int widt
 
 			int drawX = shipDrawX + offsetX;
 			int drawY = shipDrawY + offsetY;
+			if (renderHiRes) {
+				drawX = (drawX - nativeViewX) * 2;
+				drawY = (drawY - nativeViewY) * 2;
+			}
 
 			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
 			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
@@ -4025,6 +4225,9 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
 	// Uses pre-initialized _laserTexture from sprite 5 of CPITIMAG.NUT
 
 	int16 maxDuration = getShotMaxDuration();
+	const bool renderHiRes = isHiRes() && width >= 640 && height >= 400;
+	const int nativeViewX = renderHiRes ? _hiResPresentationViewX : _viewX;
+	const int nativeViewY = renderHiRes ? _hiResPresentationViewY : _viewY;
 
 	for (int i = 0; i < 2; i++) {
 		if (_turretShots[i].counter <= 0)
@@ -4032,7 +4235,7 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
 
 		// Calculate sound panning from target X position (FUN_004262f0 call)
 		// sVar1 = ((2 - counter) * (targetX - 160)) / 2, clamped to [-127, 127]
-		int16 pan = ((2 - _turretShots[i].counter) * (_turretShots[i].targetX - _viewX - 160)) / 2;
+		int16 pan = ((2 - _turretShots[i].counter) * (_turretShots[i].targetX - nativeViewX - 160)) / 2;
 		pan = CLIP<int16>(pan, -127, 127);
 		// TODO: Apply panning to sound channel i+1
 
@@ -4049,15 +4252,15 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
 			// Gun 2: (0xa0, 0x17c) = (160, 380) - center bottom (off-screen, clipped)
 			// Gun 3: (0x0a, 0xaa) = (10, 170) - left
 			drawLaserBeam(renderBitmap, pitch, width, height,
-				310 + _viewX, 170 + _viewY, targetX, targetY,
+				310 + nativeViewX, 170 + nativeViewY, targetX, targetY,
 				progress, maxDuration, 12, 8, 12);
 
 			drawLaserBeam(renderBitmap, pitch, width, height,
-				160 + _viewX, 380 + _viewY, targetX, targetY,
+				160 + nativeViewX, 380 + nativeViewY, targetX, targetY,
 				progress, maxDuration, 8, 5, 12);
 
 			drawLaserBeam(renderBitmap, pitch, width, height,
-				10 + _viewX, 170 + _viewY, targetX, targetY,
+				10 + nativeViewX, 170 + nativeViewY, targetX, targetY,
 				progress, maxDuration, 12, 8, 12);
 			break;
 
@@ -4068,11 +4271,11 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
 			// Right: (0xd2, 0xe6) = (210, 230)
 			// Assembly: widthScale=0x19(25), heightScale=8, thickness=0xC(12)
 			drawLaserBeam(renderBitmap, pitch, width, height,
-				110 + _viewX, 230 + _viewY, targetX, targetY,
+				110 + nativeViewX, 230 + nativeViewY, targetX, targetY,
 				progress, maxDuration, 25, 8, 12);
 
 			drawLaserBeam(renderBitmap, pitch, width, height,
-				210 + _viewX, 230 + _viewY, targetX, targetY,
+				210 + nativeViewX, 230 + nativeViewY, targetX, targetY,
 				progress, maxDuration, 25, 8, 12);
 			break;
 
@@ -4082,11 +4285,11 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
 			// Gun 2: (0, 0)
 			// Assembly: widthScale=0x19(25), heightScale=8, thickness=0xC(12)
 			drawLaserBeam(renderBitmap, pitch, width, height,
-				-100 + _viewX, 0 + _viewY, targetX, targetY,
+				-100 + nativeViewX, 0 + nativeViewY, targetX, targetY,
 				progress, maxDuration, 25, 8, 12);
 
 			drawLaserBeam(renderBitmap, pitch, width, height,
-				0 + _viewX, 0 + _viewY, targetX, targetY,
+				0 + nativeViewX, 0 + nativeViewY, targetX, targetY,
 				progress, maxDuration, 25, 8, 12);
 			break;
 
@@ -4097,19 +4300,19 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
 			// Assembly: widthScale=0x19(25), heightScale=8, thickness=0xC(12)
 			if ((_turretShots[i].seqNum & 1) == 0) {
 				drawLaserBeam(renderBitmap, pitch, width, height,
-					10 + _viewX, 50 + _viewY, targetX, targetY,
+					10 + nativeViewX, 50 + nativeViewY, targetX, targetY,
 					progress, maxDuration, 25, 8, 12);
 
 				drawLaserBeam(renderBitmap, pitch, width, height,
-					310 + _viewX, 130 + _viewY, targetX, targetY,
+					310 + nativeViewX, 130 + nativeViewY, targetX, targetY,
 					progress, maxDuration, 25, 8, 12);
 			} else {
 				drawLaserBeam(renderBitmap, pitch, width, height,
-					310 + _viewX, 50 + _viewY, targetX, targetY,
+					310 + nativeViewX, 50 + nativeViewY, targetX, targetY,
 					progress, maxDuration, 25, 8, 12);
 
 				drawLaserBeam(renderBitmap, pitch, width, height,
-					10 + _viewX, 130 + _viewY, targetX, targetY,
+					10 + nativeViewX, 130 + nativeViewY, targetX, targetY,
 					progress, maxDuration, 25, 8, 12);
 			}
 			break;
@@ -4448,8 +4651,9 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
 		int ch = _smush_iconsNut->getCharHeight(reticleIndex);
 
 		// Calculate crosshair position
-		int crosshairX = aimPos.x;
-		int crosshairY = aimPos.y;
+		const int reticleScale = isHiRes() ? 2 : getRebel2IndicatorScale(width, height);
+		int crosshairX = aimPos.x * reticleScale;
+		int crosshairY = aimPos.y * reticleScale;
 		if (_rebelHandler != 7) {
 			crosshairX += _viewX;
 			crosshairY += _viewY;
@@ -4458,8 +4662,8 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
 		// Handler 25 (0x19): Add view offset to crosshair position
 		// From FUN_41DB5E lines 198-199: X = DAT_00457914 + DAT_0045790c, Y = DAT_00457916 + DAT_0045790e
 		if (_rebelHandler == 25) {
-			crosshairX += _rebelViewOffsetX;
-			crosshairY += _rebelViewOffsetY;
+			crosshairX += _rebelViewOffsetX * reticleScale;
+			crosshairY += _rebelViewOffsetY * reticleScale;
 		}
 
 		// FUN_004236e0 with flags=2 applies the NUT frame offsets, then centers.
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 8f899433929..0f75ecbec27 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -882,7 +882,7 @@ static const ExtraGuiOption enableRebel2HiRes = {
 	_s("High resolution mode"),
 	_s("Run the game in 640x400 high resolution mode instead of 320x200"),
 	"rebel2_hires",
-	true,
+	false,
 	0,
 	0
 };
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 7d19565b732..ba18b97a5bf 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -401,6 +401,9 @@ ScummEngine::ScummEngine(OSystem *syst, const DetectorResult &dr)
 		// #15666, #11290, and <https://forums.scummvm.org/viewtopic.php?p=97395#p97395>).
 		if (_game.id == GID_LOOM || !ConfMan.getBool("trim_fmtowns_to_200_pixels"))
 			_screenHeight = 240;
+	} else if (_game.id == GID_REBEL2 && ConfMan.getBool("rebel2_hires")) {
+		_screenWidth = 640;
+		_screenHeight = 400;
 	} else if (_game.version == 8 || _game.heversion >= 71) {
 		// COMI uses 640x480. Likewise starting from version 7.1, HE games use
 		// 640x480, too.
@@ -1826,7 +1829,7 @@ void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
 
 	if (_game.id == GID_REBEL2) {
 		_res->allocResTypeData(rtBuffer, 0, 10, kDynamicResTypeMode);
-		initScreens(0, 200);
+		initScreens(0, _screenHeight);
 
 		_numVariables = 256;
 		_scummVars = (int32 *)calloc(_numVariables, sizeof(int32));
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
index 222b4914574..a4484f551c6 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -29,6 +29,8 @@
 #include "common/rect.h"
 #include "common/system.h"
 
+#include "graphics/blit.h"
+
 #include "scumm/scumm.h"
 #include "scumm/scumm_v7.h"
 #include "scumm/smush/smush_font.h"
@@ -58,6 +60,30 @@ bool isRebel2GameplayActive(Insane *insane) {
 	return static_cast<InsaneRebel2 *>(insane)->getHandler() != 0;
 }
 
+static void scaleNativeViewportToHiRes(byte *dst, int dstPitch, int dstWidth, int dstHeight,
+		const byte *src, int srcPitch, int srcWidth, int srcHeight, int scrollX, int scrollY) {
+	if (!dst || !src || dstPitch <= 0 || dstWidth < 640 || dstHeight < 400 ||
+			srcPitch <= 0 || srcWidth <= 0 || srcHeight <= 0)
+		return;
+
+	memset(dst, 0, (size_t)dstPitch * dstHeight);
+
+	scrollX = CLIP<int>(scrollX, 0, MAX<int>(0, srcWidth - 1));
+	scrollY = CLIP<int>(scrollY, 0, MAX<int>(0, srcHeight - 1));
+
+	const int outWidth = MIN<int>(320, dstWidth / 2);
+	const int outHeight = MIN<int>(200, dstHeight / 2);
+	const int srcViewWidth = MIN<int>(outWidth, srcWidth - scrollX);
+	const int srcViewHeight = MIN<int>(outHeight, srcHeight - scrollY);
+	if (srcViewWidth <= 0 || srcViewHeight <= 0)
+		return;
+
+	const byte *srcView = src + scrollY * srcPitch + scrollX;
+	Graphics::scaleBlit(dst, srcView, dstPitch, srcPitch,
+		srcViewWidth * 2, srcViewHeight * 2, srcViewWidth, srcViewHeight,
+		Graphics::PixelFormat::createFormatCLUT8());
+}
+
 void smushDecodeRA2Uncompressed(byte *dst, const byte *src, int left, int top,
 		int width, int height, int dstPitch, int srcPitch, int dataSize,
 		int srcSkipX, int srcSkipY) {
@@ -141,6 +167,10 @@ void SmushPlayerRebel2::initGamePlayerFields() {
 	_ra2FrameObjectOriginalWidth = 0;
 	_ra2FrameObjectSurfaceWidth = 0;
 	_ra2FrameObjectSurfaceHeight = 0;
+	_ra2LowResVideoBuffer = nullptr;
+	_ra2LowResVideoBufferSize = 0;
+	_ra2NativeFrameNeedsClear = false;
+	_ra2UsingGameplaySurface = false;
 	_ra2PendingAnimHeaderPalette = false;
 	memset(_ra2Codec45Palette, 0, sizeof(_ra2Codec45Palette));
 	memset(_ra2Codec45Lookup, 0, sizeof(_ra2Codec45Lookup));
@@ -157,6 +187,9 @@ void SmushPlayerRebel2::destroyGamePlayerFields() {
 	_lastFobjData = nullptr;
 	free(_loadBuffer);
 	_loadBuffer = nullptr;
+	free(_ra2LowResVideoBuffer);
+	_ra2LowResVideoBuffer = nullptr;
+	_ra2LowResVideoBufferSize = 0;
 }
 
 void SmushPlayerRebel2::ra2InitAudioTrackSizes() {
@@ -183,6 +216,7 @@ void SmushPlayerRebel2::ra2InitAudioTrackSizes() {
  */
 void SmushPlayerRebel2::initGameVideoState() {
 	_ra2PendingAnimHeaderPalette = false;
+	_ra2UsingGameplaySurface = false;
 
 	// Re-push the SMUSH palette to the system. Videos like O_LEVEL.SAN
 	// have no NPAL chunk and inherit the palette from the previous video.
@@ -215,11 +249,81 @@ void SmushPlayerRebel2::releaseGameVideoState() {
 	_lastFobjData = nullptr;
 	_lastFobjDataSize = 0;
 	_hasFrameFobjForGost = false;
+	_ra2NativeFrameNeedsClear = false;
 	// FUN_00423880 allocates the STOR overlay buffer once for the SMUSH subsystem,
 	// and FUN_004246d0 replays the last stored FOBJ even after a new SAN starts.
 	// Level 12 wave segments depend on this when 12P01_B.SAN starts with FTCH.
 }
 
+bool SmushPlayerRebel2::ra2IsHighResMode() const {
+	return _vm->_screenWidth >= 640 && _vm->_screenHeight >= 400;
+}
+
+bool SmushPlayerRebel2::ra2EnsureLowResVideoBuffer() {
+	const int bufSize = 320 * 200;
+	if (_ra2LowResVideoBuffer != nullptr && _ra2LowResVideoBufferSize >= bufSize)
+		return true;
+
+	byte *newBuffer = (byte *)realloc(_ra2LowResVideoBuffer, bufSize);
+	if (!newBuffer) {
+		warning("SmushPlayerRebel2::ra2EnsureLowResVideoBuffer: Failed to allocate %d-byte decode buffer", bufSize);
+		free(_ra2LowResVideoBuffer);
+		_ra2LowResVideoBuffer = nullptr;
+		_ra2LowResVideoBufferSize = 0;
+		return false;
+	}
+
+	_ra2LowResVideoBuffer = newBuffer;
+	_ra2LowResVideoBufferSize = bufSize;
+	memset(_ra2LowResVideoBuffer, 0, _ra2LowResVideoBufferSize);
+	return true;
+}
+
+void SmushPlayerRebel2::ra2ClearCurrentTarget() {
+	if (!_dst)
+		return;
+
+	int clearSize = 0;
+	if (_dst == _specialBuffer && _width > 0 && _height > 0) {
+		const int64 size64 = (int64)_width * _height;
+		if (size64 <= INT_MAX && size64 <= _specialBufferSize)
+			clearSize = (int)size64;
+	} else if (_dst == _ra2LowResVideoBuffer && _width > 0 && _height > 0) {
+		const int64 size64 = (int64)_width * _height;
+		if (size64 <= INT_MAX && size64 <= _ra2LowResVideoBufferSize)
+			clearSize = (int)size64;
+	} else {
+		clearSize = _vm->_screenWidth * _vm->_screenHeight;
+	}
+
+	if (clearSize > 0)
+		memset(_dst, 0, clearSize);
+}
+
+bool SmushPlayerRebel2::ra2PromoteCurrentFrameToHiRes(int scrollX, int scrollY) {
+	if (!ra2IsHighResMode() || !_dst || _width <= 0 || _height <= 0)
+		return false;
+
+	VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
+	byte *screen = vs->getPixels(0, 0);
+	if (_dst == screen && _width == _vm->_screenWidth && _height == _vm->_screenHeight)
+		return false;
+
+	const byte *src = _dst;
+	const int srcPitch = _width;
+	const int srcWidth = _width;
+	const int srcHeight = _height;
+
+	scaleNativeViewportToHiRes(screen, _vm->_screenWidth, _vm->_screenWidth, _vm->_screenHeight,
+		src, srcPitch, srcWidth, srcHeight, scrollX, scrollY);
+
+	_dst = screen;
+	_width = _vm->_screenWidth;
+	_height = _vm->_screenHeight;
+	setScrollOffset(0, 0);
+	return true;
+}
+
 /**
  * RA2-specific FTCH handling.
  * For Handler 25, skips FTCH to preserve overlays.
@@ -355,24 +459,31 @@ bool SmushPlayerRebel2::handleGameTextRendering(const char *str, int fontId, int
 }
 
 SmushFont *SmushPlayerRebel2::getGameFont(int font) {
-	// Font table for low-res mode (320x200). Original exe uses pointer
-	// arithmetic to select hi/lo font pairs (e.g. TKHIFONT+0x14=TALKFONT).
+	// Original exe uses pointer arithmetic to select hi/lo font pairs.
 	// Font 0: TALKFONT (TKHIFONT hi-res)
 	// Font 1: SMALFONT (SMHIFONT hi-res)
 	// Font 2: TITLFONT (TIHIFONT hi-res)
 	// Font 3: POVFONT  (POHIFONT hi-res)
-	const char *ra2_fonts[] = {
+	const char *ra2FontsLo[] = {
 		"SYSTM/TALKFONT.NUT",
 		"SYSTM/SMALFONT.NUT",
 		"SYSTM/TITLFONT.NUT",
 		"SYSTM/POVFONT.NUT"
 	};
-	int numFonts = ARRAYSIZE(ra2_fonts);
+	const char *ra2FontsHi[] = {
+		"SYSTM/TKHIFONT.NUT",
+		"SYSTM/SMHIFONT.NUT",
+		"SYSTM/TIHIFONT.NUT",
+		"SYSTM/POHIFONT.NUT"
+	};
+	const bool highRes = _vm->_screenWidth >= 640 && _vm->_screenHeight >= 400;
+	const char **ra2Fonts = highRes ? ra2FontsHi : ra2FontsLo;
+	int numFonts = ARRAYSIZE(ra2FontsLo);
 	if (font >= 0 && font < numFonts) {
-		_sf[font] = new SmushFont(_vm, ra2_fonts[font], true);
+		_sf[font] = new SmushFont(_vm, ra2Fonts[font], true);
 	} else {
 		debugC(DEBUG_SMUSH, "SmushPlayerRebel2::getGameFont: RA2 unknown font %d, using TALKFONT", font);
-		_sf[font] = new SmushFont(_vm, ra2_fonts[0], true);
+		_sf[font] = new SmushFont(_vm, ra2Fonts[0], true);
 	}
 	return _sf[font];
 }
@@ -716,6 +827,13 @@ void SmushPlayerRebel2::ra2HandleTextResource(const char *str, int fontId, int c
 										int width, int height, TextStyleFlags flg) {
 	ensureMultiFont();
 	_multiFont->setDefaultFont(fontId);
+	const int scale = (_vm->_screenWidth >= 640 && _vm->_screenHeight >= 400) ? 2 : 1;
+	pos_x *= scale;
+	pos_y *= scale;
+	left *= scale;
+	top *= scale;
+	width *= scale;
+	height *= scale;
 
 	debugC(DEBUG_SMUSH, "SmushPlayerRebel2::ra2HandleTextResource: RA2 TRES frame=%d fontId=%d color=%d flags=0x%x pos=(%d,%d) clip=(%d,%d,%d,%d) str=\"%.40s\"",
 		  _frame, fontId, color, (int)flg, pos_x, pos_y, left, top, width, height, str);
@@ -748,8 +866,25 @@ void SmushPlayerRebel2::ra2PrepareFrameObjectSurface(int left, int top, int widt
 
 bool SmushPlayerRebel2::ra2SelectFrameBuffer(int codec, int width, int height) {
 	if (codec == SMUSH_CODEC_RA2_BOMP) {
-		if (_specialBuffer != nullptr) {
+		const bool highRes = ra2IsHighResMode();
+		const bool gameplayActive = isRebel2GameplayActive(_insane);
+		if (highRes && (!gameplayActive || !_ra2UsingGameplaySurface || _specialBuffer == nullptr)) {
+			if (!ra2EnsureLowResVideoBuffer())
+				return false;
+			_dst = _ra2LowResVideoBuffer;
+			_width = 320;
+			_height = 200;
+			if (_ra2NativeFrameNeedsClear) {
+				ra2ClearCurrentTarget();
+				_ra2NativeFrameNeedsClear = false;
+			}
+			debugC(DEBUG_SMUSH, "SmushPlayerRebel2::ra2SelectFrameBuffer: Using low-res decode buffer for high-res codec 45 mask");
+		} else if (_specialBuffer != nullptr) {
 			_dst = _specialBuffer;
+			if (_ra2NativeFrameNeedsClear) {
+				ra2ClearCurrentTarget();
+				_ra2NativeFrameNeedsClear = false;
+			}
 			debugC(DEBUG_SMUSH, "SmushPlayerRebel2::ra2SelectFrameBuffer: Using _specialBuffer for codec 45 mask");
 		} else {
 			VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
@@ -760,26 +895,75 @@ bool SmushPlayerRebel2::ra2SelectFrameBuffer(int codec, int width, int height) {
 	}
 
 	// Rebel2 allocates the low-res gameplay target as 424x260 (FUN_00424730).
-	// Use that target once an oversized gameplay FOBJ appears, then keep small
-	// overlay FOBJ chunks compositing into it. Pure 320x200 gameplay videos keep
-	// drawing their small FOBJ chunks directly onto the main screen.
+	// High-res presentation still decodes video into native 320x200/424x260
+	// surfaces, then promotes the selected viewport to the 640x400 screen.
 	const int screenSize = _vm->_screenWidth * _vm->_screenHeight;
+	const int nativeScreenSize = 320 * 200;
 	const int64 fobjSize64 = (int64)width * height;
 	int surfaceWidth = width;
 	int surfaceHeight = height;
 
-	const bool useGameplaySurface = isRebel2GameplayActive(_insane) &&
-		!isRebel2FullFrameDeltaCodec(codec) &&
-		(fobjSize64 > screenSize || _specialBuffer != nullptr);
+	const bool highRes = ra2IsHighResMode();
+	const bool gameplayActive = isRebel2GameplayActive(_insane);
+	const bool fullFrameDelta = isRebel2FullFrameDeltaCodec(codec);
+
+	if (!gameplayActive && fobjSize64 <= nativeScreenSize) {
+		if (highRes) {
+			if (!ra2EnsureLowResVideoBuffer())
+				return false;
+			_dst = _ra2LowResVideoBuffer;
+			_width = 320;
+			_height = 200;
+		} else {
+			VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
+			_dst = vs->getPixels(0, 0);
+			_width = _vm->_screenWidth;
+			_height = _vm->_screenHeight;
+		}
+
+		if (_ra2NativeFrameNeedsClear) {
+			ra2ClearCurrentTarget();
+			_ra2NativeFrameNeedsClear = false;
+		}
+		debugC(DEBUG_SMUSH, "SmushPlayerRebel2::ra2SelectFrameBuffer: Using native screen target for menu/cinematic FOBJ %dx%d",
+			width, height);
+		return true;
+	}
+
+	const bool oversizedNative = fobjSize64 > nativeScreenSize ||
+		width > 320 || height > 200 ||
+		_ra2FrameObjectSurfaceWidth > 320 || _ra2FrameObjectSurfaceHeight > 200;
+	const bool useGameplaySurface = gameplayActive && !fullFrameDelta &&
+		(oversizedNative || _ra2UsingGameplaySurface);
 
 	if (useGameplaySurface) {
 		surfaceWidth = kRebel2GameplaySurfaceWidth;
 		surfaceHeight = kRebel2GameplaySurfaceHeight;
-	} else if (fobjSize64 > screenSize && !isRebel2FullFrameDeltaCodec(codec)) {
+		_ra2UsingGameplaySurface = true;
+	} else if (!highRes && fobjSize64 > screenSize && !fullFrameDelta) {
 		surfaceWidth = MAX(surfaceWidth, _ra2FrameObjectSurfaceWidth);
 		surfaceHeight = MAX(surfaceHeight, _ra2FrameObjectSurfaceHeight);
 	}
 
+	if (highRes && !useGameplaySurface && !oversizedNative) {
+		if (width <= 0 || height <= 0) {
+			debugC(DEBUG_SMUSH, "SmushPlayerRebel2::ra2SelectFrameBuffer: Skipping invalid FOBJ dimensions %dx%d", width, height);
+			return false;
+		}
+		if (!ra2EnsureLowResVideoBuffer())
+			return false;
+		_dst = _ra2LowResVideoBuffer;
+		_width = 320;
+		_height = 200;
+		if (_ra2NativeFrameNeedsClear) {
+			ra2ClearCurrentTarget();
+			_ra2NativeFrameNeedsClear = false;
+		}
+		debugC(DEBUG_SMUSH, "SmushPlayerRebel2::ra2SelectFrameBuffer: Using low-res decode buffer for high-res FOBJ %dx%d",
+			width, height);
+		return true;
+	}
+
 	const int64 bufSize64 = (int64)surfaceWidth * surfaceHeight;
 	if (width <= 0 || height <= 0 || surfaceWidth <= 0 || surfaceHeight <= 0 || bufSize64 > INT_MAX) {
 		debugC(DEBUG_SMUSH, "SmushPlayerRebel2::ra2SelectFrameBuffer: Skipping invalid FOBJ dimensions %dx%d", width, height);
@@ -787,8 +971,9 @@ bool SmushPlayerRebel2::ra2SelectFrameBuffer(int codec, int width, int height) {
 	}
 
 	const int bufSize = (int)bufSize64;
-	if (bufSize > screenSize) {
-		// Frame is larger than screen - need special buffer
+	const bool needsSpecialBuffer = useGameplaySurface || oversizedNative || bufSize > screenSize;
+	if (needsSpecialBuffer) {
+		// Frame is larger than the native target - need special buffer.
 		if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
 			byte *newSpecialBuffer = (byte *)malloc(bufSize);
 			if (newSpecialBuffer == nullptr) {
@@ -808,9 +993,13 @@ bool SmushPlayerRebel2::ra2SelectFrameBuffer(int codec, int width, int height) {
 		_height = surfaceHeight;
 	}
 
-	if (bufSize > screenSize &&
-	    _specialBuffer != nullptr && _specialBufferSize >= bufSize) {
+	if (needsSpecialBuffer &&
+			_specialBuffer != nullptr && _specialBufferSize >= bufSize) {
 		_dst = _specialBuffer;
+		if (_ra2NativeFrameNeedsClear) {
+			ra2ClearCurrentTarget();
+			_ra2NativeFrameNeedsClear = false;
+		}
 		debugC(DEBUG_SMUSH, "SmushPlayerRebel2::ra2SelectFrameBuffer: Using _specialBuffer %dx%d for oversized FOBJ %dx%d",
 			_width, _height, width, height);
 	} else {
@@ -962,6 +1151,10 @@ bool SmushPlayerRebel2::handleGameFrameBufferSelect(int codec, int width, int he
 
 bool SmushPlayerRebel2::handleGameDimensionOverride(int codec, int width, int height) {
 	if ((height != _vm->_screenHeight) || (width != _vm->_screenWidth)) {
+		if (_dst == _ra2LowResVideoBuffer && _ra2LowResVideoBuffer != nullptr) {
+			return true;
+		}
+
 		if (_insane != nullptr) {
 			InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
 			if (rebel2->getHandler() != 0) {
@@ -983,6 +1176,13 @@ bool SmushPlayerRebel2::handleGameDimensionOverride(int codec, int width, int he
 	return false;
 }
 
+int SmushPlayerRebel2::handleGameFrameObjectPitch(int pitch) {
+	if (_dst == _ra2LowResVideoBuffer && _ra2LowResVideoBuffer != nullptr && _width > 0)
+		return _width;
+
+	return pitch;
+}
+
 bool SmushPlayerRebel2::handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
 	int sourceSkipY = 0;
 	const int adjustedLeft = left + _fobjOffsetX;
@@ -997,7 +1197,27 @@ bool SmushPlayerRebel2::handleGameAdjustCoords(int codec, int &left, int &top, i
 		return true;
 	}
 
-	adjustFrameCoords(left, top, width, height, pitch, &sourceSkipY);
+	if (_dst == _ra2LowResVideoBuffer && _ra2LowResVideoBuffer != nullptr) {
+		left += _fobjOffsetX;
+		top += _fobjOffsetY;
+
+		if (top < 0) {
+			sourceSkipY = -top;
+			height += top;
+			top = 0;
+		}
+		if (left < 0) {
+			width += left;
+			left = 0;
+		}
+		if (top + height > _height)
+			height = _height - top;
+		if (left + width > pitch)
+			width = pitch - left;
+	} else {
+		adjustFrameCoords(left, top, width, height, pitch, &sourceSkipY);
+	}
+
 	if (codec == SMUSH_CODEC_LINE_UPDATE || codec == SMUSH_CODEC_LINE_UPDATE2 ||
 			codec == SMUSH_CODEC_SKIP_RLE || codec == SMUSH_CODEC_UNCOMPRESSED) {
 		_ra2FrameSourceSkipY = sourceSkipY;
@@ -1042,29 +1262,43 @@ void SmushPlayerRebel2::handleGameFrameObjectPost(int codec, const byte *data, i
 	}
 }
 
+void SmushPlayerRebel2::handleGameFrameObjectDecoded(int codec, int left, int top, int width, int height) {
+	if (!ra2IsHighResMode() || isRebel2GameplayActive(_insane))
+		return;
+
+	ra2PromoteCurrentFrameToHiRes(0, 0);
+}
+
 void SmushPlayerRebel2::handleGameFrameStart() {
 	_hasFrameFobjForGost = false;
+	_ra2NativeFrameNeedsClear = ((_curVideoFlags & 0x20) == 0);
 
 	if (_ra2PendingAnimHeaderPalette) {
 		setDirtyColors(0, 255);
 		_ra2PendingAnimHeaderPalette = false;
 	}
 
+	if (ra2IsHighResMode() && isRebel2GameplayActive(_insane)) {
+		if (_ra2UsingGameplaySurface && _specialBuffer != nullptr &&
+				_specialBufferSize >= kRebel2GameplaySurfaceWidth * kRebel2GameplaySurfaceHeight) {
+			_dst = _specialBuffer;
+			_width = kRebel2GameplaySurfaceWidth;
+			_height = kRebel2GameplaySurfaceHeight;
+		} else if (ra2EnsureLowResVideoBuffer()) {
+			_dst = _ra2LowResVideoBuffer;
+			_width = 320;
+			_height = 200;
+		}
+	}
+
 	// FUN_00424d70 clears the target buffer before decoding a frame
 	// unless playback flags contain 0x20. Codec 21 frames in levels like
 	// LEV05/05PLAY.SAN only contain non-zero literals, so stale skipped pixels
 	// must not survive from the previous frame.
 	if ((_curVideoFlags & 0x20) == 0 && _dst != nullptr) {
-		int clearSize = 0;
-		if (_dst == _specialBuffer && _width > 0 && _height > 0) {
-			const int64 size64 = (int64)_width * _height;
-			if (size64 <= INT_MAX && size64 <= _specialBufferSize)
-				clearSize = (int)size64;
-		} else {
-			clearSize = _vm->_screenWidth * _vm->_screenHeight;
-		}
-		if (clearSize > 0)
-			memset(_dst, 0, clearSize);
+		ra2ClearCurrentTarget();
+		if (!ra2IsHighResMode() || isRebel2GameplayActive(_insane) || _dst != _vm->_virtscr[kMainVirtScreen].getPixels(0, 0))
+			_ra2NativeFrameNeedsClear = false;
 	}
 }
 
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.h b/engines/scumm/smush/rebel/smush_player_ra2.h
index 81cb8ea62dd..86035fc6bc7 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.h
+++ b/engines/scumm/smush/rebel/smush_player_ra2.h
@@ -30,6 +30,7 @@ class SmushPlayerRebel2 : public SmushPlayer {
 public:
 	SmushPlayerRebel2(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
 	~SmushPlayerRebel2() override;
+	bool ra2PromoteCurrentFrameToHiRes(int scrollX, int scrollY);
 
 protected:
 	void initGamePlayerFields() override;
@@ -50,11 +51,13 @@ protected:
 	bool shouldRouteAllIACTs() const override { return true; }
 	bool handleGameFrameBufferSelect(int codec, int width, int height) override;
 	bool handleGameDimensionOverride(int codec, int width, int height) override;
+	int handleGameFrameObjectPitch(int pitch) override;
 	bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
 	bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param = 0, uint16 parm2 = 0) override;
 	bool handleGameStoreFrame() override;
 	void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
 	void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
+	void handleGameFrameObjectDecoded(int codec, int left, int top, int width, int height) override;
 	void handleGameFrameStart() override;
 	bool handleGameSkipChunk(uint32 subType, int32 subSize, Common::SeekableReadStream &b) override;
 	void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
@@ -69,6 +72,9 @@ private:
 							   int width, int height, TextStyleFlags flg);
 	void ra2PrepareFrameObjectSurface(int left, int top, int width, int height);
 	bool ra2SelectFrameBuffer(int codec, int width, int height);
+	bool ra2EnsureLowResVideoBuffer();
+	void ra2ClearCurrentTarget();
+	bool ra2IsHighResMode() const;
 	bool ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
 						int width, int height, int pitch, int dataSize);
 	void ra2HandleDeltaPalette(int32 subSize, Common::SeekableReadStream &b);
@@ -89,6 +95,10 @@ private:
 	int _ra2FrameObjectOriginalWidth;
 	int _ra2FrameObjectSurfaceWidth;
 	int _ra2FrameObjectSurfaceHeight;
+	byte *_ra2LowResVideoBuffer;
+	int _ra2LowResVideoBufferSize;
+	bool _ra2NativeFrameNeedsClear;
+	bool _ra2UsingGameplaySurface;
 	bool _ra2PendingAnimHeaderPalette;
 	byte _ra2Codec45Palette[0x300];
 	byte _ra2Codec45Lookup[0x8000];
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index a6db75dfc8e..067fe39a575 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -894,6 +894,7 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	int pitch = _vm->_screenWidth;
 	if (_dst == _specialBuffer)
 		pitch = _width;
+	pitch = handleGameFrameObjectPitch(pitch);
 
 	// Save original FOBJ position and dimensions before clipping. Codec 37/47
 	// (delta block/glyph) decode the full frame starting at (0,0). Codec 2
@@ -949,6 +950,8 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 		}
 	}
 
+	handleGameFrameObjectDecoded(codec, left, top, width, height);
+
 	if (_storeFrame && !handleGameStoreFrame()) {
 		if (_frameBuffer == nullptr) {
 			_frameBuffer = (byte *)malloc(_width * _height);
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index a5dc1ff6f79..fe55b5e7ec1 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -318,11 +318,13 @@ protected:
 	virtual bool shouldRouteAllIACTs() const { return false; }
 	virtual bool handleGameFrameBufferSelect(int codec, int width, int height) { return false; }
 	virtual bool handleGameDimensionOverride(int codec, int width, int height) { return false; }
+	virtual int handleGameFrameObjectPitch(int pitch) { return pitch; }
 	virtual bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) { return false; }
 	virtual bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param = 0, uint16 parm2 = 0) { return false; }
 	virtual bool handleGameStoreFrame() { return false; }
 	virtual void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) {}
 	virtual void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) {}
+	virtual void handleGameFrameObjectDecoded(int codec, int left, int top, int width, int height) {}
 	virtual void handleGameFrameStart() {}
 	virtual bool handleGameSkipChunk(uint32 subType, int32 subSize, Common::SeekableReadStream &b) { return false; }
 	virtual void handleGameGost(int32 subSize, Common::SeekableReadStream &b) {}


Commit: b05208d42899f28bbc523ad0727bb854d6064666
    https://github.com/scummvm/scummvm/commit/b05208d42899f28bbc523ad0727bb854d6064666
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-09T15:21:42+02:00

Commit Message:
SCUMM: RA2: perspective correction in high resolution mode for L2

Changed paths:
    engines/scumm/insane/rebel2/iact.cpp
    engines/scumm/insane/rebel2/rebel.cpp
    engines/scumm/insane/rebel2/rebel.h
    engines/scumm/insane/rebel2/render.cpp
    engines/scumm/insane/rebel2/runlevels.cpp


diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 6487dd5528b..607838a25fd 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -1769,8 +1769,9 @@ bool InsaneRebel2::handleOpcode8EmbeddedAnim(byte *renderBitmap, byte *animData,
 	//   par4=5: 320x200 background -> DAT_0048226c
 	//   par4=6: Overlay -> DAT_00482250, draws immediately
 	//   par4=7: Overlay -> DAT_00482248, draws immediately
+	//   par4=10: GRD005 - Mode 3 overlay -> DAT_00482258 / _grd005Sprite
 	if (!handled && _rebelHandler == 25) {
-		if (par4 == 1 || par4 == 2) {
+		if (par4 == 1 || par4 == 2 || par4 == 10) {
 			handled = loadHandler25GrdSprites(animData, animDataSize, par4);
 		} else if (par4 == 5) {
 			handled = loadLevel2Background(animData, animDataSize, renderBitmap);
@@ -2242,13 +2243,14 @@ bool InsaneRebel2::loadHandler25GrdSprites(byte *animData, int32 size, int16 par
 	// par4 values (from IACT data offset +6):
 	//   1: GRD001 - Primary ship sprite (DAT_00482240 / _grd001Sprite)
 	//   2: GRD002 - Secondary ship sprite (DAT_00482238 / _grd002Sprite)
+	//   10: GRD005 - Mode 3 overlay sprite (DAT_00482258 / _grd005Sprite)
 
 	if (!animData || size <= 0) {
 		return false;
 	}
 
 	// Only handle valid GRD sprite slots
-	if (par4 != 1 && par4 != 2) {
+	if (par4 != 1 && par4 != 2 && par4 != 10) {
 		return false;
 	}
 
@@ -2273,6 +2275,11 @@ bool InsaneRebel2::loadHandler25GrdSprites(byte *animData, int32 size, int16 par
 		_grd002Sprite = newNut;
 		debug("Rebel2: _grd002Sprite set with %d sprites", newNut->getNumChars());
 		break;
+	case 10: // GRD005 - Mode 3 overlay sprite
+		delete _grd005Sprite;
+		_grd005Sprite = newNut;
+		debug("Rebel2: _grd005Sprite set with %d sprites", newNut->getNumChars());
+		break;
 	default:
 		delete newNut;
 		return false;
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 93c786740df..07a9f1842ed 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -440,6 +440,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	// Initialize Handler 25 GRD ship system
 	_grd001Sprite = nullptr;     // DAT_00482240 - GRD001 primary ship
 	_grd002Sprite = nullptr;     // DAT_00482238 - GRD002 secondary ship
+	_grd005Sprite = nullptr;     // DAT_00482258 - GRD005 mode 3 overlay
 	_grdSpriteMode = 0;          // DAT_00457900 - sprite mode (1,2,3,4)
 	memset(_grdShotOriginX, 0, sizeof(_grdShotOriginX));
 	memset(_grdShotOriginY, 0, sizeof(_grdShotOriginY));
@@ -604,6 +605,7 @@ InsaneRebel2::~InsaneRebel2() {
 	// Clean up Handler 25 GRD ship sprites
 	delete _grd001Sprite;
 	delete _grd002Sprite;
+	delete _grd005Sprite;
 
 	// Clean up Handler 0x26 turret HUD overlays
 	delete _hudOverlayNut;
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index f7a5490dbf8..2a7e42c8854 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -1191,6 +1191,7 @@ public:
 	// Based on FUN_0041cadb case 6 and FUN_0041db5e disassembly:
 	// - DAT_00482240: Primary ship sprite (GRD001, par4=1)
 	// - DAT_00482238: Secondary ship sprite (GRD002, par4=2)
+	// - DAT_00482258: Mode 3 overlay sprite (GRD005, par4=10)
 	// - DAT_00457900: Sprite mode (1,2,3,4) controls which sprite to draw
 	// - DAT_00457910: Ship X screen position
 	// - DAT_00457912: Ship Y screen position
@@ -1199,6 +1200,7 @@ public:
 
 	NutRenderer *_grd001Sprite;      // DAT_00482240 - GRD001 primary ship NUT
 	NutRenderer *_grd002Sprite;      // DAT_00482238 - GRD002 secondary ship NUT
+	NutRenderer *_grd005Sprite;      // DAT_00482258 - GRD005 mode 3 overlay NUT
 
 	// Handler 25 shot-origin lookup tables from opcode 8/par4=8 text payload.
 	// Indices 5..19 are filled by the retail "%hd %hd ..." parser (FUN_0041CADB case 6).
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 50534d256e2..3d6eb84afde 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -2346,11 +2346,12 @@ void InsaneRebel2::updatePostRenderScroll(int width, int height) {
 		return;
 	}
 
-	if (_rebelHandler == 25 && !isHiRes()) {
-		// Handler 25's low-res L2 corridor layers are authored for the 320x200
-		// viewport. The backing buffer may be larger to decode unclipped FOBJ
-		// data, but panning the final copy exposes unfilled columns/rows and
-		// breaks the corridor perspective.
+	if (_rebelHandler == 25) {
+		// Handler 25's L2 corridor layers are authored for the 320x200 viewport.
+		// The backing buffer may be larger to decode unclipped FOBJ data, but
+		// panning the final copy exposes unfilled columns/rows and breaks the
+		// corridor perspective. High-res mode must scale that same locked native
+		// viewport instead of selecting a different region of the 424x260 buffer.
 		_viewX = 0;
 		_viewY = 0;
 		_player->setScrollOffset(0, 0);
@@ -3684,6 +3685,27 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
 		debug("Rebel2 Handler25 PRE: GRD001 at (%d,%d) nutOff(%d,%d) viewOff(%d,%d) size(%d,%d) mode=%d dmg=%d halfW=%d rightHalf=%d clip=[%d,%d) scale=%d",
 			drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y,
 			spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel, useHalfWidth ? 1 : 0, useRightHalf ? 1 : 0, scaledClipLeft, scaledClipRight, renderScale);
+
+		if (_grdSpriteMode == 3 && _grd005Sprite && _grd005Sprite->getNumChars() > 1) {
+			const int overlayIdx = 1;
+			int overlayW = _grd005Sprite->getCharWidth(overlayIdx);
+			int overlayH = _grd005Sprite->getCharHeight(overlayIdx);
+			int16 overlayXOffset = _grd005Sprite->getCharXOffset(overlayIdx);
+			int16 overlayYOffset = _grd005Sprite->getCharYOffset(overlayIdx);
+
+			int nativeOverlayX = _rebelViewOffset2X + overlayXOffset + nativeBufferViewX;
+			int nativeOverlayY = _rebelViewOffset2Y + overlayYOffset + nativeBufferViewY;
+			int overlayDrawX = renderHiRes ? (nativeOverlayX - nativeViewX) * renderScale : nativeOverlayX;
+			int overlayDrawY = renderHiRes ? (nativeOverlayY - nativeViewY) * renderScale : nativeOverlayY;
+
+			renderNutSpriteScaledClipped(renderBitmap, pitch, width, renderHeight,
+				0, 0, width, renderHeight,
+				overlayDrawX, overlayDrawY, _grd005Sprite, overlayIdx, false, renderScale, false);
+
+			debug("Rebel2 Handler25 PRE: GRD005 at (%d,%d) nutOff(%d,%d) viewOff(%d,%d) size(%d,%d) mode=%d scale=%d",
+				overlayDrawX, overlayDrawY, overlayXOffset, overlayYOffset,
+				_rebelViewOffset2X, _rebelViewOffset2Y, overlayW, overlayH, _grdSpriteMode, renderScale);
+		}
 	}
 }
 
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index 2048677e5ea..e75dadb9d74 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -278,6 +278,8 @@ void InsaneRebel2::resetLevelPhaseState(bool clearEnemies) {
 	_grd001Sprite = nullptr;
 	delete _grd002Sprite;
 	_grd002Sprite = nullptr;
+	delete _grd005Sprite;
+	_grd005Sprite = nullptr;
 	_grdShotOriginTableLoaded = false;
 
 	clearEmbeddedHudFrames();


Commit: f00a3eb8de71ff21e662b9fa82462abc7c5eb3d5
    https://github.com/scummvm/scummvm/commit/f00a3eb8de71ff21e662b9fa82462abc7c5eb3d5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-09T15:21:42+02:00

Commit Message:
SCUMM: RA2: gameplay corrected for in high resolution mode for L3

Changed paths:
    engines/scumm/insane/rebel2/rebel.h
    engines/scumm/insane/rebel2/render.cpp


diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 2a7e42c8854..dfb972235c8 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -604,6 +604,9 @@ public:
 
 	// Draw Handler 7 ship sprite (third-person ship - FLY sprites)
 	void renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height);
+	void renderHandler7FlySprite(byte *renderBitmap, int pitch, int width, int height,
+		bool renderHiRes, int renderScale, int nativeViewX, int nativeViewY,
+		int nativeX, int nativeY, NutRenderer *nut, int spriteIndex);
 
 	// Draw Handler 8 ship sprite (third-person on foot - POV sprites)
 	void renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height);
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 3d6eb84afde..b4789c5cb03 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -3403,6 +3403,21 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
 	}
 }
 
+// renderHandler7FlySprite -- Draw a native Handler 7 FLY sprite into the current presentation target.
+void InsaneRebel2::renderHandler7FlySprite(byte *renderBitmap, int pitch, int width, int height,
+		bool renderHiRes, int renderScale, int nativeViewX, int nativeViewY,
+		int nativeX, int nativeY, NutRenderer *nut, int spriteIndex) {
+	int dstX = renderHiRes ? (nativeX - nativeViewX) * renderScale : nativeX;
+	int dstY = renderHiRes ? (nativeY - nativeViewY) * renderScale : nativeY;
+
+	if (renderHiRes) {
+		renderNutSpriteScaledClipped(renderBitmap, pitch, width, height,
+			0, 0, width, height, dstX, dstY, nut, spriteIndex, false, renderScale, true);
+	} else {
+		renderNutSprite(renderBitmap, pitch, width, height, dstX, dstY, nut, spriteIndex);
+	}
+}
+
 // renderHandler7Ship -- Handler 7 third-person ship rendering (FUN_0040d836).
 void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height) {
 	// Handler 7 Ship Rendering (Third-Person Ship - FLY sprites)
@@ -3421,7 +3436,20 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
 	if (spriteIndex >= numSprites)
 		spriteIndex = numSprites - 1;
 
+	const bool renderHiRes = isHiRes() && width >= 640 && height >= 400;
+	const int renderScale = renderHiRes ? 2 : 1;
+	const int nativeViewX = renderHiRes ? _hiResPresentationViewX : 0;
+	const int nativeViewY = renderHiRes ? _hiResPresentationViewY : 0;
+
 	Common::Point shipDraw = getHandler7ShipDrawPoint();
+	if (renderHiRes) {
+		// Low-res draws into the native source buffer with _viewX/_viewY baked in,
+		// then SmushPlayer copies the scrolled viewport. High-res promotion has
+		// already consumed those offsets, so reconstruct the same native source
+		// position before applying the 2x presentation transform.
+		shipDraw.x += nativeViewX;
+		shipDraw.y += nativeViewY;
+	}
 	int shipCenterX = shipDraw.x + 0xd4;
 	int shipCenterY = shipDraw.y + 0x82;
 
@@ -3436,7 +3464,9 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
 				int cueIndex = _flyEffectAnimCounter % 10;
 				if (cueIndex >= 0 && cueIndex < laserChars) {
 					int cueX = ((shipCenterX - 0x28) - leftDist) - leftDist / 2;
-					renderNutSprite(renderBitmap, pitch, width, height, cueX, shipCenterY, _flyLaserSprite, cueIndex);
+					renderHandler7FlySprite(renderBitmap, pitch, width, height,
+						renderHiRes, renderScale, nativeViewX, nativeViewY,
+						cueX, shipCenterY, _flyLaserSprite, cueIndex);
 				}
 			}
 
@@ -3445,44 +3475,57 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
 				int cueIndex = (_flyEffectAnimCounter % 10) + 10;
 				if (cueIndex >= 0 && cueIndex < laserChars) {
 					int cueX = rightDist / 2 + rightDist + shipCenterX + 0x28;
-					renderNutSprite(renderBitmap, pitch, width, height, cueX, shipCenterY, _flyLaserSprite, cueIndex);
+					renderHandler7FlySprite(renderBitmap, pitch, width, height,
+						renderHiRes, renderScale, nativeViewX, nativeViewY,
+						cueX, shipCenterY, _flyLaserSprite, cueIndex);
 				}
 			}
 		} else {
 			int16 bottomDist = _corridorBottomY - _flyShipScreenY;
 			int bottomX = shipCenterX;
-			int bottomY = (_corridorBottomY - 0x82) + _perspectiveY + 100 + _viewY;
+			int bottomY = (_corridorBottomY - 0x82) + _perspectiveY + 100 +
+				(renderHiRes ? nativeViewY : _viewY);
 
 			if (bottomDist < 0x19) {
 				_flyEffectAnimCounter++;
 				int cueIndex = _flyEffectAnimCounter % 10;
 				if (cueIndex >= 0 && cueIndex < laserChars)
-					renderNutSprite(renderBitmap, pitch, width, height, bottomX, bottomY, _flyLaserSprite, cueIndex);
+					renderHandler7FlySprite(renderBitmap, pitch, width, height,
+						renderHiRes, renderScale, nativeViewX, nativeViewY,
+						bottomX, bottomY, _flyLaserSprite, cueIndex);
 			}
 			if (bottomDist < 0x32) {
 				_flyEffectAnimCounter++;
 				int cueIndex = _flyEffectAnimCounter % 10;
 				if (cueIndex >= 0 && cueIndex < laserChars)
-					renderNutSprite(renderBitmap, pitch, width, height, bottomX, bottomY, _flyLaserSprite, cueIndex);
+					renderHandler7FlySprite(renderBitmap, pitch, width, height,
+						renderHiRes, renderScale, nativeViewX, nativeViewY,
+						bottomX, bottomY, _flyLaserSprite, cueIndex);
 			}
 
 			int cueIndex = _flyEffectAnimCounter % 10;
 			if (cueIndex >= 0 && cueIndex < laserChars)
-				renderNutSprite(renderBitmap, pitch, width, height, bottomX, bottomY, _flyLaserSprite, cueIndex);
+				renderHandler7FlySprite(renderBitmap, pitch, width, height,
+					renderHiRes, renderScale, nativeViewX, nativeViewY,
+					bottomX, bottomY, _flyLaserSprite, cueIndex);
 		}
 	}
 
-	int drawX = shipDraw.x;
-	int drawY = shipDraw.y;
+	int drawX = renderHiRes ? (shipDraw.x - nativeViewX) * renderScale : shipDraw.x;
+	int drawY = renderHiRes ? (shipDraw.y - nativeViewY) * renderScale : shipDraw.y;
 
-	renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyShipSprite, spriteIndex);
+	renderHandler7FlySprite(renderBitmap, pitch, width, height,
+		renderHiRes, renderScale, nativeViewX, nativeViewY,
+		shipDraw.x, shipDraw.y, _flyShipSprite, spriteIndex);
 
 	// FUN_40D836 lines 176-180: optional FLY002 overlay pass at ship position.
 	if (_flyLaserSprite && _flyOverlayRepeatCount > 0) {
 		int overlayIndex = spriteIndex + 0x14;
 		if (overlayIndex >= 0 && overlayIndex < _flyLaserSprite->getNumChars()) {
 			for (int i = 0; i < _flyOverlayRepeatCount; i++) {
-				renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyLaserSprite, overlayIndex);
+				renderHandler7FlySprite(renderBitmap, pitch, width, height,
+					renderHiRes, renderScale, nativeViewX, nativeViewY,
+					shipDraw.x, shipDraw.y, _flyLaserSprite, overlayIndex);
 			}
 		}
 	}
@@ -3490,7 +3533,9 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
 	// FUN_40D836 lines 181-183: optional FLY003 overlay in high detail mode.
 	if (_flyTargetSprite && _rebelDetailMode >= 0 &&
 		spriteIndex >= 0 && spriteIndex < _flyTargetSprite->getNumChars()) {
-		renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyTargetSprite, spriteIndex);
+		renderHandler7FlySprite(renderBitmap, pitch, width, height,
+			renderHiRes, renderScale, nativeViewX, nativeViewY,
+			shipDraw.x, shipDraw.y, _flyTargetSprite, spriteIndex);
 	}
 
 	debug("Rebel2 Handler7Ship: draw=(%d,%d) sprite=%d/%d shipPos=(%d,%d) persp=(%d,%d) smoothVel=%d vertIn=%d fxCtr=%d fxRep=%d",
@@ -4623,6 +4668,9 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
 	if (_rebelHandler == 8) {
 		worldMousePos.x += _shipPosX;
 		worldMousePos.y += _shipPosY;
+	} else if (_rebelHandler == 7 && isHiRes() && width >= 640 && height >= 400) {
+		worldMousePos.x += _hiResPresentationViewX;
+		worldMousePos.y += _hiResPresentationViewY;
 	} else if (_rebelHandler != 7) {
 		worldMousePos.x += _viewX;
 		worldMousePos.y += _viewY;


Commit: 9644e845610eca7909dc9ec711d90e69c6a1bb16
    https://github.com/scummvm/scummvm/commit/9644e845610eca7909dc9ec711d90e69c6a1bb16
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-09T15:21:42+02:00

Commit Message:
SCUMM: RA2: implement damage recovery

Changed paths:
    engines/scumm/insane/rebel2/rebel.h
    engines/scumm/insane/rebel2/render.cpp


diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index dfb972235c8..f4d8536df49 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -590,6 +590,7 @@ public:
 	void renderGameplayPostFrame(byte *renderBitmap, int pitch, int width, int height,
 								 int videoWidth, int videoHeight, int statusBarY, int32 curFrame);
 	void updateGameplayDamageEffects(byte *renderBitmap, int pitch, int width, int height);
+	void updateGameplayDamageRecovery(int32 curFrame);
 	void checkGameplayPostRenderCollisions(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
 
 	// Draw NUT-based HUD overlays for Handler 0x26 turret modes
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index b4789c5cb03..54938cee693 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -2602,6 +2602,21 @@ void InsaneRebel2::updateGameplayDamageEffects(byte *renderBitmap, int pitch, in
 	}
 }
 
+// updateGameplayDamageRecovery -- Apply RA2's damage auto-reduction.
+void InsaneRebel2::updateGameplayDamageRecovery(int32 curFrame) {
+	// Handler 0x26 (FUN_4089AB), Handler 8 (FUN_401CCF), and Handler 7
+	// (FUN_40D836) decrement DAT_0047a7ec once every 16 frames after
+	// gameplay collision processing. Handler 25's FUN_41DB5E only awards the
+	// timed score tick in the same slot and does not reduce damage.
+	if ((_rebelHandler != 0x26 && _rebelHandler != 8 && _rebelHandler != 7) ||
+			(curFrame & 0xf) != 0 || _playerDamage <= 0) {
+		return;
+	}
+
+	_playerDamage--;
+	_playerShield = 255 - _playerDamage;
+}
+
 // checkGameplayPostRenderCollisions -- Run handler-specific collision checks.
 void InsaneRebel2::checkGameplayPostRenderCollisions(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
 	// Per-frame collision checking against registered zones.
@@ -2717,6 +2732,7 @@ void InsaneRebel2::renderGameplayPostFrame(byte *renderBitmap, int pitch, int wi
 
 	updateGameplayDamageEffects(renderBitmap, pitch, width, height);
 	checkGameplayPostRenderCollisions(renderBitmap, pitch, width, height, curFrame);
+	updateGameplayDamageRecovery(curFrame);
 
 	// Crosshair/reticle (FUN_004089ab, FUN_0040d836).
 	renderCrosshair(renderBitmap, pitch, width, height);


Commit: 74401bdb4db847e2f1c7649cd0ab3d3b03153e04
    https://github.com/scummvm/scummvm/commit/74401bdb4db847e2f1c7649cd0ab3d3b03153e04
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-09T15:21:42+02:00

Commit Message:
SCUMM: RA2: some features for yoda mode

Changed paths:
    engines/scumm/detection.h
    engines/scumm/detection_tables.h
    engines/scumm/insane/rebel2/iact.cpp
    engines/scumm/insane/rebel2/levels.cpp
    engines/scumm/insane/rebel2/menu.cpp
    engines/scumm/insane/rebel2/rebel.cpp
    engines/scumm/insane/rebel2/rebel.h
    engines/scumm/insane/rebel2/render.cpp
    engines/scumm/insane/rebel2/runlevels.cpp
    engines/scumm/metaengine.cpp


diff --git a/engines/scumm/detection.h b/engines/scumm/detection.h
index 29f49690434..c64121e80a5 100644
--- a/engines/scumm/detection.h
+++ b/engines/scumm/detection.h
@@ -42,6 +42,7 @@ namespace Scumm {
 #define GAMEOPTION_REBEL2_UNLOCK_ALL                         GUIO_GAMEOPTIONS11
 #define GAMEOPTION_REBEL1_UNLOCK_ALL                         GUIO_GAMEOPTIONS12
 #define GAMEOPTION_REBEL2_NO_DAMAGE                          GUIO_GAMEOPTIONS13
+#define GAMEOPTION_REBEL2_YODA_MODE                          GUIO_GAMEOPTIONS14
 
 /**
  * Descriptor of a specific SCUMM game. Used internally to store
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 8e14a3c0152..fb90c801359 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -230,8 +230,8 @@ static const GameSettings gameVariantsTable[] = {
 
 	{"rebel1", "", 0, GID_REBEL1, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO2(GUIO_NOMIDI, GAMEOPTION_REBEL1_UNLOCK_ALL)},
 
-	{"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO4(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE)},
-	{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO, Common::kPlatformDOS, GUIO4(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE)},
+	{"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
+	{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
 
 	{"dig",  "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 	{"dig",  "Demo", 0, GID_DIG, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 607838a25fd..a96388d0991 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -1142,7 +1142,7 @@ void InsaneRebel2::handleOpcode6Handler25(byte *renderBitmap, Common::SeekableRe
 	// Autopilot logic (lines 123-146).
 	// From original FUN_0041cadb - NO damageLevel check, toggle happens immediately.
 	// The damage level counter provides the smooth visual transition.
-	if (!_rebelInvulnerable) {
+	if (!_rebelAutoPlay) {
 		if (_rebelAutopilot == 0) {
 			// Uncovered: RIGHT button enters cover.
 			if ((_rebelControlMode & 2) != 0) {
@@ -1160,7 +1160,7 @@ void InsaneRebel2::handleOpcode6Handler25(byte *renderBitmap, Common::SeekableRe
 		// Clear control mode after processing (sticky flags consumed).
 		_rebelControlMode = 0;
 	} else {
-		// Invulnerable mode: random autopilot changes.
+		// Auto play: random autopilot changes.
 		if (_rebelAutopilot == 0) {
 			if (_vm->_rnd.getRandomNumber(100) == 0) {
 				_rebelAutopilot = 1;
@@ -1345,7 +1345,7 @@ void InsaneRebel2::handleOpcode6GenericInit(int16 par4) {
 void InsaneRebel2::updateOpcode6GenericFlightState() {
 	// Step 3: Autopilot/control mode logic (lines 123-146)
 	// This determines whether the ship flies on autopilot or manual control.
-	if (!_rebelInvulnerable) {
+	if (!_rebelAutoPlay) {
 		// Normal mode: check control mode flags.
 		if (_rebelAutopilot == 0) {
 			if ((_rebelControlMode & 2) != 0) {
@@ -1357,7 +1357,7 @@ void InsaneRebel2::updateOpcode6GenericFlightState() {
 			}
 		}
 	} else {
-		// Invulnerable mode: random autopilot changes.
+		// Auto play: random autopilot changes.
 		if (_rebelAutopilot == 0) {
 			if (_vm->_rnd.getRandomNumber(100) == 0) {
 				_rebelAutopilot = 1;
diff --git a/engines/scumm/insane/rebel2/levels.cpp b/engines/scumm/insane/rebel2/levels.cpp
index b133a232415..ebbdff25fd0 100644
--- a/engines/scumm/insane/rebel2/levels.cpp
+++ b/engines/scumm/insane/rebel2/levels.cpp
@@ -81,10 +81,17 @@ void InsaneRebel2::runGame() {
 	while (!_vm->shouldQuit()) {
 		int menuResult = runMainMenu();
 
-		if (menuResult == 0 || _vm->shouldQuit())
+		if (menuResult == kMenuQuit || _vm->shouldQuit())
 			break;
 
-		if (menuResult == kMenuNewGame || menuResult == kMenuContinue) {
+		if (menuResult == kMenuResumeDemo) {
+			playIntroSequence();
+			if (!_vm->shouldQuit())
+				showTopPilots();
+			continue;
+		}
+
+		if (menuResult == kMenuNewGame) {
 			int pilotResult = runLevelSelect();
 
 			if (pilotResult == kLevelSelectQuit || _vm->shouldQuit())
diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index 400928d1832..b80ece9239a 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -48,6 +48,7 @@ namespace Scumm {
 void InsaneRebel2::resetMenu() {
 	_menuSelection = 0;
 	_menuInactivityTimer = 0;
+	_menuInactivityTimedOut = false;
 	_menuRepeatDelay = 0;
 	_menuSelectionConfirmed = false;
 	setVirtualKeyboardVisible(false);
@@ -591,7 +592,7 @@ void InsaneRebel2::showPauseOverlay() {
 }
 
 // runMainMenu -- Main menu loop (FUN_004147B2).
-// Returns kMenuNewGame, kMenuContinue, kMenuCredits, or 0 (quit).
+// Returns kMenuNewGame, kMenuResumeDemo, or kMenuQuit.
 int InsaneRebel2::runMainMenu() {
 
 	debug("Rebel2: Entering main menu");
@@ -630,7 +631,14 @@ int InsaneRebel2::runMainMenu() {
 		// Check for quit
 		if (_vm->shouldQuit()) {
 			_menuInputActive = false;
-			return 0;
+			return kMenuQuit;
+		}
+
+		if (_menuInactivityTimedOut) {
+			debug("Rebel2: Main menu inactivity - resuming intro/demo loop");
+			_menuInactivityTimedOut = false;
+			_menuInputActive = false;
+			return kMenuResumeDemo;
 		}
 
 		// Only process selection if user explicitly confirmed (ENTER/ESC),
@@ -651,7 +659,7 @@ int InsaneRebel2::runMainMenu() {
 		//   case 0 (TRS 11): Start Game -> pilot selection, returns 2
 		//   case 1 (TRS 12): Options -> FUN_00416787 options screen
 		//   case 2 (TRS 13): Calibrate Joystick -> FUN_00425820
-		//   case 3 (TRS 14): Continue Intro -> replay O_OPEN videos
+		//   case 3 (TRS 14): Continue Intro -> return to intro/demo loop
 		//   case 4 (TRS 15): Show Top Pilots -> FUN_00420116(-1)
 		//   case 5 (TRS 16): Show Credits -> play O_CREDIT.SAN, returns 1
 		//   case 6 (TRS 17): Return to Launcher -> quit, returns 0
@@ -673,22 +681,10 @@ int InsaneRebel2::runMainMenu() {
 			// joystick calibration flow is required here.
 			break;
 
-		case 3:  // Continue Intro -> replay intro videos
-			debug("Rebel2: Continue Intro selected - replaying intro");
-			// Temporarily switch to intro state to disable menu overlay
-			// This emulates FUN_004142BD case 0 behavior
-			_gameState = kStateIntro;
+		case 3:  // Continue Intro -> return to intro/demo loop
+			debug("Rebel2: Continue Intro selected - resuming intro/demo loop");
 			_menuInputActive = false;
-			// Play intro sequence again (O_OPEN_A/B)
-			splayer->setCurVideoFlags(0x20);
-			splayer->play("OPEN/O_OPEN_A.SAN", 15);
-			if (!_vm->shouldQuit()) {
-				splayer->play("OPEN/O_OPEN_B.SAN", 15);
-			}
-			// Restore menu state
-			_gameState = kStateMainMenu;
-			_menuInputActive = true;
-			break;
+			return kMenuResumeDemo;
 
 		case 4:  // Show Top Pilots -> high score display (FUN_00420116(-1))
 			debug("Rebel2: Show Top Pilots selected");
@@ -709,7 +705,7 @@ int InsaneRebel2::runMainMenu() {
 		case 6:  // Return to Launcher -> quit game
 			debug("Rebel2: Return to Launcher selected");
 			_menuInputActive = false;
-			return 0;  // Return 0 to exit
+			return kMenuQuit;
 
 		default:
 			debug("Rebel2: Unknown menu selection %d", _menuSelection);
@@ -718,7 +714,7 @@ int InsaneRebel2::runMainMenu() {
 	}
 
 	_menuInputActive = false;
-	return 0;
+	return kMenuQuit;
 }
 
 // ---------------------------------------------------------------------------
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 07a9f1842ed..9959ca44bdd 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -208,11 +208,13 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_textOverlayFadeIn = 0;
 	_textOverlayFadeOut = 0;
 
-	// Retail globals mapped: hit counter, cooldown, invulnerability flag
+	// Retail globals mapped: hit counter, cooldown, movie/auto-play flags
 	_rebelOp6Initialized = false;
 	_rebelHitCounter = 0;
 	_rebelKillCounter = 0;
-	_rebelInvulnerable = false;
+	_rebelYodaMode = false;
+	_rebelMovieMode = false;
+	_rebelAutoPlay = false;
 	_rebelWaveState = 0;
 	_rebelPhaseState = 0;
 
@@ -476,6 +478,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	// FUN_0041f5ae uses the selectable item count for Y position calculation.
 	_menuItemCount = 7;
 	_menuInactivityTimer = 0;
+	_menuInactivityTimedOut = false;
 	_lastMenuVariant = -1;        // No previous menu video
 	_menuRepeatDelay = 0;
 	_menuSelectionConfirmed = false;
@@ -495,6 +498,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	// Set to true to bypass normal unlock progression
 	_debugUnlockAll = ConfMan.getBool("rebel2_unlock_all");
 	_noDamage = ConfMan.getBool("rebel2_no_damage");
+	_rebelYodaMode = ConfMan.getBool("rebel2_yoda_mode");
 
 	for (i = 0; i < 16; i++) {
 		// If debug unlock is enabled, unlock all chapters
@@ -533,7 +537,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 
 	// Initialize options menu state (FUN_004167A6 defaults)
 	_optionsSelection = 0;
-	_optionsItemCount = 9;
+	_optionsItemCount = 8;
 	_optMusicEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kMusicSoundType);
 	_optSfxEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kSFXSoundType);
 	_optVoicesEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kSpeechSoundType);
@@ -678,6 +682,29 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 	if (_vm->isPaused())
 		return false;
 
+	if (_rebelYodaMode && event.type == Common::EVENT_KEYDOWN && !event.kbdRepeat && event.kbd.hasFlags(Common::KBD_ALT)) {
+		switch (event.kbd.keycode) {
+		case Common::KEYCODE_m:
+			// Retail DAT_0047ab60: Yoda-mode Movie Mode skips playable
+			// sections and keeps the story/cutscene sequence moving.
+			_rebelMovieMode = !_rebelMovieMode;
+			debug("Rebel2: Movie mode %s", _rebelMovieMode ? "enabled" : "disabled");
+			if (_rebelMovieMode && splayer && _gameState == kStateGameplay && _rebelHandler != 0)
+				_vm->_smushVideoShouldFinish = true;
+			return true;
+
+		case Common::KEYCODE_p:
+			// Retail DAT_0047ab64: Yoda-mode Auto Play makes gameplay
+			// computer controlled.
+			_rebelAutoPlay = !_rebelAutoPlay;
+			debug("Rebel2: Auto play %s", _rebelAutoPlay ? "enabled" : "disabled");
+			return true;
+
+		default:
+			break;
+		}
+	}
+
 	if (_gameState == kStateGameplay && _rebelHandler == 7 &&
 			event.type == Common::EVENT_MOUSEMOVE) {
 		if (_gameplayMouseSettleUntil != 0) {
@@ -1325,7 +1352,7 @@ InsaneRebel2::LevelDifficultyParams InsaneRebel2::getDifficultyParams() const {
 }
 
 bool InsaneRebel2::applyPlayerDamage(int damage) {
-	if (_noDamage || _rebelInvulnerable || damage <= 0)
+	if (_noDamage || _rebelAutoPlay || damage <= 0)
 		return false;
 
 	_playerDamage += damage;
@@ -1615,8 +1642,16 @@ int32 InsaneRebel2::processMouse() {
 	// Shot trigger behavior:
 	// - Handler 25 keeps edge-triggered clicks due cover-toggle/sticky input semantics.
 	// - Other gameplay handlers fire while button is held; slot counters still rate-limit.
-	bool triggerShot = (_rebelHandler == 25) ? (leftPressed && !leftWasPressed) : leftPressed;
 	bool canShoot = isShootingAllowed();
+	bool autoFire = _rebelAutoPlay && canShoot && _gameState == kStateGameplay && _rebelHandler != 0;
+	if (autoFire && _player) {
+		const int autoFirePeriod = (_rebelHandler == 8) ? 6 : 7;
+		autoFire = (_player->_frame % autoFirePeriod) == 0;
+	}
+	if (autoFire)
+		_rebelControlMode |= 1;
+
+	bool triggerShot = ((_rebelHandler == 25) ? (leftPressed && !leftWasPressed) : leftPressed) || autoFire;
 	if (_rebelHandler == 8) {
 		// FUN_00401CCF uses the same per-frame fire bit both to spawn shots and
 		// to choose the POV gun sprite. Keep the sprite driven by the event-manager
@@ -1625,7 +1660,9 @@ int32 InsaneRebel2::processMouse() {
 	}
 	if (triggerShot && canShoot) {
 		Common::Point mousePos;
-		if (_rebelHandler == 7) {
+		if (autoFire) {
+			mousePos = getRebelAutoPlayAimPoint();
+		} else if (_rebelHandler == 7) {
 			mousePos = getHandler7ShotTargetPoint();
 		} else if (_rebelHandler == 8) {
 			mousePos = getHandler8ShotTargetPoint();
@@ -1776,10 +1813,47 @@ int32 InsaneRebel2::processMouse() {
 	return buttons;
 }
 
+Common::Point InsaneRebel2::getRebelAutoPlayAimPoint() {
+	Common::Point target(160, 100);
+	int bestDistance = 0x7fffffff;
+
+	for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
+		if (!it->active || it->destroyed)
+			continue;
+
+		int x = (it->rect.left + it->rect.right) / 2;
+		int y = (it->rect.top + it->rect.bottom) / 2;
+		if (_rebelHandler == 8) {
+			x -= _shipPosX;
+			y -= _shipPosY;
+		} else if (_rebelHandler != 7) {
+			x -= _viewX;
+			y -= _viewY;
+		}
+
+		if (x < -32 || x > 351 || y < -32 || y > 231)
+			continue;
+
+		const int dx = x - 160;
+		const int dy = y - 100;
+		const int distance = dx * dx + dy * dy;
+		if (distance < bestDistance) {
+			bestDistance = distance;
+			target.x = CLIP<int>(x, 0, 319);
+			target.y = CLIP<int>(y, 0, 199);
+		}
+	}
+
+	return target;
+}
+
 Common::Point InsaneRebel2::getGameplayAimPoint() {
 	// Pure getter (queried many times per frame): the aim/reticle follows the virtual
 	// mouse position. Directional controls pan that position incrementally once per frame
 	// via updateGameplayAimFromGamepad(), rather than snapping the reticle to a screen edge.
+	if (_rebelAutoPlay && _gameState == kStateGameplay && !_menuInputActive)
+		return getRebelAutoPlayAimPoint();
+
 	int x = _vm->_mouse.x;
 	int y = _vm->_mouse.y;
 	if (isHiRes()) {
@@ -1803,7 +1877,7 @@ int16 applyRebel2AnalogDeadzone(int16 axisValue) {
 }
 
 void InsaneRebel2::updateGameplayAimFromGamepad() {
-	if (_menuInputActive || _gameState != kStateGameplay)
+	if (_menuInputActive || _gameState != kStateGameplay || _rebelAutoPlay)
 		return;
 
 	const int dpadX = (_vm->getActionState(kScummActionInsaneRight) ? 1 : 0) -
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index f4d8536df49..1b263aed8ee 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -79,19 +79,17 @@ public:
 
 	// Menu selection results (return values from FUN_004147B2)
 	enum MenuResult {
-		kMenuNewGame = 2,     // case 0: New Game
-		kMenuContinue = 4,    // case 1: Continue
-		kMenuOptions = 0,     // case 2: Options (stays in menu)
-		kMenuExit = 0,        // case 3: Exit to title
-		kMenuUnknown = 0,     // case 4: Unknown function
+		kMenuQuit = -1,       // case 6: Return to Launcher
+		kMenuResumeDemo = 0,  // case 3 / inactivity: Continue Intro
 		kMenuCredits = 1,     // case 5: Show credits
-		kMenuQuit = 0         // case 6: Quit game
+		kMenuNewGame = 2      // case 0: Start Game
 	};
 
 	GameState _gameState;           // Current game state
 	int _menuSelection;             // Current menu item (0-6), mirrors DAT_00459988
 	int _menuItemCount;             // Number of menu items (7 for main menu)
 	int _menuInactivityTimer;       // Timeout counter (300 frames = ~10 sec)
+	bool _menuInactivityTimedOut;   // Main menu should return to the intro/demo loop
 	int _lastMenuVariant;           // Last random menu video shown (DAT_00482400)
 	int _menuRepeatDelay;           // Delay for key repeat (DAT_00459ce0)
 	bool _menuSelectionConfirmed;   // True only when user explicitly confirmed a selection
@@ -219,8 +217,8 @@ public:
 	// ---------------------------------------------------------------------------
 	// TRS strings: 89 (title), 90-101 (toggle labels), 103 (volume), 107/109 (back)
 	// Original settings array at DAT_00482e20[0..4]:
-	//   [0]=auto control, [1]=music, [2]=voices, [3]=sound
-	// Additional flags: DAT_0047a7fe (text/indicators), DAT_0047a80a (rapid fire/arrows)
+	//   [0]=text, [1]=music, [2]=voices, [3]=sound, [4]=hidden abort flag
+	// Additional flags: DAT_0047a7fe (controls normal/flipped), DAT_0047a80a (rapid fire)
 	// Volume: DAT_0047a804 (0-127), SFX vol: DAT_0047a802 (127-768)
 
 	void showOptionsMenu();
@@ -511,6 +509,7 @@ public:
 
 	int32 processMouse() override;
 	Common::Point getGameplayAimPoint();
+	Common::Point getRebelAutoPlayAimPoint();
 	// Per-frame: pan the gameplay reticle incrementally from the held directional controls
 	// (on-screen/physical gamepad dpad, keyboard arrows) instead of snapping it to a screen
 	// edge. Call once per frame; getGameplayAimPoint() stays a pure getter.
@@ -929,7 +928,9 @@ public:
 	bool _rebelOp6Initialized; // Guard: opcode 6 init block (clearBit/links/wave) runs once per video
 	int _rebelHitCounter;    // DAT_0047ab80 - hit counter / state tracker
 	int _rebelKillCounter;   // DAT_0047ab88 - enemies destroyed this phase
-	bool _rebelInvulnerable; // DAT_0047ab64 - toggles invulnerability / state
+	bool _rebelYodaMode;     // DAT_0047ab5a > 2 - unlocks Yoda-mode shortcuts
+	bool _rebelMovieMode;    // DAT_0047ab60 - Alt+M skips playable sections
+	bool _rebelAutoPlay;     // DAT_0047ab64 - Alt+P computer-controlled play
 
 	// Enemy wave/phase state tracking (FUN_004028c5 / FUN_00417b61)
 	// DAT_0047ab98: Per-wave enemy kill state. Bits set when enemy types are destroyed.
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 54938cee693..408e4d87411 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -1951,7 +1951,7 @@ void InsaneRebel2::checkHandler7ObstacleZones(uint16 &warningMask) {
 }
 
 bool InsaneRebel2::applyHandler7WallDamage(int wallDamage) {
-	if (_hitCooldown < 5 && !_rebelInvulnerable) {
+	if (_hitCooldown < 5 && !_rebelAutoPlay) {
 		const bool damageApplied = applyPlayerDamage(wallDamage);
 		_rebelHitCounter++;
 		_hitCooldown = 10;
@@ -2515,13 +2515,11 @@ bool InsaneRebel2::handlePostRenderMenuModes(byte *renderBitmap, int pitch, int
 		// At 12fps video rate, 300 frames = ~25 seconds of inactivity.
 		// The original checks: if (local_8 > 299) return 0.
 		if (_menuInactivityTimer > 300) {
-			debug("Rebel2: Menu inactivity timeout - ending video to loop");
-			// Signal video to end so menu loop plays new video.
-			// This emulates the attract mode behavior where a new random
-			// menu video is selected after inactivity.
+			debug("Rebel2: Menu inactivity timeout - resuming intro/demo loop");
 			_menuInactivityTimer = 0;
-			// Don't set _smushVideoShouldFinish here - let video end naturally.
-			// This will cause runMainMenu to loop and play a new random video.
+			_menuInactivityTimedOut = true;
+			_menuSelectionConfirmed = false;
+			_vm->_smushVideoShouldFinish = true;
 		}
 
 		// Draw menu selection overlay.
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index e75dadb9d74..d09d5bb4b4c 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -100,6 +100,16 @@ InsaneRebel2::WaveEndResult InsaneRebel2::processWaveEnd(int16 mask, int16 *budg
 
 	WaveEndResult result;
 
+	if (_rebelMovieMode) {
+		_skipSectionRequested = false;
+		_rebelPhaseState = mask;
+		_rebelWaveState = mask;
+		debug("Rebel2 processWaveEnd: movie mode completed gameplay wave (mask=0x%x)", (uint16)mask);
+		result.completed = true;
+		result.skipped = true;
+		return result;
+	}
+
 	// Debug shortcut path: force-end current section when requested via Shift+S.
 	if (_skipSectionRequested) {
 		_skipSectionRequested = false;
@@ -192,6 +202,16 @@ bool InsaneRebel2::playLevelSegment(const char *filename, uint16 flags, bool rec
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 
 	const bool isRecordedGameplay = recordFrame && (flags & 0x08) != 0;
+	if (isRecordedGameplay && _rebelMovieMode) {
+		debug("Rebel2: Movie mode skipping gameplay segment: %s", filename);
+		_gameplaySectionActive = false;
+		restoreIOSGamepadController();
+		if (recordFrame)
+			_deathFrame = 0;
+		restoreDamageFlashPalette();
+		return true;
+	}
+
 	if (isRecordedGameplay) {
 		// Center only at the section boundary; looped wave videos are continuations.
 		if (!_gameplaySectionActive && (flags & 0x40) == 0)
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 0f75ecbec27..ba6e3e32042 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -905,6 +905,15 @@ static const ExtraGuiOption enableRebel2NoDamage = {
 	0
 };
 
+static const ExtraGuiOption enableRebel2YodaMode = {
+	_s("Yoda mode"),
+	_s("Enable original Yoda mode shortcuts, including movie mode and auto play"),
+	"rebel2_yoda_mode",
+	false,
+	0,
+	0
+};
+
 const ExtraGuiOption enableRebel1UnlockAll = {
 	_s("Unlock all levels"),
 	_s("All levels will be available without requiring passwords"),
@@ -964,6 +973,9 @@ const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &
 	if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL2_NO_DAMAGE)) {
 		options.push_back(enableRebel2NoDamage);
 	}
+	if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL2_YODA_MODE)) {
+		options.push_back(enableRebel2YodaMode);
+	}
 	if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL1_UNLOCK_ALL)) {
 		options.push_back(enableRebel1UnlockAll);
 	}


Commit: 50674c1a69af97ec6a98cb5296640baf66ca9153
    https://github.com/scummvm/scummvm/commit/50674c1a69af97ec6a98cb5296640baf66ca9153
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-09T15:21:42+02:00

Commit Message:
SCUMM: RA1: no damage mode

Changed paths:
    engines/scumm/detection.h
    engines/scumm/detection_tables.h
    engines/scumm/insane/rebel1/iact.cpp
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/metaengine.cpp


diff --git a/engines/scumm/detection.h b/engines/scumm/detection.h
index c64121e80a5..b04b215f58b 100644
--- a/engines/scumm/detection.h
+++ b/engines/scumm/detection.h
@@ -43,6 +43,7 @@ namespace Scumm {
 #define GAMEOPTION_REBEL1_UNLOCK_ALL                         GUIO_GAMEOPTIONS12
 #define GAMEOPTION_REBEL2_NO_DAMAGE                          GUIO_GAMEOPTIONS13
 #define GAMEOPTION_REBEL2_YODA_MODE                          GUIO_GAMEOPTIONS14
+#define GAMEOPTION_REBEL1_NO_DAMAGE                          GUIO_GAMEOPTIONS15
 
 /**
  * Descriptor of a specific SCUMM game. Used internally to store
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index fb90c801359..f6baccb46fa 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -228,7 +228,7 @@ static const GameSettings gameVariantsTable[] = {
 	{"ft",   "Remastered", 0, GID_FT,  7, 0, MDT_NONE, GF_DOUBLEFINE_PAK, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 	{"ft",   "Demo", 0, GID_FT,  7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 
-	{"rebel1", "", 0, GID_REBEL1, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO2(GUIO_NOMIDI, GAMEOPTION_REBEL1_UNLOCK_ALL)},
+	{"rebel1", "", 0, GID_REBEL1, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL1_UNLOCK_ALL, GAMEOPTION_REBEL1_NO_DAMAGE)},
 
 	{"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
 	{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL, GAMEOPTION_REBEL2_NO_DAMAGE, GAMEOPTION_REBEL2_YODA_MODE)},
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 9c96d37650a..59c2e7b5861 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -1223,7 +1223,7 @@ void InsaneRebel1::updateShipPhysics() {
 
 	// Damage guard/mask from FUN_1DEB5: (_damageFlags & 0x96) != 0
 	// damageFlags & 0x96 = bits 1,2,4,7 = wall collisions (0x16) + projectile hit (0x80)
-	if ((_damageFlags & 0x96) != 0 && _damageCooldown == 0 &&
+	if (!_noDamage && (_damageFlags & 0x96) != 0 && _damageCooldown == 0 &&
 		_health >= 0 && _deathTimer <= 0) {
 		// Projectile hit (bit 7 = 0x80)
 		if (_damageFlags & 0x80)
@@ -1392,7 +1392,7 @@ void InsaneRebel1::updateTurretPhysics() {
 	_turretFrameShipCenterValid = true;
 
 	// Damage gate from FUN_1E6A7.
-	if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer <= 0) {
+	if (!_noDamage && _damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer <= 0) {
 		if (_damageFlags == 0x80)
 			_health -= _tuning.shot;
 		else
@@ -1603,7 +1603,7 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 	// Damage application (FUN_1CDA7 lines 20-41)
 	// Original 0x0B mapping: 0x80 -> +0x13, 0x40 -> +0x0F, 0x20 -> +0x11.
 	// No cooldown — all three damage types can stack each frame
-	if (_damageFlags != 0 && _health >= 0 && _deathTimer < 1) {
+	if (!_noDamage && _damageFlags != 0 && _health >= 0 && _deathTimer < 1) {
 		const int16 oldHealth = _health;
 		const byte appliedDamageFlags = _damageFlags;
 		_screenFlash = 5;
@@ -2035,7 +2035,7 @@ void InsaneRebel1::updateOnFootSequence() {
 	// --- Damage handling (from HandleGameOp19_OnFootSequence) ---
 	// On-foot damage uses the same heavy-damage tuning byte as ship shot/collision
 	// damage in the original, not the miss penalty.
-	if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer < 1) {
+	if (!_noDamage && _damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer < 1) {
 		const int16 oldHealth = _health;
 		_health -= _tuning.shot;
 		if (_health < 0) {
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 4e981793ec0..e4bfb41ef65 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -307,6 +307,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_hudDirtyFlag = 0;
 	_maxChapterUnlocked = 0;
 	_unlockAllLevels = ConfMan.getBool("rebel1_unlock_all");
+	_noDamage = ConfMan.getBool("rebel1_no_damage");
 	_interactiveVideoActive = false;
 	_preserveInteractiveRuntimeState = false;
 	_interactiveVideoCheatSkipped = false;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index a110ab1e295..71138104d32 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -502,6 +502,7 @@ private:
 	byte _hudDirtyFlag;          // 0x7601: 0xFF after HUD redraw (set by renderHUD)
 	int16 _maxChapterUnlocked;   // 0x7730: highest unlocked passcode slot (0=none)
 	bool _unlockAllLevels;       // ScummVM option: expose level select without passcodes
+	bool _noDamage;              // ScummVM option: suppress player damage
 
 	static const int16 kMaxHealth = 98;
 	static const int16 kDeathTimerInit = 30;
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index ba6e3e32042..06c6bd74354 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -923,6 +923,15 @@ const ExtraGuiOption enableRebel1UnlockAll = {
 	0
 };
 
+const ExtraGuiOption enableRebel1NoDamage = {
+	_s("No damage"),
+	_s("Disable player damage"),
+	"rebel1_no_damage",
+	false,
+	0,
+	0
+};
+
 const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &target) const {
 	ExtraGuiOptions options;
 	// Query the GUI options
@@ -979,6 +988,9 @@ const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &
 	if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL1_UNLOCK_ALL)) {
 		options.push_back(enableRebel1UnlockAll);
 	}
+	if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL1_NO_DAMAGE)) {
+		options.push_back(enableRebel1NoDamage);
+	}
 	if (target.empty() || gameid == "comi") {
 		options.push_back(comiObjectLabelsOption);
 




More information about the Scummvm-git-logs mailing list