2017年2月8日 星期三

淺談簡易掃描線模擬

對於比較有年紀的人,小時候玩遊戲多數是使用CRT有線掃瞄電視,LCD這種東西是比較晚期才出現的產物,應該是說PS2出現前主流還是有線掃顯示器,因此小時候玩電視遊樂器的畫質也都是掃描線來呈現,而現在的模擬器畫面顯示在主流LCD上,那種  fu  就跟印象中不太一樣,重點是雖然LCD點陣顯示技術每個pixel都很清楚,但過去時後遊戲主機所提供的解析度其實並不高,不然就是 1:1 顯示在LCD上(如果有使用螢幕的原生解析度pixel to piexl的話,每個pixel單位長寬比都是1:1,可以在電視上一個顯是單位長寬比並不是1:1) , 不然就是倍率放大後產生方格或是鋸齒效果 , 再不然就是使用各種效果不一的濾鏡技術來處理遊戲畫面放大後方格或是鋸齒的問題 , 簡單來說不管怎樣處理, LCD這種清晰的顯示技術拿來呈現低解析度的遊戲 , 效果好像怎樣就是不比傳統電視有線掃瞄來得"適合"或是符合"印像" , 於是掃描線模擬 filter 技術也隨之而生.

掃描線模擬的方法可以很簡單,也可以很講究背後的原理(特別是NTSC Video視訊這塊),如果你只是要一個"很像"掃描線呈現的質感,網路上查到的方式多數是利用掃描線pattern依照一定比例混合圖片,製造出明暗相間的線條排列效果,關鍵字 可以搜尋 photoshop scanline patterm , 範例教學 http://photoshopcafe.com/tutorials/scanlines/scan.html ,如果你要自己寫程式,有基本的影像處理概念,應該並不困難,甚至也不需要pattern樣版,基偶線固定減少一定的亮度交叉,其實就是掃描線的"樣子" , 不過這種做法說穿了就是做做樣子,其背後沒有什麼很嚴謹的模擬原理,不過的確會多一些 fu .....

真正說起來掃描線模擬如果要考慮到NTSC訊號的特性等等 , 那麼做起來就比較複雜了 , 首先訊號源要從RGB改成以 明亮度 . 色度 . 濃度來表示的色彩空間資訊,接著是重新取樣,和上掃描線,然後加入一些不完美的缺陷 (漏色.閃爍.雜訊.模糊有的沒的...) ,這中間流程其實我也並沒有完全搞懂,出來的效果會更為理想有古早味,下面就是一款相當棒的NTSC模擬libraray   http://slack.net/~ant/libs/ntsc.html ,還有針對不同的輸入特性做差異表現,但目前還在study階段中,之前嘗試移植到c#也失敗,有東西沒弄好,後來就沒用了,想打算自己搞懂多些東西來實作自己的版本(主要是針對自己的喜好來優化調整).

其實我個人並沒有很喜好太古早味太爛的畫面效果,但遊樂器主機這種低解析度的畫面確實還是以前的CRT電視表現的比較好,簡單來說那種呈現方式加上輕度模糊,恰巧就是最佳的放大與抗鋸齒效果,這中間拿捏其實是需要平衡和參考各人喜好的.

PS.補充一款重口味的NTSC電視畫面模擬程式,應該是早期很爛很爛無線類比廣播電視的那種效果... http://www.reenigne.org/blog/ntsc-hacking/ 這個大概是5.6年級生比較早期的回憶畫面...我沒啥經歷過無線類比廣播電視的那個時代....

2017.02.09 更新
目前已經開發出我自己還算喜好的掃描線模擬效果範例程式(以後或許會嘗試高速優化加到自己的程式內當FILTER去用).大概把說法流程說一下,沒有很限定怎樣處理才是正確或是好,自己衡量參考就好.

step 1. RGB圖片使用 nearest法,高度拉長兩倍 new_ h = h * 2 ,寬透過公式計算拉伸到正確的比例 ( 因為pixel的顯示長寬比並非 1:1 ) , 公式為  new_w = ((w - 1) / 3 + 1) * 7 (參考Blargg的做法) ,最後 new_w 取能夠被4整除的最接近倍數長度 (方便後期單位取樣處理). 原圖可以使用效果不錯的XBrz先upscale,效果更好.

step 2.RGB轉換成YIQ色域,其實YUV也可以,但NES輸出訊號是以YIQ為訊源(至少官方機種是這樣).

step 3. 以 4:1:1 取樣,會降低一些畫質,但這才接近於真實畫面輸出的效果,會有很輕微的些許漏色效果(在色彩對比差異比較大的對比邊界處). 可以參考 https://zh.wikipedia.org/wiki/%E8%89%B2%E5%BA%A6%E6%8A%BD%E6%A0%B7

step 4. 簡單進行輕微模糊,同時也有抗鋸齒的效果,採用的是要處理的pixel跟左右兩邊的pixel取某種比例的平均.

經過step 1~4出來的結果基本上已經有比較考究的味道了,包括畫面長寬比例修正.RGB轉YIQ出來的色彩感覺,以及subsample後的色彩溢出,最後就是不論是AV端子或是S端子,類比訊號經過線材傳送,以及類比訊號處理的結果,就不可能如同數位訊號那麼清晰,反來有抗鋸齒效果.

step 5. 最後套用上scanlie pattern 效果,其實scanlien pattern也是有不同的參數可以調整(線寬度,明暗比例大小)

大體上經過 step 1 到 5步驟後,已經算是比較考究的效果了...至於週期性閃爍.雜訊或是殘影效果,這個我就不太清楚,而實際上那些效果加入後,我反來不是很喜歡,而且會把問題變得很複雜,需要進階考慮到ntsc訊號的timing等等之類問題,有些濾鏡還會再加入非平面的電視crt螢幕的輕微球圓狀變型效果.


把一個大概的sampe放到GITHUB去 https://github.com/erspicu/ScanLineBuilder
程式還沒最佳高速化

預覽畫面因為經過再壓縮,所以看起來掃描線質感會於實際程式不符合.




2017年1月20日 星期五

6502 的 dummy read 與 write

這種行為是光看功能spec不會發現的現像,簡單來說反正就是不知道這顆cpu是怎樣去設計的,在讀取時候,指令對於資料讀寫的目標位址分成高低各1byte組成,如果 低byte發生carry (cross page) , 會產生多一次的讀取或是寫入行為(所以指令的cycle也會多出來) , 對於一般的記憶體相同資料寫入兩次或是讀取兩次,並沒有任何影響性,但任天堂紅白機記憶體區域有部分帶有 buffer或是latch的特性,特別是一些register io的 io mapping 區域,這時候就得針對這種奇怪的行為做處理,至於指令規格描述單就功能層面並不會提到這回事.

其實紅白機真的有很多奇奇怪怪的硬體特性,有些部分像是秘技一樣,早期前輩先開路摸索,現在這些秘密多半已經公開文件化,還有一些為了測試正確性而開發的ROM給模擬器開發者測試,雖然已經不用再像以前直接測試硬體做功能測試,但看那些描述文件或是網路資料常常也不是非常簡單理解就是.


2016年12月12日 星期一

C#使用指標雜談

C#使用指標並不是什麼很厲害的技術,新手除外,稍微對C#有更深入一些經驗的人,我猜多數都有在C#使用指標的經驗,所以我這邊主要也不是要介紹C#使用指標這件事情,而是雜談些別的.

C#雖然可以使用指標,但如果是一般的方式
https://msdn.microsoft.com/zh-tw/library/f58wzh21.aspx
也就是搭配 fixed 陳述式 , 跟 C 指標操作存取相比會顯得非常沒彈性 , 所以在C#達不到C操作指標的靈活度.......但如果使用 Marshal 來進行 unmanaged 的記憶體配置拿到指標 , 則在C#中則"幾乎"(還有不少確認和爭議空間)可以達到 C 操作指標的靈活度

Marshal.AllocHGlobal
Marshal.FreeHGlobal

這兩的東西好用至極,之後要存取物件的習慣也幾乎跟C一樣,詳細的使用介紹就自行google,不多談.

這邊要說到的是有了這項能力後,處理模擬器其中一個問題顯然簡單而且有效率很多,電腦世界中雖然最基本的資料單位是 bits , 但硬體上資料能存取的最基本單元是 bytes , 8bit CPU 要模擬記憶體的硬體機本上宣告一個固定大小以byte為資料型態的array即可 , 不過到了32bit CPU一次存取可能就是 half-word (2bytes) 或是 word (4byte) , 有時候你從一個位址讀資料可能是要讀一個 byte , 有時候可能是一個 ushort , 有時候可能是一個 uint .



如果要讀4個byte, 你可以自己對 byte array 讀取4次  , 然後用 bitwise 的操作自己串成一個 uint ,顯然是麻煩 , 使用指標的狀況下 ,你可直接指定說我要從這指標的某個offset讀取 byte ushort uint 位元長度資料.

            byte* bytepointer = (byte*)Marshal.AllocHGlobal(sizeof(byte) * 16);

            bytepointer[0] = 0x00;
            bytepointer[1] = 0x11;
            bytepointer[2] = 0x22;
            bytepointer[3] = 0x33;
            bytepointer[4] = 0x44;
            bytepointer[5] = 0x55;
            bytepointer[6] = 0x66;
            bytepointer[7] = 0x77;
            bytepointer[8] = 0x88;
            bytepointer[9] = 0x99;
            bytepointer[10] = 0xaa;
            bytepointer[11] = 0xbb;
            bytepointer[12] = 0xcc;
            bytepointer[13] = 0xdd;
            bytepointer[14] = 0xee;
            bytepointer[15] = 0xff;

            ushort t_us;
            t_us =   ((ushort*) &bytepointer[4])[0];
            Console.WriteLine(t_us.ToString("x4"));
            t_us = ((ushort*)&bytepointer[9])[0];
            Console.WriteLine(t_us.ToString("x4"));

            uint t_ui;
            t_ui = ((uint*)&bytepointer[2])[2];
            Console.WriteLine(t_ui.ToString("x8"));
            t_ui = ((uint*)&bytepointer[4])[1];
            Console.WriteLine(t_ui.ToString("x8"));
            t_ui = ((uint*)&bytepointer[9])[0];
            Console.WriteLine(t_ui.ToString("x8"));
            Marshal.FreeHGlobal((IntPtr)bytepointer);

資料存取變得像切豆腐一樣容易,對於資料長度讀取的轉換可以任意有彈性來做.

不管是Read 8/16/32 或是 Write 8/16/32 ,把資料長度型態改變, 對可以對應了.

2016年12月6日 星期二

移植雜談

測試了一下網路上別人寫出的ntsc掃描線模擬函庫,出來的效果完全是我的想要的效果,太完美了,嘗試實作自己的一套,但要去理解ntsc畫面訊號規格太複雜,至少對我來說需要不少時間,於是打算使用移植的方式,從C移植到C#去,最後C#整個都在跟指標打交道,出現照理來說C#不常看到的 -> & 等等符號...但其實從C/C++移植最難的反來不是指標(甚至照理來說應該要把指標的處理方式給改寫掉比較符合風格上的正確),而是寫C的人很習慣把玩巨集和一些前置條件,而寫C++寫到狂的人則超愛用一堆複雜的樣板到讓人有走火入魔的感覺,還有一堆詭異C++近年來的新語法規格,這些移植到C#都相當麻煩.

2016年11月19日 星期六

Win3mu

作者解說
https://hackernoon.com/win3mu-part-1-why-im-writing-a-16-bit-windows-emulator-2eae946c935d#.ca6ygh8cl

專案發佈站
http://www.win3mu.com/

可以在現在64位元 win10環境下跑以前 win3時代16bit的應用軟體.

作者手法相當有趣.

cpu是撰寫出來模擬的,但跟api服務有關的部分全都靠remp到64位元系統相似api去吃.

由 upernes 談起...

http://blog.vreemdelabs.com/2016/05/08/upernes-a-nes-to-supernes-game-recompiler-passing-the-tests/


這專案的概念其實有相當多的討論議題延伸,大概簡單說一下, nes 主機用的是 6502 , snes 用的是 65c816 , 其實可以視為是 6502的16bit功能延伸版,本身其實有設計8bit相容模式,所以曾傳聞超級任天堂有考慮過向下相容任天堂紅白機遊戲,但因為cost考量取消這相容能力....

到這邊大家會想到,那既然如此有沒有可能將任天堂的遊戲放到超任上跑,透過某些轉換的方式?

理論上當然可以,但實際上問題當然很多....程式能否執行cpu相容的問題其實只佔部分比例,還有很多層面的問題要考量,光snes硬體的memory map 跟 gpu 等等架構就跟 nes 不同了, 光cpu指令理論上可以達成相容互通又如何??

這專案的確示範了某種概念,但發展有限,可以執行很簡單的demo,估計是做了很多 remap 的工作.

而像是任天堂的GBA或是NDS用的是ARM系列CPU,現在手機很多也多數是用ARM處理器,當然隨著ARM版本不同,指令集會有部分差異,所以理論上在ARM環境上最高效率的執行ARM遊戲主機遊戲的方式應該是透過一個轉換器,針對少部分指令的差異重編達成相容,然後只需要模擬CPU以外的硬體環境即可,但想也可以知道理論上好像越簡單的東西,實作上問題就是更多,目前也沒有看過任何人發展出這種方式來執行.

目前也只有看過把NES的ROM透過LLVM工具編譯成執行檔在X86環境上執行.

簡單來說,能否執行光只靠CPU能互通這點條件是不足的.

由xBRZ filter淺談平行處理問題

xBRz是目前我用過在cpu cost與畫質上cp值頗高的影像放大濾鏡演算法
https://sourceforge.net/projects/xbrz
原始版本是C++ , 有人移植到C# ,我再接手進行一些效率優化的處理以及把一些最新xBRZ C++官方版本的最新功能加入,至少套用在電玩畫面的高效率輸出,即使用C#來寫效率也是OK的 .

但這演算法有一套很可惜的地方,導致移植到平行處理的方式會有問題,只能用某些折中的方式來進行平行多核處理.

首先這套演算法有每一條平行線與平行線之間的計算順序上的前後相依性,而且目前我還沒辦法找到方式去改寫這塊,因為這似乎就是xBrz演算法其中必須的核心精神要項之一,要拆除順序相依的問題,可能得自己研發變種版xBRZ.

因為這種相依性,平行計算下去,在計算順序無法控制預期的狀況下,出來的結果就是亂七八糟的.

目前找到兩套解法各有優缺點.

1.把畫面切成4大塊,4大概各自平行處理,效率滿好的,缺點只有兩個,一個是四塊區域銜接的邊界其實運算結果有可能是錯誤的,但因為其實原始只佔一pixl的誤差,一般沒很刻意去注意和比較根本不可能查覺,二是目前有太多電腦cpu核心數量其實超過4核,把整個運算切成4份,其實還浪費掉其他閒置率高的cpu.

2.跟線與線有關的順序性結果先算好出來,這部分沒平行加速幫忙,因為也幫不上忙,剩下再用平行的處理方式下去跑 (高的長度處理迴圈用parallel) , 缺點當然是有部分的計算是單核跑,且跟第一個方法比起來,context switch的cost當然也高不少(這還有改善空間就是,未必要以高度數量來當工作數量),優點來說的話就至少算出來的畫面是沒有瑕疵的,且cpu核心至少在處理高度迴圈時是有整個盡用的.

像這種順序相依問題一但拆解不掉,拿到GPU去跑一樣無法,甚至更慘,以前曾想過拿GPU來算一些結果,特別是畫面的計算這塊,但後來發現結果沒比CPU好,好壞結果真的很難論,最後未必有絕對性的優勢下,考慮利弊拿捏,最後沒往那方向走.

先撇開相依性的問題不談,假設一個龐大的計算工作,可以同時拆解成很多小任務丟給GPU跑,光是把計算資料COPY到GPU記憶體,和把GPU算好結果COPY回一般記憶體這段這段COST就把優勢佔光了....還不包括GPU計算初始化時間....這種問題,比較新的opencl加特定條件的硬體環境,似乎可以改善,總之大概就是這樣,平行處理並不是萬能仙丹,滿多問題會導致無法使用這種計算方式,即使可以算很多因素也未必說效率就一定會提高,應該是特定問題的處理適用.

至於透過網路來分散平行處理,更不用說...網路傳輸cost成本必定得計算在內.