blob: 3a1e5e267ad5c79e656651e4900ea0a4c549fd52 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/command_line.h"
#include "base/process/process.h"
#include "base/threading/platform_thread.h"
#include "gpu/command_buffer/client/gles2_cmd_helper.h"
#include "gpu/command_buffer/client/gles2_implementation.h"
#include "gpu/command_buffer/client/gpu_control.h"
#include "gpu/command_buffer/client/shared_memory_limits.h"
#include "gpu/command_buffer/client/transfer_buffer.h"
#include "gpu/command_buffer/common/constants.h"
#include "gpu/command_buffer/common/context_creation_attribs.h"
#include "gpu/command_buffer/common/sync_token.h"
#include "gpu/command_buffer/service/command_buffer_direct.h"
#include "gpu/command_buffer/service/context_group.h"
#include "gpu/command_buffer/service/gles2_cmd_decoder.h"
#include "gpu/command_buffer/service/gpu_switches.h"
#include "gpu/command_buffer/service/gpu_tracer.h"
#include "gpu/command_buffer/service/image_manager.h"
#include "gpu/command_buffer/service/logger.h"
#include "gpu/command_buffer/service/mailbox_manager_impl.h"
#include "gpu/command_buffer/service/memory_tracking.h"
#include "gpu/command_buffer/service/passthrough_discardable_manager.h"
#include "gpu/command_buffer/service/service_discardable_manager.h"
#include "gpu/command_buffer/service/service_utils.h"
#include "gpu/command_buffer/service/shared_image_manager.h"
#include "gpu/command_buffer/service/sync_point_manager.h"
#include "gpu/command_buffer/service/transfer_buffer_manager.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/perf/perf_test.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gl/gl_context_stub.h"
#include "ui/gl/gl_share_group.h"
#include "ui/gl/gl_surface_stub.h"
#include "ui/gl/init/gl_factory.h"
namespace gpu {
namespace {
constexpr int kDefaultRuns = 8;
constexpr int kDefaultIterations = 50000;
// A command buffer that can record and replay commands
// This goes through 3 states, allowing setting up of initial state before
// record/replay a tight loop:
// - kDirect directly sends commands to the service on Flush
// - kRecord doesn't send anything on Flush, but keeps track of the put pointer
// - kReplay allows replaying of commands recorded in the kRecord state
//
// The initial state is kDirect. AdvanceMode is used to transition from one
// state to the next. The transition from kDirect to kRecord requires the
// GetBuffer to have been freed, allowing a fresh start for record.
class RecordReplayCommandBuffer : public CommandBufferDirect {
public:
enum Mode { kDirect, kRecord, kReplay };
RecordReplayCommandBuffer() = default;
~RecordReplayCommandBuffer() override = default;
void AdvanceMode() {
switch (mode_) {
case kDirect:
mode_ = kRecord;
DCHECK_EQ(current_get_buffer_, -1);
DCHECK_EQ(service()->GetState().get_offset, 0);
break;
case kRecord:
mode_ = kReplay;
DCHECK_NE(saved_get_buffer_, -1);
CommandBufferDirect::SetGetBuffer(saved_get_buffer_);
break;
case kReplay:
mode_ = kDirect;
break;
}
}
void Flush(int32_t put_offset) override {
DCHECK_NE(mode_, kReplay);
if (mode_ == kDirect) {
CommandBufferDirect::Flush(put_offset);
} else {
DCHECK_GE(put_offset, saved_put_offset_);
saved_put_offset_ = put_offset;
}
}
CommandBuffer::State WaitForTokenInRange(int32_t start,
int32_t end) override {
DCHECK_EQ(mode_, kDirect);
return CommandBufferDirect::WaitForTokenInRange(start, end);
}
CommandBuffer::State WaitForGetOffsetInRange(uint32_t set_get_buffer_count,
int32_t start,
int32_t end) override {
DCHECK_EQ(mode_, kDirect);
return CommandBufferDirect::WaitForGetOffsetInRange(set_get_buffer_count,
start, end);
}
void SetGetBuffer(int32_t transfer_buffer_id) override {
switch (mode_) {
case kDirect:
current_get_buffer_ = transfer_buffer_id;
CommandBufferDirect::SetGetBuffer(transfer_buffer_id);
break;
case kRecord:
DCHECK_EQ(saved_get_buffer_, -1);
saved_get_buffer_ = transfer_buffer_id;
break;
case kReplay:
NOTREACHED();
break;
}
}
void OnParseError() override {
ASSERT_EQ(service()->GetState().error, error::kNoError);
}
void Replay() {
DCHECK_EQ(mode_, kReplay);
SetGetOffsetForTest(0);
CommandBufferDirect::Flush(saved_put_offset_);
}
int32_t saved_put_offset() const { return saved_put_offset_; }
Mode mode() const { return mode_; }
private:
Mode mode_ = kDirect;
int32_t saved_put_offset_ = 0;
int32_t saved_get_buffer_ = -1;
int32_t current_get_buffer_ = -1;
};
GpuPreferences GetGpuPreferences() {
GpuPreferences preferences;
if (gles2::UsePassthroughCommandDecoder(
base::CommandLine::ForCurrentProcess()))
preferences.use_passthrough_cmd_decoder = true;
return preferences;
}
// This wraps a RecordReplayCommandBuffer and gives it a back-end decoder, as
// well as a front-end GLES2Implementation. This allows recording commands at
// the GL level and replaying them to the driver (or a stub).
class RecordReplayContext : public GpuControl {
public:
RecordReplayContext()
: gpu_preferences_(GetGpuPreferences()),
share_group_(new gl::GLShareGroup),
translator_cache_(gpu_preferences_) {
bool bind_generates_resource = false;
if (base::CommandLine::ForCurrentProcess()->HasSwitch("use-stub")) {
surface_ = new gl::GLSurfaceStub;
scoped_refptr<gl::GLContextStub> context_stub =
new gl::GLContextStub(share_group_.get());
context_stub->SetGLVersionString("OpenGL ES 3.1");
context_stub->SetUseStubApi(true);
context_ = context_stub;
} else {
gl::GLContextAttribs attribs;
if (gpu_preferences_.use_passthrough_cmd_decoder)
attribs.bind_generates_resource = bind_generates_resource;
surface_ = gl::init::CreateOffscreenGLSurface(gfx::Size());
context_ = gl::init::CreateGLContext(share_group_.get(), surface_.get(),
attribs);
}
context_->MakeCurrent(surface_.get());
scoped_refptr<gles2::FeatureInfo> feature_info = new gles2::FeatureInfo();
scoped_refptr<gles2::ContextGroup> context_group = new gles2::ContextGroup(
gpu_preferences_, true, &mailbox_manager_, nullptr /* memory_tracker */,
&translator_cache_, &completeness_cache_, feature_info,
bind_generates_resource, &image_manager_, nullptr /* image_factory */,
nullptr /* progress_reporter */, GpuFeatureInfo(),
&discardable_manager_, &passthrough_discardable_manager_,
&shared_image_manager_);
command_buffer_.reset(new RecordReplayCommandBuffer());
decoder_.reset(gles2::GLES2Decoder::Create(
command_buffer_.get(), command_buffer_->service(), &outputter_,
context_group.get()));
command_buffer_->set_handler(decoder_.get());
decoder_->GetLogger()->set_log_synthesized_gl_errors(false);
ContextCreationAttribs attrib_helper;
attrib_helper.offscreen_framebuffer_size = gfx::Size(16, 16);
attrib_helper.red_size = 8;
attrib_helper.green_size = 8;
attrib_helper.blue_size = 8;
attrib_helper.alpha_size = 8;
attrib_helper.depth_size = 0;
attrib_helper.stencil_size = 0;
attrib_helper.context_type = CONTEXT_TYPE_OPENGLES3;
ContextResult result =
decoder_->Initialize(surface_.get(), context_.get(), true,
gles2::DisallowedFeatures(), attrib_helper);
DCHECK_EQ(result, ContextResult::kSuccess);
capabilities_ = decoder_->GetCapabilities();
const SharedMemoryLimits limits;
gles2_helper_.reset(new gles2::GLES2CmdHelper(command_buffer_.get()));
result = gles2_helper_->Initialize(limits.command_buffer_size);
DCHECK_EQ(result, ContextResult::kSuccess);
// Create a transfer buffer.
transfer_buffer_.reset(new TransferBuffer(gles2_helper_.get()));
// Create the object exposing the OpenGL API.
const bool lose_context_when_out_of_memory = false;
const bool support_client_side_arrays = false;
gles2_implementation_.reset(new gles2::GLES2Implementation(
gles2_helper_.get(), nullptr, transfer_buffer_.get(),
bind_generates_resource, lose_context_when_out_of_memory,
support_client_side_arrays, this));
result = gles2_implementation_->Initialize(limits);
DCHECK_EQ(result, ContextResult::kSuccess);
}
~RecordReplayContext() override {
while (command_buffer_->mode() != RecordReplayCommandBuffer::kDirect)
command_buffer_->AdvanceMode();
gles2_implementation_.reset();
transfer_buffer_.reset();
gles2_helper_.reset();
command_buffer_.reset();
decoder_->Destroy(true);
decoder_.reset();
}
void StartRecord() {
DCHECK_EQ(command_buffer_->mode(), RecordReplayCommandBuffer::kDirect);
gles2_helper_->FreeRingBuffer();
command_buffer_->AdvanceMode();
}
void StartReplay() {
DCHECK_EQ(command_buffer_->mode(), RecordReplayCommandBuffer::kRecord);
gles2_helper_->FlushLazy();
command_buffer_->AdvanceMode();
}
void Replay() { command_buffer_->Replay(); }
gles2::GLES2Implementation* gl() { return gles2_implementation_.get(); }
private:
// GpuControl implementation;
void SetGpuControlClient(GpuControlClient*) override {}
const Capabilities& GetCapabilities() const override { return capabilities_; }
int32_t CreateImage(ClientBuffer buffer,
size_t width,
size_t height) override {
NOTIMPLEMENTED();
return -1;
}
void DestroyImage(int32_t id) override { NOTREACHED(); }
void SignalQuery(uint32_t query, base::OnceClosure callback) override {
NOTREACHED();
}
void CreateGpuFence(uint32_t gpu_fence_id, ClientGpuFence source) override {
NOTREACHED();
}
void GetGpuFence(uint32_t gpu_fence_id,
base::OnceCallback<void(std::unique_ptr<gfx::GpuFence>)>
callback) override {
NOTREACHED();
}
void SetLock(base::Lock*) override { NOTREACHED(); }
void EnsureWorkVisible() override { NOTREACHED(); }
gpu::CommandBufferNamespace GetNamespaceID() const override {
return gpu::CommandBufferNamespace::INVALID;
}
CommandBufferId GetCommandBufferID() const override {
return gpu::CommandBufferId();
}
void FlushPendingWork() override { NOTREACHED(); }
uint64_t GenerateFenceSyncRelease() override {
NOTREACHED();
return 0;
}
bool IsFenceSyncReleased(uint64_t release) override {
NOTREACHED();
return true;
}
void SignalSyncToken(const gpu::SyncToken& sync_token,
base::OnceClosure callback) override {
NOTREACHED();
}
void WaitSyncToken(const gpu::SyncToken& sync_token) override {
NOTREACHED();
}
bool CanWaitUnverifiedSyncToken(const gpu::SyncToken& sync_token) override {
NOTREACHED();
return true;
}
GpuPreferences gpu_preferences_;
gles2::MailboxManagerImpl mailbox_manager_;
scoped_refptr<gl::GLShareGroup> share_group_;
gles2::ImageManager image_manager_;
ServiceDiscardableManager discardable_manager_;
PassthroughDiscardableManager passthrough_discardable_manager_;
SharedImageManager shared_image_manager_;
scoped_refptr<gl::GLSurface> surface_;
scoped_refptr<gl::GLContext> context_;
gles2::ShaderTranslatorCache translator_cache_;
gles2::FramebufferCompletenessCache completeness_cache_;
std::unique_ptr<RecordReplayCommandBuffer> command_buffer_;
gles2::TraceOutputter outputter_;
std::unique_ptr<gles2::GLES2Decoder> decoder_;
gpu::Capabilities capabilities_;
std::unique_ptr<gles2::GLES2CmdHelper> gles2_helper_;
std::unique_ptr<TransferBuffer> transfer_buffer_;
std::unique_ptr<gles2::GLES2Implementation> gles2_implementation_;
};
// This abstracts the performance capture loop, iterating through a warmup run
// and then a number of performance capturing runs.
class PerfIterator {
public:
PerfIterator(std::string name, int runs, int iterations)
: name_(std::move(name)), runs_(runs), iterations_(iterations) {
// When running under linux-perf, we try to isolate the microbenchmark
// performance:
// 1- sleep 1 second after warmup so that one can skip perf for
// intitialization with 'perf record -D 1000'
// 2- exit immediately after the capture loop is finished to skip teardown.
// 3- avoid unneeded syscalls (time, print).
for_linux_perf_ =
base::CommandLine::ForCurrentProcess()->HasSwitch("for-linux-perf");
if (base::CommandLine::ForCurrentProcess()->HasSwitch("fast-run")) {
runs_ = 1;
iterations_ = 100;
}
}
bool Iterate() {
if (--current_iterations_ > 0)
return true;
return NextOuter();
}
private:
bool NextOuter() {
base::TimeTicks time;
if (warmup_) {
warmup_ = false;
if (for_linux_perf_)
base::PlatformThread::Sleep(base::TimeDelta::FromSeconds(1));
else
time = base::TimeTicks::Now();
} else if (!for_linux_perf_) {
time = base::TimeTicks::Now();
double ns = (time - run_start_time_).InNanoseconds() / iterations_;
perf_test::PrintResult(name_, "", "wall_time", ns, "ns", true);
}
if (runs_ == 0) {
if (for_linux_perf_)
base::Process::TerminateCurrentProcessImmediately(0);
return false;
}
--runs_;
current_iterations_ = iterations_;
run_start_time_ = time;
return true;
}
static constexpr int kWarmupIterations = 2;
std::string name_;
base::TimeTicks run_start_time_;
int runs_;
int iterations_;
int current_iterations_ = 1 + kWarmupIterations;
bool warmup_ = true;
bool for_linux_perf_ = false;
DISALLOW_COPY_AND_ASSIGN(PerfIterator);
};
class DecoderPerfTest : public testing::Test {
public:
~DecoderPerfTest() override = default;
void SetUp() override {
context_ = std::make_unique<RecordReplayContext>();
gl_ = context_->gl();
gl_->GenRenderbuffers(1, &renderbuffer_);
gl_->BindRenderbuffer(GL_RENDERBUFFER, renderbuffer_);
gl_->RenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 256, 256);
gl_->GenFramebuffers(1, &framebuffer_);
gl_->BindFramebuffer(GL_FRAMEBUFFER, framebuffer_);
gl_->FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_RENDERBUFFER, renderbuffer_);
gl_->Viewport(0, 0, 256, 256);
}
void TearDown() override { context_.reset(); }
void StartRecord() { context_->StartRecord(); }
void StartReplay() { context_->StartReplay(); }
void Replay() { context_->Replay(); }
GLuint CompileShader(GLenum type, const char* source) {
GLuint shader = gl_->CreateShader(type);
GLint length = base::checked_cast<GLint>(strlen(source));
gl_->ShaderSource(shader, 1, &source, &length);
gl_->CompileShader(shader);
GLint compile_status = 0;
gl_->GetShaderiv(shader, GL_COMPILE_STATUS, &compile_status);
if (!compile_status) {
GLint log_length = 0;
gl_->GetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_length);
if (log_length) {
std::unique_ptr<GLchar[]> log(new GLchar[log_length]);
GLsizei returned_log_length = 0;
gl_->GetShaderInfoLog(shader, log_length, &returned_log_length,
log.get());
LOG(ERROR) << std::string(log.get(), returned_log_length);
}
gl_->DeleteShader(shader);
return 0;
}
return shader;
}
struct AttribBinding {
const char* name;
GLuint location;
};
GLuint CreateAndLinkProgram(
const char* vertex_shader,
const char* fragment_shader,
const std::initializer_list<AttribBinding>& attrib_bindings) {
GLuint program = gl_->CreateProgram();
GLuint vshader = CompileShader(GL_VERTEX_SHADER, vertex_shader);
DCHECK_NE(0u, vshader);
gl_->AttachShader(program, vshader);
gl_->DeleteShader(vshader);
GLuint fshader = CompileShader(GL_FRAGMENT_SHADER, fragment_shader);
DCHECK_NE(0u, fshader);
gl_->AttachShader(program, fshader);
gl_->DeleteShader(fshader);
for (const auto& attrib : attrib_bindings)
gl_->BindAttribLocation(program, attrib.location, attrib.name);
gl_->LinkProgram(program);
GLint link_status = 0;
gl_->GetProgramiv(program, GL_LINK_STATUS, &link_status);
DCHECK_EQ(link_status, GL_TRUE);
return program;
}
void CreateBasicTexture(GLuint texture) {
gl_->BindTexture(GL_TEXTURE_2D, texture);
gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
gl_->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
gl_->TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 4, 4, 0, GL_RGBA,
GL_UNSIGNED_BYTE, nullptr);
}
protected:
std::unique_ptr<RecordReplayContext> context_;
gles2::GLES2Implementation* gl_;
GLuint renderbuffer_ = 0;
GLuint framebuffer_ = 0;
};
constexpr const char kVertexShader[] =
"attribute vec2 position;\n"
"uniform vec2 scale;\n"
"uniform vec2 offset;\n"
"varying vec2 texcoords;\n"
"void main () {\n"
" gl_Position = vec4(position * scale + offset, 0.0, 1.0);\n"
" texcoords = position;\n"
"}\n";
constexpr const char kFragmentShader[] =
"precision mediump float;\n"
"varying vec2 texcoords;\n"
"uniform sampler2D texture;\n"
"void main() {\n"
" gl_FragColor = texture2D(texture, texcoords);\n"
"}\n";
constexpr const float kVertices[] = {
0.f, 0.f, 0.f, 1.f, 1.f, 0.f, 1.f, 1.f,
};
// Measures a loop with Uniform2f and DrawArrays.
TEST_F(DecoderPerfTest, BasicDraw) {
GLuint program =
CreateAndLinkProgram(kVertexShader, kFragmentShader, {{"postition", 0}});
gl_->UseProgram(program);
GLint scale_location = gl_->GetUniformLocation(program, "scale");
GLint offset_location = gl_->GetUniformLocation(program, "offset");
GLint texture_location = gl_->GetUniformLocation(program, "texture");
GLuint texture;
gl_->GenTextures(1, &texture);
CreateBasicTexture(texture);
GLuint buffer;
gl_->GenBuffers(1, &buffer);
gl_->BindBuffer(GL_ARRAY_BUFFER, buffer);
gl_->BufferData(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices,
GL_STATIC_DRAW);
gl_->VertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat),
nullptr);
gl_->EnableVertexAttribArray(0);
gl_->Uniform1i(texture_location, 0);
constexpr int N = 10;
gl_->Uniform2f(scale_location, 2.f / N, 2.f / N);
StartRecord();
for (int x = 0; x < N; ++x) {
float xpos = 2.f * x / N - 1.f;
for (int y = 0; y < N; ++y) {
float ypos = 2.f * y / N - 1.f;
gl_->Uniform2f(offset_location, xpos, ypos);
gl_->DrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
}
StartReplay();
PerfIterator iterator("decoder_basic_draw_100", kDefaultRuns,
kDefaultIterations);
while (iterator.Iterate())
Replay();
}
// Measures a loop with changing the texture binding between draws.
TEST_F(DecoderPerfTest, TextureDraw) {
GLuint program =
CreateAndLinkProgram(kVertexShader, kFragmentShader, {{"position", 0}});
gl_->UseProgram(program);
GLint scale_location = gl_->GetUniformLocation(program, "scale");
GLint offset_location = gl_->GetUniformLocation(program, "offset");
GLint texture_location = gl_->GetUniformLocation(program, "texture");
constexpr size_t kTextures = 16;
GLuint textures[kTextures];
gl_->GenTextures(kTextures, textures);
for (GLuint texture : textures)
CreateBasicTexture(texture);
GLuint buffer;
gl_->GenBuffers(1, &buffer);
gl_->BindBuffer(GL_ARRAY_BUFFER, buffer);
gl_->BufferData(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices,
GL_STATIC_DRAW);
gl_->VertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat),
nullptr);
gl_->EnableVertexAttribArray(0);
gl_->Uniform1i(texture_location, 0);
constexpr int N = 10;
gl_->Uniform2f(scale_location, 2.f / N, 2.f / N);
StartRecord();
size_t texture = 0;
for (int x = 0; x < N; ++x) {
float xpos = 2.f * x / N - 1.f;
for (int y = 0; y < N; ++y) {
float ypos = 2.f * y / N - 1.f;
gl_->BindTexture(GL_TEXTURE_2D, textures[texture]);
gl_->Uniform2f(offset_location, xpos, ypos);
gl_->DrawArrays(GL_TRIANGLE_STRIP, 0, 4);
texture = (texture + 1) % kTextures;
}
}
StartReplay();
PerfIterator iterator("decoder_texture_draw_100", kDefaultRuns,
kDefaultIterations);
while (iterator.Iterate())
Replay();
}
// Measures a loop with changing the program between draws.
TEST_F(DecoderPerfTest, ProgramDraw) {
const char kVertexShader2[] =
"attribute vec2 position;\n"
"uniform vec2 scale;\n"
"uniform vec2 offset;\n"
"void main () {\n"
" gl_Position = vec4(position * scale + offset, 0.0, 1.0);\n"
"}\n";
const char kFragmentShader2[] =
"precision mediump float;\n"
"uniform vec4 color;\n"
"void main() {\n"
" gl_FragColor = color;\n"
"}\n";
GLuint programs[2];
programs[0] =
CreateAndLinkProgram(kVertexShader, kFragmentShader, {{"position", 0}});
GLint scale_location1 = gl_->GetUniformLocation(programs[0], "scale");
GLint texture_location1 = gl_->GetUniformLocation(programs[0], "texture");
programs[1] =
CreateAndLinkProgram(kVertexShader2, kFragmentShader2, {{"position", 0}});
GLint scale_location2 = gl_->GetUniformLocation(programs[1], "scale");
GLint color_location2 = gl_->GetUniformLocation(programs[1], "color");
GLuint texture;
gl_->GenTextures(1, &texture);
CreateBasicTexture(texture);
GLuint buffer;
gl_->GenBuffers(1, &buffer);
gl_->BindBuffer(GL_ARRAY_BUFFER, buffer);
gl_->BufferData(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices,
GL_STATIC_DRAW);
gl_->VertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat),
nullptr);
gl_->EnableVertexAttribArray(0);
constexpr int N = 10;
gl_->UseProgram(programs[0]);
gl_->Uniform1i(texture_location1, 0);
gl_->Uniform2f(scale_location1, 2.f / N, 2.f / N);
gl_->UseProgram(programs[1]);
gl_->Uniform2f(scale_location2, 2.f / N, 2.f / N);
gl_->Uniform4f(color_location2, 1.f, 0.f, 0.f, 1.f);
GLint offset_locations[2] = {gl_->GetUniformLocation(programs[0], "offset"),
gl_->GetUniformLocation(programs[1], "offset")};
StartRecord();
size_t program = 0;
for (int x = 0; x < N; ++x) {
float xpos = 2.f * x / N - 1.f;
for (int y = 0; y < N; ++y) {
float ypos = 2.f * y / N - 1.f;
gl_->UseProgram(programs[program]);
gl_->Uniform2f(offset_locations[program], xpos, ypos);
gl_->DrawArrays(GL_TRIANGLE_STRIP, 0, 4);
program = 1 - program;
}
}
StartReplay();
PerfIterator iterator("decoder_program_draw_100", kDefaultRuns,
kDefaultIterations);
while (iterator.Iterate())
Replay();
}
} // anonymous namespace
} // namespace gpu