OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "ui/gl/gl_image_memory.h" | 5 #include "ui/gl/gl_image_memory.h" |
6 | 6 |
7 #include "base/logging.h" | 7 #include "base/logging.h" |
8 #include "base/trace_event/trace_event.h" | 8 #include "base/trace_event/trace_event.h" |
| 9 #include "ui/gfx/buffer_format_util.h" |
9 #include "ui/gl/gl_bindings.h" | 10 #include "ui/gl/gl_bindings.h" |
| 11 #include "ui/gl/gl_context.h" |
| 12 #include "ui/gl/gl_version_info.h" |
10 | 13 |
11 namespace gfx { | 14 namespace gfx { |
12 namespace { | 15 namespace { |
13 | 16 |
14 bool ValidInternalFormat(unsigned internalformat) { | 17 bool ValidInternalFormat(unsigned internalformat) { |
15 switch (internalformat) { | 18 switch (internalformat) { |
16 case GL_ATC_RGB_AMD: | 19 case GL_ATC_RGB_AMD: |
17 case GL_ATC_RGBA_INTERPOLATED_ALPHA_AMD: | 20 case GL_ATC_RGBA_INTERPOLATED_ALPHA_AMD: |
18 case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: | 21 case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: |
19 case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: | 22 case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: |
20 case GL_ETC1_RGB8_OES: | 23 case GL_ETC1_RGB8_OES: |
21 case GL_R8: | 24 case GL_R8: |
| 25 case GL_RGB: |
22 case GL_RGBA: | 26 case GL_RGBA: |
23 case GL_BGRA_EXT: | 27 case GL_BGRA_EXT: |
24 return true; | 28 return true; |
25 default: | 29 default: |
26 return false; | 30 return false; |
27 } | 31 } |
28 } | 32 } |
29 | 33 |
30 bool ValidFormat(BufferFormat format) { | 34 bool ValidFormat(BufferFormat format) { |
31 switch (format) { | 35 switch (format) { |
32 case BufferFormat::ATC: | 36 case BufferFormat::ATC: |
33 case BufferFormat::ATCIA: | 37 case BufferFormat::ATCIA: |
34 case BufferFormat::DXT1: | 38 case BufferFormat::DXT1: |
35 case BufferFormat::DXT5: | 39 case BufferFormat::DXT5: |
36 case BufferFormat::ETC1: | 40 case BufferFormat::ETC1: |
37 case BufferFormat::R_8: | 41 case BufferFormat::R_8: |
38 case BufferFormat::RGBA_4444: | 42 case BufferFormat::RGBA_4444: |
| 43 case BufferFormat::RGBX_8888: |
39 case BufferFormat::RGBA_8888: | 44 case BufferFormat::RGBA_8888: |
| 45 case BufferFormat::BGRX_8888: |
40 case BufferFormat::BGRA_8888: | 46 case BufferFormat::BGRA_8888: |
41 return true; | 47 return true; |
42 case BufferFormat::BGRX_8888: | |
43 case BufferFormat::YUV_420: | 48 case BufferFormat::YUV_420: |
44 case BufferFormat::YUV_420_BIPLANAR: | 49 case BufferFormat::YUV_420_BIPLANAR: |
45 case BufferFormat::UYVY_422: | 50 case BufferFormat::UYVY_422: |
46 return false; | 51 return false; |
47 } | 52 } |
48 | 53 |
49 NOTREACHED(); | 54 NOTREACHED(); |
50 return false; | 55 return false; |
51 } | 56 } |
52 | 57 |
53 bool IsCompressedFormat(BufferFormat format) { | 58 bool IsCompressedFormat(BufferFormat format) { |
54 switch (format) { | 59 switch (format) { |
55 case BufferFormat::ATC: | 60 case BufferFormat::ATC: |
56 case BufferFormat::ATCIA: | 61 case BufferFormat::ATCIA: |
57 case BufferFormat::DXT1: | 62 case BufferFormat::DXT1: |
58 case BufferFormat::DXT5: | 63 case BufferFormat::DXT5: |
59 case BufferFormat::ETC1: | 64 case BufferFormat::ETC1: |
60 return true; | 65 return true; |
61 case BufferFormat::R_8: | 66 case BufferFormat::R_8: |
62 case BufferFormat::RGBA_4444: | 67 case BufferFormat::RGBA_4444: |
| 68 case BufferFormat::RGBX_8888: |
63 case BufferFormat::RGBA_8888: | 69 case BufferFormat::RGBA_8888: |
| 70 case BufferFormat::BGRX_8888: |
64 case BufferFormat::BGRA_8888: | 71 case BufferFormat::BGRA_8888: |
65 case BufferFormat::BGRX_8888: | |
66 case BufferFormat::YUV_420: | 72 case BufferFormat::YUV_420: |
67 case BufferFormat::YUV_420_BIPLANAR: | 73 case BufferFormat::YUV_420_BIPLANAR: |
68 case BufferFormat::UYVY_422: | 74 case BufferFormat::UYVY_422: |
69 return false; | 75 return false; |
70 } | 76 } |
71 | 77 |
72 NOTREACHED(); | 78 NOTREACHED(); |
73 return false; | 79 return false; |
74 } | 80 } |
75 | 81 |
76 GLenum TextureFormat(BufferFormat format) { | 82 GLenum TextureFormat(BufferFormat format) { |
77 switch (format) { | 83 switch (format) { |
78 case BufferFormat::ATC: | 84 case BufferFormat::ATC: |
79 return GL_ATC_RGB_AMD; | 85 return GL_ATC_RGB_AMD; |
80 case BufferFormat::ATCIA: | 86 case BufferFormat::ATCIA: |
81 return GL_ATC_RGBA_INTERPOLATED_ALPHA_AMD; | 87 return GL_ATC_RGBA_INTERPOLATED_ALPHA_AMD; |
82 case BufferFormat::DXT1: | 88 case BufferFormat::DXT1: |
83 return GL_COMPRESSED_RGB_S3TC_DXT1_EXT; | 89 return GL_COMPRESSED_RGB_S3TC_DXT1_EXT; |
84 case BufferFormat::DXT5: | 90 case BufferFormat::DXT5: |
85 return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; | 91 return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; |
86 case BufferFormat::ETC1: | 92 case BufferFormat::ETC1: |
87 return GL_ETC1_RGB8_OES; | 93 return GL_ETC1_RGB8_OES; |
88 case BufferFormat::R_8: | 94 case BufferFormat::R_8: |
89 return GL_RED; | 95 return GL_RED; |
90 case BufferFormat::RGBA_4444: | 96 case BufferFormat::RGBA_4444: |
91 case BufferFormat::RGBA_8888: | 97 case BufferFormat::RGBA_8888: |
92 return GL_RGBA; | 98 return GL_RGBA; |
93 case BufferFormat::BGRA_8888: | 99 case BufferFormat::BGRA_8888: |
94 return GL_BGRA_EXT; | 100 return GL_BGRA_EXT; |
| 101 case BufferFormat::RGBX_8888: |
95 case BufferFormat::BGRX_8888: | 102 case BufferFormat::BGRX_8888: |
| 103 return GL_RGB; |
96 case BufferFormat::YUV_420: | 104 case BufferFormat::YUV_420: |
97 case BufferFormat::YUV_420_BIPLANAR: | 105 case BufferFormat::YUV_420_BIPLANAR: |
98 case BufferFormat::UYVY_422: | 106 case BufferFormat::UYVY_422: |
99 NOTREACHED(); | 107 NOTREACHED(); |
100 return 0; | 108 return 0; |
101 } | 109 } |
102 | 110 |
103 NOTREACHED(); | 111 NOTREACHED(); |
104 return 0; | 112 return 0; |
105 } | 113 } |
106 | 114 |
107 GLenum DataFormat(BufferFormat format) { | 115 GLenum DataFormat(BufferFormat format) { |
108 return TextureFormat(format); | 116 switch (format) { |
| 117 case BufferFormat::RGBX_8888: |
| 118 return GL_RGBA; |
| 119 case BufferFormat::BGRX_8888: |
| 120 return GL_BGRA_EXT; |
| 121 case BufferFormat::RGBA_4444: |
| 122 case BufferFormat::RGBA_8888: |
| 123 case BufferFormat::BGRA_8888: |
| 124 case BufferFormat::R_8: |
| 125 case BufferFormat::ATC: |
| 126 case BufferFormat::ATCIA: |
| 127 case BufferFormat::DXT1: |
| 128 case BufferFormat::DXT5: |
| 129 case BufferFormat::ETC1: |
| 130 case BufferFormat::YUV_420: |
| 131 case BufferFormat::YUV_420_BIPLANAR: |
| 132 case BufferFormat::UYVY_422: |
| 133 return TextureFormat(format); |
| 134 } |
| 135 |
| 136 NOTREACHED(); |
| 137 return 0; |
109 } | 138 } |
110 | 139 |
111 GLenum DataType(BufferFormat format) { | 140 GLenum DataType(BufferFormat format) { |
112 switch (format) { | 141 switch (format) { |
113 case BufferFormat::RGBA_4444: | 142 case BufferFormat::RGBA_4444: |
114 return GL_UNSIGNED_SHORT_4_4_4_4; | 143 return GL_UNSIGNED_SHORT_4_4_4_4; |
| 144 case BufferFormat::RGBX_8888: |
115 case BufferFormat::RGBA_8888: | 145 case BufferFormat::RGBA_8888: |
| 146 case BufferFormat::BGRX_8888: |
116 case BufferFormat::BGRA_8888: | 147 case BufferFormat::BGRA_8888: |
117 case BufferFormat::R_8: | 148 case BufferFormat::R_8: |
118 return GL_UNSIGNED_BYTE; | 149 return GL_UNSIGNED_BYTE; |
119 case BufferFormat::ATC: | 150 case BufferFormat::ATC: |
120 case BufferFormat::ATCIA: | 151 case BufferFormat::ATCIA: |
121 case BufferFormat::DXT1: | 152 case BufferFormat::DXT1: |
122 case BufferFormat::DXT5: | 153 case BufferFormat::DXT5: |
123 case BufferFormat::ETC1: | 154 case BufferFormat::ETC1: |
124 case BufferFormat::BGRX_8888: | |
125 case BufferFormat::YUV_420: | 155 case BufferFormat::YUV_420: |
126 case BufferFormat::YUV_420_BIPLANAR: | 156 case BufferFormat::YUV_420_BIPLANAR: |
127 case BufferFormat::UYVY_422: | 157 case BufferFormat::UYVY_422: |
128 NOTREACHED(); | 158 NOTREACHED(); |
129 return 0; | 159 return 0; |
130 } | 160 } |
131 | 161 |
132 NOTREACHED(); | 162 NOTREACHED(); |
133 return 0; | 163 return 0; |
134 } | 164 } |
135 | 165 |
136 GLsizei SizeInBytes(const Size& size, BufferFormat format) { | 166 template <typename F> |
137 size_t stride_in_bytes = 0; | 167 scoped_ptr<uint8_t[]> GLES2RGBData(const Size& size, |
138 bool valid_stride = GLImageMemory::StrideInBytes( | 168 BufferFormat format, |
139 size.width(), format, &stride_in_bytes); | 169 const uint8_t* data, |
140 DCHECK(valid_stride); | 170 F const& data_to_rgb, |
141 return static_cast<GLsizei>(stride_in_bytes * size.height()); | 171 GLenum* data_format, |
| 172 GLenum* data_type) { |
| 173 TRACE_EVENT2("gpu", "GLES2RGBData", "width", size.width(), "height", |
| 174 size.height()); |
| 175 |
| 176 // Four-byte row alignment as specified by glPixelStorei with argument |
| 177 // GL_UNPACK_ALIGNMENT set to 4. |
| 178 size_t gles2_rgb_data_stride = (size.width() * 3 + 3) & ~3; |
| 179 scoped_ptr<uint8_t[]> gles2_rgb_data( |
| 180 new uint8_t[gles2_rgb_data_stride * size.height()]); |
| 181 size_t data_stride = RowSizeForBufferFormat(size.width(), format, 0); |
| 182 |
| 183 for (int y = 0; y < size.height(); ++y) { |
| 184 for (int x = 0; x < size.width(); ++x) { |
| 185 data_to_rgb(&data[y * data_stride + x * 4], |
| 186 &gles2_rgb_data[y * gles2_rgb_data_stride + x * 3]); |
| 187 } |
| 188 } |
| 189 |
| 190 *data_format = GL_RGB; |
| 191 *data_type = GL_UNSIGNED_BYTE; |
| 192 return gles2_rgb_data.Pass(); |
| 193 } |
| 194 |
| 195 scoped_ptr<uint8_t[]> GLES2Data(const Size& size, |
| 196 BufferFormat format, |
| 197 const uint8_t* data, |
| 198 GLenum* data_format, |
| 199 GLenum* data_type) { |
| 200 switch (format) { |
| 201 case BufferFormat::RGBX_8888: |
| 202 return GLES2RGBData(size, format, |
| 203 data, [](const uint8_t* src, uint8_t* dst) { |
| 204 dst[0] = src[0]; |
| 205 dst[1] = src[1]; |
| 206 dst[2] = src[2]; |
| 207 }, data_format, data_type); |
| 208 case BufferFormat::BGRX_8888: |
| 209 return GLES2RGBData(size, format, |
| 210 data, [](const uint8_t* src, uint8_t* dst) { |
| 211 dst[0] = src[2]; |
| 212 dst[1] = src[1]; |
| 213 dst[2] = src[0]; |
| 214 }, data_format, data_type); |
| 215 case BufferFormat::RGBA_4444: |
| 216 case BufferFormat::RGBA_8888: |
| 217 case BufferFormat::BGRA_8888: |
| 218 case BufferFormat::R_8: |
| 219 case BufferFormat::ATC: |
| 220 case BufferFormat::ATCIA: |
| 221 case BufferFormat::DXT1: |
| 222 case BufferFormat::DXT5: |
| 223 case BufferFormat::ETC1: |
| 224 case BufferFormat::YUV_420: |
| 225 case BufferFormat::YUV_420_BIPLANAR: |
| 226 case BufferFormat::UYVY_422: |
| 227 // No data conversion needed. |
| 228 return nullptr; |
| 229 } |
| 230 |
| 231 NOTREACHED(); |
| 232 return 0; |
142 } | 233 } |
143 | 234 |
144 } // namespace | 235 } // namespace |
145 | 236 |
146 GLImageMemory::GLImageMemory(const Size& size, unsigned internalformat) | 237 GLImageMemory::GLImageMemory(const Size& size, unsigned internalformat) |
147 : size_(size), | 238 : size_(size), |
148 internalformat_(internalformat), | 239 internalformat_(internalformat), |
149 memory_(nullptr), | 240 memory_(nullptr), |
150 format_(BufferFormat::RGBA_8888) {} | 241 format_(BufferFormat::RGBA_8888) {} |
151 | 242 |
152 GLImageMemory::~GLImageMemory() { | 243 GLImageMemory::~GLImageMemory() { |
153 DCHECK(!memory_); | 244 DCHECK(!memory_); |
154 } | 245 } |
155 | 246 |
156 // static | |
157 bool GLImageMemory::StrideInBytes(size_t width, | |
158 BufferFormat format, | |
159 size_t* stride_in_bytes) { | |
160 base::CheckedNumeric<size_t> checked_stride = width; | |
161 switch (format) { | |
162 case BufferFormat::ATCIA: | |
163 case BufferFormat::DXT5: | |
164 *stride_in_bytes = width; | |
165 return true; | |
166 case BufferFormat::ATC: | |
167 case BufferFormat::DXT1: | |
168 case BufferFormat::ETC1: | |
169 DCHECK_EQ(width % 2, 0u); | |
170 *stride_in_bytes = width / 2; | |
171 return true; | |
172 case BufferFormat::R_8: | |
173 checked_stride += 3; | |
174 if (!checked_stride.IsValid()) | |
175 return false; | |
176 *stride_in_bytes = checked_stride.ValueOrDie() & ~0x3; | |
177 return true; | |
178 case BufferFormat::RGBA_4444: | |
179 checked_stride *= 2; | |
180 if (!checked_stride.IsValid()) | |
181 return false; | |
182 *stride_in_bytes = checked_stride.ValueOrDie(); | |
183 return true; | |
184 case BufferFormat::RGBA_8888: | |
185 case BufferFormat::BGRA_8888: | |
186 checked_stride *= 4; | |
187 if (!checked_stride.IsValid()) | |
188 return false; | |
189 *stride_in_bytes = checked_stride.ValueOrDie(); | |
190 return true; | |
191 case BufferFormat::BGRX_8888: | |
192 case BufferFormat::YUV_420: | |
193 case BufferFormat::YUV_420_BIPLANAR: | |
194 case BufferFormat::UYVY_422: | |
195 NOTREACHED(); | |
196 return false; | |
197 } | |
198 | |
199 NOTREACHED(); | |
200 return false; | |
201 } | |
202 | |
203 bool GLImageMemory::Initialize(const unsigned char* memory, | 247 bool GLImageMemory::Initialize(const unsigned char* memory, |
204 BufferFormat format) { | 248 BufferFormat format) { |
205 if (!ValidInternalFormat(internalformat_)) { | 249 if (!ValidInternalFormat(internalformat_)) { |
206 LOG(ERROR) << "Invalid internalformat: " << internalformat_; | 250 LOG(ERROR) << "Invalid internalformat: " << internalformat_; |
207 return false; | 251 return false; |
208 } | 252 } |
209 | 253 |
210 if (!ValidFormat(format)) { | 254 if (!ValidFormat(format)) { |
211 LOG(ERROR) << "Invalid format: " << static_cast<int>(format); | 255 LOG(ERROR) << "Invalid format: " << static_cast<int>(format); |
212 return false; | 256 return false; |
(...skipping 26 matching lines...) Expand all Loading... |
239 | 283 |
240 bool GLImageMemory::CopyTexImage(unsigned target) { | 284 bool GLImageMemory::CopyTexImage(unsigned target) { |
241 TRACE_EVENT2("gpu", "GLImageMemory::CopyTexImage", "width", size_.width(), | 285 TRACE_EVENT2("gpu", "GLImageMemory::CopyTexImage", "width", size_.width(), |
242 "height", size_.height()); | 286 "height", size_.height()); |
243 | 287 |
244 // GL_TEXTURE_EXTERNAL_OES is not a supported target. | 288 // GL_TEXTURE_EXTERNAL_OES is not a supported target. |
245 if (target == GL_TEXTURE_EXTERNAL_OES) | 289 if (target == GL_TEXTURE_EXTERNAL_OES) |
246 return false; | 290 return false; |
247 | 291 |
248 if (IsCompressedFormat(format_)) { | 292 if (IsCompressedFormat(format_)) { |
249 glCompressedTexImage2D(target, 0, TextureFormat(format_), size_.width(), | 293 glCompressedTexImage2D( |
250 size_.height(), 0, SizeInBytes(size_, format_), | 294 target, 0, TextureFormat(format_), size_.width(), size_.height(), 0, |
251 memory_); | 295 static_cast<GLsizei>(BufferSizeForBufferFormat(size_, format_)), |
| 296 memory_); |
252 } else { | 297 } else { |
| 298 scoped_ptr<uint8_t[]> gles2_data; |
| 299 GLenum data_format = DataFormat(format_); |
| 300 GLenum data_type = DataType(format_); |
| 301 |
| 302 if (GLContext::GetCurrent()->GetVersionInfo()->is_es) |
| 303 gles2_data = GLES2Data(size_, format_, memory_, &data_format, &data_type); |
| 304 |
253 glTexImage2D(target, 0, TextureFormat(format_), size_.width(), | 305 glTexImage2D(target, 0, TextureFormat(format_), size_.width(), |
254 size_.height(), 0, DataFormat(format_), DataType(format_), | 306 size_.height(), 0, data_format, data_type, |
255 memory_); | 307 gles2_data ? gles2_data.get() : memory_); |
256 } | 308 } |
257 | 309 |
258 return true; | 310 return true; |
259 } | 311 } |
260 | 312 |
261 bool GLImageMemory::CopyTexSubImage(unsigned target, | 313 bool GLImageMemory::CopyTexSubImage(unsigned target, |
262 const Point& offset, | 314 const Point& offset, |
263 const Rect& rect) { | 315 const Rect& rect) { |
264 TRACE_EVENT2("gpu", "GLImageMemory::CopyTexSubImage", "width", rect.width(), | 316 TRACE_EVENT2("gpu", "GLImageMemory::CopyTexSubImage", "width", rect.width(), |
265 "height", rect.height()); | 317 "height", rect.height()); |
266 | 318 |
267 // GL_TEXTURE_EXTERNAL_OES is not a supported target. | 319 // GL_TEXTURE_EXTERNAL_OES is not a supported target. |
268 if (target == GL_TEXTURE_EXTERNAL_OES) | 320 if (target == GL_TEXTURE_EXTERNAL_OES) |
269 return false; | 321 return false; |
270 | 322 |
271 // Sub width is not supported. | 323 // Sub width is not supported. |
272 if (rect.width() != size_.width()) | 324 if (rect.width() != size_.width()) |
273 return false; | 325 return false; |
274 | 326 |
275 // Height must be a multiple of 4 if compressed. | 327 // Height must be a multiple of 4 if compressed. |
276 if (IsCompressedFormat(format_) && rect.height() % 4) | 328 if (IsCompressedFormat(format_) && rect.height() % 4) |
277 return false; | 329 return false; |
278 | 330 |
279 size_t stride_in_bytes = 0; | 331 const uint8_t* data = |
280 bool rv = StrideInBytes(size_.width(), format_, &stride_in_bytes); | 332 memory_ + rect.y() * RowSizeForBufferFormat(size_.width(), format_, 0); |
281 DCHECK(rv); | |
282 DCHECK(memory_); | |
283 const unsigned char* data = memory_ + rect.y() * stride_in_bytes; | |
284 if (IsCompressedFormat(format_)) { | 333 if (IsCompressedFormat(format_)) { |
285 glCompressedTexSubImage2D(target, 0, offset.x(), offset.y(), rect.width(), | 334 glCompressedTexSubImage2D( |
286 rect.height(), DataFormat(format_), | 335 target, 0, offset.x(), offset.y(), rect.width(), rect.height(), |
287 SizeInBytes(rect.size(), format_), data); | 336 DataFormat(format_), |
| 337 static_cast<GLsizei>(BufferSizeForBufferFormat(rect.size(), format_)), |
| 338 data); |
288 } else { | 339 } else { |
| 340 GLenum data_format = DataFormat(format_); |
| 341 GLenum data_type = DataType(format_); |
| 342 scoped_ptr<uint8_t[]> gles2_data; |
| 343 |
| 344 if (GLContext::GetCurrent()->GetVersionInfo()->is_es) { |
| 345 gles2_data = |
| 346 GLES2Data(rect.size(), format_, data, &data_format, &data_type); |
| 347 } |
| 348 |
289 glTexSubImage2D(target, 0, offset.x(), offset.y(), rect.width(), | 349 glTexSubImage2D(target, 0, offset.x(), offset.y(), rect.width(), |
290 rect.height(), DataFormat(format_), DataType(format_), | 350 rect.height(), data_format, data_type, |
291 data); | 351 gles2_data ? gles2_data.get() : data); |
292 } | 352 } |
293 | 353 |
294 return true; | 354 return true; |
295 } | 355 } |
296 | 356 |
297 bool GLImageMemory::ScheduleOverlayPlane(AcceleratedWidget widget, | 357 bool GLImageMemory::ScheduleOverlayPlane(AcceleratedWidget widget, |
298 int z_order, | 358 int z_order, |
299 OverlayTransform transform, | 359 OverlayTransform transform, |
300 const Rect& bounds_rect, | 360 const Rect& bounds_rect, |
301 const RectF& crop_rect) { | 361 const RectF& crop_rect) { |
302 return false; | 362 return false; |
303 } | 363 } |
304 | 364 |
| 365 // static |
| 366 unsigned GLImageMemory::GetInternalFormatForTesting(BufferFormat format) { |
| 367 DCHECK(ValidFormat(format)); |
| 368 return TextureFormat(format); |
| 369 } |
| 370 |
305 } // namespace gfx | 371 } // namespace gfx |
OLD | NEW |