intent算法
使一系列順序操做瞬時或同步出現。
數組
motivation
架構
在他們心中,計算機是順序野獸。它的力量來源於把大的工做分紅不少個小步驟一個接一個的執行。儘管通常是,咱們的用戶看到的是一個單一瞬時的任務或多任務同時執行。
ide
一個經典的例子,並且是每一個遊戲引擎都會涉及的是渲染。當遊戲繪製世界時,它一次要作這麼一件事-遠處的的山,起伏的坡,樹木,這些個輪流。若是玩家看到以增量方式繪製畫面,那麼連貫世界的景象會破裂掉。場景必須平滑而快速的更新,展現一連串完整的幀,每一幀都要馬上出現。
函數
雙緩衝能夠解決這個問題,可是要理解如何解決,咱們要先回顧計算機如何顯示圖形。spa
how computer graphics work(briefly)指針
像計算機顯示器顯示圖像是一次只能繪製一個像素。它從左到右橫掃每一行而後移動到下一行。當它到達右下角以後,它又回到左上角重複以前的動做。它作得很快-大約一秒60次-以致於咱們的眼睛看不到這個掃描過程。對咱們來講,它就成了一個彩色像素的靜態區域-一張圖片。code
你能夠想象成用一個水管把像素射到屏幕。不一樣的像素從水管後部進入,而後水管把它們噴射到屏幕。那麼,水管如何知道一個像素應該被射到哪裏?
視頻
在多數計算機中,答案是水管從一個幀緩衝中取得像素。一個幀緩衝是內存中的一個數組用來存儲像素的,一塊RAM,每幾個字節表明一個像素。當水管要噴射像素時,它從這個數組裏一個字節一個字節地讀取顏色值。
接口
最終,爲了使遊戲畫面顯示,咱們要作的就是把數據寫到這個數組中。全部瘋狂先進的圖形算法濃縮一下就是:設置幀緩衝的字節值。可是有一個小問題。
上面,我說過計算機是順序的。若是機器正在執行渲染代碼,咱們不要指望它同時作別的事。大多數是對的,可是仍是會出現一些問題在程序運行中。其中之一就是,當遊戲運行顯示視頻時會持續從幀緩衝讀取數據。這會致使一個問題。
咱們假設咱們要在屏幕上顯示一個笑臉。遊戲開始循環向幀緩衝中填充顏色。咱們沒意識到的是當咱們向幀緩衝寫數據時,視頻驅動正從中讀取數據。隨着它掃描咱們已經寫入的數據,笑臉開始出現,可是它超過了咱們,進入咱們還沒寫入的內存。結果就是「撕裂」,一個醜陋的可見的bug出如今屏幕上。
這就是爲何咱們須要這個模式。咱們的程序一次只能繪製一個像素,可是視頻驅動卻要當即看到所有的像素-在一幀沒有笑臉,下一幀出現一個笑臉。雙緩衝能夠解決這個。我會經過分析進行解釋
act 1, scene 1
假設用戶正在觀看咱們作的戲劇。場景1結束,場景2開始,咱們須要改變舞臺佈置。若是等場景結束纔開始拖動道具,就會破壞連貫性。咱們能夠滅燈當這麼作時(固然,真正地劇院就是這麼作的),可是觀衆仍然知道發生了什麼事。咱們想使場景之間沒有間隔。
咱們提出一種方法:咱們建造兩個觀衆都能看到的舞臺。每個有本身的燈光。咱們稱爲舞臺a,舞臺b。場景1在舞臺a上,同時舞臺b滅燈,由工做人員佈置場景2。場景1一結束,舞臺a滅燈,舞臺b亮燈。觀衆看向舞臺b,場景2當即開始。
同時,工做人員又跑到舞臺a,開始佈置場景3。場景2一結束,舞臺a又從新亮燈。咱們在整個戲劇中進行這種處理,老是在滅燈的舞臺佈置下一個場景。每個場景切換,咱們只需切換兩個舞臺的燈光。觀衆看到一個沒有間隔的連續的表演。他們從未看到一個工做人員。
back to graphics
這就是雙緩衝工做原理,這個過程成爲你見到的全部遊戲的渲染系統的基礎。咱們使用兩個幀緩衝,而不是一個。一個表示當前幀。它是視頻卡要讀取的那個。不管什麼時刻,只要GPU想掃描它就能夠掃描。
同時,程序向另外一個幀緩衝寫數據。當渲染代碼繪製完畢,經過交換(swap)幀緩衝實現燈光切換。這個告訴顯卡從第二個幀緩衝讀取數據而不是第一個。只要在刷新時交換,畫面就不會撕裂,整個場景馬上會出現。
同時,前一個幀緩衝就可使用了,咱們開始向其中渲染下一個場景。歐耶。
The pattern
用一個類封裝緩衝區:一塊能夠修改的狀態。這個緩衝區是以增量的方式編輯的,可是咱們但願外部代碼,以原子方式讀取整個緩衝區。爲此,類保留兩個緩衝區的實例:一個當前(current)緩衝,一個下一個(next)緩衝。
當從緩衝區讀取信息時,老是從current緩衝讀。當向緩衝區寫數據時,老是向next緩衝區寫。當寫入完畢,一個swap操做當即交換兩個緩衝,這樣新緩衝就對外可見了。原先的current就變成了next了。
when to use it
這個模式是你知道應該什麼時候使用它的模式之一。若是你有個系統,不使用雙緩衝就會明顯出錯(畫面撕裂)或者行爲出錯。可是,「你知道什麼時候使用它」並無告訴你太多。更明確得,當下列狀況爲真時,這個模式是合適的:
有一些狀態,增量地修改。
這些狀態可能會在修改中被訪問。
咱們想要阻止代碼看到修改的過程。
咱們想要讀取狀態但不想被狀態的改寫所阻塞。
Keep in mind
不像大型的架構模式,雙緩衝是一個更底層的實現。所以,它對其餘代碼有很小的聯繫-大多數代碼甚至不知道它的存在。然而,仍是有幾條警告。
the swap itself takes time
雙緩衝須要一個交換操做一旦狀態發生了改變。這個操做必須是原子的-當交換時其餘代碼不能訪問任何一個狀態。通常,這與爲指針賦值同樣快,可是若是它花的時間比修改狀態的時間長的話,那就沒有意義。
we have to have two buffers
另外一個影響是增長內存使用。顧名思義,這個模式須要兩份狀態的副本。對有內存限制的設備,這多是個很大的代價。若是你沒法承擔兩個buffer,那麼你就要用其餘的方法來保證,當修改狀態時不讓其它代碼訪問狀態。
sample code
如今,咱們瞭解了理論,讓咱們看看實際中如何工做。咱們將寫一個有基本要素的圖形系統,可讓咱們在幀緩衝中繪製像素。在大多數主機和pc中,顯卡提供了圖形系統底層的部分,可是手動實現它會讓咱們明白到底怎麼回事。首先是緩衝:
class Framebuffer { public: Framebuffer() { clear(); } void clear() { for (int i = 0; i < WIDTH * HEIGHT; i++) { pixels_[i] = WHITE; } } void draw(int x, int y) { pixels_[(WIDTH * y) + x] = BLACK; } const char* getPixels() { return pixels_; } private: static const int WIDTH = 160; static const int HEIGHT = 120; char pixels_[WIDTH * HEIGHT]; };
它有一些基本的操做,用白色清空緩衝,單獨設置一個像素的值。還有一個函數getPixels()獲取緩衝。咱們不會在例子中看到它,可是顯卡會常常調用它把像素噴射到屏幕上。咱們把這個緩衝封裝進Scene類中。它的工做是調用許多draw()來繪製東西:
class Scene { public: void draw() { buffer_.clear(); buffer_.draw(1, 1); buffer_.draw(4, 1); buffer_.draw(1, 3); buffer_.draw(2, 4); buffer_.draw(3, 4); buffer_.draw(4, 3); } Framebuffer& getBuffer() { return buffer_; } private: Framebuffer buffer_; };
每一幀,遊戲都會通知Scene繪製。Scene每次會clear,而後調用許多draw。它也提供訪問內部buffer的接口getBuffer,因此顯卡能夠訪問它。
這個好像至關直白,可是若是咱們這樣看,它將出現問題。問題是顯卡能夠在任意時間調用getBuffer,甚至像這樣:
buffer_.draw(1, 1); buffer_.draw(4, 1); // <- Video driver reads pixels here! buffer_.draw(1, 3); buffer_.draw(2, 4); buffer_.draw(3, 4); buffer_.draw(4, 3);
當發生時,這幀只看到眼睛,而嘴巴消失了。下一幀,不知道又在什麼地方打斷繪製。最終結果就是可怕的忽隱忽現的圖像。咱們將經過雙緩衝解決問題:
class Scene { public: Scene() : current_(&buffers_[0]), next_(&buffers_[1]) {} void draw() { next_->clear(); next_->draw(1, 1); // ... next_->draw(4, 3); swap(); } Framebuffer& getBuffer() { return *current_; } private: void swap() { // Just switch the pointers. Framebuffer* temp = current_; current_ = next_; next_ = temp; } Framebuffer buffers_[2]; Framebuffer* current_; Framebuffer* next_; };
如今scene有兩個buffer了,存在buffer_數組中。咱們不直接引用數組,而是用兩個指針next_和current_指向數組。當咱們繪製時,咱們向next_中繪製。當顯卡須要讀取像素時,老是從current_中讀取。
這樣,顯卡從不會訪問正在寫入的buffer。惟一的謎團是當咱們繪製完swap()。交換操做就是簡單地交換next_和current_。下一次顯卡調用getBuffer(),就是取得的剛繪製完的buffer,並把它噴射到屏幕上。不會再有撕裂和難堪的小問題。