--- nono/vm/videoctlr.cpp 2026/04/29 17:05:10 1.1.1.6 +++ nono/vm/videoctlr.cpp 2026/04/29 17:05:33 1.1.1.11 @@ -16,134 +16,299 @@ // 以上の 4KB をミラー。 #include "videoctlr.h" -#include "mpu.h" - -// グローバル参照用 -VideoCtlrDevice *gVideoCtlr; +#include "mainapp.h" +#include "planevram.h" +#include "renderer.h" +#include "scheduler.h" +#include "uimessage.h" +#include + +// 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("VideoCtlr") + : inherited(OBJ_VIDEOCTLR) { - devaddr = baseaddr; - devlen = 0x2000; + textbmp.Create(768, 512); + hostcolor.resize(16); + +#if defined(HAVE_AVX2) + enable_avx2 = gMainApp.enable_avx2; +#endif +#if defined(HAVE_NEON) + enable_neon = gMainApp.enable_neon; +#endif } // デストラクタ VideoCtlrDevice::~VideoCtlrDevice() { - gVideoCtlr = NULL; +} + +// 初期化 +bool +VideoCtlrDevice::Init() +{ + if (inherited::Init() == false) { + return false; + } + + planevram = GetPlaneVRAMDevice(); + renderer = GetRenderer(); + + // 約30fps。これ以上速くても分からない。 + contrast_event.func = ToEventCallback(&VideoCtlrDevice::ContrastCallback); + contrast_event.time = 30_msec; + contrast_event.SetName("VideoCtlr Analog Contrast"); + scheduler->RegistEvent(contrast_event); + + return true; } // リセット void VideoCtlrDevice::ResetHard(bool poweron) { - // XXX not yet + if (poweron) { + // 本当かどうかは知らないが、とりあえず。 + // 初期状態は放電されていれば 0 のはずで、 + // ビデオコントローラ側の出力の初期値はおそらく High のはず。 + running_contrast = 0; + SetContrast(15); + } } -// offset は何ワード目のポートかという意味になるので -// $E82002 が offset=1 (番目のワード)。 -uint64 -VideoCtlrDevice::Read(uint32 offset) +/*static*/ inline uint32 +VideoCtlrDevice::Decoder(uint32 addr) { - uint32 data; - - gMPU->AddCycle(9); // InsideOut p.135 - - if (offset < 0x400 / 2) { // パレット - data = palette.w[offset]; - - } else if (offset < 0x500 / 2) { // $E82400 (R0) - data = videoctlr.r0; - - } else if (offset < 0x600 / 2) { // $E82500 (R1) - data = videoctlr.r1; - - } else if (offset < 0x700 / 2) { // $E82600 (R2) - data = videoctlr.r2; + return addr & 0xfff; +} - } else { // それ以降は $00 が読めるようだ - data = 0; +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; } -uint64 -VideoCtlrDevice::Write(uint32 offset, uint32 data) +busdata +VideoCtlrDevice::Write(busaddr addr, uint32 data) { - gMPU->AddCycle(11); // InsideOut p.135 - - if (offset < 0x400 / 2) { // パレット - putlog(3, "パレット $%06x.W <- $%04x", baseaddr + offset * 2, data); - palette.w[offset] = data; - MakePalette(); - - } else if (offset < 0x500 / 2) { // $E82400 (R0) - SetR0(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 (NOT IMPLEMENTED)", + rn, ((offset & 1U) ? 'H' : 'L'), data); + } else { + putlogn("R%u <- $%04x (NOT IMPLEMENTED)", rn, data); + } + } + } - } else if (offset < 0x600 / 2) { // $E82500 (R1) - SetR1(data); + if (datasize == 1) { + Set8(offset, data); + } else { + Set16(offset, data); + } + busdata r = write_wait; + r |= busdata::Size(datasize); + return r; +} - } else if (offset < 0x700 / 2) { // $E82600 (R2) - SetR2(data); +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; + } +} - } else { // それ以降はたぶん何も起きない? +bool +VideoCtlrDevice::Poke1(uint32 addr, uint32 data) +{ + // パレットのみ編集可能とする。 + uint32 offset = Decoder(addr); + if (offset < 0x400) { + if ((int32)data >= 0) { + Set8(offset, data); + } + return true; } - return 0; + return false; } -uint64 -VideoCtlrDevice::Peek(uint32 offset) +// R0, R1, R2 のアドレスオフセットからレジスタ番号 0, 1, 2 を返す。 +// offset は R0, R1, R2 の領域を指していること。 +/*static*/ inline uint32 +VideoCtlrDevice::Offset2Rn(uint32 offset) { - uint32 data; - - if (offset < 0x400 / 2) { // パレット - data = palette.w[offset]; + assert(0x400 <= offset && offset < 0x700); + return (offset >> 8) - 4; +} - } else if (offset < 0x500 / 2) { // $E82400 (R0) - data = videoctlr.r0; +// offset の位置の内容をワードで返す。 +// offset は偶数であること。 +uint32 +VideoCtlrDevice::Get16(uint32 offset) const +{ + assert((offset & 1U) == 0); - } else if (offset < 0x600 / 2) { // $E82500 (R1) - data = videoctlr.r1; + if (offset < 0x400) { // パレット + return palette[offset >> 1]; + } else if (offset < 0x700) { // R0,R1,R2 + uint rn = Offset2Rn(offset); + return reg[rn]; + } else { + return 0x00; + } +} - } else if (offset < 0x700 / 2) { // $E82600 (R2) - data = videoctlr.r2; +// offset の位置のバイトを更新する。 +void +VideoCtlrDevice::Set8(uint32 offset, uint32 data) +{ + uint32 offset2 = offset & ~1U; - } else { // それ以降は $00 が読めるようだ - data = 0; + uint32 tmp = Get16(offset2); + if ((offset & 1U) == 0) { + tmp = (data << 8) | (tmp & 0xff); + } else { + tmp = (tmp & 0xff00) | data; } - - return data; + Set16(offset2, tmp); } -// R0 への書き込み +// offset の位置のワードを更新する。 +// offset は偶数であること。 void -VideoCtlrDevice::SetR0(uint32 data) +VideoCtlrDevice::Set16(uint32 offset, uint32 data) { - putlog(0, "R0 <- $%04x 未実装書き込み", data); - videoctlr.r0 = data & 3; + 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 + } } -// R1 への書き込み +// コントラストを設定する。 +// 指定値は 0-15 だが、内部では 0-xff で保持。 void -VideoCtlrDevice::SetR1(uint32 data) +VideoCtlrDevice::SetContrast(uint contrast_) { - putlog(0, "R1 <- $%04x 未実装書き込み", data); - videoctlr.r1 = data & 0x3fff; + target_contrast = contrast_ * 0x11; + + if (running_contrast != target_contrast) { + initial_contrast = running_contrast; + contrast_time0 = scheduler->GetVirtTime(); + scheduler->RestartEvent(contrast_event); + } } -// R2 への書き込み +// コントラスト変化イベント。 +// +// Human68k 電源オフ時などの画面フェードアウトは ROM や Human68k がソフト +// ウェアで時間をかけてコントラストを変えているのではなく、ハードウェアで +// 実装してある。(わざわざ?) RC 回路が入っているのでおそらく意図していると +// 思われ。電源オフ時は ROM ルーチンがシステムポートにコントラスト 0 を1回 +// だけ書き込んでいる。 +// +// 細かいことは無視して RC 回路の過渡現象の式だけ持ってきて使う。 +// +// 下がる時は E * exp(-t / RC) で、 +// E は開始時の値なので initial_contrast、原点は target_contrast とする。 +// +// E |* +// |* +// | * +// | *** +// | **** +// 0 +--------- +// +// 上がる時は E * (1 - exp(-t / RC)) で、 +// この場合 E は定常値なので target_contrast、原点が initial_contrast となる。 +// +// E | **** +// | *** +// | * +// |* +// |* +// 0 +--------- +// +// 途中まで充電されてる状態から始まる過渡現象がこれと同じかどうかは知らない +// けど、とりあえず毎回 initial と target の差だけで動作する。 void -VideoCtlrDevice::SetR2(uint32 data) +VideoCtlrDevice::ContrastCallback(Event& ev) { - putlog(0, "R2 <- $%04x 未実装書き込み", data); - videoctlr.r2 = (data & 0xff7f); + 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) / 1e9; + 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() { @@ -151,19 +316,161 @@ VideoCtlrDevice::MakePalette() // パレット前半 256 ワードはグラフィックパレット、 // テキストパレットは後半。 - p = &palette.w[256]; + p = &palette[256]; // パレット情報から Color 形式の色データを作成 // X680x0 のパレットは %GGGGG'RRRRR'BBBBB'I で並んでいる。 for (int i = 0; i < 16; i++) { - int I = (p[i] & 1) << 2; // 輝度ビット - int R = (((p[i] >> 6) & 0x1f) << 3) + I; - int G = (((p[i] >> 11)) << 3) + I; - int B = (((p[i] >> 1) & 0x1f) << 3) + I; - cooked_palette[i].r = R; - cooked_palette[i].g = G; - cooked_palette[i].b = B; + uint I = (p[i] & 1) << 2; // 輝度ビット + uint R = (((p[i] >> 6) & 0x1f) << 3) + I; + uint G = (((p[i] >> 11)) << 3) + I; + uint B = (((p[i] >> 1) & 0x1f) << 3) + I; + hostcolor[i].r = R; + hostcolor[i].g = G; + hostcolor[i].b = B; } - // XXX たぶんこの辺でレンダラにパレット変更を通知するはず + // パレット変更が起きたことをテキストレンダラに通知。 + // この後ここでグラフィックレンダラにも通知。 + planevram->Invalidate2(); + + // UI にも通知 + UIMessage::Post(UIMessage::PALETTE); +} + +// ゲストパレットを uint32 形式にしたものの一覧を返す。(GUI 用) +// XXX 今の所テキスト16色しか考えてない +const std::vector +VideoCtlrDevice::GetIntPalette() const +{ + std::vector ipal; + const uint16 *p = &palette[256]; + + for (int i = 0; i < 16; i++) { + ipal.push_back(p[i]); + } + return ipal; +} + +// 画面の作成。CRTC から VDisp のタイミングで呼ばれる。 +// Renderer に作画指示を出すかどうかを決める。 +void +VideoCtlrDevice::VDisp() +{ + bool update_needed = false; + + // 今はテキスト画面しかない。 + if (planevram->Snap()) { + update_needed = true; + } + + // コントラストが変わっていても再描画。 + if (__predict_false(rendering_contrast != running_contrast)) { + pending_contrast = running_contrast; + update_needed = true; + } + + if (update_needed) { + renderer->NotifyRender(); + } +} + +// 画面合成。 +// レンダリングスレッドから呼ばれる。 +bool +VideoCtlrDevice::Render(BitmapRGBX& dst) +{ + // 今はテキスト画面しかない。 + bool updated = planevram->Render(textbmp); + + // 最後にコントラスト適用。 + int contrast = pending_contrast.exchange(rendering_contrast); + if (contrast != rendering_contrast) { + rendering_contrast = contrast; + updated = true; + } + if (__predict_false(updated)) { + RenderContrast(dst, textbmp); + 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(dst.GetWidth() == src.GetWidth()); + assert(dst.GetHeight() == src.GetHeight()); + assert(src.GetWidth() % 4 == 0); + + uint32 contrast = rendering_contrast; + if (contrast == 255) { + dst.CopyFrom(&src); + } 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(); + if (++perf_count % 100 == 0) { + printf("%" PRIu64 " usec\n", perf_total / perf_count / 1000); + } +#endif + } +} + +// コントラスト (0-254) を適用。C++ 版 +/*static*/ void +VideoCtlrDevice::RenderContrast_gen(BitmapRGBX& dst, const BitmapRGBX& src, + uint32 contrast) +{ + std::array lut; + + for (uint i = 0; i < lut.size(); i++) { + lut[i] = (i * contrast) >> 8; + } + + // この変換は一次元配列とみなしても行える。 + // また必ずテキスト画面全域なので端数の考慮は不要。 + uint pxlen = src.GetWidth() * src.GetHeight(); + const uint32 *s32 = (const uint32 *)src.GetRowPtr(0); + const uint32 *s32end = s32 + pxlen; + uint32 *d32 = (uint32 *)dst.GetRowPtr(0); + + // 1ピクセルずつ処理する + for (; s32 < s32end; ) { + 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; + } }