--- nono/vm/videoctlr.cpp 2026/04/29 17:05:17 1.1.1.8 +++ nono/vm/videoctlr.cpp 2026/04/29 17:05:24 1.1.1.9 @@ -18,13 +18,29 @@ #include "videoctlr.h" #include "mpu.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(OBJ_VIDEOCTLR) { + textbmp.Create(768, 512); hostcolor.resize(16); + +#if defined(HAVE_AVX2) + enable_avx2 = gMainApp.enable_avx2; +#endif + + // 約30fps。これ以上速くても分からない。 + contrast_event.func = ToEventCallback(&VideoCtlrDevice::ContrastCallback); + contrast_event.time = 30_msec; + contrast_event.Regist("VideoCtlr Analog Contrast"); } // デストラクタ @@ -40,6 +56,7 @@ VideoCtlrDevice::Init() return false; } + planevram = GetPlaneVRAMDevice(); renderer = GetRenderer(); return true; @@ -49,7 +66,13 @@ VideoCtlrDevice::Init() void VideoCtlrDevice::ResetHard(bool poweron) { - // XXX not yet + if (poweron) { + // 本当かどうかは知らないが、とりあえず。 + // 初期状態は放電されていれば 0 のはずで、 + // ビデオコントローラ側の出力の初期値はおそらく High のはず。 + running_contrast = 0; + SetContrast(15); + } } inline uint32 @@ -58,17 +81,15 @@ VideoCtlrDevice::Decoder(uint32 addr) co return addr & 0xfff; } -uint64 +busdata VideoCtlrDevice::Read8(uint32 addr) { - uint32 data; - - mpu->AddCycle(9); // InsideOut p.135 + busdata data; uint32 offset = Decoder(addr); if (offset < 0x400) { // パレット data = palette[HB(offset)]; - putlog(3, "Palette $%06x.B -> $%02x", addr, data); + putlog(3, "Palette $%06x.B -> $%02x", addr, data.Data()); } else if (offset < 0x700) { // R0,R1,R2 uint nr = Offset2NR(offset); @@ -76,45 +97,43 @@ VideoCtlrDevice::Read8(uint32 addr) putlog(3, "R%d.%c -> $%02x", (nr / 2), (nr % 2) == 0 ? 'H' : 'L', - data); + data.Data()); } else { data = 0; } + data |= read_wait; return data; } -uint64 +busdata VideoCtlrDevice::Read16(uint32 addr) { - uint32 data; - - mpu->AddCycle(9); // InsideOut p.135 + busdata data; uint32 offset = Decoder(addr); if (offset < 0x400) { // パレット data = *(uint16 *)&palette[offset]; - putlog(3, "Palette $%06x.W -> $%04x", addr, data); + putlog(3, "Palette $%06x.W -> $%04x", addr, data.Data()); } else if (offset < 0x700) { // R0,R1,R2 uint nr = Offset2NR(offset); data = GetNR(nr) << 8; data |= GetNR(nr + 1); - putlog(3, "R%d -> $%04x", (nr / 2), data); + putlog(3, "R%d -> $%04x", (nr / 2), data.Data()); } else { data = 0; } + data |= read_wait; return data; } -uint64 +busdata VideoCtlrDevice::Write8(uint32 addr, uint32 data) { - mpu->AddCycle(11); // InsideOut p.135 - uint32 offset = Decoder(addr); if (offset < 0x400) { // パレット putlog(2, "Palette $%06x.B <- $%02x", addr, data); @@ -133,14 +152,12 @@ VideoCtlrDevice::Write8(uint32 addr, uin // たぶん何も起きない? } - return 0; + return write_wait; } -uint64 +busdata VideoCtlrDevice::Write16(uint32 addr, uint32 data) { - mpu->AddCycle(11); // InsideOut p.135 - uint32 offset = Decoder(addr); if (offset < 0x400) { // パレット putlog(2, "Palette $%06x.W <- $%04x", addr, data); @@ -157,10 +174,10 @@ VideoCtlrDevice::Write16(uint32 addr, ui // たぶん何も起きない? } - return 0; + return write_wait; } -uint64 +busdata VideoCtlrDevice::Peek8(uint32 addr) { uint32 offset = Decoder(addr); @@ -176,7 +193,7 @@ VideoCtlrDevice::Peek8(uint32 addr) } } -uint64 +bool VideoCtlrDevice::Poke8(uint32 addr, uint32 data) { uint32 offset = Decoder(addr); @@ -186,10 +203,10 @@ VideoCtlrDevice::Poke8(uint32 addr, uint // Poke による編集でもパレットに反映させる MakePalette(); } - return 0; + return true; } - return -1; + return false; } // R0, R1, R2 のオフセットから内部仮想レジスタ番号 NR0-NR5 に変換する。 @@ -241,7 +258,86 @@ VideoCtlrDevice::SetNR(uint reg, uint32 } } +// コントラストを設定する。 +// 指定値は 0-15 だが、内部では 0-xff で保持。 +void +VideoCtlrDevice::SetContrast(int 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 +--------- +// +// 上がる時は E * (1 - exp(-t / RC)) で、 +// この場合 E は定常値なので target_contrast、原点が initial_contrast となる。 +// +// E | **** +// | *** +// | * +// |* +// |* +// 0 +--------- +// +// 途中まで充電されてる状態から始まる過渡現象がこれと同じかどうかは知らない +// けど、とりあえず毎回 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) / 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; + + // 強制再描画をレンダラに通知 + renderer->Invalidate2(); + } + + scheduler->RestartEvent(ev); + } +} + void VideoCtlrDevice::MakePalette() { @@ -283,3 +379,99 @@ VideoCtlrDevice::GetIntPalette() const } return ipal; } + +// 画面合成。 +// レンダリングスレッドから呼ばれる。 +bool +VideoCtlrDevice::Render(BitmapRGBX& dst, ModifyInfo& modify) +{ + // 今はテキスト画面しかない。 + bool updated = planevram->Render(textbmp, modify); + + // 最後にコントラスト適用。 + // アトミックじゃなくても大丈夫のはず。 + if (__predict_false(rendering_contrast != running_contrast)) { + rendering_contrast = running_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 int 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 + { + RenderContrast_gen(dst, src, contrast); + } + +#if defined(PERFCONT) + sw.Stop(); + perf_total += sw.Elapsed(); + if (++perf_count % 100 == 0) { + printf("%" PRIu64 "\n", perf_total / perf_count); + } +#endif + } +} + +// コントラスト (0-254) を適用。C++ 版 +/*static*/ void +VideoCtlrDevice::RenderContrast_gen(BitmapRGBX& dst, const BitmapRGBX& src, + uint32 contrast) +{ + std::array lut; + + for (int i = 0; i < lut.size(); i++) { + lut[i] = (i * contrast) >> 8; + } + + for (int y = 0, yend = src.GetHeight(); y < yend; y++) { + const uint32 *s = (const uint32 *)src.GetRowPtr(y); + uint32 *d = (uint32 *)dst.GetRowPtr(y); + + const uint32 *send = s + src.GetWidth(); + for (; s < send; ) { + // 1ピクセルずつ処理する + // 445.1 usec + + uint32 c = htole32(*s++); + + uint32 r = lut[c & 0xff]; + uint32 g = lut[(c >> 8) & 0xff]; + uint32 b = lut[(c >> 16) & 0xff]; + + c = r | (g << 8 ) | (b << 16); + *d++ = le32toh(c); + } + } +}