1 // Copyright 2018 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/android/android_surface_control_compat.h"
6
7 #include <android/data_space.h>
8 #include <dlfcn.h>
9
10 #include "base/android/build_info.h"
11 #include "base/atomic_sequence_num.h"
12 #include "base/bind.h"
13 #include "base/debug/crash_logging.h"
14 #include "base/logging.h"
15 #include "base/memory/ptr_util.h"
16 #include "base/no_destructor.h"
17 #include "base/strings/string_number_conversions.h"
18 #include "base/system/sys_info.h"
19 #include "base/trace_event/trace_event.h"
20 #include "ui/gfx/color_space.h"
21
22 extern "C" {
23 typedef struct ASurfaceTransactionStats ASurfaceTransactionStats;
24 typedef void (*ASurfaceTransaction_OnComplete)(void* context,
25 ASurfaceTransactionStats* stats);
26
27 // ASurface
28 using pASurfaceControl_createFromWindow =
29 ASurfaceControl* (*)(ANativeWindow* parent, const char* name);
30 using pASurfaceControl_create = ASurfaceControl* (*)(ASurfaceControl* parent,
31 const char* name);
32 using pASurfaceControl_release = void (*)(ASurfaceControl*);
33
34 // ASurfaceTransaction enums
35 enum {
36 ASURFACE_TRANSACTION_TRANSPARENCY_TRANSPARENT = 0,
37 ASURFACE_TRANSACTION_TRANSPARENCY_TRANSLUCENT = 1,
38 ASURFACE_TRANSACTION_TRANSPARENCY_OPAQUE = 2,
39 };
40
41 // ANativeWindow_FrameRateCompatibility enums
42 enum {
43 ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT = 0,
44 ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_FIXED_SOURCE = 1
45 };
46
47 // ASurfaceTransaction
48 using pASurfaceTransaction_create = ASurfaceTransaction* (*)(void);
49 using pASurfaceTransaction_delete = void (*)(ASurfaceTransaction*);
50 using pASurfaceTransaction_apply = int64_t (*)(ASurfaceTransaction*);
51 using pASurfaceTransaction_setOnComplete =
52 void (*)(ASurfaceTransaction*, void* ctx, ASurfaceTransaction_OnComplete);
53 using pASurfaceTransaction_setVisibility = void (*)(ASurfaceTransaction*,
54 ASurfaceControl*,
55 int8_t visibility);
56 using pASurfaceTransaction_setZOrder =
57 void (*)(ASurfaceTransaction* transaction, ASurfaceControl*, int32_t z);
58 using pASurfaceTransaction_setBuffer =
59 void (*)(ASurfaceTransaction* transaction,
60 ASurfaceControl*,
61 AHardwareBuffer*,
62 int32_t fence_fd);
63 using pASurfaceTransaction_setGeometry =
64 void (*)(ASurfaceTransaction* transaction,
65 ASurfaceControl* surface,
66 const ARect& src,
67 const ARect& dst,
68 int32_t transform);
69 using pASurfaceTransaction_setBufferTransparency =
70 void (*)(ASurfaceTransaction* transaction,
71 ASurfaceControl* surface,
72 int8_t transparency);
73 using pASurfaceTransaction_setDamageRegion =
74 void (*)(ASurfaceTransaction* transaction,
75 ASurfaceControl* surface,
76 const ARect rects[],
77 uint32_t count);
78 using pASurfaceTransaction_setBufferDataSpace =
79 void (*)(ASurfaceTransaction* transaction,
80 ASurfaceControl* surface,
81 uint64_t data_space);
82 using pASurfaceTransaction_setFrameRate =
83 void (*)(ASurfaceTransaction* transaction,
84 ASurfaceControl* surface_control,
85 float frameRate,
86 int8_t compatibility);
87
88 // ASurfaceTransactionStats
89 using pASurfaceTransactionStats_getPresentFenceFd =
90 int (*)(ASurfaceTransactionStats* stats);
91 using pASurfaceTransactionStats_getLatchTime =
92 int64_t (*)(ASurfaceTransactionStats* stats);
93 using pASurfaceTransactionStats_getASurfaceControls =
94 void (*)(ASurfaceTransactionStats* stats,
95 ASurfaceControl*** surface_controls,
96 size_t* size);
97 using pASurfaceTransactionStats_releaseASurfaceControls =
98 void (*)(ASurfaceControl** surface_controls);
99 using pASurfaceTransactionStats_getPreviousReleaseFenceFd =
100 int (*)(ASurfaceTransactionStats* stats, ASurfaceControl* surface_control);
101 }
102
103 namespace gfx {
104 namespace {
105
106 base::AtomicSequenceNumber g_next_transaction_id;
107
108 uint64_t g_agb_required_usage_bits = AHARDWAREBUFFER_USAGE_COMPOSER_OVERLAY;
109
110 #define LOAD_FUNCTION(lib, func) \
111 do { \
112 func##Fn = reinterpret_cast<p##func>(dlsym(lib, #func)); \
113 if (!func##Fn) { \
114 supported = false; \
115 LOG(ERROR) << "Unable to load function " << #func; \
116 } \
117 } while (0)
118
119 #define LOAD_FUNCTION_MAYBE(lib, func) \
120 do { \
121 func##Fn = reinterpret_cast<p##func>(dlsym(lib, #func)); \
122 } while (0)
123
124 struct SurfaceControlMethods {
125 public:
Getgfx::__anon7b31ec160311::SurfaceControlMethods126 static const SurfaceControlMethods& Get() {
127 static const base::NoDestructor<SurfaceControlMethods> instance;
128 return *instance;
129 }
130
SurfaceControlMethodsgfx::__anon7b31ec160311::SurfaceControlMethods131 SurfaceControlMethods() {
132 void* main_dl_handle = dlopen("libandroid.so", RTLD_NOW);
133 if (!main_dl_handle) {
134 LOG(ERROR) << "Couldnt load android so";
135 supported = false;
136 return;
137 }
138
139 LOAD_FUNCTION(main_dl_handle, ASurfaceControl_createFromWindow);
140 LOAD_FUNCTION(main_dl_handle, ASurfaceControl_create);
141 LOAD_FUNCTION(main_dl_handle, ASurfaceControl_release);
142
143 LOAD_FUNCTION(main_dl_handle, ASurfaceTransaction_create);
144 LOAD_FUNCTION(main_dl_handle, ASurfaceTransaction_delete);
145 LOAD_FUNCTION(main_dl_handle, ASurfaceTransaction_apply);
146 LOAD_FUNCTION(main_dl_handle, ASurfaceTransaction_setOnComplete);
147 LOAD_FUNCTION(main_dl_handle, ASurfaceTransaction_setVisibility);
148 LOAD_FUNCTION(main_dl_handle, ASurfaceTransaction_setZOrder);
149 LOAD_FUNCTION(main_dl_handle, ASurfaceTransaction_setBuffer);
150 LOAD_FUNCTION(main_dl_handle, ASurfaceTransaction_setGeometry);
151 LOAD_FUNCTION(main_dl_handle, ASurfaceTransaction_setBufferTransparency);
152 LOAD_FUNCTION(main_dl_handle, ASurfaceTransaction_setDamageRegion);
153 LOAD_FUNCTION(main_dl_handle, ASurfaceTransaction_setBufferDataSpace);
154 LOAD_FUNCTION_MAYBE(main_dl_handle, ASurfaceTransaction_setFrameRate);
155
156 LOAD_FUNCTION(main_dl_handle, ASurfaceTransactionStats_getPresentFenceFd);
157 LOAD_FUNCTION(main_dl_handle, ASurfaceTransactionStats_getLatchTime);
158 LOAD_FUNCTION(main_dl_handle, ASurfaceTransactionStats_getASurfaceControls);
159 LOAD_FUNCTION(main_dl_handle,
160 ASurfaceTransactionStats_releaseASurfaceControls);
161 LOAD_FUNCTION(main_dl_handle,
162 ASurfaceTransactionStats_getPreviousReleaseFenceFd);
163 }
164
165 ~SurfaceControlMethods() = default;
166
167 bool supported = true;
168 // Surface methods.
169 pASurfaceControl_createFromWindow ASurfaceControl_createFromWindowFn;
170 pASurfaceControl_create ASurfaceControl_createFn;
171 pASurfaceControl_release ASurfaceControl_releaseFn;
172
173 // Transaction methods.
174 pASurfaceTransaction_create ASurfaceTransaction_createFn;
175 pASurfaceTransaction_delete ASurfaceTransaction_deleteFn;
176 pASurfaceTransaction_apply ASurfaceTransaction_applyFn;
177 pASurfaceTransaction_setOnComplete ASurfaceTransaction_setOnCompleteFn;
178 pASurfaceTransaction_setVisibility ASurfaceTransaction_setVisibilityFn;
179 pASurfaceTransaction_setZOrder ASurfaceTransaction_setZOrderFn;
180 pASurfaceTransaction_setBuffer ASurfaceTransaction_setBufferFn;
181 pASurfaceTransaction_setGeometry ASurfaceTransaction_setGeometryFn;
182 pASurfaceTransaction_setBufferTransparency
183 ASurfaceTransaction_setBufferTransparencyFn;
184 pASurfaceTransaction_setDamageRegion ASurfaceTransaction_setDamageRegionFn;
185 pASurfaceTransaction_setBufferDataSpace
186 ASurfaceTransaction_setBufferDataSpaceFn;
187 pASurfaceTransaction_setFrameRate ASurfaceTransaction_setFrameRateFn;
188
189 // TransactionStats methods.
190 pASurfaceTransactionStats_getPresentFenceFd
191 ASurfaceTransactionStats_getPresentFenceFdFn;
192 pASurfaceTransactionStats_getLatchTime
193 ASurfaceTransactionStats_getLatchTimeFn;
194 pASurfaceTransactionStats_getASurfaceControls
195 ASurfaceTransactionStats_getASurfaceControlsFn;
196 pASurfaceTransactionStats_releaseASurfaceControls
197 ASurfaceTransactionStats_releaseASurfaceControlsFn;
198 pASurfaceTransactionStats_getPreviousReleaseFenceFd
199 ASurfaceTransactionStats_getPreviousReleaseFenceFdFn;
200 };
201
RectToARect(const gfx::Rect & rect)202 ARect RectToARect(const gfx::Rect& rect) {
203 return ARect{rect.x(), rect.y(), rect.right(), rect.bottom()};
204 }
205
OverlayTransformToWindowTransform(gfx::OverlayTransform transform)206 int32_t OverlayTransformToWindowTransform(gfx::OverlayTransform transform) {
207 // Note that the gfx::OverlayTransform expresses rotations in anticlockwise
208 // direction while the ANativeWindow rotations are in clockwise direction.
209 switch (transform) {
210 case gfx::OVERLAY_TRANSFORM_INVALID:
211 DCHECK(false) << "Invalid Transform";
212 return ANATIVEWINDOW_TRANSFORM_IDENTITY;
213 case gfx::OVERLAY_TRANSFORM_NONE:
214 return ANATIVEWINDOW_TRANSFORM_IDENTITY;
215 case gfx::OVERLAY_TRANSFORM_FLIP_HORIZONTAL:
216 return ANATIVEWINDOW_TRANSFORM_MIRROR_HORIZONTAL;
217 case gfx::OVERLAY_TRANSFORM_FLIP_VERTICAL:
218 return ANATIVEWINDOW_TRANSFORM_MIRROR_VERTICAL;
219 case gfx::OVERLAY_TRANSFORM_ROTATE_90:
220 return ANATIVEWINDOW_TRANSFORM_ROTATE_270;
221 case gfx::OVERLAY_TRANSFORM_ROTATE_180:
222 return ANATIVEWINDOW_TRANSFORM_ROTATE_180;
223 case gfx::OVERLAY_TRANSFORM_ROTATE_270:
224 return ANATIVEWINDOW_TRANSFORM_ROTATE_90;
225 };
226 NOTREACHED();
227 return ANATIVEWINDOW_TRANSFORM_IDENTITY;
228 }
229
ColorSpaceToADataSpace(const gfx::ColorSpace & color_space)230 uint64_t ColorSpaceToADataSpace(const gfx::ColorSpace& color_space) {
231 if (!color_space.IsValid() || color_space == gfx::ColorSpace::CreateSRGB())
232 return ADATASPACE_SRGB;
233
234 if (color_space == gfx::ColorSpace::CreateSCRGBLinear())
235 return ADATASPACE_SCRGB_LINEAR;
236
237 if (color_space == gfx::ColorSpace::CreateDisplayP3D65())
238 return ADATASPACE_DISPLAY_P3;
239
240 // TODO(khushalsagar): Check if we can support BT2020 using
241 // ADATASPACE_BT2020_PQ.
242 return ADATASPACE_UNKNOWN;
243 }
244
ToTransactionStats(ASurfaceTransactionStats * stats)245 SurfaceControl::TransactionStats ToTransactionStats(
246 ASurfaceTransactionStats* stats) {
247 SurfaceControl::TransactionStats transaction_stats;
248 transaction_stats.present_fence = base::ScopedFD(
249 SurfaceControlMethods::Get().ASurfaceTransactionStats_getPresentFenceFdFn(
250 stats));
251 transaction_stats.latch_time =
252 base::TimeTicks() +
253 base::TimeDelta::FromNanoseconds(
254 SurfaceControlMethods::Get().ASurfaceTransactionStats_getLatchTimeFn(
255 stats));
256 if (transaction_stats.latch_time == base::TimeTicks())
257 transaction_stats.latch_time = base::TimeTicks::Now();
258
259 ASurfaceControl** surface_controls = nullptr;
260 size_t size = 0u;
261 SurfaceControlMethods::Get().ASurfaceTransactionStats_getASurfaceControlsFn(
262 stats, &surface_controls, &size);
263 transaction_stats.surface_stats.resize(size);
264 for (size_t i = 0u; i < size; ++i) {
265 transaction_stats.surface_stats[i].surface = surface_controls[i];
266 int fence_fd = SurfaceControlMethods::Get()
267 .ASurfaceTransactionStats_getPreviousReleaseFenceFdFn(
268 stats, surface_controls[i]);
269 if (fence_fd != -1) {
270 transaction_stats.surface_stats[i].fence = base::ScopedFD(fence_fd);
271 }
272 }
273 SurfaceControlMethods::Get()
274 .ASurfaceTransactionStats_releaseASurfaceControlsFn(surface_controls);
275
276 return transaction_stats;
277 }
278
279 struct TransactionAckCtx {
280 int id = 0;
281 scoped_refptr<base::SingleThreadTaskRunner> task_runner;
282 SurfaceControl::Transaction::OnCompleteCb callback;
283 };
284
285 // Note that the framework API states that this callback can be dispatched on
286 // any thread (in practice it should be the binder thread).
OnTransactionCompletedOnAnyThread(void * context,ASurfaceTransactionStats * stats)287 void OnTransactionCompletedOnAnyThread(void* context,
288 ASurfaceTransactionStats* stats) {
289 auto* ack_ctx = static_cast<TransactionAckCtx*>(context);
290 auto transaction_stats = ToTransactionStats(stats);
291 TRACE_EVENT_NESTABLE_ASYNC_END0("gpu,benchmark", "SurfaceControlTransaction",
292 ack_ctx->id);
293
294 if (ack_ctx->task_runner) {
295 ack_ctx->task_runner->PostTask(
296 FROM_HERE, base::BindOnce(std::move(ack_ctx->callback),
297 std::move(transaction_stats)));
298 } else {
299 std::move(ack_ctx->callback).Run(std::move(transaction_stats));
300 }
301
302 delete ack_ctx;
303 }
304 } // namespace
305
306 // static
IsSupported()307 bool SurfaceControl::IsSupported() {
308 if (!base::android::BuildInfo::GetInstance()->is_at_least_q())
309 return false;
310
311 CHECK(SurfaceControlMethods::Get().supported);
312 return true;
313 }
314
SupportsColorSpace(const gfx::ColorSpace & color_space)315 bool SurfaceControl::SupportsColorSpace(const gfx::ColorSpace& color_space) {
316 return ColorSpaceToADataSpace(color_space) != ADATASPACE_UNKNOWN;
317 }
318
RequiredUsage()319 uint64_t SurfaceControl::RequiredUsage() {
320 if (!IsSupported())
321 return 0u;
322 return g_agb_required_usage_bits;
323 }
324
EnableQualcommUBWC()325 void SurfaceControl::EnableQualcommUBWC() {
326 g_agb_required_usage_bits |= AHARDWAREBUFFER_USAGE_VENDOR_0;
327 }
328
SupportsSetFrameRate()329 bool SurfaceControl::SupportsSetFrameRate() {
330 // TODO(khushalsagar): Assert that this function is always available on R.
331 return IsSupported() &&
332 SurfaceControlMethods::Get().ASurfaceTransaction_setFrameRateFn !=
333 nullptr;
334 }
335
336 SurfaceControl::Surface::Surface() = default;
337
Surface(const Surface & parent,const char * name)338 SurfaceControl::Surface::Surface(const Surface& parent, const char* name) {
339 surface_ = SurfaceControlMethods::Get().ASurfaceControl_createFn(
340 parent.surface(), name);
341 if (!surface_)
342 LOG(ERROR) << "Failed to create ASurfaceControl : " << name;
343 }
344
Surface(ANativeWindow * parent,const char * name)345 SurfaceControl::Surface::Surface(ANativeWindow* parent, const char* name) {
346 surface_ = SurfaceControlMethods::Get().ASurfaceControl_createFromWindowFn(
347 parent, name);
348 if (!surface_)
349 LOG(ERROR) << "Failed to create ASurfaceControl : " << name;
350 }
351
~Surface()352 SurfaceControl::Surface::~Surface() {
353 if (surface_)
354 SurfaceControlMethods::Get().ASurfaceControl_releaseFn(surface_);
355 }
356
357 SurfaceControl::SurfaceStats::SurfaceStats() = default;
358 SurfaceControl::SurfaceStats::~SurfaceStats() = default;
359
360 SurfaceControl::SurfaceStats::SurfaceStats(SurfaceStats&& other) = default;
361 SurfaceControl::SurfaceStats& SurfaceControl::SurfaceStats::operator=(
362 SurfaceStats&& other) = default;
363
364 SurfaceControl::TransactionStats::TransactionStats() = default;
365 SurfaceControl::TransactionStats::~TransactionStats() = default;
366
367 SurfaceControl::TransactionStats::TransactionStats(TransactionStats&& other) =
368 default;
369 SurfaceControl::TransactionStats& SurfaceControl::TransactionStats::operator=(
370 TransactionStats&& other) = default;
371
Transaction()372 SurfaceControl::Transaction::Transaction()
373 : id_(g_next_transaction_id.GetNext()) {
374 transaction_ = SurfaceControlMethods::Get().ASurfaceTransaction_createFn();
375 DCHECK(transaction_);
376 }
377
~Transaction()378 SurfaceControl::Transaction::~Transaction() {
379 if (transaction_)
380 SurfaceControlMethods::Get().ASurfaceTransaction_deleteFn(transaction_);
381 }
382
Transaction(Transaction && other)383 SurfaceControl::Transaction::Transaction(Transaction&& other)
384 : id_(other.id_), transaction_(other.transaction_) {
385 other.transaction_ = nullptr;
386 other.id_ = 0;
387 }
388
operator =(Transaction && other)389 SurfaceControl::Transaction& SurfaceControl::Transaction::operator=(
390 Transaction&& other) {
391 if (transaction_)
392 SurfaceControlMethods::Get().ASurfaceTransaction_deleteFn(transaction_);
393
394 transaction_ = other.transaction_;
395 id_ = other.id_;
396
397 other.transaction_ = nullptr;
398 other.id_ = 0;
399 return *this;
400 }
401
SetVisibility(const Surface & surface,bool show)402 void SurfaceControl::Transaction::SetVisibility(const Surface& surface,
403 bool show) {
404 SurfaceControlMethods::Get().ASurfaceTransaction_setVisibilityFn(
405 transaction_, surface.surface(), show);
406 }
407
SetZOrder(const Surface & surface,int32_t z)408 void SurfaceControl::Transaction::SetZOrder(const Surface& surface, int32_t z) {
409 SurfaceControlMethods::Get().ASurfaceTransaction_setZOrderFn(
410 transaction_, surface.surface(), z);
411 }
412
SetBuffer(const Surface & surface,AHardwareBuffer * buffer,base::ScopedFD fence_fd)413 void SurfaceControl::Transaction::SetBuffer(const Surface& surface,
414 AHardwareBuffer* buffer,
415 base::ScopedFD fence_fd) {
416 SurfaceControlMethods::Get().ASurfaceTransaction_setBufferFn(
417 transaction_, surface.surface(), buffer,
418 fence_fd.is_valid() ? fence_fd.release() : -1);
419 }
420
SetGeometry(const Surface & surface,const gfx::Rect & src,const gfx::Rect & dst,gfx::OverlayTransform transform)421 void SurfaceControl::Transaction::SetGeometry(const Surface& surface,
422 const gfx::Rect& src,
423 const gfx::Rect& dst,
424 gfx::OverlayTransform transform) {
425 SurfaceControlMethods::Get().ASurfaceTransaction_setGeometryFn(
426 transaction_, surface.surface(), RectToARect(src), RectToARect(dst),
427 OverlayTransformToWindowTransform(transform));
428 }
429
SetOpaque(const Surface & surface,bool opaque)430 void SurfaceControl::Transaction::SetOpaque(const Surface& surface,
431 bool opaque) {
432 int8_t transparency = opaque ? ASURFACE_TRANSACTION_TRANSPARENCY_OPAQUE
433 : ASURFACE_TRANSACTION_TRANSPARENCY_TRANSLUCENT;
434 SurfaceControlMethods::Get().ASurfaceTransaction_setBufferTransparencyFn(
435 transaction_, surface.surface(), transparency);
436 }
437
SetDamageRect(const Surface & surface,const gfx::Rect & rect)438 void SurfaceControl::Transaction::SetDamageRect(const Surface& surface,
439 const gfx::Rect& rect) {
440 auto a_rect = RectToARect(rect);
441 SurfaceControlMethods::Get().ASurfaceTransaction_setDamageRegionFn(
442 transaction_, surface.surface(), &a_rect, 1u);
443 }
444
SetColorSpace(const Surface & surface,const gfx::ColorSpace & color_space)445 void SurfaceControl::Transaction::SetColorSpace(
446 const Surface& surface,
447 const gfx::ColorSpace& color_space) {
448 auto data_space = ColorSpaceToADataSpace(color_space);
449
450 // Log the data space in crash keys for debugging crbug.com/997592.
451 static auto* kCrashKey = base::debug::AllocateCrashKeyString(
452 "data_space_for_buffer", base::debug::CrashKeySize::Size256);
453 auto crash_key_value = base::NumberToString(data_space);
454 base::debug::ScopedCrashKeyString scoped_crash_key(kCrashKey,
455 crash_key_value);
456
457 SurfaceControlMethods::Get().ASurfaceTransaction_setBufferDataSpaceFn(
458 transaction_, surface.surface(), data_space);
459 }
460
SetFrameRate(const Surface & surface,float frame_rate)461 void SurfaceControl::Transaction::SetFrameRate(const Surface& surface,
462 float frame_rate) {
463 DCHECK(SupportsSetFrameRate());
464
465 // We always used fixed source here since a non-default value is only used for
466 // videos which have a fixed playback rate.
467 SurfaceControlMethods::Get().ASurfaceTransaction_setFrameRateFn(
468 transaction_, surface.surface(), frame_rate,
469 ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
470 }
471
SetOnCompleteCb(OnCompleteCb cb,scoped_refptr<base::SingleThreadTaskRunner> task_runner)472 void SurfaceControl::Transaction::SetOnCompleteCb(
473 OnCompleteCb cb,
474 scoped_refptr<base::SingleThreadTaskRunner> task_runner) {
475 TransactionAckCtx* ack_ctx = new TransactionAckCtx;
476 ack_ctx->callback = std::move(cb);
477 ack_ctx->task_runner = std::move(task_runner);
478 ack_ctx->id = id_;
479
480 SurfaceControlMethods::Get().ASurfaceTransaction_setOnCompleteFn(
481 transaction_, ack_ctx, &OnTransactionCompletedOnAnyThread);
482 }
483
Apply()484 void SurfaceControl::Transaction::Apply() {
485 TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("gpu,benchmark",
486 "SurfaceControlTransaction", id_);
487 SurfaceControlMethods::Get().ASurfaceTransaction_applyFn(transaction_);
488 }
489
490 } // namespace gfx
491