天天看點

Chromium硬體加速渲染的GPU資料上傳機制分析

       在Chromium中,WebGL端、Render端和Browser端通過指令緩沖區将GPU指令發送給GPU程序執行。GPU指令攜帶的簡單參數也通過指令緩沖區發送給GPU程序,但複雜參數,例如紋理資料,有可能太大,以緻于指令緩沖區無法容納,是以要通過其它機制傳遞給GPU程序。本文接下來就主要以紋理資料上傳為例,分析WebGL端、Render端和Browser端将GPU指令資料傳遞給GPU程序的機制。

老羅的新浪微網誌:http://weibo.com/shengyangluo,歡迎關注!

《Android系統源代碼情景分析》一書正在進擊的程式員網(http://0xcc0xcd.com)中連載,點選進入!

       WebGL端、Render端和Browser端将GPU指令附攜帶的大資料傳遞給GPU程序的基本思路通過其它的共享緩沖區進行傳遞。也就是先将GPU指令攜帶的大資料寫入到共享緩沖區中,然後再将GPU指令攜帶的大資料參數修改為前面已經寫入了資料的共享緩沖區的ID。GPU程序通過這個ID就可以找到對應的共享緩沖區,進而得到真正的GPU指令資料,最後就可以執行對應的OpenGL函數。

       有些作業系統對能建立的共享記憶體的大小有限制。當一個GPU指令攜帶的資料的大小超過這個限制的時候,那麼就不能通過一塊共享緩沖區一次性将資料傳遞給GPU程序。這時候就需要對資料進行分塊傳輸。有些GPU指令的資料本身就支援分塊傳輸,這種情況的處理就比較簡單。例如,對于紋理上傳指令gles2::cmds::TexImage2D,可以通過gles2::cmds::TexSubImage2D指令對其攜帶的紋理資料進行分塊傳輸,如圖1所示:

Chromium硬體加速渲染的GPU資料上傳機制分析

圖1 紋理資料分塊上傳機制

       在圖1中,我們假設一個gles2::cmds::TexImage2D指令要上傳的紋理資料可以劃分為1、2和3三個子塊,每一個子塊都可以通過一塊共享緩沖區進行傳遞。這時候一個gles2::cmds::TexImage2D指令就被分拆成三個gles2::cmds::TexSubImage2D子指令,每一個gles2::cmds::TexSubImage2D子指令負責處理一個子資料塊。這些gles2::cmds::TexSubImage2D子指令最終在GPU程序中轉化為OpenGL函數glTexSubImage2D調用,每一個glTexSubImage2D函數都負責上傳一個資料子塊到GPU中。

       很不幸,并不是所有的GPU指令都像gles2::cmds::TexImage2D指令一樣,存在對應的子指令,例如gles2::cmds::ShaderSource指令,它不存在對應的gles2::cmds::ShaderSubSource子指令。這時候就需要使用一種稱為Bucket的機制來分塊上傳GPU指令資料。以gles2::cmds::ShaderSource指令為例,它攜帶的Shader源代碼的分塊上傳機制如圖2所示:

Chromium硬體加速渲染的GPU資料上傳機制分析

圖2 Shader源代碼分塊上傳機制

       在圖2中,我們同樣假設gles2::cmds::ShaderSource指令要上傳的資料可以劃分為1、2和3三個子塊,每一個子都可以通過一塊共享緩沖區進行傳遞。每一個資料子塊都是通過一個gles2::cmds::SetBucketData指令儲存在GPU程序中的同一個Bucket中的。每一個Bucket都具有一個ID,前面已經準備好了資料的Bucket的ID接下來再通過一個gles2::cmds::ShaderSourceBucket指令傳遞給GPU程序。GPU程序有了這個Bucket的ID之後,就可以獲得它裡面的資料,進而可以調用OpenGL函數glShaderSource,進而完成對gles2::cmds::ShaderSource指令的處理。

       不難發現,上面描述的兩種GPU指令資料分塊上傳機制都是通過共享緩沖區進行的,這就涉及到這些共享緩沖區的管理問題,也就是配置設定和釋放的問題。為了更好地認識這個問題,我們首先簡單介紹一下Chromium的紋理上傳機制。Chromium提供了同步和異步紋理上傳機制。

       我們知道,在Chromium中,所有的GPU指令都是在GPU程序的一個線程中執行的,這個線程稱為GPU主線程。将紋理上傳指令全部交給GPU主線程執行就稱為同步紋理上傳,如圖3所示:

Chromium硬體加速渲染的GPU資料上傳機制分析

圖3 同步紋理上傳

       在圖3中,我們假設一個指令緩沖區有五個GPU指令需要執行,其中第一個GPU指令是同步紋理上傳指令gles2::cmds::TexImage2D,對應的OpenGL函數是glTexImage2D。由于使用的是同步紋理上傳方式,是以,在紋理上傳指令執行完成之前,後面的四個指令是不能執行的。

       與GPU的圖形計算和渲染速度相比,将資料從CPU傳遞到GPU的速度是相當慢的。這就使得紋理上傳操作是GPU的一個瓶頸,特别是資料量很大的紋理。對于圖3來說,就會造成後面的四個指令需要等待比較長時間才會被執行。

       為了解決紋理上傳速度慢的問題,Chromium提供了另外一種紋理上傳方式——異步紋理上傳,如圖4所示:

Chromium硬體加速渲染的GPU資料上傳機制分析

圖4 異步紋理上傳

       在異步紋理上傳方式中,GPU程序中使用一個專門的線程用作紋理上傳,這個線程稱為GPU傳輸線程。在圖4中,我們同樣假設一個指令緩沖區有五個GPU指令需要執行,其中第一個GPU指令是異步紋理上傳指令gles2::cmds::AsyncTexImage2DCHROMIUM。這個異步紋理上傳指令被GPU主線程發送給GPU傳輸線程處理。GPU傳輸線程調用OpenGL函數執行異步紋理上傳指令。與此同時,GPU主線程也在執行後面的四個指令。

       GPU傳輸線程上傳紋理完畢,會将通過EGL函數eglCreateImageKHR将已經上傳的紋理封裝成一個EGLImageKHR對象。GPU主線程會在空閑的時候檢查異步上傳的紋理是否已經上傳完成。對于已經上傳完成的紋理,GPU主線程會通過OpenGL函數glEGLImageTargetTexture2DOES将綁定在目前激活的OpenGL上下文中。這意味着GPU主線程可以通路在GPU傳輸線程中上傳完畢的紋理。這是一種跨線程的紋理共享機制。正是由于這個紋理共享機制,才使得異步紋理上傳成為可能。

       有時候,GPU程序的Client端需要知道一個異步紋理上傳指令什麼時候執行完成,這時候它可以向GPU程序發送一個gles2::cmds::WaitAsyncTexImage2DCHROMIUM。GPU主線程在處理gles2::cmds::WaitAsyncTexImage2DCHROMIUM的時候,就會等待GPU傳輸線程完成紋理上傳。當然,如果這時候GPU傳輸線程已經完成紋理上傳,那麼GPU主線程就不用等待。GPU主線程結束等待之後,也會檢查已經上傳完成的紋理是否已經綁定在目前激活的OpenGL上下文中。如果還沒有綁定,那麼也會通過OpenGL函數glEGLImageTargetTexture2DOES将綁定在目前激活的OpenGL上下文中。

       從圖4我們就可以看到,通過異步紋理上傳方式,後面的四個指令可以與前面的紋理上傳指令并發執行,進而提高了全部五個指令的執行時間,進而可以在一定程度上解決紋理上傳速度慢的問題。

       GPU程序的Client端向GPU程序發送的同步紋理上傳指令gles2::cmds::TexImage2D和異步紋理上傳指令gles2::cmds::AsyncTexImage2DCHROMIUM,都指定了一個共享緩沖區,這個共享緩沖區儲存了要上傳的紋理資料。GPU程序的Client端不知道這個共享緩沖區什麼時候釋放,因為它不知道GPU程序什麼時候使用完成這個共享緩沖區,也就是不知道紋理上傳指令什麼時候被執行。但是GPU程序的Client端必須知道上述共享緩沖區什麼時候使用完成,以便可以對它進行回收。那麼GPU程序的Client端是通過什麼方式知道一個共享緩沖區什麼時候不再被GPU程序使用的呢?我們分同步紋理上傳和異步紋理上傳兩種情況讨論。

       在同步紋理上傳方式中,GPU主線程執行完成一個gles2::cmds::TexImage2D指令,就意味着該gles2::cmds::TexImage2D指令引用的共享緩沖區已經使用完畢。GPU程序的Client端往指令緩沖區寫入一個gles2::cmds::TexImage2D指令之後,會接着在後面再寫入一個gpu::cmd::SetToken指令,如圖5所示:

Chromium硬體加速渲染的GPU資料上傳機制分析

圖5 同步Token機制

      上述gpu::cmd::SetToken指令關聯了一個同步Token值。這個同步Token值由GPU程序的Client端維護,并且與前面被gles2::cmds::TexImage2D指令引用的共享緩沖區對應。GPU程序的Client每往指令緩沖區寫入一個gpu::cmd::SetToken指令,都會将其維護的同步Token值增加1,作為下一個gpu::cmd::SetToken指令的同步Token值。

       GPU主線程處理完成一個gpu::cmd::SetToken指令之後,會在目前激活的OpenGL上下文中記錄該gpu::cmd::SetToken指令關聯的同步Token值。這樣,GPU程序的Client端通過比較一個共享緩沖區的同步Token值與該Client端在GPU程序中對應的OpenGL上下文記錄的目前同步Token值的大小,就可以知道該共享緩沖區是否可以進行回收。

       在異步紋理上傳方式中,通過上述的同步Token機制,不能确定一個共享緩沖區是否能夠進行回收,因為緊跟在異步紋理上傳指令後面的gpu::cmd::SetToken指令有可能比異步紋理上傳指令本身要提前執行完成。這時候需要使用另外一種異步Token機制,如圖6所示:

Chromium硬體加速渲染的GPU資料上傳機制分析

圖6 異步Token機制

       異步紋理上傳指令不僅gles2::cmds::AsyncTexImage2DCHROMIUM不僅指定了一個共享緩沖區,還指定了一個異步Token值。這個異步Token值同樣也是由GPU程序的Client端維護的,并且與異步紋理上傳指令指定的共享緩沖區相關聯。GPU傳輸線程執行完成一個異步紋理上傳指令之後,會通過函數SetAsyncUploadToken将該異步紋理上傳指令指定的異步Token值設定到目前激活的OpenGL上下文中去。這樣,GPU程序的Client端通過比較一個共享緩沖區的異步Token值與該Client端在GPU程序中對應的OpenGL上下文記錄的目前異步Token值的大小,就可以知道該共享緩沖區是否可以進行回收。

       通過上述的同步和異步Token機制,GPU的Client端就可以對那些用來傳遞資料的共享緩沖區進行管理了。接下來我們就首先分析這些共享緩沖區的管理。

       在前面Chromium硬體加速渲染的OpenGL指令執行過程分析一文中提到,GPU程序的Client端在初始化OpenGL上下文的過程中,會建立和初始化一個TransferBuffer對象和一個BufferTracker對象,如下所示:

bool GLES2Implementation::Initialize(  
    unsigned int starting_transfer_buffer_size,  
    unsigned int min_transfer_buffer_size,  
    unsigned int max_transfer_buffer_size,  
    unsigned int mapped_memory_limit) {  
  ......  
  
  if (!transfer_buffer_->Initialize(  
      starting_transfer_buffer_size,  
      kStartingOffset,  
      min_transfer_buffer_size,  
      max_transfer_buffer_size,  
      kAlignment,  
      kSizeToFlush)) {  
    return false;  
  }  
  
  mapped_memory_.reset(  
      new MappedMemoryManager(  
          helper_,  
          base::Bind(&GLES2Implementation::PollAsyncUploads,  
                     // The mapped memory manager is owned by |this| here, and  
                     // since its destroyed before before we destroy ourselves  
                     // we don't need extra safety measures for this closure.  
                     base::Unretained(this)),  
          mapped_memory_limit));  
  
  ......  
  
  buffer_tracker_.reset(new BufferTracker(mapped_memory_.get()));  
  ......  
  
  return true;  
}  
           

       這個函數定義在檔案external/chromium_org/gpu/command_buffer/client/gles2_implementation.cc中。

       GLES2Implementation類的成員變量transfer_buffer_指向的是一個TransferBuffer對象,GLES2Implementation類的成員函數Initialize調用它的成員函數Initialize對它進行初始化。這個TransferBuffer對象初始化完成後,GPU程序的Client端就可以通過它配置設定一些共享緩存區,用來和GPU程序傳遞資料。

       GLES2Implementation類的成員函數Initialize接下來建立了一個MappedMemoryManager對象,并且以這個MappedMemoryManager對象為參數,建立了一個BufferTracker對象,儲存在成員變量buffer_tracker_中。以後GPU程序的Client端也可以通過這個BufferTracker配置設定共享緩沖區,用來和GPU程序傳遞資料。

       接下來,我們就繼續分析TransferBuffer類和BufferTracker類的實作,以便了解它們是如何管理共享緩沖區的。

       我們從TransferBuffer類的成員函數Initialize開始分析TransferBuffer類的實作,如下所示:

bool TransferBuffer::Initialize(
    unsigned int default_buffer_size,
    unsigned int result_size,
    unsigned int min_buffer_size,
    unsigned int max_buffer_size,
    unsigned int alignment,
    unsigned int size_to_flush) {
  result_size_ = result_size;
  default_buffer_size_ = default_buffer_size;
  min_buffer_size_ = min_buffer_size;
  max_buffer_size_ = max_buffer_size;
  alignment_ = alignment;
  size_to_flush_ = size_to_flush;
  ReallocateRingBuffer(default_buffer_size_ - result_size);
  return HaveBuffer();
}
           

       這個函數定義在檔案external/chromium_org/gpu/command_buffer/client/transfer_buffer.cc中。

       各個參數的含義如下所示:

       default_buffer_size:表示預設配置設定的共享緩沖區的大小。

       result_size:當TransferBuffer用來将資料從GPU程序傳回給GPU程序的Client端時,配置設定出來的緩沖區的頭部用來填寫傳回值。這個頭部的大小就由參數result_size描述。

       min_buffer_size:表示允許配置設定的共享緩沖區的最小值。

       max_buffer_size:表示允許配置設定的共享緩沖區的最大值。

       alignment:配置設定出來的共享緩沖區被劃分為一個個的子緩沖區進行使用,這些子緩沖區的大小要對齊到參數alignment描述的值。

       size_to_flush:當從共享緩沖區配置設定出去的子緩沖區的大小達到參數size_to_flush描述的值後,GPU程序的Client端就會請求GPU程序執行指令緩沖區的指令,以便可以回收這些指令引用的子緩沖區。

       TransferBuffer類的成員函數Initialize将上述參數分别儲存在對應的成員變量中後,就調用另外一個成員函數ReallocateRingBuffer配置設定共享緩沖區,如下所示:

void TransferBuffer::ReallocateRingBuffer(unsigned int size) {
  // What size buffer would we ask for if we needed a new one?
  unsigned int needed_buffer_size = ComputePOTSize(size + result_size_);
  needed_buffer_size = std::max(needed_buffer_size, min_buffer_size_);
  needed_buffer_size = std::max(needed_buffer_size, default_buffer_size_);
  needed_buffer_size = std::min(needed_buffer_size, max_buffer_size_);

  if (usable_ && (!HaveBuffer() || needed_buffer_size > buffer_->size())) {
    if (HaveBuffer()) {
      Free();
    }
    AllocateRingBuffer(needed_buffer_size);
  }
}
           

       這個函數定義在檔案external/chromium_org/gpu/command_buffer/client/transfer_buffer.cc中。

       TransferBuffer類的成員函數ReallocateRingBuffer首先根據參數size和成員變量result_size_、min_buffer_size_和max_buffer_size_計算出應該配置設定的共享緩沖區的大小needed_buffer_size。

       TransferBuffer類的成員變量usable_ 的值初始化為true,接下來如果因為大小限制不能成功配置設定到共享緩沖區,那麼該成員變量的值就會被設定為false。

       當TransferBuffer類的成員變量buffer_的值不等于NULL時,它指向的是一個Buffer對象,該Buffer對象描述的就是一個共享緩沖區,這時候調用TransferBuffer類的成員函數HaveBuffer得到的傳回值就為true,并且調用該Buffer對象的成員函數size可以獲得的它描述的共享緩沖區的大小。

       是以,TransferBuffer類的成員函數ReallocateRingBuffer所做的事情就是判斷要求配置設定的共享緩沖區的大小是否合适。如果合适,并且之前還沒有配置設定過共享緩沖區,或者之前已經配置設定過,但是配置設定的大小小于前面計算出來的應該配置設定的大小,那麼就需要重新配置設定一塊大小等于needed_buffer_size的共享緩沖區。當然,如果之前已經配置設定過共享緩沖區,那麼這塊共享緩沖區會首先被釋放掉,這是通過調用TransferBuffer類的成員函數Free實作的。

       最後,TransferBuffer類的成員函數ReallocateRingBuffer通過調用另外一個成員函數AllocateRingBuffer配置設定一塊大小等于needed_buffer_size的共享緩沖區,如下所示:

void TransferBuffer::AllocateRingBuffer(unsigned int size) {
  for (;size >= min_buffer_size_; size /= 2) {
    int32 id = -1;
    scoped_refptr<gpu::Buffer> buffer =
        helper_->command_buffer()->CreateTransferBuffer(size, &id);
    if (id != -1) {
      DCHECK(buffer);
      buffer_ = buffer;
      ring_buffer_.reset(new RingBuffer(
          alignment_,
          result_size_,
          buffer_->size() - result_size_,
          helper_,
          static_cast<char*>(buffer_->memory()) + result_size_));
      buffer_id_ = id;
      result_buffer_ = buffer_->memory();
      result_shm_offset_ = 0;
      return;
    }
    // we failed so don't try larger than this.
    max_buffer_size_ = size / 2;
  }
  usable_ = false;
}
           

       這個函數定義在檔案external/chromium_org/gpu/command_buffer/client/transfer_buffer.cc中。

       TransferBuffer類的成員函數AllocateRingBuffer在保證要配置設定的共享緩沖區的大小size不小于允許的最小值min_buffer_size_的前提下,配置設定一個大小等于size的共享緩沖區。

       如果能成功配置設定,那麼配置設定出來的共享緩沖區使用一個Buffer對象來描述。這個Buffer對象儲存在TransferBuffer類的成員變量buffer_中。并且配置設定出的共享緩沖區的ID儲存在TransferBuffer類的成員變量buffer_id_中。前面提到,配置設定出來的共享緩沖區的頭部用來儲存從GPU程序讀取資料時的結果,是以這個頭部的位址就等于配置設定出來的共享緩沖區的起始位址,儲存在TransferBuffer類的成員變量result_buffer_中。同時,TransferBuffer類的成員變量result_shm_offset_表示上述用來儲存結果的頭部位于配置設定出來的共享緩沖區的偏移位置,它的值被設定為0。

       如果不能成功配置設定,那麼就可能是請求配置設定的大小太大了,于是就嘗試減少一半的大小,即以(size / 2)的大小,再次嘗試配置設定。這時候也需要相應地調整允許配置設定的共享緩沖區的最大值max_buffer_size_,也就是允許配置設定的共享緩沖區的最大值max_buffer_size_就等于(size / 2)。這個過程一直持續下去,直到配置設定成功,或者請求配置設定的大小小于允許的最小值min_buffer_size_為止。如果是後一種情況,那麼TransferBuffer類的成員變量usable_的值就會被設定為false,表示請求配置設定的共享緩沖區大小不合适,導緻不能成功到一塊共享緩沖區。

       從前面Chromium硬體加速渲染的OpenGL指令執行過程分析一文可以知道,TransferBuffer類的成員變量helper_指向的是一個GLES2CmdHelper對象,調用這個GLES2CmdHelper對象的成員函數command_buffer可以獲得一個CommandBufferProxyImpl對象。有了這個CommandBufferProxyImpl對象之後,就可以調用它的成員函數CreateTransferBuffer建立一個能夠與GPU程序進行共享的緩沖區,并且這個緩沖區會注冊在GPU程序中。

       通過CommandBufferProxyImpl類的成員函數CreateTransferBuffer配置設定的共享緩沖區都有一個相應的ID,以後GPU程序的Client端通過一個ID值就可以告訴GPU程序它是通過哪一個共享緩沖區來傳遞資料的。關于CommandBufferProxyImpl類的成員函數CreateTransferBuffer的實作,可以參考前面Chromium硬體加速渲染的OpenGL指令執行過程分析一文。

       前面提到,配置設定出來的共享緩沖區是劃分成一個個子緩沖區使用的。這些子緩沖區通過一個RingBuffer對象來管理。這個RingBuffer對象儲存在TransferBuffer類的成員變量ring_buffer_中。也就是說,以後我們想在上述共享緩沖區拿出一小塊來傳遞資料給GPU程序或者從GPU程序讀回資料時,就可以通過TransferBuffer類的成員變量ring_buffer_描述的RingBuffer對象配置設定這一小塊緩沖區,并且在使用完畢時,将這一小塊緩沖區重新交給它管理。

       GPU程序的Client端可以通過調用TransferBuffer類的成員函數AllocUpTo或者Alloc從它的成員變量ring_buffer_描述的共享緩沖區中配置設定一小塊指定大小的緩沖區,接下來我們就分别分析它們的實作。

       TransferBuffer類的成員函數AllocUpTo的實作如下所示:

void* TransferBuffer::AllocUpTo(
    unsigned int size, unsigned int* size_allocated) {
  ......

  unsigned int max_size = ring_buffer_->GetLargestFreeOrPendingSize();
  *size_allocated = std::min(max_size, size);
  bytes_since_last_flush_ += *size_allocated;
  return ring_buffer_->Alloc(*size_allocated);
}
           

       這個函數定義在檔案external/chromium_org/gpu/command_buffer/client/transfer_buffer.cc中。

       TransferBuffer類的成員函數AllocUpTo調用成員變量ring_buffer_指向的一個RingBuffer對象的成員函數GetLargestFreeOrPendingSize可以獲得該RingBuffer對象描述的共享緩沖區可配置設定的子緩沖區的最大值max_size,即空閑部分的大小。這時候如果請求配置設定的大小size大于可配置設定的最大值max_size,那麼實際配置設定的大小就會調整為可配置設定的最大值max_size,并且記錄在輸出參數size_allocated中。

       确定了實際要配置設定的子緩沖區的大小之後,就會調用TransferBuffer類的成員函數AllocUpTo就會調用ring_buffer_指向的RingBuffer對象的成員函數Alloc進行配置設定。在配置設定之前,也會相應地增加TransferBuffer類的成員變量bytes_since_last_flush_的值,這個成員變量描述的是自從上次向GPU程序送出新的GPU指令以來,從成員變量ring_buffer_指向的RingBuffer對象描述的共享緩沖區配置設定出去的子緩沖區的大小。以後會通過這個值來決定是否要向GPU程序送出新的GPU指令。

       TransferBuffer類的成員函數Alloc的實作如下所示:

void* TransferBuffer::Alloc(unsigned int size) {
  ......

  unsigned int max_size = ring_buffer_->GetLargestFreeOrPendingSize();
  if (size > max_size) {
    return NULL;
  }

  bytes_since_last_flush_ += size;
  return ring_buffer_->Alloc(size);
}
           

       這個函數定義在檔案external/chromium_org/gpu/command_buffer/client/transfer_buffer.cc中。

       與前面分析的TransferBuffer類的成員函數AllocUpTo不同,TransferBuffer類的成員函數Alloc如果發現請求配置設定的子緩沖區的大小size大于可配置設定的最大值max_size,那麼就會導緻配置設定失敗。

       另一方面,如果請求配置設定的子緩沖區的大小size小于等于可配置設定的最大值max_size,那麼TransferBuffer類的成員函數Alloc也是通過調用ring_buffer_指向的RingBuffer對象的成員函數Alloc來配置設定子緩沖區。

       RingBuffer類的成員函數Alloc的實作如下所示:

void* RingBuffer::Alloc(unsigned int size) {
  ......

  // Similarly to malloc, an allocation of 0 allocates at least 1 byte, to
  // return different pointers every time.
  if (size == 0) size = 1;
  // Allocate rounded to alignment size so that the offsets are always
  // memory-aligned.
  size = RoundToAlignment(size);

  // Wait until there is enough room.
  while (size > GetLargestFreeSizeNoWaiting()) {
    FreeOldestBlock();
  }

  if (size + free_offset_ > size_) {
    // Add padding to fill space before wrapping around
    blocks_.push_back(Block(free_offset_, size_ - free_offset_, PADDING));
    free_offset_ = 0;
  }

  Offset offset = free_offset_;
  blocks_.push_back(Block(offset, size, IN_USE));
  free_offset_ += size;
  if (free_offset_ == size_) {
    free_offset_ = 0;
  }
  return GetPointer(offset + base_offset_);
}
           

      這個函數定義在檔案external/chromium_org/gpu/command_buffer/client/ring_buffer.cc中。

      我們通過圖7來了解RingBuffer類的成員函數Alloc的實作,如下所示:

Chromium硬體加速渲染的GPU資料上傳機制分析

圖7 子緩沖區配置設定過程

       整個共享緩沖區的大小為size_,請求配置設定的子緩沖區的大小為size。目前正在使用的所有子緩沖在位址空間上是連續的。這塊連續的位址空間的起始位址為in_use_offset_中。目前空閑的緩沖區可能分為兩部分。一部分位于共享緩沖區的頭部,另一部分位于尾部。其中,位于尾部的空閑緩沖區緊跟在正在使用的緩沖區的結束位置上。這個位置記錄在free_offset_中。

      目前正在使用的每一個子緩沖區,也就是配置設定出去的子緩沖區,都關聯有一個Token值,并且它們處于兩種狀态之一。一種狀态是FREE_PENDING_TOKEN,另一種狀态是IN_USE。IN_USE是指一個子緩沖區正在被GPU程序的Client端使用,同時也被GPU指令緩沖區引用。FREE_PENDING_TOKEN是指一個子緩沖區不被GPU程序的Client端使用,但是被GPU指令緩沖區引用。

      例如,GPU程序的Client端請求GPU程序執行一個紋理上傳操作,上傳的紋理資料要拷貝到一個子緩沖區去。在拷貝的過程中,這個子緩沖區的狀态為IN_USE。拷貝完畢,GPU程序的Client端往GPU指令緩沖區寫入到一個gles2::cmds::TexImage2D指令,該指令引用了上述子緩沖區。這時候GPU程序的Client端就将儲存了紋理資料的子緩沖區的狀态設定為FREE_PENDING_TOKEN,并且在指令緩沖區中寫入一個gpu::cmd::SetToken指令,表示雖然GPU程序的Client端不需要使用該子緩沖區了,但是GPU指令緩沖區仍然引用着它。

      接下來,GPU程序先後從GPU指令緩沖區将前面寫入的gles2::cmds::TexImage2D指令和gpu::cmd::SetToken指令讀取出來處理。處理完成gles2::cmds::TexImage2D指令的時候,就意味着該gles2::cmds::TexImage2D指令引用的子緩沖區使用完畢,但是GPU程序的Client端并不知道。處理完成gpu::cmd::SetToken指令的時候,GPU程序将該gpu::cmd::SetToken指令關聯的Token值記錄為目前激活的OpenGL上下文的Token值。

      GPU程序的Client端發現空閑緩沖區大小不足時,就會獲得GPU程序為它建立的OpenGL上下文的目前Token值,儲存在last_read_token中。如果一個處于FREE_PENDING_TOKEN狀态的子緩沖關聯的token值小于等于這個last_read_token值,那麼就說明這個子緩沖也不再被GPU指令緩沖區引用了,這時候它的狀态就可以修改為FREE。

       有了上面的背景的知識之後,我們就通過圖7來分析RingBuffer類的成員函數Alloc。假設這時候共享緩沖區的狀态如A所示。在A中,共享緩沖區連續的可配置設定的空閑緩沖區大小小于請求配置設定的子緩沖區的大小size。這時候RingBuffer類的成員函數Alloc就調用成員函數GetLargestFreeSizeNoWaiting回收那些處于FREE_PENDING_TOKEN狀态的、并須Token值小于等于last_read_token的子緩沖區。注意,這些子緩沖區是可以馬上回收的,不需要等待。

       回收了處于FREE_PENDING_TOKEN狀态的、并且Token值小于等于last_read_token的子緩沖區之後,假設共享緩沖區的狀态如B所示。這時候共享緩沖區連續的可配置設定的空閑緩沖區大小仍然小于請求配置設定的子緩沖區的大小size。于是RingBuffer類的成員函數Alloc就調用成員函數FreeOldestBlock回收最早配置設定出去的子緩沖區。注意,這個子緩沖區的狀态有可能是處于FREE_PENDING_TOKEN狀态的,但是它的Token值大于last_read_token,也有可能是處于IN_USE狀态。無論是哪一種,回收它都需要進行等待,也就是要等待GPU程序處理完畢GPU指令緩沖區中引用了它的指令。

      上述過程持續進行,直到共享緩沖區的狀态如C所示。這時候共享緩沖區連續的可配置設定的空閑緩沖區大小大于等于請求配置設定的子緩沖區的大小size。 但是這部分空閑緩沖區可能是位于共享緩沖區頭部的。這意味着尾部的空閑緩沖區由于大小不足,必須要跳過。在跳過之前,它的狀态被設定為PADDING。狀态為PADDING的子緩沖被當作是已經被配置設定出去的,但是沒有實際使用。等到它前面處于IN_USE狀态的子緩沖區被回收後,它們就可以合在一起重新被使用。

       跳過尾部大小不足的空閑緩沖區之後,共享緩沖區的狀态如D所示。這時候free_offset_被設定為0,表示要從共享緩沖區的起始位置開始配置設定子緩沖區。

       以上就是RingBuffer類的成員函數Alloc的實作邏輯。還有兩點需要注意:

       1. 請求配置設定的子緩沖區的大小至少為1個位元組,并且需要對齊到前面分析TransferBuffer類的成員函數Initialize時提到的參數alignment的值。

       2. 每一個配置設定出去的子緩沖區都使用一個Block對象描述,并且儲存在RingBuffer類的成員變量blocks_描述的一個std::deque中。

       從上面的分析我們就可以知道,通過RingBuffer類配置設定的子緩沖區的主要狀态變遷過程為:FREE=>IN_USE=>FREE_PENDING_TOKEN=>FREE。

       接下來,我們繼續分析RingBuffer類的成員函數FreeOldestBlock的實作,以便了解它是如何回收一個配置設定出去的子緩沖區的,如下所示:

void RingBuffer::FreeOldestBlock() {
  DCHECK(!blocks_.empty()) << "no free blocks";
  Block& block = blocks_.front();
  DCHECK(block.state != IN_USE)
      << "attempt to allocate more than maximum memory";
  if (block.state == FREE_PENDING_TOKEN) {
    helper_->WaitForToken(block.token);
  }
  in_use_offset_ += block.size;
  if (in_use_offset_ == size_) {
    in_use_offset_ = 0;
  }
  // If they match then the entire buffer is free.
  if (in_use_offset_ == free_offset_) {
    in_use_offset_ = 0;
    free_offset_ = 0;
  }
  blocks_.pop_front();
}
           

       這個函數定義在檔案external/chromium_org/gpu/command_buffer/client/ring_buffer.cc中。

       前面提到,所有已經配置設定出去的子緩沖區儲存在RingBuffer類的成員變量blocks_描述的一個std::deque中。其中,最早配置設定出去的子緩沖區儲存這個std::deque的頭部。在調用RingBuffer類的成員函數FreeOldestBlock的時候,必須要保證頭部的子緩沖區不是處于IN_USE狀态的。如果不是處于IN_USE狀态,那麼根據前面的分析,就是處于PADDING或者FREE_PENDING_TOKEN狀态。處于PADDING狀态的子緩沖區沒有實際使用,是以可以直接回收。但是處于FREE_PENDING_TOKEN狀态的子緩沖區,正在被GPU指令緩沖區引用,是以需要進行等待。等待結束後,就重新設定共享緩沖區的狀态,即相應地調整in_use_offset_、free_offset_的值,以及将位于頭部的子緩沖區從成員變量blocks_描述的一個std::deque移除。

       等待GPU指令緩沖區使用完畢一個子緩沖區是通過調用RingBuffer類的成員變量helper_描述的一個GLES2CmdHelper對象的成員函數WaitForToken實作的。GLES2CmdHelper類的成員函數WaitForToken是從父類CommandBufferHelper繼承下來的,是以接下來我們分析CommandBufferHelper類的成員函數WaitForToken的實作,如下所示:

void CommandBufferHelper::WaitForToken(int32 token) {
  ......
  if (token > token_) return;  // we wrapped
  if (last_token_read() >= token)
    return;
  Flush();
  command_buffer_->WaitForTokenInRange(token, token_);
}
           

      這個函數定義在檔案external/chromium_org/gpu/command_buffer/client/cmd_buffer_helper.cc中。

      後面我們會看到,參數token描述的Token值是通過CommandBufferHelper類的成員函數InsertToken配置設定出來的。CommandBufferHelper類的成員函數InsertToken每次被調用時,都會将成員變量token_的值加1,然後将得到的結果傳回給調用者。CommandBufferHelper類的成員變量token_是一個int32值,它不能無限增加。當增加到最大值0x7FFFFFFF時,就需要重置為0,然後開始新一輪的遞增。當CommandBufferHelper類的成員變量token_被重置為0的時候,CommandBufferHelper類的成員函數InsertToken會請求GPU程序處理GPU指令緩沖區的所有新寫入的指令,以便保證前面寫入的所有gpu::cmd::SetToken指令都已經處理完畢。這樣,當參數token的值大于CommandBufferHelper類的成員變量token_的時候,就意味着CommandBufferHelper類的成員函數WaitForToken不需要等待,因為這時候可以確定參數token關聯的子緩沖區已經不再被GPU指令緩沖區引用了。

      當參數token的值小于等于CommandBufferHelper類的成員變量token_的時候,CommandBufferHelper類的成員函數WaitForToken調用成員函數last_token_read獲得GPU程序為目前OpenGL上下文記錄的Token值。如果這個Token值大于等于參數token的值,那麼就說明CommandBufferHelper類的成員函數WaitForToken不需要等待,因為條件已經滿足。否則的話,接下來就會調用另外一個成員函數Flush請求GPU程序執行GPU指令緩沖區的指令。關于CommandBufferHelper類的成員函數Flush的實作,可以參考前面Chromium硬體加速渲染的OpenGL指令執行過程分析一文。

      請求了GPU程序執行GPU指令緩沖區的指令之後,CommandBufferHelper類的成員函數WaitForToken調用成員變量command_buffer_描述的一個CommandBufferProxyImpl對象的成員函數WaitForTokenInRange等待GPU程序處理GPU指令緩沖區的指令,直到處理到一個Token值設定為token的gpu::cmd::SetToken指令。

      CommandBufferProxyImpl類的成員函數WaitForTokenInRange的實作如下所示:

void CommandBufferProxyImpl::WaitForTokenInRange(int32 start, int32 end) {
  ......
  TryUpdateState();
  if (!InRange(start, end, last_state_.token) &&
      last_state_.error == gpu::error::kNoError) {
    gpu::CommandBuffer::State state;
    if (Send(new GpuCommandBufferMsg_WaitForTokenInRange(
            route_id_, start, end, &state)))
      OnUpdateState(state);
  }
  ......
}
           

       這個函數定義在檔案external/chromium_org/content/common/gpu/client/command_buffer_proxy_impl.cc中。

       CommandBufferProxyImpl類的成員函數WaitForTokenInRange首先調用另外一個成員函數TryUpdateState獲得GPU程序的狀态。獲得的狀态資訊記錄在CommandBufferProxyImpl類的成員變量last_state_描述的一個State對象。這個State對象的成員變量token記錄了GPU程序為目前OpenGL上下文記錄的Token值。如果這個Token值處于參數start和end描述的範圍中,那麼CommandBufferProxyImpl類的成員函數WaitForTokenInRange就不用等待了。否則的話,CommandBufferProxyImpl類的成員函數WaitForTokenInRange就會向GPU程序發送一個類型為GpuCommandBufferMsg_WaitForTokenInRange的同步IPC消息,等待GPU程序為目前OpenGL上下文記錄的Token值處于參數start和end描述的範圍中。

       類型為GpuCommandBufferMsg_WaitForTokenInRange的IPC消息是由與目前正處理的CommandBufferProxyImpl對象對應的一個運作在GPU程序中的GpuCommandBufferStub對象的成員函數OnMessageReceived接收的,如下所示:

bool GpuCommandBufferStub::OnMessageReceived(const IPC::Message& message) {  
  ......  
  
  bool handled = true;  
  IPC_BEGIN_MESSAGE_MAP(GpuCommandBufferStub, message)  
    ......  
    IPC_MESSAGE_HANDLER_DELAY_REPLY(GpuCommandBufferMsg_WaitForTokenInRange,
                                    OnWaitForTokenInRange);

    ......  
    IPC_MESSAGE_UNHANDLED(handled = false)  
  IPC_END_MESSAGE_MAP()  
  
  CheckCompleteWaits();  
  
  ......  
  
  return handled;  
} 
           

      這個函數定義在檔案external/chromium_org/content/common/gpu/gpu_command_buffer_stub.cc中。

      從這裡可以看到,GpuCommandBufferStub類的成員函數OnMessageReceived将類型為GpuCommandBufferMsg_WaitForTokenInRange的IPC消息分發給成員函數OnWaitForTokenInRange處理。

      GpuCommandBufferStub類的成員函數OnWaitForTokenInRange的實作如下所示: 

void GpuCommandBufferStub::OnWaitForTokenInRange(int32 start,
                                                 int32 end,
                                                 IPC::Message* reply_message) {
  ......
  wait_for_token_ =
      make_scoped_ptr(new WaitForCommandState(start, end, reply_message));
  CheckCompleteWaits();
}
           

      這個函數定義在檔案external/chromium_org/content/common/gpu/gpu_command_buffer_stub.cc中。

      GpuCommandBufferStub類的成員函數OnWaitForTokenInRange首先将參數start、end和reply_message封裝在一個WaitForCommandState對象中,并且将該WaitForCommandState對象儲存在成員變量wait_for_token_中,接着調用另外一個成員函數CheckCompleteWaits檢查GPU程序是否已經處理了GPU指令緩沖區中的一個Token值介于start和end之間的gpu::cmd::SetToken指令。

       GpuCommandBufferStub類的成員函數CheckCompleteWaits的實作如下所示:

void GpuCommandBufferStub::CheckCompleteWaits() {
  if (wait_for_token_ || wait_for_get_offset_) {
    gpu::CommandBuffer::State state = command_buffer_->GetLastState();
    if (wait_for_token_ &&
        (gpu::CommandBuffer::InRange(
             wait_for_token_->start, wait_for_token_->end, state.token) ||
         state.error != gpu::error::kNoError)) {
      ReportState();
      GpuCommandBufferMsg_WaitForTokenInRange::WriteReplyParams(
          wait_for_token_->reply.get(), state);
      Send(wait_for_token_->reply.release());
      wait_for_token_.reset();
    }
    ......
  }
}
           

       這個函數定義在檔案external/chromium_org/content/common/gpu/gpu_command_buffer_stub.cc中。

       在前面Chromium硬體加速渲染的OpenGL指令執行過程分析一文中,我們分析過GpuCommandBufferStub類的成員函數CheckCompleteWaits是如何處理成員變量wait_for_get_offset_不等于NULL的情況的。當GpuCommandBufferStub類的成員變量wait_for_get_offset_不等于NULL的時候,它指向的也是一個WaitForCommandState對象,表示GPU程序的一個Client端正在等待GPU程序處理新送出的GPU指令,以便在GPU指令緩沖區中騰出更多的空閑空間來。

       GpuCommandBufferStub類的成員函數CheckCompleteWaits處理成員變量wait_for_token_不等于NULL的情況也是類似的,它首先調用成員變量command_buffer_指向的一個CommandBufferService對象的成員函數GetLastState獲得GPU指令緩沖區的處理狀态資訊。獲得的狀态資訊封裝在一個State對象中,這個State對象的成員變量token記錄的就是最後一個處理的gpu::cmd::SetToken指令設定的Token值。如果這個Token值介于成員變量wait_for_token_描述的WaitForCommandState對象指定的範圍,那麼就可以向正在等待的Client端發送一個IPC消息,作為該Client端之前發送過來的類型為GpuCommandBufferMsg_WaitForTokenInRange的IPC消息的回複。Client端收到這個回複消息之後,就可以結束等待了。

       以上我們分析的就是通過TransferBuffer類的成員函數AllocUpTo和Alloc從一個共享緩沖區中配置設定子緩沖區的過程,接下來我們繼續分析通過TransferBuffer類的成員函數FreePendingToken釋放子緩沖區的過程,它的實作如下所示:

void TransferBuffer::FreePendingToken(void* p, unsigned int token) {
  ring_buffer_->FreePendingToken(p, token);
  if (bytes_since_last_flush_ >= size_to_flush_ && size_to_flush_ > 0) {
    helper_->Flush();
    bytes_since_last_flush_ = 0;
  }
}
           

       這個函數定義在檔案external/chromium_org/gpu/command_buffer/client/transfer_buffer.cc中。

       TransferBuffer類的成員函數FreePendingToken首先調用成員變量ring_buffer_指向的一個RingBuffer對象的成員函數FreePendingToken釋放參數p描述的子緩沖區,并且給該子緩沖區關聯一個Token值。

       TransferBuffer類的成員函數FreePendingToken接下來判斷從上次請求GPU程序處理GPU指令緩沖區的指令以來,又配置設定出去的子緩沖區的位元組數bytes_since_last_flush_是否已經超出預先設定的閥值size_to_flush_。如果超出的話,就再次調用成員變量helper_指向的一個GLES2CmdHelper對象的成員函數Flush請求GPU程序處理GPU指令緩沖區的新指令,并且将成員變量bytes_since_last_flush_的值重置為0。

       接下來,我們繼續分析RingBuffer類的成員函數FreePendingToken的實作,以便可以了解子緩沖區釋放的過程,如下所示:

void RingBuffer::FreePendingToken(void* pointer,
                                  unsigned int token) {
  Offset offset = GetOffset(pointer);
  offset -= base_offset_;
  ......
  for (Container::reverse_iterator it = blocks_.rbegin();
        it != blocks_.rend();
        ++it) {
    Block& block = *it;
    if (block.offset == offset) {
      ......
      block.token = token;
      block.state = FREE_PENDING_TOKEN;
      return;
    }
  }
  ......
}
           

      這個函數定義在檔案external/chromium_org/gpu/command_buffer/client/ring_buffer.cc中。

      RingBuffer類的成員函數FreePendingToken首先找到參數pointer描述的子緩沖區在成員變量base_offset_描述的共享緩沖區中的偏移位置,然後根據這個偏移位置從成員變量blocks_描述的一個std::deque中找到一個對應的Block對象,最後将參數token描述的Token值設定給前面找到的Block對象,并且将該Block對象的狀态設定為FREE_PENDING_TOKEN。這相當于是将一個子緩沖區從狀态IN_USE修改為FREE_PENDING_TOKEN,這也意味着該子緩沖區還沒有真正釋放掉,因為這時候它可能還被GPU指令緩沖區引用。

       以上我們分析的就是通過TransferBuffer類的成員函數FreePendingToken釋放一個從共享緩沖區中配置設定出來的子緩沖區的過程。為了友善GPU程序的Client端釋放一個子緩沖區時,自動往GPU指令緩沖區插入一個gpu::cmd::SetToken指令,以及關聯一個Token值,Chromium提供了一個工具類ScopedTransferBufferPtr,用來配置設定和釋放子緩沖區。

       當我們建立一個ScopedTransferBufferPtr對象時,會通過它的構造函數自動從一個共享緩沖區中配置設定一個子緩沖區,如下所示:

class GPU_EXPORT ScopedTransferBufferPtr {
 public:
  ScopedTransferBufferPtr(
      unsigned int size,
      CommandBufferHelper* helper,
      TransferBufferInterface* transfer_buffer)
      : buffer_(NULL),
        size_(0),
        helper_(helper),
        transfer_buffer_(transfer_buffer) {
    Reset(size);
  }

  ......
};
           

       這個函數定義在檔案external/chromium_org/gpu/command_buffer/client/transfer_buffer.h中。

       參數transfer_buffer指向的是一個TransferBuffer對象,該TransferBuffer對象内部建立有一塊共享緩沖區,參數size表示要從上述共享緩沖區中配置設定的子緩沖區的大小,另外一個參數helper指向的是一個GLES2CmdHelper對象,用來往GPU指令緩沖區插入gpu::cmd::SetToken指令。

       ScopedTransferBufferPtr類的構造函數調用另外一個成員函數Reset從參數transfer_buffer指向的TransferBuffer對象中配置設定一個子緩沖區,它的實作如下所示:

void ScopedTransferBufferPtr::Reset(unsigned int new_size) {
  Release();
  buffer_ = transfer_buffer_->AllocUpTo(new_size, &size_);
}
           

       這個函數定義在檔案external/chromium_org/gpu/command_buffer/client/transfer_buffer.cc中。

       ScopedTransferBufferPtr類的成員函數Reset調用成員變量transfer_buffer_描述的一個TransferBuffer對象的成員函數AllocUpTo配置設定一塊大小為new_size的子緩沖區,配置設定出來的子緩沖區用一個Buffer對象描述,該Buffer對象儲存在成員變量buffer_中。

       注意,目前正在處理的ScopedTransferBufferPtr有可能之前已經配置設定過子緩沖區,這時候在配置設定新的子緩沖區之前,需要調用另外一個成員函數Release釋放舊的子緩沖區。後面我們再分析ScopedTransferBufferPtr類的成員函數Release的實作。

       當一個ScopedTransferBufferPtr對象超出其生命周期範圍時,會通過它的析構函數自動釋放之前配置設定的子緩沖區,如下所示:

class GPU_EXPORT ScopedTransferBufferPtr {
 public:
  ......

  ~ScopedTransferBufferPtr() {
    Release();
  }

  ......
};
           

      這個函數定義在檔案external/chromium_org/gpu/command_buffer/client/transfer_buffer.h中。

      ScopedTransferBufferPtr類的析構函數調用另外一個成員函數Release釋放之前通過構造函數配置設定的子緩沖區,如下所示:

void ScopedTransferBufferPtr::Release() {
  if (buffer_) {
    transfer_buffer_->FreePendingToken(buffer_, helper_->InsertToken());
    buffer_ = NULL;
    size_ = 0;
  }
}
           

      這個函數定義在檔案external/chromium_org/gpu/command_buffer/client/transfer_buffer.cc中。

      當成員變量buffer_不等于NULL的時候,就說明目前正在處理的ScopedTransferBufferPtr對象之前從成員變量transfer_buffer_描述的一個TransferBuffer對象中配置設定過子緩沖區,是以這時候就需要調用該TransferBuffer對象的成員函數FreePendingToken釋放該子緩沖區。在釋放之前,還會調用成員變量helper_指向的一個GLES2CmdHelper對象的成員函數InsertToken往GPU指令緩沖區寫入一個gpu::cmd::SetToken指令。等到該gpu::cmd::SetToken指令被GPU程序處理後,ScopedTransferBufferPtr類的成員變量buffer_描述的子緩沖區才可以真正釋放。

      接下來我們繼續分析GLES2CmdHelper類的成員函數InsertToken的實作,以便可以了解gpu::cmd::SetToken指令的處理過程。

      GLES2CmdHelper類的成員函數InsertToken是從父類CommandBufferHelper繼承下來的,是以接下來我們分析CommandBufferHelper類的成員函數InsertToken的實作,如下所示:

int32 CommandBufferHelper::InsertToken() {
  ......
  token_ = (token_ + 1) & 0x7FFFFFFF;
  cmd::SetToken* cmd = GetCmdSpace<cmd::SetToken>();
  if (cmd) {
    cmd->Init(token_);
    if (token_ == 0) {
      TRACE_EVENT0("gpu", "CommandBufferHelper::InsertToken(wrapped)");
      // we wrapped
      Finish();
      DCHECK_EQ(token_, las