Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(580)

Side by Side Diff: ui/gfx/compositor/compositor_win.cc

Issue 7067029: Prototype compositor to render views to a texture using d3d 10. This (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Fix gyp Created 9 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ui/gfx/compositor/compositor.h"
6
7 #include <algorithm>
8 #include <d3dx10.h>
9 #include <vector>
10
11 #include "base/compiler_specific.h"
12 #include "base/stl_util-inl.h"
13 #include "base/string_piece.h"
14 #include "base/win/scoped_comptr.h"
15 #include "grit/gfx_resources.h"
16 #include "third_party/skia/include/core/SkBitmap.h"
17 #include "ui/base/resource/resource_bundle.h"
18 #include "ui/gfx/canvas_skia.h"
19 #include "ui/gfx/rect.h"
20 #include "ui/gfx/transform.h"
21
22 // TODO(sky): this is a hack, figure out real error handling.
23 #define RETURN_IF_FAILED(error) \
24 if (error != S_OK) { \
25 this->Errored(error); \
26 VLOG(1) << "D3D failed" << error; \
27 return; \
28 }
29
30 using base::win::ScopedComPtr;
31
32 namespace ui {
33
34 namespace {
35
36 class ViewTexture;
37
38 // ViewTexture talks to its host by way of this interface.
39 class ViewTextureHost {
40 public:
41 // Invoked to update the perspective needed by this texture. |transform| is
42 // the transform for the texture, and |size| the size of the texture.
43 virtual void UpdatePerspective(const ui::Transform& transform,
44 const gfx::Size& size) = 0;
45
46 // Returns the overall size of the compositor.
47 virtual const gfx::Size& GetHostSize() = 0;
48
49 protected:
50 virtual ~ViewTextureHost() {}
51 };
52
53 // D3D 10 Texture implementation. Creates a quad representing the view and
54 // a texture with the bitmap data. The quad has an origin of 0,0,0 with a size
55 // matching that of |SetBitmap|.
56 class ViewTexture : public Texture {
57 public:
58 ViewTexture(ViewTextureHost* host,
59 ID3D10Device* device,
60 ID3D10Effect* effect);
61
62 ~ViewTexture();
63
64 void Init();
65
66 // Texture:
67 virtual void SetBitmap(const SkBitmap& bitmap,
68 const gfx::Point& origin,
69 const gfx::Size& overall_size) OVERRIDE;
70 virtual void Draw(const ui::Transform& transform) OVERRIDE;
71
72 private:
73 struct Vertex {
74 D3DXVECTOR3 position;
75 D3DXVECTOR2 texture_offset;
76 };
77
78 void Errored(HRESULT result);
79
80 void ConvertBitmapToD3DData(const SkBitmap& bitmap,
81 scoped_array<uint32>* converted_data);
82
83 void CreateVertexBuffer(const gfx::Size& size);
84
85 // TODO: this should be shared among all textures.
86 void CreateIndexBuffer();
87
88 ViewTextureHost* host_;
89
90 // Size of the corresponding View.
91 gfx::Size view_size_;
92
93 ScopedComPtr<ID3D10Device> device_;
94 ScopedComPtr<ID3D10Effect, NULL> effect_;
95 ScopedComPtr<ID3D10Texture2D> texture_;
96 ScopedComPtr<ID3D10ShaderResourceView> shader_view_;
97 ScopedComPtr<ID3D10Buffer> vertex_buffer_;
98 ScopedComPtr<ID3D10Buffer> index_buffer_;
99
100 DISALLOW_COPY_AND_ASSIGN(ViewTexture);
101 };
102
103 ViewTexture::ViewTexture(ViewTextureHost* host,
104 ID3D10Device* device,
105 ID3D10Effect* effect)
106 : host_(host),
107 device_(device),
108 effect_(effect) {
109 }
110
111 ViewTexture::~ViewTexture() {
112 }
113
114 void ViewTexture::Init() {
115 CreateIndexBuffer();
116 }
117
118 void ViewTexture::SetBitmap(const SkBitmap& bitmap,
119 const gfx::Point& origin,
120 const gfx::Size& overall_size) {
121 if (view_size_ != overall_size)
122 CreateVertexBuffer(overall_size);
123 view_size_ = overall_size;
124
125 scoped_array<uint32> converted_data;
126 ConvertBitmapToD3DData(bitmap, &converted_data);
127 if (gfx::Size(bitmap.width(), bitmap.height()) == overall_size) {
128 shader_view_.Release();
129 texture_.Release();
130
131 D3D10_TEXTURE2D_DESC texture_desc;
132 texture_desc.Width = bitmap.width();
133 texture_desc.Height = bitmap.height();
134 texture_desc.MipLevels = 1;
135 texture_desc.ArraySize = 1;
136 texture_desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
137 texture_desc.SampleDesc.Count = 1;
138 texture_desc.SampleDesc.Quality = 0;
139 texture_desc.Usage = D3D10_USAGE_DEFAULT;
140 texture_desc.BindFlags = D3D10_BIND_SHADER_RESOURCE;
141 texture_desc.CPUAccessFlags = 0;
142 texture_desc.MiscFlags = 0;
143 D3D10_SUBRESOURCE_DATA texture_data;
144 texture_data.pSysMem = converted_data.get();
145 texture_data.SysMemPitch = texture_desc.Width * 4;
146 texture_data.SysMemSlicePitch = 0;
147 RETURN_IF_FAILED(device_->CreateTexture2D(&texture_desc,
148 &texture_data,
149 texture_.Receive()));
150 RETURN_IF_FAILED(
151 device_->CreateShaderResourceView(texture_.get(), NULL,
152 shader_view_.Receive()));
153 } else {
154 // Only part of the texture was updated.
155 DCHECK(texture_.get());
156 D3D10_BOX dst_box = { origin.x(), origin.y(), 0,
157 origin.x() + bitmap.width(),
158 origin.y() + bitmap.height(), 0 };
159 device_->UpdateSubresource(texture_.get(), 0, &dst_box,
160 converted_data.get(), bitmap.width() * 4, 0);
161 }
162 }
163
164 void ViewTexture::Draw(const ui::Transform& transform) {
165 host_->UpdatePerspective(transform, view_size_);
166
167 // Make texture active.
168 RETURN_IF_FAILED(
169 effect_->GetVariableByName("textureMap")->AsShaderResource()->
170 SetResource(shader_view_.get()));
171
172 ID3D10EffectTechnique* technique = effect_->GetTechniqueByName("ViewTech");
173 DCHECK(technique);
174 D3D10_TECHNIQUE_DESC tech_desc;
175 technique->GetDesc(&tech_desc);
176 for(UINT p = 0; p < tech_desc.Passes; ++p)
177 technique->GetPassByIndex(p)->Apply(0);
178
179 UINT stride = sizeof(Vertex);
180 UINT offset = 0;
181 ID3D10Buffer* vertex_buffer = vertex_buffer_.get();
182 device_->IASetVertexBuffers(0, 1, &vertex_buffer, &stride, &offset);
183 device_->IASetIndexBuffer(index_buffer_.get(), DXGI_FORMAT_R32_UINT, 0);
184 device_->DrawIndexed(6, 0, 0);
185 }
186
187 void ViewTexture::Errored(HRESULT result) {
188 // TODO: figure out error handling.
189 DCHECK(false);
190 }
191
192 void ViewTexture::ConvertBitmapToD3DData(const SkBitmap& bitmap,
193 scoped_array<uint32>* converted_data) {
194 int width = bitmap.width();
195 int height = bitmap.height();
196 SkAutoLockPixels pixel_lock(bitmap);
197 // D3D wants the data in a different format (and not pre-multiplied).
198 converted_data->reset(new uint32[width * height]);
199 for (int x = 0; x < width; ++x) {
200 for (int y = 0; y < height; ++y) {
201 SkColor color = bitmap.getColor(x, y);
202 int alpha = SkColorGetA(color);
203 (*converted_data)[y * width + x] =
204 (SkColorGetA(color) << 24) |
205 (SkColorGetB(color) << 16) |
206 (SkColorGetG(color) << 8) |
207 (SkColorGetR(color));
208 }
209 }
210 }
211
212 void ViewTexture::CreateVertexBuffer(const gfx::Size& size) {
213 vertex_buffer_.Release();
214 const gfx::Size& host_size = host_->GetHostSize();
215 float x = static_cast<float>(host_size.width()) / 2.0f;
216 float y = static_cast<float>(host_size.height()) / 2.0f;
217 float w = static_cast<float>(size.width());
218 float h = static_cast<float>(size.height());
219 Vertex vertices[] = {
220 { D3DXVECTOR3(0.0f, -h, 0.0f), D3DXVECTOR2(0.0f, 1.0f) },
221 { D3DXVECTOR3(0.0f, 0.0f, 0.0f), D3DXVECTOR2(0.0f, 0.0f) },
222 { D3DXVECTOR3( w, 0.0f, 0.0f), D3DXVECTOR2(1.0f, 0.0f) },
223 { D3DXVECTOR3( w, -h, 0.0f), D3DXVECTOR2(1.0f, 1.0f) },
224 };
225
226 // Create the vertex buffer containing the points.
227 D3D10_BUFFER_DESC buffer_desc;
228 buffer_desc.Usage = D3D10_USAGE_IMMUTABLE;
229 buffer_desc.ByteWidth = sizeof(Vertex) * ARRAYSIZE_UNSAFE(vertices);
230 buffer_desc.BindFlags = D3D10_BIND_VERTEX_BUFFER;
231 buffer_desc.CPUAccessFlags = 0;
232 buffer_desc.MiscFlags = 0;
233 D3D10_SUBRESOURCE_DATA init_data;
234 init_data.pSysMem = vertices;
235 RETURN_IF_FAILED(device_->CreateBuffer(&buffer_desc, &init_data,
236 vertex_buffer_.Receive()));
237 }
238
239 void ViewTexture::CreateIndexBuffer() {
240 index_buffer_.Release();
241
242 // Then the index buffer.
243 DWORD indices[] = {
244 0, 1, 2,
245 0, 2, 3,
246 };
247 D3D10_BUFFER_DESC index_buffer;
248 index_buffer.Usage = D3D10_USAGE_IMMUTABLE;
249 index_buffer.ByteWidth = sizeof(DWORD) * ARRAYSIZE_UNSAFE(indices);
250 index_buffer.BindFlags = D3D10_BIND_INDEX_BUFFER;
251 index_buffer.CPUAccessFlags = 0;
252 index_buffer.MiscFlags = 0;
253 D3D10_SUBRESOURCE_DATA init_data2;
254 init_data2.pSysMem = indices;
255 RETURN_IF_FAILED(device_->CreateBuffer(&index_buffer, &init_data2,
256 index_buffer_.Receive()));
257 }
258
259 // D3D 10 Compositor implementation.
260 class CompositorWin : public Compositor, public ViewTextureHost {
261 public:
262 explicit CompositorWin(gfx::AcceleratedWidget widget);
263
264 void Init();
265
266 // ViewTextureHost.
267 virtual void UpdatePerspective(const ui::Transform& transform,
268 const gfx::Size& view_size) OVERRIDE;
269 virtual const gfx::Size& GetHostSize() OVERRIDE;
270
271 // Compositor:
272 virtual Texture* CreateTexture() OVERRIDE;
273 virtual void NotifyStart() OVERRIDE;
274 virtual void NotifyEnd() OVERRIDE;
275
276 private:
277 ~CompositorWin();
278
279 void Errored(HRESULT error_code);
280
281 // Returns the bounds of the hosting window.
282 gfx::Rect HostBounds();
283
284 void CreateDevice();
285
286 void LoadEffects();
287
288 void InitVertexLayout();
289
290 void Resize(const gfx::Rect& bounds);
291
292 gfx::AcceleratedWidget host_;
293
294 // Bounds the device was last created at.
295 gfx::Rect last_bounds_;
296
297 ScopedComPtr<ID3D10Device> device_;
298 ScopedComPtr<IDXGISwapChain> swap_chain_;
299 ScopedComPtr<ID3D10RenderTargetView> render_target_view_;
300 ScopedComPtr<ID3D10Texture2D> depth_stencil_buffer_;
301 ScopedComPtr<ID3D10DepthStencilView> depth_stencil_view_;
302 ScopedComPtr<ID3D10Effect, NULL> fx_;
303 ID3D10EffectTechnique* technique_;
304 ScopedComPtr<ID3D10InputLayout> vertex_layout_;
305
306 DISALLOW_COPY_AND_ASSIGN(CompositorWin);
307 };
308
309 CompositorWin::CompositorWin(gfx::AcceleratedWidget widget)
310 : host_(widget),
311 technique_(NULL) {
312 }
313
314 void CompositorWin::Init() {
315 CreateDevice();
316 LoadEffects();
317 Resize(last_bounds_);
318 InitVertexLayout();
319 }
320
321 void CompositorWin::UpdatePerspective(const ui::Transform& transform,
322 const gfx::Size& view_size) {
323 // Apply transform from view.
324 const SkMatrix& sk_matrix(transform.matrix());
325 // Use -1 * kMTransY for y-translation as origin for views is upper left.
326 D3DXMATRIX transform_matrix(
327 // row 1
328 sk_matrix[SkMatrix::kMScaleX], sk_matrix[SkMatrix::kMSkewX], 0.0f,
329 sk_matrix[SkMatrix::kMPersp0],
330 // row 2
331 sk_matrix[SkMatrix::kMSkewY], sk_matrix[SkMatrix::kMScaleY], 0.0f,
332 sk_matrix[SkMatrix::kMPersp1],
333 // row 3
334 0.0f, 0.0f, 1.0f, sk_matrix[SkMatrix::kMPersp2],
335 // row 4.
336 sk_matrix[SkMatrix::kMTransX], -sk_matrix[SkMatrix::kMTransY], 0.0f,
337 1.0f);
338
339 // Scale so x and y are from 0-2.
340 D3DXMATRIX scale_matrix;
341 D3DXMatrixScaling(
342 &scale_matrix,
343 2.0f / static_cast<float>(last_bounds_.width()),
344 2.0f / static_cast<float>(last_bounds_.height()),
345 1.0f);
346
347 // Translate so x and y are from -1,-1 to 1,1.
348 D3DXMATRIX translate_matrix;
349 D3DXMatrixTranslation(&translate_matrix, -1.0f, 1.0f, 0.0f);
350
351 D3DXMATRIX projection_matrix;
352 D3DXMatrixIdentity(&projection_matrix);
353 D3DXMatrixPerspectiveFovLH(&projection_matrix,
354 atanf(.5f) * 2.0f, 1.0f, 1.0f, 1000.0f);
355 D3DXVECTOR3 pos(0.0f, 0.0f, -2.0f);
356 D3DXVECTOR3 target(0.0f, 0.0f, 0.0f);
357 D3DXVECTOR3 up(0.0f, 1.0f, 0.0f);
358 D3DXMATRIX view;
359 D3DXMatrixIdentity(&view);
360 D3DXMatrixLookAtLH(&view, &pos, &target, &up);
361
362 D3DXMATRIX wvp = transform_matrix * scale_matrix * translate_matrix * view *
363 projection_matrix;
364 fx_->GetVariableByName("gWVP")->AsMatrix()->SetMatrix((float*)&wvp);
365 }
366
367 const gfx::Size& CompositorWin::GetHostSize() {
368 return last_bounds_.size();
369 }
370
371 Texture* CompositorWin::CreateTexture() {
372 ViewTexture* texture = new ViewTexture(this, device_.get(), fx_.get());
373 texture->Init();
374 return texture;
375 }
376
377 void CompositorWin::NotifyStart() {
378 gfx::Rect bounds = HostBounds();
379 if (bounds != last_bounds_)
380 Resize(bounds);
381
382 // Clear the background and stencil view.
383 device_->ClearRenderTargetView(render_target_view_.get(),
384 D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f));
385 device_->ClearDepthStencilView(
386 depth_stencil_view_.get(), D3D10_CLEAR_DEPTH|D3D10_CLEAR_STENCIL,
387 1.0f, 0);
388
389 // TODO: these steps may not be necessary each time through.
390 device_->OMSetDepthStencilState(0, 0);
391 float blend_factors[] = {0.0f, 0.0f, 0.0f, 0.0f};
392 device_->OMSetBlendState(0, blend_factors, 0xffffffff);
393 device_->IASetInputLayout(vertex_layout_.get());
394 device_->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
395 }
396
397 void CompositorWin::NotifyEnd() {
398 swap_chain_->Present(0, 0);
399
400 // We may delete the shader resource view before drawing again. Unset it so
401 // that d3d doesn't generate a warning when we do that.
402 fx_->GetVariableByName("textureMap")->AsShaderResource()->
403 SetResource(NULL);
404 D3D10_TECHNIQUE_DESC tech_desc;
405 technique_->GetDesc(&tech_desc);
406 for(UINT i = 0; i < tech_desc.Passes; ++i)
407 technique_->GetPassByIndex(i)->Apply(0);
408 }
409
410 CompositorWin::~CompositorWin() {
411 }
412
413 void CompositorWin::Errored(HRESULT error_code) {
414 // TODO: figure out error handling.
415 DCHECK(false);
416 }
417
418 gfx::Rect CompositorWin::HostBounds() {
419 RECT client_rect;
420 GetClientRect(host_, &client_rect);
421 return gfx::Rect(client_rect);
422 }
423
424 void CompositorWin::CreateDevice() {
425 last_bounds_ = HostBounds();
426
427 DXGI_SWAP_CHAIN_DESC sd;
428 sd.BufferDesc.Width = last_bounds_.width();
429 sd.BufferDesc.Height = last_bounds_.height();
430 sd.BufferDesc.RefreshRate.Numerator = 60;
431 sd.BufferDesc.RefreshRate.Denominator = 1;
432 sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
433 sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
434 sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
435
436 // No multisampling.
437 sd.SampleDesc.Count = 1;
438 sd.SampleDesc.Quality = 0;
439
440 sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
441 sd.BufferCount = 1;
442 sd.OutputWindow = host_;
443 sd.Windowed = true;
444 sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
445 sd.Flags = 0;
446
447 // Create the device.
448 UINT createDeviceFlags = 0;
449 #if !defined(NDEBUG)
450 createDeviceFlags |= D3D10_CREATE_DEVICE_DEBUG;
451 #endif
452 RETURN_IF_FAILED(
453 D3D10CreateDeviceAndSwapChain(
454 0, //default adapter
455 D3D10_DRIVER_TYPE_HARDWARE,
456 0, // no software device
457 createDeviceFlags,
458 D3D10_SDK_VERSION,
459 &sd,
460 swap_chain_.Receive(),
461 device_.Receive()));
462 }
463
464 void CompositorWin::LoadEffects() {
465 DWORD shader_flags = D3D10_SHADER_ENABLE_STRICTNESS;
466 #if !defined(NDEBUG)
467 shader_flags |= D3D10_SHADER_DEBUG | D3D10_SHADER_SKIP_OPTIMIZATION;
468 #endif
469 ScopedComPtr<ID3D10Blob> compilation_errors;
470 const base::StringPiece& fx_data = ResourceBundle::GetSharedInstance().
471 GetRawDataResource(IDR_COMPOSITOR_FX);
472 DCHECK(!fx_data.empty());
473 RETURN_IF_FAILED(
474 D3DX10CreateEffectFromMemory(
475 fx_data.data(), fx_data.size(), "compositor.fx", NULL, NULL,
476 "fx_4_0", shader_flags, 0, device_.get(), NULL, NULL, fx_.Receive(),
477 compilation_errors.Receive(), NULL));
478 technique_ = fx_->GetTechniqueByName("ViewTech");
479 DCHECK(technique_);
480 }
481
482 void CompositorWin::InitVertexLayout() {
483 D3D10_INPUT_ELEMENT_DESC vertex_desc[] = {
484 { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
485 D3D10_INPUT_PER_VERTEX_DATA, 0 },
486 { "TEXC", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12,
487 D3D10_INPUT_PER_VERTEX_DATA, 0 },
488 };
489
490 // Create the input layout
491 D3D10_PASS_DESC pass_desc;
492 RETURN_IF_FAILED(technique_->GetPassByIndex(0)->GetDesc(&pass_desc));
493 RETURN_IF_FAILED(
494 device_->CreateInputLayout(vertex_desc, ARRAYSIZE_UNSAFE(vertex_desc),
495 pass_desc.pIAInputSignature,
496 pass_desc.IAInputSignatureSize,
497 vertex_layout_.Receive()));
498 }
499
500 void CompositorWin::Resize(const gfx::Rect& bounds) {
501 render_target_view_ = NULL;
502 depth_stencil_buffer_ = NULL;
503 depth_stencil_view_ = NULL;
504
505 // Resize the swap chain and recreate the render target view.
506 RETURN_IF_FAILED(swap_chain_->ResizeBuffers(
507 1, bounds.width(), bounds.height(), DXGI_FORMAT_R8G8B8A8_UNORM, 0));
508 ScopedComPtr<ID3D10Texture2D> back_buffer;
509 RETURN_IF_FAILED(swap_chain_->GetBuffer(
510 0, __uuidof(ID3D10Texture2D),
511 reinterpret_cast<void**>(back_buffer.Receive())));
512 RETURN_IF_FAILED(device_->CreateRenderTargetView(
513 back_buffer.get(), 0, render_target_view_.Receive()));
514
515 // Create the depth/stencil buffer and view.
516 D3D10_TEXTURE2D_DESC depth_stencil_desc;
517 depth_stencil_desc.Width = bounds.width();
518 depth_stencil_desc.Height = bounds.height();
519 depth_stencil_desc.MipLevels = 1;
520 depth_stencil_desc.ArraySize = 1;
521 depth_stencil_desc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
522 depth_stencil_desc.SampleDesc.Count = 1; // multisampling must match
523 depth_stencil_desc.SampleDesc.Quality = 0; // swap chain values.
524 depth_stencil_desc.Usage = D3D10_USAGE_DEFAULT;
525 depth_stencil_desc.BindFlags = D3D10_BIND_DEPTH_STENCIL;
526 depth_stencil_desc.CPUAccessFlags = 0;
527 depth_stencil_desc.MiscFlags = 0;
528
529 RETURN_IF_FAILED(device_->CreateTexture2D(&depth_stencil_desc, 0,
530 depth_stencil_buffer_.Receive()));
531 RETURN_IF_FAILED(device_->CreateDepthStencilView(
532 depth_stencil_buffer_.get(), 0,
533 depth_stencil_view_.Receive()));
534
535
536 // Bind the render target view and depth/stencil view to the pipeline.
537 ID3D10RenderTargetView* target_view = render_target_view_.get();
538 device_->OMSetRenderTargets(1, &target_view, depth_stencil_view_.get());
539
540 // Set the viewport transform.
541 D3D10_VIEWPORT vp;
542 vp.TopLeftX = bounds.x();
543 vp.TopLeftY = bounds.y();
544 vp.Width = bounds.width();
545 vp.Height = bounds.height();
546 vp.MinDepth = 0.0f;
547 vp.MaxDepth = 1.0f;
548
549 device_->RSSetViewports(1, &vp);
550
551 last_bounds_ = bounds;
552 }
553
554 } // namespace
555
556 // static
557 Compositor* Compositor::Create(gfx::AcceleratedWidget widget) {
558 CompositorWin* compositor = new CompositorWin(widget);
559 compositor->Init();
560 return compositor;
561 }
562
563 } // namespace ui
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698