1 // Copyright (c) 2013 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.
5 #include "content/browser/renderer_host/compositing_iosurface_transformer_mac.h"
7 #include <OpenGL/CGLCurrent.h>
8 #include <OpenGL/CGLRenderers.h>
9 #include <OpenGL/CGLTypes.h>
10 #include <OpenGL/OpenGL.h>
11 #include <OpenGL/gl.h>
12 #include <OpenGL/glu.h>
19 #include "base/logging.h"
20 #include "base/memory/scoped_ptr.h"
21 #include "content/browser/renderer_host/compositing_iosurface_shader_programs_mac.h"
22 #include "media/base/yuv_convert.h"
23 #include "testing/gtest/include/gtest/gtest.h"
24 #include "third_party/skia/include/core/SkBitmap.h"
25 #include "third_party/skia/include/core/SkCanvas.h"
26 #include "third_party/skia/include/core/SkColor.h"
27 #include "third_party/skia/include/core/SkRect.h"
28 #include "ui/gfx/rect.h"
32 #define EXPECT_NO_GL_ERROR(stmt) \
35 const GLenum error_code = glGetError(); \
36 EXPECT_TRUE(GL_NO_ERROR == error_code) \
37 << "for error code " << error_code \
38 << ": " << gluErrorString(error_code); \
43 const GLenum kGLTextureTarget
= GL_TEXTURE_RECTANGLE_ARB
;
45 enum RendererRestriction
{
47 RESTRICTION_SOFTWARE_ONLY
,
48 RESTRICTION_HARDWARE_ONLY
51 bool InitializeGLContext(CGLContextObj
* context
,
52 RendererRestriction restriction
) {
53 std::vector
<CGLPixelFormatAttribute
> attribs
;
54 // Select off-screen renderers only.
55 attribs
.push_back(kCGLPFAOffScreen
);
56 // By default, the library will prefer hardware-accelerated renderers, but
57 // falls back on the software ones if necessary. However, there are use cases
58 // where we want to force a restriction (e.g., benchmarking performance).
59 if (restriction
== RESTRICTION_SOFTWARE_ONLY
) {
60 attribs
.push_back(kCGLPFARendererID
);
61 attribs
.push_back(static_cast<CGLPixelFormatAttribute
>(
62 kCGLRendererGenericFloatID
));
63 } else if (restriction
== RESTRICTION_HARDWARE_ONLY
) {
64 attribs
.push_back(kCGLPFAAccelerated
);
66 attribs
.push_back(static_cast<CGLPixelFormatAttribute
>(0));
68 CGLPixelFormatObj format
;
69 GLint num_pixel_formats
= 0;
71 if (CGLChoosePixelFormat(&attribs
.front(), &format
, &num_pixel_formats
) !=
73 LOG(ERROR
) << "Error choosing pixel format.";
76 if (success
&& num_pixel_formats
<= 0) {
77 LOG(ERROR
) << "num_pixel_formats <= 0; actual value is "
81 if (success
&& CGLCreateContext(format
, NULL
, context
) != kCGLNoError
) {
82 LOG(ERROR
) << "Error creating context.";
85 CGLDestroyPixelFormat(format
);
89 // Returns a decent test pattern for testing all of: 1) orientation, 2) scaling,
90 // 3) color space conversion (e.g., 4 pixels --> one U or V pixel), and 4)
91 // texture alignment/processing. Example 32x32 bitmap:
93 // GGGGGGGGGGGGGGGGRRBBRRBBRRBBRRBB
94 // GGGGGGGGGGGGGGGGRRBBRRBBRRBBRRBB
95 // GGGGGGGGGGGGGGGGYYCCYYCCYYCCYYCC
96 // GGGGGGGGGGGGGGGGYYCCYYCCYYCCYYCC
97 // GGGGGGGGGGGGGGGGRRBBRRBBRRBBRRBB
98 // GGGGGGGGGGGGGGGGRRBBRRBBRRBBRRBB
99 // GGGGGGGGGGGGGGGGYYCCYYCCYYCCYYCC
100 // GGGGGGGGGGGGGGGGYYCCYYCCYYCCYYCC
101 // RRBBRRBBRRBBRRBBRRBBRRBBRRBBRRBB
102 // RRBBRRBBRRBBRRBBRRBBRRBBRRBBRRBB
103 // YYCCYYCCYYCCYYCCYYCCYYCCYYCCYYCC
104 // YYCCYYCCYYCCYYCCYYCCYYCCYYCCYYCC
105 // RRBBRRBBRRBBRRBBRRBBRRBBRRBBRRBB
106 // RRBBRRBBRRBBRRBBRRBBRRBBRRBBRRBB
107 // YYCCYYCCYYCCYYCCYYCCYYCCYYCCYYCC
108 // YYCCYYCCYYCCYYCCYYCCYYCCYYCCYYCC
110 // Key: G = Gray, R = Red, B = Blue, Y = Yellow, C = Cyan
111 SkBitmap
GenerateTestPatternBitmap(const gfx::Size
& size
) {
113 bitmap
.setConfig(SkBitmap::kARGB_8888_Config
, size
.width(), size
.height());
114 CHECK(bitmap
.allocPixels());
115 SkAutoLockPixels
lock_bitmap(bitmap
);
116 bitmap
.eraseColor(SK_ColorGRAY
);
117 for (int y
= 0; y
< size
.height(); ++y
) {
118 uint32_t* p
= bitmap
.getAddr32(0, y
);
119 for (int x
= 0; x
< size
.width(); ++x
, ++p
) {
120 if ((x
< (size
.width() / 2)) && (y
< (size
.height() / 2)))
121 continue; // Leave upper-left quadrant gray.
122 *p
= SkColorSetARGB(255,
125 x
% 4 < 2 ? 0 : 255);
131 // Creates a new texture consisting of the given |bitmap|.
132 GLuint
CreateTextureWithImage(const SkBitmap
& bitmap
) {
134 EXPECT_NO_GL_ERROR(glGenTextures(1, &texture
));
135 EXPECT_NO_GL_ERROR(glBindTexture(kGLTextureTarget
, texture
));
137 SkAutoLockPixels
lock_bitmap(bitmap
);
138 EXPECT_NO_GL_ERROR(glTexImage2D(
139 kGLTextureTarget
, 0, GL_RGBA
, bitmap
.width(), bitmap
.height(), 0,
140 GL_BGRA
, GL_UNSIGNED_INT_8_8_8_8_REV
, bitmap
.getPixels()));
142 glBindTexture(kGLTextureTarget
, 0);
146 // Read back a texture from the GPU, returning the image data as an SkBitmap.
147 SkBitmap
ReadBackTexture(GLuint texture
, const gfx::Size
& size
, GLenum format
) {
149 result
.setConfig(SkBitmap::kARGB_8888_Config
, size
.width(), size
.height());
150 CHECK(result
.allocPixels());
153 EXPECT_NO_GL_ERROR(glGenFramebuffersEXT(1, &frame_buffer
));
155 glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT
, frame_buffer
));
156 EXPECT_NO_GL_ERROR(glFramebufferTexture2DEXT(
157 GL_READ_FRAMEBUFFER_EXT
, GL_COLOR_ATTACHMENT0_EXT
, kGLTextureTarget
,
159 DCHECK(glCheckFramebufferStatusEXT(GL_READ_FRAMEBUFFER_EXT
) ==
160 GL_FRAMEBUFFER_COMPLETE_EXT
);
163 SkAutoLockPixels
lock_result(result
);
164 EXPECT_NO_GL_ERROR(glReadPixels(
165 0, 0, size
.width(), size
.height(), format
, GL_UNSIGNED_INT_8_8_8_8_REV
,
166 result
.getPixels()));
169 EXPECT_NO_GL_ERROR(glDeleteFramebuffersEXT(1, &frame_buffer
));
174 // Returns the |src_rect| region of |src| scaled to |to_size| by drawing on a
175 // Skia canvas, and using bilinear filtering (just like a GPU would).
176 SkBitmap
ScaleBitmapWithSkia(const SkBitmap
& src
,
177 const gfx::Rect
& src_rect
,
178 const gfx::Size
& to_size
) {
179 SkBitmap cropped_src
;
180 if (src_rect
== gfx::Rect(0, 0, src
.width(), src
.height())) {
183 CHECK(src
.extractSubset(
185 SkIRect::MakeXYWH(src_rect
.x(), src_rect
.y(),
186 src_rect
.width(), src_rect
.height())));
190 result
.setConfig(cropped_src
.config(), to_size
.width(), to_size
.height());
191 CHECK(result
.allocPixels());
193 SkCanvas
canvas(result
);
194 canvas
.scale(static_cast<double>(result
.width()) / cropped_src
.width(),
195 static_cast<double>(result
.height()) / cropped_src
.height());
197 paint
.setFilterBitmap(true); // Use bilinear filtering.
198 canvas
.drawBitmap(cropped_src
, 0, 0, &paint
);
203 // The maximum value by which a pixel value may deviate from the expected value
204 // before considering it "significantly different." This is meant to account
205 // for the slight differences in filtering techniques used between the various
206 // GPUs and software implementations.
207 const int kDifferenceThreshold
= 16;
209 // Returns the number of pixels significantly different between |expected| and
211 int ImageDifference(const SkBitmap
& expected
, const SkBitmap
& actual
) {
212 SkAutoLockPixels
lock_expected(expected
);
213 SkAutoLockPixels
lock_actual(actual
);
215 // Sanity-check assumed image properties.
216 DCHECK_EQ(expected
.width(), actual
.width());
217 DCHECK_EQ(expected
.height(), actual
.height());
218 DCHECK_EQ(SkBitmap::kARGB_8888_Config
, expected
.config());
219 DCHECK_EQ(SkBitmap::kARGB_8888_Config
, actual
.config());
221 // Compare both images.
222 int num_pixels_different
= 0;
223 for (int y
= 0; y
< expected
.height(); ++y
) {
224 const uint32_t* p
= expected
.getAddr32(0, y
);
225 const uint32_t* q
= actual
.getAddr32(0, y
);
226 for (int x
= 0; x
< expected
.width(); ++x
, ++p
, ++q
) {
227 if (abs(static_cast<int>(SkColorGetR(*p
)) -
228 static_cast<int>(SkColorGetR(*q
))) > kDifferenceThreshold
||
229 abs(static_cast<int>(SkColorGetG(*p
)) -
230 static_cast<int>(SkColorGetG(*q
))) > kDifferenceThreshold
||
231 abs(static_cast<int>(SkColorGetB(*p
)) -
232 static_cast<int>(SkColorGetB(*q
))) > kDifferenceThreshold
) {
233 ++num_pixels_different
;
238 return num_pixels_different
;
241 // Returns the number of pixels significantly different between |expected| and
242 // |actual|. It is understood that |actual| contains 4-byte quads, and so we
243 // may need to be ignoring a mod-4 number of pixels at the end of each of its
245 int ImagePlaneDifference(const uint8
* expected
, const SkBitmap
& actual
,
246 const gfx::Size
& dst_size
) {
247 SkAutoLockPixels
actual_lock(actual
);
249 int num_pixels_different
= 0;
250 for (int y
= 0; y
< dst_size
.height(); ++y
) {
251 const uint8
* p
= expected
+ y
* dst_size
.width();
252 const uint8
* const p_end
= p
+ dst_size
.width();
254 reinterpret_cast<uint8
*>(actual
.getPixels()) + y
* actual
.rowBytes();
255 for (; p
< p_end
; ++p
, ++q
) {
256 if (abs(static_cast<int>(*p
) - static_cast<int>(*q
)) >
257 kDifferenceThreshold
) {
258 ++num_pixels_different
;
263 return num_pixels_different
;
268 // Note: All tests fixtures operate within an off-screen OpenGL context.
269 class CompositingIOSurfaceTransformerTest
: public testing::Test
{
271 CompositingIOSurfaceTransformerTest() {
272 // TODO(miu): Try to use RESTRICTION_NONE to speed up the execution time of
273 // unit tests, once it's established that the trybots and buildbots behave
274 // well when using the GPU.
275 CHECK(InitializeGLContext(&context_
, RESTRICTION_SOFTWARE_ONLY
));
276 CGLSetCurrentContext(context_
);
277 shader_program_cache_
.reset(new CompositingIOSurfaceShaderPrograms());
278 transformer_
.reset(new CompositingIOSurfaceTransformer(
279 kGLTextureTarget
, false, shader_program_cache_
.get()));
282 virtual ~CompositingIOSurfaceTransformerTest() {
283 transformer_
->ReleaseCachedGLObjects();
284 shader_program_cache_
->Reset();
285 CGLSetCurrentContext(NULL
);
286 CGLDestroyContext(context_
);
290 void RunResizeTest(const SkBitmap
& src_bitmap
, const gfx::Rect
& src_rect
,
291 const gfx::Size
& dst_size
) {
292 SCOPED_TRACE(::testing::Message()
293 << "src_rect=(" << src_rect
.x() << ',' << src_rect
.y()
294 << ")x[" << src_rect
.width() << 'x' << src_rect
.height()
295 << "]; dst_size=[" << dst_size
.width() << 'x'
296 << dst_size
.height() << ']');
298 // Do the scale operation on the GPU.
299 const GLuint original_texture
= CreateTextureWithImage(src_bitmap
);
300 ASSERT_NE(0u, original_texture
);
301 GLuint scaled_texture
= 0u;
302 ASSERT_TRUE(transformer_
->ResizeBilinear(
303 original_texture
, src_rect
, dst_size
, &scaled_texture
));
304 EXPECT_NE(0u, scaled_texture
);
305 CGLFlushDrawable(context_
); // Account for some buggy driver impls.
306 const SkBitmap result_bitmap
=
307 ReadBackTexture(scaled_texture
, dst_size
, GL_BGRA
);
308 EXPECT_NO_GL_ERROR(glDeleteTextures(1, &original_texture
));
310 // Compare the image read back to the version produced by a known-working
311 // software implementation. Allow up to 2 lines of mismatch due to how
312 // implementations disagree on resolving the processing of edges.
313 const SkBitmap expected_bitmap
=
314 ScaleBitmapWithSkia(src_bitmap
, src_rect
, dst_size
);
315 EXPECT_GE(std::max(expected_bitmap
.width(), expected_bitmap
.height()) * 2,
316 ImageDifference(expected_bitmap
, result_bitmap
));
319 void RunTransformRGBToYV12Test(
320 const SkBitmap
& src_bitmap
, const gfx::Rect
& src_rect
,
321 const gfx::Size
& dst_size
) {
322 SCOPED_TRACE(::testing::Message()
323 << "src_rect=(" << src_rect
.x() << ',' << src_rect
.y()
324 << ")x[" << src_rect
.width() << 'x' << src_rect
.height()
325 << "]; dst_size=[" << dst_size
.width() << 'x'
326 << dst_size
.height() << ']');
328 // Perform the RGB to YV12 conversion.
329 const GLuint original_texture
= CreateTextureWithImage(src_bitmap
);
330 ASSERT_NE(0u, original_texture
);
331 GLuint texture_y
= 0u;
332 GLuint texture_u
= 0u;
333 GLuint texture_v
= 0u;
334 gfx::Size packed_y_size
;
335 gfx::Size packed_uv_size
;
336 ASSERT_TRUE(transformer_
->TransformRGBToYV12(
337 original_texture
, src_rect
, dst_size
,
338 &texture_y
, &texture_u
, &texture_v
, &packed_y_size
, &packed_uv_size
));
339 EXPECT_NE(0u, texture_y
);
340 EXPECT_NE(0u, texture_u
);
341 EXPECT_NE(0u, texture_v
);
342 EXPECT_FALSE(packed_y_size
.IsEmpty());
343 EXPECT_FALSE(packed_uv_size
.IsEmpty());
344 EXPECT_NO_GL_ERROR(glDeleteTextures(1, &original_texture
));
346 // Read-back the texture for each plane.
347 CGLFlushDrawable(context_
); // Account for some buggy driver impls.
348 const GLenum format
= shader_program_cache_
->rgb_to_yv12_output_format();
349 const SkBitmap result_y_bitmap
=
350 ReadBackTexture(texture_y
, packed_y_size
, format
);
351 const SkBitmap result_u_bitmap
=
352 ReadBackTexture(texture_u
, packed_uv_size
, format
);
353 const SkBitmap result_v_bitmap
=
354 ReadBackTexture(texture_v
, packed_uv_size
, format
);
356 // Compare the Y, U, and V planes read-back to the version produced by a
357 // known-working software implementation. Allow up to 2 lines of mismatch
358 // due to how implementations disagree on resolving the processing of edges.
359 const SkBitmap expected_bitmap
=
360 ScaleBitmapWithSkia(src_bitmap
, src_rect
, dst_size
);
361 const gfx::Size
dst_uv_size(
362 (dst_size
.width() + 1) / 2, (dst_size
.height() + 1) / 2);
363 scoped_ptr
<uint8
[]> expected_y_plane(
364 new uint8
[dst_size
.width() * dst_size
.height()]);
365 scoped_ptr
<uint8
[]> expected_u_plane(
366 new uint8
[dst_uv_size
.width() * dst_uv_size
.height()]);
367 scoped_ptr
<uint8
[]> expected_v_plane(
368 new uint8
[dst_uv_size
.width() * dst_uv_size
.height()]);
370 SkAutoLockPixels
src_bitmap_lock(expected_bitmap
);
371 media::ConvertRGB32ToYUV(
372 reinterpret_cast<const uint8
*>(expected_bitmap
.getPixels()),
373 expected_y_plane
.get(), expected_u_plane
.get(),
374 expected_v_plane
.get(),
375 expected_bitmap
.width(), expected_bitmap
.height(),
376 expected_bitmap
.rowBytes(),
377 dst_size
.width(), (dst_size
.width() + 1) / 2);
380 std::max(expected_bitmap
.width(), expected_bitmap
.height()) * 2,
381 ImagePlaneDifference(expected_y_plane
.get(), result_y_bitmap
, dst_size
))
382 << " for RGB --> Y Plane";
384 std::max(expected_bitmap
.width(), expected_bitmap
.height()),
385 ImagePlaneDifference(expected_u_plane
.get(), result_u_bitmap
,
387 << " for RGB --> U Plane";
389 std::max(expected_bitmap
.width(), expected_bitmap
.height()),
390 ImagePlaneDifference(expected_v_plane
.get(), result_v_bitmap
,
392 << " for RGB --> V Plane";
395 CompositingIOSurfaceShaderPrograms
* shader_program_cache() const {
396 return shader_program_cache_
.get();
400 CGLContextObj context_
;
401 scoped_ptr
<CompositingIOSurfaceShaderPrograms
> shader_program_cache_
;
402 scoped_ptr
<CompositingIOSurfaceTransformer
> transformer_
;
405 DISALLOW_COPY_AND_ASSIGN(CompositingIOSurfaceTransformerTest
);
408 TEST_F(CompositingIOSurfaceTransformerTest
, ShaderProgramsCompileAndLink
) {
409 // Attempt to use each program, binding its required uniform variables.
410 EXPECT_NO_GL_ERROR(shader_program_cache()->UseBlitProgram());
411 EXPECT_NO_GL_ERROR(shader_program_cache()->UseSolidWhiteProgram());
412 EXPECT_NO_GL_ERROR(shader_program_cache()->UseRGBToYV12Program(1, 1.0f
));
413 EXPECT_NO_GL_ERROR(shader_program_cache()->UseRGBToYV12Program(2, 1.0f
));
415 EXPECT_NO_GL_ERROR(glUseProgram(0));
420 const struct TestParameters
{
425 } kTestParameters
[] = {
426 // Test 1:1 copies, but exposing varying pixel packing configurations.
437 // Even-size, one or both dimensions upscaled.
438 { 32, 32, 64, 32 }, { 32, 32, 32, 64 }, { 32, 32, 64, 64 },
439 // Even-size, one or both dimensions downscaled by 2X.
440 { 32, 32, 16, 32 }, { 32, 32, 32, 16 }, { 32, 32, 16, 16 },
441 // Even-size, one or both dimensions downscaled by 1 pixel.
442 { 32, 32, 31, 32 }, { 32, 32, 32, 31 }, { 32, 32, 31, 31 },
443 // Even-size, one or both dimensions downscaled by 2 pixels.
444 { 32, 32, 30, 32 }, { 32, 32, 32, 30 }, { 32, 32, 30, 30 },
445 // Even-size, one or both dimensions downscaled by 3 pixels.
446 { 32, 32, 29, 32 }, { 32, 32, 32, 29 }, { 32, 32, 29, 29 },
448 // Odd-size, one or both dimensions upscaled.
449 { 33, 33, 66, 33 }, { 33, 33, 33, 66 }, { 33, 33, 66, 66 },
450 // Odd-size, one or both dimensions downscaled by 2X.
451 { 33, 33, 16, 33 }, { 33, 33, 33, 16 }, { 33, 33, 16, 16 },
452 // Odd-size, one or both dimensions downscaled by 1 pixel.
453 { 33, 33, 32, 33 }, { 33, 33, 33, 32 }, { 33, 33, 32, 32 },
454 // Odd-size, one or both dimensions downscaled by 2 pixels.
455 { 33, 33, 31, 33 }, { 33, 33, 33, 31 }, { 33, 33, 31, 31 },
456 // Odd-size, one or both dimensions downscaled by 3 pixels.
457 { 33, 33, 30, 33 }, { 33, 33, 33, 30 }, { 33, 33, 30, 30 },
462 TEST_F(CompositingIOSurfaceTransformerTest
, ResizesTexturesCorrectly
) {
463 for (size_t i
= 0; i
< arraysize(kTestParameters
); ++i
) {
464 SCOPED_TRACE(::testing::Message() << "kTestParameters[" << i
<< ']');
466 const TestParameters
& params
= kTestParameters
[i
];
467 const gfx::Size
src_size(params
.src_width
, params
.src_height
);
468 const gfx::Size
dst_size(params
.scaled_width
, params
.scaled_height
);
469 const SkBitmap src_bitmap
= GenerateTestPatternBitmap(src_size
);
471 // Full texture resize test.
472 RunResizeTest(src_bitmap
, gfx::Rect(src_size
), dst_size
);
473 // Subrect resize test: missing top row in source.
474 RunResizeTest(src_bitmap
,
475 gfx::Rect(0, 1, params
.src_width
, params
.src_height
- 1),
477 // Subrect resize test: missing left column in source.
478 RunResizeTest(src_bitmap
,
479 gfx::Rect(1, 0, params
.src_width
- 1, params
.src_height
),
481 // Subrect resize test: missing top+bottom rows, and left column in source.
482 RunResizeTest(src_bitmap
,
483 gfx::Rect(1, 1, params
.src_width
- 1, params
.src_height
- 2),
485 // Subrect resize test: missing top row, and left+right columns in source.
486 RunResizeTest(src_bitmap
,
487 gfx::Rect(1, 1, params
.src_width
- 2, params
.src_height
- 1),
492 TEST_F(CompositingIOSurfaceTransformerTest
, TransformsRGBToYV12
) {
493 static const GLenum kOutputFormats
[] = { GL_BGRA
, GL_RGBA
};
495 for (size_t i
= 0; i
< arraysize(kOutputFormats
); ++i
) {
496 SCOPED_TRACE(::testing::Message() << "kOutputFormats[" << i
<< ']');
498 shader_program_cache()->SetOutputFormatForTesting(kOutputFormats
[i
]);
500 for (size_t j
= 0; j
< arraysize(kTestParameters
); ++j
) {
501 SCOPED_TRACE(::testing::Message() << "kTestParameters[" << j
<< ']');
503 const TestParameters
& params
= kTestParameters
[j
];
504 const gfx::Size
src_size(params
.src_width
, params
.src_height
);
505 const gfx::Size
dst_size(params
.scaled_width
, params
.scaled_height
);
506 const SkBitmap src_bitmap
= GenerateTestPatternBitmap(src_size
);
508 // Full texture resize test.
509 RunTransformRGBToYV12Test(src_bitmap
, gfx::Rect(src_size
), dst_size
);
510 // Subrect resize test: missing top row in source.
511 RunTransformRGBToYV12Test(
512 src_bitmap
, gfx::Rect(0, 1, params
.src_width
, params
.src_height
- 1),
514 // Subrect resize test: missing left column in source.
515 RunTransformRGBToYV12Test(
516 src_bitmap
, gfx::Rect(1, 0, params
.src_width
- 1, params
.src_height
),
518 // Subrect resize test: missing top+bottom rows, and left column in
520 RunTransformRGBToYV12Test(
522 gfx::Rect(1, 1, params
.src_width
- 1, params
.src_height
- 2),
524 // Subrect resize test: missing top row, and left+right columns in source.
525 RunTransformRGBToYV12Test(
527 gfx::Rect(1, 1, params
.src_width
- 2, params
.src_height
- 1),
533 } // namespace content