File:  [Isaki's NoNo m68k/m88k emulator] / nono / vm / lunafb.cpp
Revision 1.1.1.11 (vendor branch): download - view: text, annotated - select for diffs
Wed Apr 29 17:05:59 2026 UTC (2 months, 3 weeks ago) by root
Branches: MAIN, Isaki
CVS tags: v027, HEAD
nono 1.7.0

//
// nono
// Copyright (C) 2020 nono project
// Licensed under nono-license.txt
//

//
// LUNA ビットマッププレーン
//

// SX-9100 のほうはこう書いてあってこっちが正しいようだ。
//      +00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
// b100 |RF|RF|RF|RF|BS|        |共 通 B M P|プレーン#0 |
// b110 |プレーン#1 |プレーン#2 |プレーン#3 |プレーン#4 |
// b120 |プレーン#5 |プレーン#6 |プレーン#7 |FC|        |
// b130 |F0|        |F1|        |F2|        |F3|        |
// b140 |F4|        |F5|        |F6|        |F7|        |
//
// SX-7100 のほうはこう書いてあるがこうはなっていないようだ。
//      +00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
// b100 |RF|        |BS|        |共 通 B M P|FC|        |
// b110 |プレーン#0 |プレーン#1 |プレーン#2 |プレーン#3 |
// b120 |プレーン#4 |プレーン#5 |プレーン#6 |プレーン#7 |
// b130 |F0|        |F1|        |F2|        |F3|        |
// b140 |F4|        |F5|        |F6|        |F7|        |
//
// RF: RFCNT (4バイト)。b100'0000 から b104'0000 の手前までこの1ポートのミラー。
// BS: BMSEL (4バイト)。こっちは未確認だがたぶん RFCNT と同じ?
// FC: 共通ファンクションセット
// Fn: プレーン#n ファンクションセット
// 1区画が 64KB。空白のところは未調査。
// 未装着のプレーン(#4..#7とか)は 0xff が読めるようだ。
// ファンクションセットが 64バイトで折り返してるか未調査だが、
// b130'0000 も b133'0000 も 0xff が読めるのでこの範囲全域でミラーかも。
// b150'0000 以降(b1ff'ffffまで) は BUSERR のようだ。

// Lunafb の VRAM はロングワード単位でホストバイトオーダ配置なので、
// バイトアクセス、ワードアクセス時は HLB(), HLW() マクロを使用のこと。

#include "lunafb.h"
#include "bt45x.h"
#include "builtinrom.h"
#include "busio.h"
#include "monitor.h"
#include "mpu.h"

// コンストラクタ
LunafbDevice::LunafbDevice()
	: inherited(2048, 1024)
{
	SetName("Lunafb");
	ClearAlias();
	AddAlias("Lunafb");

	monitor = gMonitorManager->Regist(ID_MONITOR_LUNAVC, this);
	monitor->SetCallback(&LunafbDevice::MonitorScreen);
	monitor->SetSize(39, 11);
}

// デストラクタ
LunafbDevice::~LunafbDevice()
{
}

// 初期化
bool
LunafbDevice::Init()
{
	if (inherited::Init() == false) {
		return false;
	}

	auto bt45x = GetBT45xDevice();
	nplane = bt45x->GetPlaneCount();

	plane_end = plane0base + 0x40000 * nplane;
	fcset_end = fcset0base + 0x40000 * nplane;

	// プレーン数によってメモリを確保
	size_t memsize = 0x40000 * nplane;
	mem.reset(new(std::nothrow) uint8 [memsize]);
	if ((bool)mem == false) {
		warnx("Could not allocate %zu bytes at %s", memsize, __method__);
		return false;
	}

	// ホストパレットを取得。
	palette = &(bt45x->GetHostPalette())[0];

	// XXX とりあえず RAM と同じウェイトを入れておく (未調査)
	wait = busdata::Wait(1 * mpu->GetClock_tsec());

	return true;
}

// 電源オン/リセット
void
LunafbDevice::ResetHard(bool poweron)
{
	inherited::ResetHard(poweron);

	if (poweron) {
		// XXX 実際は不定値だと思うけど、観測手段がないのでとりあえず。
		memset(&mem[0], 0xff, 0x40000 * nplane);
	}

	// 厳密にどの時点でリセットされるのかは知らないけど
	bmsel = 0;

	// XXX 初期値の観測は困難なので適当
	rfcnt = 0;
	xscroll = 0;
	yscroll = 0;

	for (int i = 0; i < MAX_PLANES; i++) {
		fcset[i] = ROP::THRU;
		fcmask[i] = 0xffffffff;
	}
}

busdata
LunafbDevice::Read(busaddr addr)
{
	uint32 paddr = addr.Addr();
	busdata data;

	if (plane0base <= paddr && paddr < plane_end) {
		// プレーンの読み出し
		data = *(uint32 *)&mem[(paddr & ~3U) - plane0base];
	} else {
		// 残りは書き込み専用または不在
		putlog(1, "Read not-readable $%08x", paddr);
		data = 0xffffffff;
	}
	data |= wait;
	data |= BusData::Size4;
	return data;
}

busdata
LunafbDevice::Write(busaddr addr, uint32 data)
{
	uint32 paddr = addr.Addr();

	// RFCNT と BMSEL だけバスの処理が別。というよりバスの観点から言えば
	// 本来別デバイスだが便宜上同じデバイスに同居している感じ。
	if (paddr < 0xb1080000) {
		return WriteReg(addr, data);
	}

	uint32 reqsize = addr.GetSize();
	// 3バイトアクセスは面倒なので回避して .B/.W/.L に整列させる。
	uint32 datasize;
	if (reqsize == 4 && (paddr & 3U) == 0) {
		datasize = 4;
	} else {
		datasize = std::min(2 - (paddr & 1U), reqsize);
	}
	data >>= (reqsize - datasize) * 8;

	if (paddr < plane0base) {
		// 共通ビットマッププレーン
		if (datasize == 4) {
			WriteCommonBitmap32(paddr, data);
		} else if (datasize == 2) {
			WriteCommonBitmap16(paddr, data);
		} else {
			WriteCommonBitmap8(paddr, data);
		}

	} else if (__predict_true(paddr < plane_end)) {
		// 存在するビットマッププレーン
		uint32 reladdr = paddr - plane0base;
		uint plane = reladdr / 0x40000;
		uint32 offset = reladdr % 0x40000;

		putlog(3, "Plane%u $%08x <- $%0*x", plane, paddr, datasize * 2, data);

		if (datasize == 4) {
			WritePlane32(plane, offset, data);
		} else if (datasize == 2) {
			WritePlane16(plane, offset, data);
		} else {
			WritePlane8(plane, offset, data);
		}
		SetDirty(offset);

	} else if (__predict_false(paddr < plane7end)) {
		// HW 設定によっては存在しないプレーン
		uint32 reladdr = paddr - plane0base;
		uint plane = reladdr / 0x40000;
		putlog(3, "Not-equipped Plane%u $%08x <- $%0*x",
			plane, paddr, datasize * 2, data);

	} else if (paddr < 0xb1300000) {
		// 共通ファンクションセット
		// XXX ワード、バイトアクセスはどうなる?
		uint rop = (paddr & 0x3f) / 4;
		putlog(2, "Common FCSet $%08x <- $%0*x (%s)",
			paddr, datasize * 2, data, RopStr(rop));
		WriteCommonFCSet(rop, data);

	} else if (__predict_true(paddr < fcset_end)) {
		// 存在するファンクションセット
		// XXX ワード、バイトアクセスはどうなる?
		uint plane = (paddr - 0xb1300000) / 0x40000;
		uint rop = (paddr & 0x3f) / 4;
		putlog(2, "FCSet%u $%08x <- $%0*x (%s)",
			plane, paddr, datasize * 2, data, RopStr(rop));
		fcset[plane] = rop;
		fcmask[plane] = data;

	} else if (paddr < fcset7end) {
		// HW 設定によっては存在しないファンクションセット
		uint plane = (paddr - 0xb1300000) / 0x40000;
		putlog(3, "Not-equipped FCSet%u $%08x <- $%0*x",
			plane, paddr, datasize * 2, data);

	} else {
		// 残りは全部空きのはずだが挙動未調査。
		putlog(0, "$%08x <- $%0*x (NOT IMPLEMENTED)",
			paddr, datasize * 2, data);

	}

	busdata r = wait;
	r |= busdata::Size(datasize);
	return r;
}

busdata
LunafbDevice::WriteReg(busaddr addr, uint32 data)
{
	// RFCNT レジスタ部分は BusIO_L, NPORT=1 相当。
	//
	// LUNA-88K PROM で `cw b1040000 0` した後、
	// `cs b1040000 0100` (RFCNT の上位ワードに $0100 を書き込む) を実行する
	// と、垂直位置だけが変わる。RFCNT は上位ワードの下位 8 ビットが水平
	// スクロール位置、下位ワードの下位 10 ビットが垂直スクロール位置なので、
	// ロングワード全体に $0100'0100 を $00ff'03ff でマスクした結果の
	// $0000'0100 が書き込まれたことになる。
	// m88200 のデータバスへの置き方と、このポートがロングワード固定で DBE*
	// を細かくデコードしていないとすれば辻褄が合う。
	// 同様に `cs b1040002 0020` を実行すると LUNA-88K 実機では垂直位置と
	// ともに水平位置も変わる。
	//
	// BMSEL レジスタ部分は確認が難しいけどたぶん同じだと思うことにする。
	data = BusIO::DataMultiplexer_Write_L(addr, data);

	if (addr.Addr() < 0xb1040000) {
		// RFCNT
		WriteRFCNT(data);
	} else {
		// BMSEL
		bmsel = data & ((1U << nplane) - 1);
		putlog(1, "BMSEL <- $%08x", data);
	}

	busdata r = wait;
	r |= BusData::Size4;
	return r;
}

busdata
LunafbDevice::Peek1(uint32 addr)
{
	if (plane0base <= addr && addr < plane_end) {
		// プレーンの読み出し
		return mem[HLB(addr - plane0base)];
	}
	// 残りは書き込み専用
	return 0xff;
}

bool
LunafbDevice::Poke1(uint32 addr, uint32 data)
{
	// 通常プレーンのみ越権書き込み可能とする?
	if (plane0base <= addr && addr < plane_end) {
		if ((int32)data >= 0) {
			uint32 reladdr = addr - plane0base;
			uint32 offset = reladdr % 0x40000;
			// ROP の影響は受けずにメモリの内容を変更
			mem[HLB(reladdr)] = data;
			// この場合も画面を更新する
			SetDirty(offset);
		}
		return true;
	}
	return false;
}

void
LunafbDevice::MonitorScreen(Monitor *, TextScreen& screen)
{
	int i;

	screen.Clear();

	// 0         1         2         3         4
	// 012345678901234567890123456789012345678901
	// RFCNT: $12345678 (X=-123dot, Y=1023dot)
	//
	//    BMSEL=$4 ROP  Mask
	// Plane#0 Sel THRU $ffffffff
	screen.Print(0, 0, "RFCNT: $%08x (X=%4ddot, Y=%4ddot)",
		rfcnt, xscroll, yscroll);
	screen.Print(2, 2, "BMSEL=$%02x ROP  Mask", bmsel);

	for (i = 0; i < nplane; i++) {
		screen.Print(0, 3 + i, "Plane#%u %s %s $%08x",
			i,
			(bmsel & (1U << i)) ? "Sel" : "---",
			RopStr(fcset[i]), fcmask[i]);
	}
	for (; i < MAX_PLANES; i++) {
		screen.Print(0, 3 + i, TA::Disable, "Plane#%u (Not installed)", i);
	}
}

// offset の位置に対応する更新フラグを立てる。
// offset はプレーン先頭からのバイトオフセット。
inline void
LunafbDevice::SetDirty(uint32 offset)
{
	// VRAM は横 2048 ドット(256バイト) x 縦 1024 ドット。
	//
	//                1                               0
	//        4 3 2 1 0 f e d c b a 9 8 7 6 5 4 3 2 1 0
	//       +-----------+-------+-------+-------+-----+
	// VRAM  |  Y座標 (1024dot)  |  X (256byte)  : 8bit|
	//       +-----------+-------+-------+-------+-----+
	//       :                   :
	//       +-------------------+
	// Dirty |                   |
	//       +-------------------+
	//        9 8 7 6 5 4 3 2 1 0

	// 更新フラグはラスタ単位
	uint y = (offset >> 8);
	dirty.line[y] = true;
}

// X 方向にはみ出したときに対応する仮想画面の Y 座標を返す。
int
LunafbDevice::GetWrapY(int y) const
{
	// Lunafb の VRAM は、はみ出すと 256 ラスタ先のラスタの先頭から
	// 表示されるようだ。
	return (y + 256) % 1024;
}

// RFCNT への書き込み
void
LunafbDevice::WriteRFCNT(uint32 data)
{
	// レジスタ値
	rfcnt = data;

	// 水平オフセットを計算。
	// レジスタに設定できる値は 0..255 で、何ピクセル目から表示を開始するかを
	// 4 ピクセル単位で指定するもの。
	// ただし、実際に表示されるのは、(4ピクセル単位で) 設定値 +9 の位置から
	// (謎資料には 8 (というか -8) と書いてあるが実測は +9 のようだ)。
	// 設定値が 0 の時 X=36 ピクセル目から 1315 ピクセル目までを表示。
	// :
	// 設定値が 183 の時 X=768 ピクセル目から 2047 ピクセル目までを表示。
	// これが正常に表示できる範囲となる。
	// 184 以降を指定すると、右のはみ出る部分 (2048 ピクセル目以降に相当) は
	// 256 ラスタ先の X=0 からのデータが描画される (GetWrapY() 参照)。
	// 表示開始位置は設定値に必ず +9 (*4 [pixel]) されるため、X=0..35 の
	// ピクセルを (折り返しにより Y がずれた位置に見えることを除けば)
	// まともに表示する方法はない。まじかよ。
	xscroll = (rfcnt >> 16) & 0xff;
	xscroll = (xscroll + 9) * 4;

	// ラスタ(垂直)カウンタ。
	// XXX 26 は実際には CRTC2 によるパラメータ
	yscroll = (rfcnt + 26) & 0x3ff;

	// 全画面更新
	Invalidate();

	putlog(1, "RFCNT <- $%08x (H=%ddot, V=%ddot)", data, xscroll, yscroll);
}

// 共通ビットマッププレーン バイト書き込み
void
LunafbDevice::WriteCommonBitmap8(uint32 addr, uint32 data)
{
	uint32 offset = addr - 0xb1080000;
	putlog(2, "Common Plane $%08x <- $%02x", addr, data);
	for (int i = 0; i < nplane; i++) {
		if ((bmsel & (1U << i))) {
			WritePlane8(i, offset, data);
		}
	}
	SetDirty(offset);
}

// 共通ビットマッププレーン ワード書き込み
void
LunafbDevice::WriteCommonBitmap16(uint32 addr, uint32 data)
{
	uint32 offset = addr - 0xb1080000;
	putlog(2, "Common Plane $%08x <- $%04x", addr, data);
	for (int i = 0; i < nplane; i++) {
		if ((bmsel & (1U << i))) {
			WritePlane16(i, offset, data);
		}
	}
	SetDirty(offset);
}

// 共通ビットマッププレーン ロング書き込み
void
LunafbDevice::WriteCommonBitmap32(uint32 addr, uint32 data)
{
	uint32 offset = addr - 0xb1080000;
	putlog(2, "Common Plane $%08x <- $%08x", addr, data);
	for (int i = 0; i < nplane; i++) {
		if ((bmsel & (1U << i))) {
			WritePlane32(i, offset, data);
		}
	}
	SetDirty(offset);
}

// 共通ファンクションセット設定
void
LunafbDevice::WriteCommonFCSet(uint rop, uint32 data)
{
	for (int i = 0; i < nplane; i++) {
		if ((bmsel & (1U << i))) {
			fcset[i] = rop;
			fcmask[i] = data;
		}
	}
}

// 現在の VRAM の値 vram と、書き込まれた値 data から
// 演算後 VRAM に書き戻すことになる値を返す。
// ここではマスクはまだ評価しない。
/*static*/ uint32
LunafbDevice::RasterOp(uint rop, uint32 vram, uint32 data)
{
	switch (rop) {
	 case ROP::ZERO:
		vram = 0;
		break;
	 case ROP::AND1:
		vram = vram & data;
		break;
	 case ROP::AND2:
		vram = vram & ~data;
		break;
	 case ROP::cmd:
	 {
		// 使用不可。本当はここにシリアルモード変更コマンドがあるらしい。

		// ログを出すためにはインスタンスが必要だが、普段は static 関数で
		// 十分なので、ログを出す時だけグローバルインスタンスを参照する。
		auto planevram = GetPlaneVRAMDevice();
		if (planevram->loglevel >= 0) {
			planevram->putlogn("ROP 3 is used");
		}
		break;
	 }
	 case ROP::AND3:
		vram = ~vram & data;
		break;
	 case ROP::THRU:
		vram = data;
		break;
	 case ROP::EOR:
		vram = vram ^ data;
		break;
	 case ROP::OR1:
		vram = vram | data;
		break;
	 case ROP::NOR:
		vram = ~vram & ~data;
		break;
	 case ROP::ENOR:
		vram = (vram & data) | (~vram & ~data);
		break;
	 case ROP::INV1:
		vram = ~data;
		break;
	 case ROP::OR2:
		vram = vram | ~data;
		break;
	 case ROP::INV2:
		vram = ~vram;
		break;
	 case ROP::OR3:
		vram = ~vram | data;
		break;
	 case ROP::NAND:
		vram = ~vram | ~data;
		break;
	 case ROP::ONE:
		vram = 0xffffffff;
		break;
	 default:
		VMPANIC("corrupted rop=%u", rop);
	}
	return vram;
}

// ROP 文字列を返す
const char *
LunafbDevice::RopStr(uint rop)
{
	static const char * ropstr[] = {
		"ZERO",
		"AND1",
		"AND2",
		"(3)",
		"AND3",
		"THRU",	// THROUGH
		"EOR",
		"OR1",
		"NOR",
		"ENOR",
		"INV1",
		"OR2",
		"INV2",
		"OR3",
		"NAND",
		"ONE",
	};
	return ropstr[rop];
}

// 個別プレーンへのバイト書き込み
// (dirty は変更しない)
void
LunafbDevice::WritePlane8(uint plane, uint32 offset, uint32 data)
{
	uint32 memoffset = plane * 0x40000 + offset;
	uint32 vram;
	uint32 mask;

	vram = mem[HLB(memoffset)];
	data = RasterOp(fcset[plane], vram, data);
	mask = fcmask[plane] >> (24 - (memoffset & 3) * 8);
	mem[HLB(memoffset)] = (data & mask) | (vram & ~mask);
}

// 個別プレーンへのワード書き込み
// (dirty は変更しない)
void
LunafbDevice::WritePlane16(uint plane, uint32 offset, uint32 data)
{
	uint32 memoffset = plane * 0x40000 + offset;
	uint32 vram;
	uint32 mask;

	vram = *(uint16 *)&mem[HLW(memoffset)];
	data = RasterOp(fcset[plane], vram, data);
	if ((memoffset & 2) == 0) {
		// 上位ワード
		mask = fcmask[plane] >> 16;
	} else {
		// 下位ワード
		mask = fcmask[plane];
	}
	*(uint16 *)&mem[HLW(memoffset)] = (data & mask) | (vram & ~mask);
}

// 個別プレーンへのロングワード書き込み
// (dirty は変更しない)
void
LunafbDevice::WritePlane32(uint plane, uint32 offset, uint32 data)
{
	uint32 memoffset = plane * 0x40000 + offset;
	uint32 vram;
	uint32 mask;

	vram = *(uint32 *)&mem[memoffset];
	data = RasterOp(fcset[plane], vram, data);
	mask = fcmask[plane];
	*(uint32 *)&mem[memoffset] = (data & mask) | (vram & ~mask);
}

// (x, y) 位置のピクセル情報を screen に描画。(ビットマップモニタの情報欄)
void
LunafbDevice::UpdateInfo(TextScreen& screen, int x, int y) const
{
	screen.Clear();

	//0         1         2         3         4         5         6
	//012345678901234567890123456789012345678901234567890123456789012345
	//X=1024 Y=2024: (P0) $b10c0000: $00 (%00000000) ColorCode=$d

	screen.Puts(0, 0, "X=     Y=");
	for (int i = 0; i < nplane; i++) {
		screen.Print(15, i, "(P%u)", i);
	}

	// カーソルが範囲外ならここまで。
	if (x < 0) {
		return;
	}

	screen.Print(2, 0, "%4d Y=%4d", x, y);

	// 1プレーン内のバイトオフセット
	uint32 planeoffset = ((uint)y * 256) + ((uint)x / 8);

	uint cc = 0;
	for (int i = 0; i < nplane; i++) {
		// プレーン0先頭からのバイトオフセット
		uint32 totaloffset = 0x40000 * i + planeoffset;

		uint8 data = mem[HLB(totaloffset)];
		screen.Print(15, i, "(P%u) $%08x: $%02x (%%",
			i, plane0base + totaloffset, data);

		// ビットパターン
		for (int m = 0x80; m != 0; m >>= 1) {
			screen.Putc((data & m) ? '1' : '0');
		}
		screen.Putc(')');

		// カーソル位置をハイライト
		screen.Locate(37 + (x % 8), i);
		screen.SetAttr(TA::Em);

		// カラーコードを計算
		if ((data & (0x80U >> (x % 8))) != 0) {
			cc |= (1U << i);
		}
	}

	screen.Print(47, 0, "ColorCode=$%x", cc);
}


// エミュレータによる画面出力
//
// VM 内からの操作ではなく、エミュレータ本体から画面を操作する
// エミュレーション ROM によるモニタコンソール用の機能。
// サービスを使いたい側 (EmuROM など) は EmuInitScreen() をコールすると
// (現在のパレットのまま) 画面をクリアする。
// 以降は EmuPutc() で文字を表示する。

// エミュレータ出力用に画面を初期化する
void
LunafbDevice::EmuInitScreen()
{
	// 同時書き込み設定
	bmsel = (1U << nplane) - 1;

	// ROP を初期化
	WriteCommonFCSet(ROP::THRU, 0xffffffff);

	// 全画面をクリア
	EmuFill(0, 0, 2048, 1024, 0);

	// 書き込みはプレーン#0 だけ。
	bmsel = 0x01;
}

// 矩形領域を data で埋める。
void
LunafbDevice::EmuFill(uint x, uint y, uint w, uint h, uint32 data)
{
	// ロングワードアラインしていないケースは対応しない
	assert(x % 32 == 0);
	assert(w % 32 == 0);

	for (uint py = y; py < y + h; py++) {
		uint32 addr = 0xb1080000 + (x / 32) * 4 + py * 256;
		for (uint px = x; px < x + w; px += 32) {
			WriteCommonBitmap32(addr, data);
			addr += 4;
		}
	}
}

// ピクセル座標 x, y を左上として文字 c を出力。
// 文字が VRAM からはみ出ないようにすること。
void
LunafbDevice::EmuPutc(uint x, uint y, uint c)
{
	EmuPutc(x, y, c, ROP::THRU);
}

// ピクセル座標 x, y を左上として文字 c を出力。
// 文字が VRAM からはみ出ないようにすること。
void
LunafbDevice::EmuPutc(uint x, uint y, uint c, uint rop)
{
	uint32 addr = 0xb1080000 + (x / 32) * 4 + y * 256;
	uint32 bofs = x % 32;
	int height = (c < 0x80) ? 24 : 12;

	// マスク設定。上から 12bit にマスクを置いといて..
	union64 mask {};
	mask.h = 0xfff00000;

	// うまい具合にシフト
	mask.q >>= bofs;

	// その文字のフォントデータ上でのアドレス
	const uint8 * const fontaddr = (c < 0x80)
		? Builtin::CGROM12x24(c)
		: Builtin::CGROM12x12(c & 0x7f);

	for (int i = 0; i < height; i++) {
		// その文字のフォントデータ。
		// データは MSB 側 12bit に入っているのでこれを 64bit MSB 詰めにする
		union64 data {};
		data.h = be16toh(*(const uint16*)(fontaddr + i * 2)) << 16;

		// うまい具合にシフト
		data.q >>= bofs;

		WriteCommonFCSet(rop, mask.h);
		WriteCommonBitmap32(addr, data.h);
		if (mask.l) {
			WriteCommonFCSet(rop, mask.l);
			WriteCommonBitmap32(addr + 4, data.l);
		}
		addr += 256;	// 1ライン進める
	}

	// ROP を元に戻す
	WriteCommonFCSet(ROP::THRU, 0xffffffff);
}

// 画面を縦スクロールさせる。上スクロールのみサポート。
void
LunafbDevice::EmuScrollY(uint dy, uint sy, uint h)
{
	assert(dy < sy);

	uint32 da = dy * 256;
	uint32 sa = sy * 256;

	for (uint y = 0; y < h; y++) {
		for (int i = 0; i < nplane; i++) {
			memcpy(&mem[da + i * 0x40000], &mem[sa + i * 0x40000], 160);
		}
		da += 256;
		sa += 256;
	}

	Invalidate();
}

unix.superglobalmegacorp.com

This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.