File:  [Isaki's NoNo m68k/m88k emulator] / nono / vm / videoctlr.cpp
Revision 1.1.1.15 (vendor branch): download - view: text, annotated - select for diffs
Wed Apr 29 17:06:00 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
//

//
// ビデオコントローラ
//

// e82000..e823ff パレット
// e82400..e824ff R0(e82400.w) のミラー
// e82500..e825ff R1(e82500.w) のミラー
// e82600..e826ff R2(e82600.w) のミラー
// e82700..e82fff $00 が読めるようだ
// 以上の 4KB をミラー。

#include "videoctlr.h"
#include "event.h"
#include "gvram.h"
#include "mainapp.h"
#include "planevram.h"
#include "renderer.h"
#include "scheduler.h"
#include "textscreen.h"
#include "uimessage.h"
#include <cmath>

// InsideOut p.135
static const busdata read_wait  = busdata::Wait( 9 * 40_nsec);
static const busdata write_wait = busdata::Wait(11 * 40_nsec);

// コンストラクタ
VideoCtlrDevice::VideoCtlrDevice()
	: inherited(OBJ_VIDEOCTLR)
{
	txbmp.Create(1024, 1024);
	grbmp.Create(1024, 1024);
	mixbmp.Create(1024, 1024);

	palette.resize(512);
	hostcolor.resize(512);

#if defined(HAVE_AVX2)
	enable_avx2 = gMainApp.enable_avx2;
#endif
#if defined(HAVE_NEON)
	enable_neon = gMainApp.enable_neon;
#endif
}

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

// 初期化
bool
VideoCtlrDevice::Init()
{
	gvram = GetGVRAMDevice();
	planevram = GetPlaneVRAMDevice();
	renderer = GetVideoRenderer();
	uimessage = gMainApp.GetUIMessage();

	auto evman = GetEventManager();
	contrast_event = evman->Regist(this,
		ToEventCallback(&VideoCtlrDevice::ContrastCallback),
		"VideoCtlr Analog Contrast");
	// 約30fps。これ以上速くても分からない。
	contrast_event->time = 30_msec;

	return true;
}

// リセット
void
VideoCtlrDevice::ResetHard(bool poweron)
{
	if (poweron) {
		// 本当かどうかは知らないが、とりあえず。
		// 初期状態は放電されていれば 0 のはずで、
		// ビデオコントローラ側の出力の初期値はおそらく High のはず。
		running_contrast = 0;
		SetContrast(15);
	}

	// リセット直後に再描画を起こすため、一致しない値にしておく。
	prev_reg1 = -1;
	prev_reg2 = -1;
}

/*static*/ inline uint32
VideoCtlrDevice::Decoder(uint32 addr)
{
	return addr & 0xfff;
}

busdata
VideoCtlrDevice::Read(busaddr addr)
{
	uint32 paddr = addr.Addr();
	uint32 offset = Decoder(paddr);
	busdata data;

	data = Get16(offset & ~1U);
	if (__predict_false(loglevel >= 3)) {
		if (offset < 0x400) {			// パレット
			putlogn("Palette $%06x -> $%04x", (paddr & ~1U), data.Data());
		} else if (offset < 0x700) {	// R0,R1,R2
			uint rn = Offset2Rn(offset);
			putlogn("R%u -> $%04x", rn, data.Data());
		}
	}
	data |= read_wait;
	data |= BusData::Size2;
	return data;
}

busdata
VideoCtlrDevice::Write(busaddr addr, uint32 data)
{
	uint32 offset = Decoder(addr.Addr());
	uint32 reqsize = addr.GetSize();
	uint32 datasize = std::min(2 - (offset & 1U), reqsize);
	data >>= (reqsize - datasize) * 8;

	if (__predict_false(loglevel >= 2)) {
		if (offset < 0x400) {			// パレット
			putlogn("Palette $%06x.%c <- $%0*x", addr.Addr(),
				(datasize == 1 ? 'B' : 'W'), datasize * 2, data);
		} else if (offset < 0x700) {	// R0,R1,R2
			uint rn = Offset2Rn(offset);
			if (datasize == 1) {
				putlogn("R%u.%c <- $%02x",
					rn, ((offset & 1U) ? 'H' : 'L'), data);
			} else {
				putlogn("R%u <- $%04x", rn, data);
			}
		}
	}

	if (datasize == 1) {
		Set8(offset, data);
	} else {
		Set16(offset, data);
	}
	busdata r = write_wait;
	r |= busdata::Size(datasize);
	return r;
}

busdata
VideoCtlrDevice::Peek1(uint32 addr)
{
	uint32 offset = Decoder(addr);
	uint32 data = Get16(offset & ~1U);
	if ((offset & 1U) == 0) {
		return data >> 8;
	} else {
		return data & 0xff;
	}
}

bool
VideoCtlrDevice::Poke1(uint32 addr, uint32 data)
{
	// パレットのみ編集可能とする。
	uint32 offset = Decoder(addr);
	if (offset < 0x400) {
		if ((int32)data >= 0) {
			Set8(offset, data);
		}
		return true;
	}

	return false;
}

// モニタ更新の下請け。CRTC から呼ばれる。
int
VideoCtlrDevice::MonitorScreen(TextScreen& screen, int y, uint32 crtc_r20)
{
	std::array<uint16, 3> tmp = reg;

	for (uint i = 0; i < tmp.size(); i++) {
		screen.Print(0, y + i, "$%06x R%u: $%04x",
			baseaddr + 0x400 + i * 0x100, i, tmp[i]);
	}

	// R0 (p.234)
	// CRTC R20 の bit10-8 と同じでなければいけないと書いてあるが、
	// 違ったら CRTC のほうが使われてるっぽいので、こっちを赤とかにしてみる。
	// (NetBSD/x68k の X)
	uint tmp0 = tmp[0] & 7;
	uint diff = tmp0 ^ (crtc_r20 >> 8);
	screen.Print(18, y, ((diff & R0_SIZ) ? TA::ReverseRed : TA::Normal),
		"SIZ=%s", siz_str[tmp0 >> 2]);
	screen.Print(32, y, ((diff & R0_COL) ? TA::ReverseRed : TA::Normal),
		"COL=%s", col_str[tmp0 & R0_COL]);
	// ちょっと間借りして内部情報を表示。
	{
		char sobuf[32];
		int len = strlcpy(sobuf, "(SCREEN_ORDER=", sizeof(sobuf));
		for (uint32 s = screen_order; s != 0; s >>= 4) {
			static const char so_str[] = ".STGBK";
			sobuf[len++] = so_str[s & 0xf];
		}
		sobuf[len] = '\0';
		len = strlcat(sobuf, ")", sizeof(sobuf));
		screen.Puts(screen.GetCol() - len, y, sobuf);
	}
	y++;

	// R1 (p.188)
	{
		std::array<const char *, 4> plist {};
		uint sp = (tmp[1] >> 12) & 3;
		uint tp = (tmp[1] >> 10) & 3;
		uint gp = (tmp[1] >>  8) & 3;
		plist[sp] = "SP";
		plist[tp] = "TX";
		plist[gp] = "GR";
		std::string pstr;
		if (plist[3]) {
			pstr = "Invalid";
		} else if (plist[0] == NULL || plist[1] == NULL || plist[2] == NULL) {
			pstr = "Conflict";
		} else {
			pstr = string_format("%s > %s > %s", plist[0], plist[1], plist[2]);
		}

		uint gn =  tmp[1] & 0xff;
		std::string gstr;
		switch (tmp0 & 3) {
		 case 0:	// 16色モード、グラフィックページは4枚。
		 {
			std::array<const char *, 4> glist {};
			uint p3 = (tmp[1] >> 6) & 3;
			uint p2 = (tmp[1] >> 4) & 3;
			uint p1 = (tmp[1] >> 2) & 3;
			uint p0 =  tmp[1]       & 3;
			glist[p0] = "P0";
			glist[p1] = "P1";
			glist[p2] = "P2";
			glist[p3] = "P3";
			if (glist[0] && glist[1] && glist[2] && glist[3]) {
				gstr = string_format("%s > %s > %s > %s",
					glist[0], glist[1], glist[2], glist[3]);
			} else {
				gstr = "Conflict";
			}
			break;
		 }

		 case 1:	// 256色モード、グラフィックページは2枚。
			// この場合 $e4 か $4e だけが有効。
			if (gn == 0xe4) {
				gstr = "P0 > P1";
			} else if (gn == 0x4e) {
				gstr = "P1 > P0";
			} else {
				gstr = "Invalid";
			}
			break;

		 case 2:	// 未定義
			gstr = "?";
			break;

		 case 3:	// 65536色モード、グラフィックページは1枚。
			gstr = "P0";
			break;

		 default:
			__unreachable();
		}

		//   2         3         4         5         6
		// 89012345678901234567890123456789012345678901
		// Priority=SP > TX > GR,  GP=P0 > P1 > P2 > P3
		screen.Print(18, y, "Priority=%s,", pstr.c_str());
		screen.Print(42, y, "GP=%s", gstr.c_str());
	}
	y++;

	// R2 (p.210)
	uint x = 18;
	uint tmp2 = tmp[2];
	screen.Print(x +  0, y, TA::OnOff(tmp2 & R2_YS), "YS");
	screen.Print(x +  3, y, TA::OnOff(tmp2 & R2_AH), "AH");
	screen.Print(x +  6, y, TA::OnOff(tmp2 & R2_VH), "VH");
	screen.Print(x +  9, y, TA::OnOff(tmp2 & R2_EX), "EX");
	screen.Print(x + 12, y, TA::OnOff(tmp2 & R2_HP), "HP");
	screen.Print(x + 15, y, TA::OnOff(tmp2 & R2_BP), "BP");
	screen.Print(x + 18, y, TA::OnOff(tmp2 & R2_GG), "GG");
	screen.Print(x + 21, y, TA::OnOff(tmp2 & R2_GT), "GT");
	screen.Print(x + 24, y, TA::OnOff(tmp2 & 0x0080), "0");
	screen.Print(x + 26, y, TA::OnOff(tmp2 & R2_SP_ON), "SP");
	screen.Print(x + 29, y, TA::OnOff(tmp2 & R2_TX_ON), "TX");
	screen.Print(x + 32, y, TA::OnOff(tmp2 & R2_GR_ON), "GR");
	screen.Print(x + 35, y, TA::OnOff(tmp2 & R2_G3_ON), "G3");
	screen.Print(x + 38, y, TA::OnOff(tmp2 & R2_G2_ON), "G2");
	screen.Print(x + 41, y, TA::OnOff(tmp2 & R2_G1_ON), "G1");
	screen.Print(x + 44, y, TA::OnOff(tmp2 & R2_G0_ON), "G0");
	y++;

	return y;
}

// CRTC からも使う。
/*static*/ const char * const
VideoCtlrDevice::siz_str[2] = {
	"512x512",
	"1024x1024",
};

/*static*/ const char * const
VideoCtlrDevice::col_str[4] = {
	"16",
	"256",
	"undef",
	"65536",
};

// R0, R1, R2 のアドレスオフセットからレジスタ番号 0, 1, 2 を返す。
// offset は R0, R1, R2 の領域を指していること。
/*static*/ inline uint32
VideoCtlrDevice::Offset2Rn(uint32 offset)
{
	assert(0x400 <= offset && offset < 0x700);
	return (offset >> 8) - 4;
}

// offset の位置の内容をワードで返す。
// offset は偶数であること。
uint32
VideoCtlrDevice::Get16(uint32 offset) const
{
	assert((offset & 1U) == 0);

	if (offset < 0x400) {				// パレット
		return palette[offset >> 1];
	} else if (offset < 0x700) {		// R0,R1,R2
		uint rn = Offset2Rn(offset);
		return reg[rn];
	} else {
		return 0x00;
	}
}

// offset の位置のバイトを更新する。
void
VideoCtlrDevice::Set8(uint32 offset, uint32 data)
{
	uint32 offset2 = offset & ~1U;

	uint32 tmp = Get16(offset2);
	if ((offset & 1U) == 0) {
		tmp = (data << 8) | (tmp & 0xff);
	} else {
		tmp = (tmp & 0xff00) | data;
	}
	Set16(offset2, tmp);
}

// offset の位置のワードを更新する。
// offset は偶数であること。
void
VideoCtlrDevice::Set16(uint32 offset, uint32 data)
{
	assert((offset & 1U) == 0);

	if (offset < 0x400) {				// パレット
		palette[offset >> 1] = data;
		MakePalette();
	} else if (offset < 0x700) {		// R0,R1,R2
		uint rn = Offset2Rn(offset);
		switch (rn) {
		 case 0:	reg[rn] = data & 0x0003U;	break;
		 case 1:	reg[rn] = data & 0x3fffU;	break;
		 case 2:	reg[rn] = data & 0xff7fU;	break;
		 default:
			break;
		}
	} else {
		// nop
	}
}

// コントラストを設定する。
// 指定値は 0-15 だが、内部では 0-xff で保持。
void
VideoCtlrDevice::SetContrast(uint contrast_)
{
	target_contrast = contrast_ * 0x11;

	if (running_contrast != target_contrast) {
		initial_contrast = running_contrast;
		contrast_time0 = scheduler->GetVirtTime();
		scheduler->RestartEvent(contrast_event);
	}
}

// コントラスト変化イベント。
//
// Human68k 電源オフ時などの画面フェードアウトは ROM や Human68k がソフト
// ウェアで時間をかけてコントラストを変えているのではなく、ハードウェアで
// 実装してある。(わざわざ?) RC 回路が入っているのでおそらく意図していると
// 思われ。電源オフ時は ROM ルーチンがシステムポートにコントラスト 0 を1回
// だけ書き込んでいる。
//
// 細かいことは無視して RC 回路の過渡現象の式だけ持ってきて使う。
//
// 下がる時は E * exp(-t / RC) で、
// E は開始時の値なので initial_contrast、原点は target_contrast とする。
//
//  E |*
//    |*
//    | *
//    |  ***
//    |     ****
//  0 +---------> t
//
// 上がる時は E * (1 - exp(-t / RC)) で、
// この場合 E は定常値なので target_contrast、原点が initial_contrast となる。
//
//  E |     ****
//    |  ***
//    | *
//    |*
//    |*
//  0 +---------> t
//
// 途中まで充電されてる状態から始まる過渡現象がこれと同じかどうかは知らない
// けど、とりあえず毎回 initial と target の差だけで動作する。
void
VideoCtlrDevice::ContrastCallback(Event *ev)
{
	const float RC = 0.18;	// 時定数 1.8[KΩ]*100[uF]

	if (running_contrast != target_contrast) {
		// 下がる時は (initial - target) * exp(-t / RC)
		// 上がる時は (target - initial) * (1 - exp(-t / RC))
		uint64 now = scheduler->GetVirtTime();
		float t = (float)(now - contrast_time0) / 1_sec;
		float e = std::exp(-t / RC);
		float n;
		if (initial_contrast > target_contrast) {
			float v = (float)(initial_contrast - target_contrast);
			n = target_contrast + v * e;
		} else {
			float v = (float)(target_contrast - initial_contrast);
			n = initial_contrast + v * (1 - e);
		}
		uint32 new_contrast = (uint32)(n + 0.5);
		putlog(2, "Contrast %x->%x: t=%g e=%g new=%x",
			initial_contrast, target_contrast, t, e, new_contrast);

		if (new_contrast != running_contrast) {
			running_contrast = new_contrast;
		}

		scheduler->RestartEvent(ev);
	}
}

void
VideoCtlrDevice::MakePalette()
{
	// パレット情報から Color 形式の色データを作成
	// X680x0 のパレットは %GGGGG'RRRRR'BBBBB'I で並んでいる。
	for (int i = 0; i < palette.size(); i++) {
		uint I = (palette[i] & 1) << 2;	// 輝度ビット
		uint R = (((palette[i] >> 6) & 0x1f) << 3) + I;
		uint G = (((palette[i] >> 11)) << 3) + I;
		uint B = (((palette[i] >> 1) & 0x1f) << 3) + I;
		hostcolor[i].r = R;
		hostcolor[i].g = G;
		hostcolor[i].b = B;
	}

	// パレット変更が起きたことをテキストレンダラに通知。
	// この後ここでグラフィックレンダラにも通知。
	planevram->Invalidate2();

	// UI にも通知
	uimessage->Post(UIMessage::PALETTE);
}

// 画面の作成。CRTC から VDisp のタイミングで呼ばれる。
// VideoRenderer に作画指示を出すかどうかを決める。
void
VideoCtlrDevice::VDisp()
{
	bool update_needed = false;

	// 各画面に更新があるか。モニタ用にもなるためオフでも行う。
	if (planevram->Snap()) {
		update_needed = true;
	}
	if (gvram->Snap()) {
		update_needed = true;
	}

	// プライオリティかオンオフが変わっていても再描画。
	if (reg[1] != prev_reg1 || (reg[2] & 0xff) != prev_reg2) {
		std::array<uint32, 4> list {};
		// オンオフと描画順。
		// XXX グラフィック画面のプレーン(ページ)は未対応
#if 0
		if ((reg[2] & R2_SP_ON)) {
			uint sp = (reg[1] >> 12) & 3;
			list[sp] = SORDER_SP;
		}
#endif
		if ((reg[2] & R2_TX_ON)) {
			uint tp = (reg[1] >> 10) & 3;
			list[tp] = SORDER_TX;
		}
		if ((reg[2] & R2_GR_ON)) {
			uint gp = (reg[1] >>  8) & 3;
			list[gp] = SORDER_GR;
		}

		uint32 new_order = 0;
		for (uint i = 0; i < 3; i++) {
			if (list[i] != SORDER_NONE) {
				new_order <<= 4;
				new_order |= list[i];
			}
		}
		// 全レイヤーオフなら仮想的な黒画面を出力。
		if (new_order == 0) {
			new_order = SORDER_BLACK;
		}
		// 変化があれば再描画。
		if (new_order != screen_order) {
			screen_order = new_order;
			update_needed = true;
		}

		prev_reg1 = reg[1];
		prev_reg2 = reg[2] & 0xff;
	}

	// コントラストが変わっていても再描画。
	if (__predict_false(rendering_contrast != running_contrast)) {
		pending_contrast = running_contrast;
		update_needed = true;
	}

	if (update_needed) {
		renderer->NotifyRender();
	}
}

// 画面合成。
// レンダリングスレッドから呼ばれる。
bool
VideoCtlrDevice::Render(BitmapRGBX& dst)
{
	uint32 local_screen_order = screen_order;

	// 各画面を描画。各画面(レイヤー)はモニタ表示用としても使うため、
	// 表示オンオフに関わらず、常に作成は行う。
	bool tx_updated = planevram->Render(txbmp);
	bool gr_updated = gvram->Render(grbmp);

	// mixbmp に合成。mixbmp もモニタ表示に使う。
	// XXX 重ね合わせはまだ
	bool mix_updated = false;
	for (uint32 s = local_screen_order; s != 0; s >>= 4) {
		switch (s & 0xf) {
		 case SORDER_TX:
			mixbmp.CopyFrom(&txbmp);
			mix_updated = tx_updated;
			break;
		 case SORDER_GR:
			mixbmp.CopyFrom(&grbmp);
			mix_updated = gr_updated;
			break;
		 case SORDER_BLACK:
			mixbmp.Fill(0);
			mix_updated = true;
			break;
		 case SORDER_SP:
		 case SORDER_BG:
		 default:
			break;
		}
	}

	// 最後にコントラスト適用。
	bool updated = false;
	int contrast = pending_contrast.exchange(rendering_contrast);
	if (contrast != rendering_contrast) {
		rendering_contrast = contrast;
		updated = true;
	}
	if (__predict_false(mix_updated || updated)) {
		RenderContrast(dst, mixbmp);
		updated = true;
	}

	return updated;
}

#if 0
#define PERFCONT
#include "stopwatch.h"
static uint64 perf_total;
static uint perf_count;
#endif

// コントラストを適用。
void
VideoCtlrDevice::RenderContrast(BitmapRGBX& dst, const BitmapRGBX& src)
{
	assert(src.GetWidth() % 4 == 0);

	uint32 contrast = rendering_contrast;
	if (__predict_false(contrast == 255)) {
		Rect rect(0, 0, dst.GetWidth(), dst.GetHeight());
		dst.DrawBitmap(0, 0, src, rect);
	} else if (__predict_false(contrast == 0)) {
		uint8 *d = dst.GetBuf();
		memset(d, 0, dst.GetStride() * dst.GetHeight());
	} else {
#if defined(PERFCONT)
		Stopwatch sw;
		sw.Start();
#endif

#if defined(HAVE_AVX2)
		if (__predict_true(enable_avx2)) {
			RenderContrast_avx2(dst, src, contrast);
		} else
#endif
#if defined(HAVE_NEON)
		if (__predict_true(enable_neon)) {
			RenderContrast_neon(dst, src, contrast);
		} else
#endif
		{
			RenderContrast_gen(dst, src, contrast);
		}

#if defined(PERFCONT)
		sw.Stop();
		perf_total += sw.Elapsed_nsec();
		if (++perf_count % 100 == 0) {
			printf("%" PRIu64 " usec\n", nsec_to_usec(perf_total) / perf_count);
		}
#endif
	}
}

// コントラスト (1-254) を適用。dst サイズでクリップする。C++ 版。
/*static*/ void
VideoCtlrDevice::RenderContrast_gen(BitmapRGBX& dst, const BitmapRGBX& src,
	uint32 contrast)
{
	std::array<uint8, 256> lut;

	for (uint i = 0; i < lut.size(); i++) {
		lut[i] = (i * contrast) >> 8;
	}

	for (uint y = 0, yend = dst.GetHeight(); y < yend; y++) {
		const uint32 *s32 = (const uint32 *)src.GetRowPtr(y);
		uint32 *d32 = (uint32 *)dst.GetRowPtr(y);
		for (uint x = 0, xend = dst.GetWidth(); x < xend; x++) {
			// 1ピクセルずつ処理する。
			Color cs(*s32++);

			uint32 r = lut[cs.r];
			uint32 g = lut[cs.g];
			uint32 b = lut[cs.b];

			Color cd(r, g, b);
			*d32++ = cd.u32;
		}
	}
}

unix.superglobalmegacorp.com

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