--- nono/vm/tvram.cpp 2026/04/29 17:04:28 1.1.1.1 +++ nono/vm/tvram.cpp 2026/04/29 17:04:39 1.1.1.4 @@ -1,20 +1,36 @@ // // nono -// Copyright (C) 2017 isaki@NetBSD.org +// Copyright (C) 2020 nono project +// Licensed under nono-license.txt +// + // // TVRAM // -#include "renderer.h" #include "tvram.h" -#include "vicon.h" +#include "crtc.h" +#include "renderer.h" +#include "videoctlr.h" +#if defined(__x86_64__) +#include +#endif + +#if defined(PERF) +// テキスト画面合成のパフォーマンス測定。PERF をどっかで有効にしてビルド。 +// X68030 IPLROM で起動してくるあたりの画面更新で測定する。 +#include +static int total_elapsed; +static int total_count; +static std::list fpsarray; +#endif // // TVRAM はワード単位でホストバイトオーダ配置なので、 // バイトアクセス時は HB() マクロを使用のこと。 // -TVRAMDevice *gTVRAM; +std::unique_ptr gTVRAM; TVRAMDevice::TVRAMDevice() { @@ -23,14 +39,23 @@ TVRAMDevice::TVRAMDevice() devaddr = baseaddr; devlen = 512 * 1024; - mem = new uint8[devlen](); + mem.reset(new uint8[devlen]); + + // レンダリング時に使うテーブルを事前に計算。 + // %abcdefgh の8ビットを + // %000h000g'000f000e'000d000c'000b000a + // の32ビットに伸張する。(各 nibble の最下位ビットに対応させる) + for (int i = 0; i < 256; i++) { + uint32_t res = 0; + for (int bit = 0; bit < 8; bit++) { + res |= ((i >> bit) & 1) << (28 - bit * 4); + } + deptable[i] = res; + } } TVRAMDevice::~TVRAMDevice() { - if (mem) { - delete[] mem; - } } void @@ -38,26 +63,37 @@ TVRAMDevice::ResetHard() { // XXX ここ? Invalidate(); + +#if defined(PERF) + // 分かりやすさのために表示 + printf("measuring TVRAM performance...\n"); +#endif +} + +inline uint64 +TVRAMDevice::Decoder(uint32 addr) const +{ + return addr - baseaddr; } uint64 TVRAMDevice::Read8(uint32 addr) { - uint32 offset = addr - baseaddr; + uint32 offset = Decoder(addr); return mem[HB(offset)]; } uint64 TVRAMDevice::Read16(uint32 addr) { - uint32 offset = addr - baseaddr; + uint32 offset = Decoder(addr); return *(uint16 *)&mem[offset]; } uint64 TVRAMDevice::Write8(uint32 addr, uint32 data) { - uint32 offset = addr - baseaddr; + uint32 offset = Decoder(addr); // XXX とりあえずね putlog(3, "Write $%06x <- $%02x", addr, data); @@ -81,7 +117,7 @@ TVRAMDevice::Write8(uint32 addr, uint32 uint64 TVRAMDevice::Write16(uint32 addr, uint32 data) { - uint32 offset = addr - baseaddr; + uint32 offset = Decoder(addr); // XXX とりあえずね putlog(3, "Write $%06x <- $%04x", addr, data); @@ -105,7 +141,8 @@ TVRAMDevice::Write16(uint32 addr, uint32 uint64 TVRAMDevice::Peek8(uint32 addr) { - return mem[HB(addr - baseaddr)]; + uint32 offset = Decoder(addr); + return mem[HB(offset)]; } // 全画面の更新フラグを立てる @@ -117,6 +154,20 @@ TVRAMDevice::Invalidate() } } +void +TVRAMDevice::SetScrollX(int x) +{ + tvram.xscroll = x; +} + +void +TVRAMDevice::SetScrollY(int y) +{ + tvram.yscroll = y; + // 全ラインを無効にする XXX とりあえずね + Invalidate(); +} + // CRTC R21 での同時アクセス設定を反映する // CRTC から呼ばれる void @@ -145,8 +196,7 @@ TVRAMDevice::set_crtc_21(uint16 data) bool TVRAMDevice::Render(uint8 *imagebuf) { - const uint16 *palette_w; - uint8 pal[16][3]; + const uint32 *pal; const uint16 *plane0; const uint16 *plane1; const uint16 *plane2; @@ -154,21 +204,14 @@ TVRAMDevice::Render(uint8 *imagebuf) uint8 *dst0; bool updated; +#if defined(PERF) + auto start = std::chrono::system_clock::now(); +#endif + dst0 = imagebuf; - // パレット取得 - palette_w = gVicon->GetPalette(); - // パレット前半 256 ワードはグラフィックパレット、 - // テキストパレットは後半。 - palette_w += 0x100; - - // パレット情報から RGB24 色データを作成 - // XXX ちょっと輝度は置いとく - for (int i = 0; i < 16; i++) { - pal[i][0] = ((palette_w[i] >> 6) & 0x1f) << 3; // R - pal[i][1] = ((palette_w[i] >> 11)) << 3; // G - pal[i][2] = ((palette_w[i] >> 1) & 0x1f) << 3; // B - } + // 加工済みパレットを取得 + pal = gVideoCtlr->GetCookedPalette(); updated = false; @@ -182,50 +225,223 @@ TVRAMDevice::Render(uint8 *imagebuf) // XXX スクロールは未実装 // テキスト画面 - plane0 = (uint16 *)mem; - plane1 = (uint16 *)(mem + 0x20000); - plane2 = (uint16 *)(mem + 0x40000); - plane3 = (uint16 *)(mem + 0x60000); - int sy = 0; + plane0 = (uint16 *)&mem[0x00000]; + plane1 = (uint16 *)&mem[0x20000]; + plane2 = (uint16 *)&mem[0x40000]; + plane3 = (uint16 *)&mem[0x60000]; + int sy = tvram.yscroll; for (int dy = 0; dy < 512; dst0 += 768 * Renderer::BPP, dy++, sy++) { sy &= 0x3ff; +#if defined(PERF) + // 計測中は常に全域を展開。 + // 起動直後のタイトルが消えるくらいまでではほぼ更新がなく + // ほぼ atomic_cas の時間を測定してるだけになってしまうので。 + atomic_dirty[sy] = 2; + if (1) { +#else if (atomic_cas_8(&atomic_dirty[sy], 1, 2) == 1) { - uint8 *dst = dst0; +#endif + uint32 *dst = (uint32 *)dst0; + uint32 *prb = rb; + // sx はメモリ上の X オフセット (ワード単位) // dx はレンダラ画面の X オフセット (ワード単位) int sx = sy * 1024 / 16; for (int dx = 0; dx < 768 / 16; dx++, sx++) { +#if defined(__SSE2__) && !defined(NO_SSE) + __m128i a; + // 各プレーン 16 ビットを DWORD * 4 に読み込む。 + // plane0 = A, plane1 = B, plane2 = C, plane3 = D とすると + // a = $0000DDDD'0000CCCC'0000BBBB'0000AAAA; + a = _mm_set_epi32( + plane3[sx], + plane2[sx], + plane1[sx], + plane0[sx]); + // 各DWORD中の上位ワードに移動 + // a = $DDDD0000'CCCC0000'BBBB0000'AAAA0000; + a = _mm_slli_si128(a, 2); + for (int j = 0; j < 16; j++) { + // 各DWORDの最上位ビットを計4ビット取り出す + int c = _mm_movemask_ps(_mm_castsi128_ps(a)); + // 各DWORDを左シフト + a = _mm_add_epi32(a, a); + *prb++ = pal[c]; + } +#else + // t0..t3 は 16 ドット× 4 プレーン分の TVRAM 生データ。 + // このうち先頭の2ドットのカラーコードが %abcd, %efgh で + // 表される場合、t0..t3 は以下のようになっている。 + // t0 = %dhxxxxxx'xxxxxxxx + // t1 = %cgxxxxxx'xxxxxxxx + // t2 = %bfxxxxxx'xxxxxxxx + // t3 = %aexxxxxx'xxxxxxxx + // + // deptable[] によってこの t0..t3 の 16ビットを + // d0..d3 の 64ビット(16 nibble) に展開する。 + // d0 = %000x'000x' .. '000h'000d + // d1 = %000x'000x' .. '000g'000c + // d2 = %000x'000x' .. '000f'000b + // d3 = %000x'000x' .. '000e'000a + // + // これを 1ビットずつずらして合成する。 + // cc = %xxxx'xxxx' .. 'efgh'abcd + // + // 16進数で表すと + // cc = $PONMLKJI'HGFEDCBA で + // $A が +0 ドット目のカラーコード、 + // $B が +1 ドット目のカラーコード、 + // : + // $P が +15ドット目のカラーコード、 + // となる。 + uint16 t0 = plane0[sx]; uint16 t1 = plane1[sx]; uint16 t2 = plane2[sx]; uint16 t3 = plane3[sx]; - // 1ワードの 16bit が横 16dot に相当 + union64 d0, d1, d2, d3; + d0.l = deptable[t0 >> 8]; + d1.l = deptable[t1 >> 8]; + d2.l = deptable[t2 >> 8]; + d3.l = deptable[t3 >> 8]; + d0.h = deptable[t0 & 0xff]; + d1.h = deptable[t1 & 0xff]; + d2.h = deptable[t2 & 0xff]; + d3.h = deptable[t3 & 0xff]; + + uint64 cc = d0.q | (d1.q << 1) | (d2.q << 2) | (d3.q << 3); + for (int j = 0; j < 16; j++) { - uint c = 0; - // カラーコード(パレット番号)を作成 - if ((t0 & 0x8000)) c |= 1; - if ((t1 & 0x8000)) c |= 2; - if ((t2 & 0x8000)) c |= 4; - if ((t3 & 0x8000)) c |= 8; - t0 <<= 1; - t1 <<= 1; - t2 <<= 1; - t3 <<= 1; - - *dst++ = pal[c][0]; - *dst++ = pal[c][1]; - *dst++ = pal[c][2]; + uint32 c = cc & 0xf; + *prb++ = pal[c]; + cc >>= 4; } +#endif } +#if defined(__SSE4_1__) && !defined(NO_SSE) + // XBGR32 16ピクセル (16 ロングワード = 64バイト) から + // RGB24 16ピクセル (12 ロングワード = 48バイト) に変換する + for (int j = 0; j < 768; j += 16) { + // rb[0,1,2,3] = $00B0G0R0,$00B1G1R1,$00B2G2R2,$00B3G3R3 + // なので LE で 128ビット値としてレジスタに読み込むと + // a[0] = $00B3G3R3'00B2G2R2'00B1G1R1'00B0G0R0 となる。 + // + // ここは1文字が1バイトで x111 = $X1B1G1R1 だと思いねえ。 + // 入力の rb[0..15] と a[0..3] (どちらも64バイト) はこう。 + // rb[0,1,2,3] = a[0] = x333 x222 x111 x000 + // rb[4,5,6,7] = a[1] = x777 x666 x555 x444 + // rb[8,9,a,b] = a[2] = xbbb xaaa x999 x888 + // rb[c,d,e,f] = a[3] = xfff xeee xddd xccc + // + // suff(shuffle index) を使って a[0..3] を b[0..3] にする。 + // この次のステージで OR しやすい順を意識して並べ替える。 + // **** は使わないところ。 + // b[0] = **** 3332 2211 1000 + // b[1] = 5444 **** 7776 6655 + // b[2] = aa99 9888 **** bbba + // b[3] = fffe eedd dccc **** + // + // {P,Q,R,S} 表記を4ロングワードそれぞれに対するマスクとして、 + // c[0] = (b[0] & {0,1,1,1}) | (b[1] & {1,0,0,0}) + // c[1] = (b[1] & {0,0,1,1}) | (b[2] & {1,1,0,0}) + // c[2] = (b[2] & {0,0,0,1}) | (b[3] & {1,1,1,0}) + // で + // c[0] = 5444 3332 2211 1000 + // c[1] = aa99 9888 7776 6655 + // c[2] = fffe eedd dccc bbba + // ができる。 + + __m128i suff0 = _mm_set_epi8( + -1,-1,-1,-1, 14,13,12,10, 9, 8, 6, 5, 4, 2, 1, 0); + __m128i suff1 = _mm_set_epi8( + 4, 2, 1, 0, -1,-1,-1,-1, 14,13,12,10, 9, 8, 6, 5); + __m128i suff2 = _mm_set_epi8( + 9, 8, 6, 5, 4, 2, 1, 0, -1,-1,-1,-1, 14,13,12,10); + __m128i suff3 = _mm_set_epi8( + 14,13,12,10, 9, 8, 6, 5, 4, 2, 1, 0, -1,-1,-1,-1); + + __m128i a0 = _mm_load_si128((__m128i *)(&rb[j + 0])); + __m128i a1 = _mm_load_si128((__m128i *)(&rb[j + 4])); + __m128i a2 = _mm_load_si128((__m128i *)(&rb[j + 8])); + __m128i a3 = _mm_load_si128((__m128i *)(&rb[j + 12])); + + __m128i b0 = _mm_shuffle_epi8(a0, suff0); + __m128i b1 = _mm_shuffle_epi8(a1, suff1); + __m128i b2 = _mm_shuffle_epi8(a2, suff2); + __m128i b3 = _mm_shuffle_epi8(a3, suff3); + +#define PS(x) _mm_castsi128_ps(x) +#define SI(x) _mm_castps_si128(x) + __m128i c0 = SI(_mm_blend_ps(PS(b0), PS(b1), 0b1000)); + __m128i c1 = SI(_mm_blend_ps(PS(b1), PS(b2), 0b1100)); + __m128i c2 = SI(_mm_blend_ps(PS(b2), PS(b3), 0b1110)); + + _mm_storeu_si128((__m128i *)(&dst[0]), c0); + _mm_storeu_si128((__m128i *)(&dst[4]), c1); + _mm_storeu_si128((__m128i *)(&dst[8]), c2); + dst += 12; + } +#else + // xBGR32 4つ(16バイト)を RGB24 3つ(12バイト)に変換して + // ロングワードで書き出す。 + for (int j = 0; j < 768; j += 4) { + // rb[0] = $x0B0G0R0 + // rb[1] = $x1B1G1R1 + // rb[2] = $x2B2G2R2 + // rb[3] = $x3B3G3R3 + // + // dst[0] = $00B0G0R0 | $R1000000 = $R1B0G0R0 + // dst[1] = $0000B1G1 | $G2R20000 = $G2R2B1G1 + // dst[2] = $000000B2 | $B3G3R300 = $B3G3R3B2 + + *dst++ = le32toh((rb[j + 0] ) | (rb[j + 1] << 24)); + *dst++ = le32toh((rb[j + 1] >> 8) | (rb[j + 2] << 16)); + *dst++ = le32toh((rb[j + 2] >> 16) | (rb[j + 3] << 8)); + } +#endif + // 2の時だけ0に戻す atomic_cas_8(&atomic_dirty[sy], 2, 0); updated = true; } } +#if defined(PERF) + auto end = std::chrono::system_clock::now(); + total_elapsed += std::chrono::duration_cast + (end - start).count(); + if (++total_count >= 100) { + int fps = 1000 * 1000 / (total_elapsed / total_count); + total_elapsed = 0; + total_count = 0; + printf("%d fps", fps); + fpsarray.push_back(fps); + if (fpsarray.size() > 4) { + fpsarray.pop_front(); + } + if (fpsarray.size() > 2) { + int minv = 1e9; + int sum = 0; + for (auto v : fpsarray) { + if (v < minv) + minv = v; + sum += v; + } + // 最悪値1つを除いた移動平均 + sum -= minv; + int avg = sum / (fpsarray.size() - 1); + printf(" (avg %d)", avg); + } + printf("\n"); + } +#endif + return updated; } + +// スタティック変数 +/*static*/ uint32 TVRAMDevice::deptable[256];