Line data Source code
1 :
2 : /* Copyright (c) 2006-2017, Stefan Eilemann <eile@equalizergraphics.com>
3 : * Daniel Nachbaur <danielnachbaur@gmail.com>
4 : * Cedric Stalder <cedric.stalder@gmail.com>
5 : * Enrique G. Paredes <egparedes@ifi.uzh.ch>
6 : *
7 : * This library is free software; you can redistribute it and/or modify it under
8 : * the terms of the GNU Lesser General Public License version 2.1 as published
9 : * by the Free Software Foundation.
10 : *
11 : * This library is distributed in the hope that it will be useful, but WITHOUT
12 : * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 : * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
14 : * details.
15 : *
16 : * You should have received a copy of the GNU Lesser General Public License
17 : * along with this library; if not, write to the Free Software Foundation, Inc.,
18 : * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 : */
20 :
21 : #include "image.h"
22 :
23 : #include "gl.h"
24 : #include "half.h"
25 : #include "log.h"
26 : #include "pixelData.h"
27 : #include "transferFinder.h"
28 :
29 : #include <eq/fabric/renderContext.h>
30 : #include <eq/gl.h>
31 : #include <eq/util/frameBufferObject.h>
32 : #include <eq/util/objectManager.h>
33 :
34 : #include <lunchbox/buffer.h>
35 : #include <lunchbox/memoryMap.h>
36 : #include <pression/compressor.h>
37 : #include <pression/decompressor.h>
38 : #include <pression/downloader.h>
39 : #include <pression/pluginRegistry.h>
40 : #include <pression/uploader.h>
41 :
42 : #include <boost/filesystem.hpp>
43 : #include <fstream>
44 :
45 : #ifdef _WIN32
46 : #include <malloc.h>
47 : #else
48 : #include <alloca.h>
49 : #endif
50 :
51 : #ifdef EQUALIZER_USE_OPENSCENEGRAPH
52 : #include <osgDB/WriteFile>
53 : #endif
54 :
55 : #include <co/dataIStream.h>
56 : #include <co/dataOStream.h>
57 :
58 : namespace eq
59 : {
60 : namespace
61 : {
62 : /** @internal Raw image data. */
63 20 : struct Memory : public PixelData
64 : {
65 : public:
66 20 : Memory()
67 20 : : state(INVALID)
68 20 : , hasAlpha(true)
69 : {
70 19 : }
71 :
72 0 : Memory(const Memory& rhs)
73 0 : : PixelData(rhs)
74 0 : , state(rhs.state)
75 : , localBuffer(rhs.localBuffer)
76 0 : , hasAlpha(rhs.hasAlpha)
77 : {
78 0 : if (rhs.localBuffer.isEmpty())
79 : {
80 0 : const size_t size = rhs.pvp.w * rhs.pvp.h * rhs.pixelSize;
81 0 : localBuffer.resize(size);
82 0 : memcpy(localBuffer.getData(), rhs.pixels, size);
83 : }
84 0 : pixels = localBuffer.getData();
85 0 : }
86 :
87 4 : void flush()
88 : {
89 4 : PixelData::reset();
90 4 : state = INVALID;
91 4 : localBuffer.clear();
92 4 : hasAlpha = true;
93 4 : }
94 :
95 38 : void useLocalBuffer()
96 : {
97 38 : LBASSERT(internalFormat != 0);
98 38 : LBASSERT(externalFormat != 0);
99 38 : LBASSERT(pixelSize > 0);
100 38 : LBASSERT(pvp.hasArea());
101 :
102 38 : localBuffer.resize(pvp.getArea() * pixelSize);
103 38 : pixels = localBuffer.getData();
104 38 : }
105 :
106 : enum State
107 : {
108 : INVALID,
109 : VALID,
110 : DOWNLOAD // async RB is in progress
111 : };
112 :
113 : State state; //!< The current state of the memory
114 :
115 : /** During the call of setPixelData or writeImage, we have to
116 : * manage an internal buffer to copy the data. Otherwise the downloader
117 : * allocates the memory. */
118 : lunchbox::Bufferb localBuffer;
119 :
120 : bool hasAlpha; //!< The uncompressed pixels contain alpha
121 : };
122 :
123 0 : co::DataOStream& operator<<(co::DataOStream& os, const Memory& mem)
124 : {
125 0 : if (!mem.pixels)
126 0 : return os << false;
127 0 : const size_t size = mem.pvp.w * mem.pvp.h * mem.pixelSize;
128 0 : return os << true << mem.hasAlpha << mem.state << mem.externalFormat
129 0 : << mem.internalFormat << mem.pixelSize << size
130 0 : << co::Array<void>(mem.pixels, size) << mem.pvp;
131 : }
132 :
133 0 : co::DataIStream& operator>>(co::DataIStream& is, Memory& mem)
134 : {
135 0 : size_t size = 0;
136 0 : bool haveData = false;
137 0 : is >> haveData;
138 0 : if (!haveData)
139 0 : return is;
140 :
141 0 : is >> mem.hasAlpha >> mem.state >> mem.externalFormat >>
142 0 : mem.internalFormat >> mem.pixelSize >> size;
143 :
144 0 : mem.localBuffer.resize(size);
145 0 : is >> co::Array<void>(mem.localBuffer.getData(), size) >> mem.pvp;
146 0 : mem.pixels = mem.localBuffer.getData();
147 0 : return is;
148 : }
149 :
150 : enum ActivePlugin
151 : {
152 : PLUGIN_FULL,
153 : PLUGIN_LOSSY,
154 : PLUGIN_ALL
155 : };
156 :
157 : /** @internal The individual parameters for a buffer. */
158 : struct Attachment
159 : {
160 : ActivePlugin active;
161 : pression::Compressor compressor[PLUGIN_ALL];
162 : pression::Decompressor decompressor[PLUGIN_ALL];
163 : pression::Downloader downloader[PLUGIN_ALL];
164 :
165 : float quality; //!< the minimum quality
166 :
167 : /** The texture name for this image component (texture images). */
168 : util::Texture texture;
169 :
170 : /** Current pixel data (memory images). */
171 : Memory memory;
172 :
173 : Zoom zoom; //!< zoom factor of pending readback
174 :
175 20 : Attachment()
176 20 : : active(PLUGIN_FULL)
177 : , quality(1.f)
178 20 : , texture(GL_TEXTURE_RECTANGLE_ARB)
179 : {
180 20 : }
181 :
182 0 : Attachment(const Attachment& rhs)
183 0 : : active(rhs.active)
184 0 : , quality(rhs.quality)
185 : , texture(GL_TEXTURE_RECTANGLE_ARB)
186 : , memory(rhs.memory)
187 0 : , zoom(rhs.zoom)
188 : {
189 0 : }
190 :
191 20 : ~Attachment()
192 20 : {
193 20 : LBASSERT(!compressor[PLUGIN_FULL].isGood());
194 20 : LBASSERT(!compressor[PLUGIN_LOSSY].isGood());
195 20 : LBASSERT(!decompressor[PLUGIN_FULL].isGood());
196 20 : LBASSERT(!decompressor[PLUGIN_LOSSY].isGood());
197 20 : LBASSERT(!downloader[PLUGIN_FULL].isGood());
198 20 : LBASSERT(!downloader[PLUGIN_LOSSY].isGood());
199 20 : }
200 :
201 4 : void flush()
202 : {
203 4 : memory.flush();
204 4 : texture.flush();
205 4 : resetPlugins();
206 4 : }
207 :
208 6 : void resetPlugins()
209 : {
210 6 : compressor[PLUGIN_FULL].clear();
211 6 : compressor[PLUGIN_LOSSY].clear();
212 6 : decompressor[PLUGIN_FULL].clear();
213 6 : decompressor[PLUGIN_LOSSY].clear();
214 6 : downloader[PLUGIN_FULL].clear();
215 6 : downloader[PLUGIN_LOSSY].clear();
216 6 : }
217 : };
218 :
219 0 : co::DataOStream& operator<<(co::DataOStream& os, const Attachment& at)
220 : {
221 0 : return os << at.active << at.memory << at.quality << at.zoom;
222 : }
223 :
224 0 : co::DataIStream& operator>>(co::DataIStream& is, Attachment& at)
225 : {
226 0 : return is >> at.active >> at.memory >> at.quality >> at.zoom;
227 : }
228 : }
229 :
230 : namespace detail
231 : {
232 10 : class Image
233 : {
234 : public:
235 10 : Image()
236 10 : : type(eq::Frame::TYPE_MEMORY)
237 : , ignoreAlpha(false)
238 10 : , hasPremultipliedAlpha(false)
239 : {
240 10 : }
241 :
242 0 : Image(const Image& rhs)
243 0 : : pvp(rhs.pvp)
244 : , context(rhs.context)
245 : , zoom(rhs.zoom)
246 0 : , type(rhs.type)
247 : , color(rhs.color)
248 : , depth(rhs.depth)
249 0 : , ignoreAlpha(rhs.ignoreAlpha)
250 0 : , hasPremultipliedAlpha(rhs.hasPremultipliedAlpha)
251 : {
252 0 : }
253 :
254 : /** The rectangle of the current pixel data. */
255 : PixelViewport pvp;
256 :
257 : /** The render context producing the image. */
258 : RenderContext context;
259 :
260 : /** Zoom factor used for compositing. */
261 : Zoom zoom;
262 :
263 : /** The storage type for the pixel data. */
264 : eq::Frame::Type type;
265 :
266 : Attachment color;
267 : Attachment depth;
268 :
269 : /** Alpha channel significance. */
270 : bool ignoreAlpha;
271 :
272 : bool hasPremultipliedAlpha;
273 :
274 1067 : Attachment& getAttachment(const eq::Frame::Buffer buffer)
275 : {
276 1067 : switch (buffer)
277 : {
278 : case eq::Frame::Buffer::color:
279 779 : return color;
280 : case eq::Frame::Buffer::depth:
281 288 : return depth;
282 : default:
283 0 : LBUNIMPLEMENTED;
284 : }
285 0 : return color;
286 : }
287 :
288 12 : const Attachment& getAttachment(const eq::Frame::Buffer buffer) const
289 : {
290 12 : switch (buffer)
291 : {
292 : case eq::Frame::Buffer::color:
293 9 : return color;
294 : case eq::Frame::Buffer::depth:
295 3 : return depth;
296 : default:
297 0 : LBUNIMPLEMENTED;
298 : }
299 0 : return color;
300 : }
301 :
302 984 : Memory& getMemory(const eq::Frame::Buffer buffer)
303 : {
304 984 : return getAttachment(buffer).memory;
305 : }
306 : const Memory& getMemory(const eq::Frame::Buffer buffer) const
307 : {
308 : return getAttachment(buffer).memory;
309 : }
310 :
311 12 : EqCompressorInfos findTransferers(const eq::Frame::Buffer buffer,
312 : const GLEWContext* gl) const
313 : {
314 12 : const Attachment& attachment = getAttachment(buffer);
315 12 : const Memory& memory = attachment.memory;
316 12 : TransferFinder finder(memory.internalFormat, memory.externalFormat, 0,
317 36 : attachment.quality, ignoreAlpha, gl);
318 12 : pression::PluginRegistry::getInstance().accept(finder);
319 24 : return finder.result;
320 : }
321 : };
322 : }
323 :
324 10 : Image::Image()
325 10 : : _impl(new detail::Image)
326 : {
327 10 : reset();
328 10 : }
329 :
330 0 : Image::Image(const Image& rhs)
331 0 : : _impl(new detail::Image(*rhs._impl))
332 : {
333 0 : }
334 :
335 0 : Image& Image::operator=(Image&& rhs)
336 : {
337 0 : _impl = std::move(rhs._impl);
338 0 : rhs._impl = new detail::Image;
339 0 : rhs.reset();
340 0 : return *this;
341 : }
342 :
343 25 : Image::~Image()
344 : {
345 10 : delete _impl;
346 15 : }
347 :
348 12 : void Image::reset()
349 : {
350 12 : _impl->ignoreAlpha = false;
351 12 : _impl->hasPremultipliedAlpha = false;
352 12 : setPixelViewport(PixelViewport());
353 13 : setContext(RenderContext());
354 13 : }
355 :
356 2 : void Image::flush()
357 : {
358 2 : _impl->color.flush();
359 2 : _impl->depth.flush();
360 2 : }
361 :
362 1 : void Image::resetPlugins()
363 : {
364 1 : _impl->color.resetPlugins();
365 1 : _impl->depth.resetPlugins();
366 1 : }
367 :
368 2 : void Image::deleteGLObjects(util::ObjectManager& om)
369 : {
370 2 : const char* key = reinterpret_cast<const char*>(this);
371 10 : for (size_t i = 0; i < 4; ++i)
372 : {
373 8 : om.deleteEqUploader(key + i);
374 8 : om.deleteEqTexture(key + i);
375 : }
376 2 : }
377 :
378 0 : const void* Image::_getBufferKey(const Frame::Buffer buffer) const
379 : {
380 0 : switch (buffer)
381 : {
382 : // Check also deleteGLObjects!
383 : case Frame::Buffer::color:
384 0 : return (reinterpret_cast<const char*>(this) + 0);
385 : case Frame::Buffer::depth:
386 0 : return (reinterpret_cast<const char*>(this) + 1);
387 : default:
388 0 : LBUNIMPLEMENTED;
389 0 : return (reinterpret_cast<const char*>(this) + 2);
390 : }
391 : }
392 :
393 1 : const void* Image::_getCompressorKey(const Frame::Buffer buffer) const
394 : {
395 1 : const Attachment& attachment = _impl->getAttachment(buffer);
396 :
397 1 : switch (buffer)
398 : {
399 : // Check also deleteGLObjects!
400 : case Frame::Buffer::color:
401 1 : if (attachment.quality == 1.0f)
402 1 : return (reinterpret_cast<const char*>(this) + 0);
403 0 : return (reinterpret_cast<const char*>(this) + 1);
404 : case Frame::Buffer::depth:
405 0 : if (attachment.quality == 1.0f)
406 0 : return (reinterpret_cast<const char*>(this) + 2);
407 0 : return (reinterpret_cast<const char*>(this) + 3);
408 : default:
409 0 : LBUNIMPLEMENTED;
410 0 : return (reinterpret_cast<const char*>(this) + 0);
411 : }
412 : }
413 :
414 45 : uint32_t Image::getPixelDataSize(const Frame::Buffer buffer) const
415 : {
416 45 : const Memory& memory = _impl->getMemory(buffer);
417 45 : return memory.pvp.getArea() * memory.pixelSize;
418 : }
419 :
420 15 : void Image::_setExternalFormat(const Frame::Buffer buffer,
421 : const uint32_t externalFormat,
422 : const uint32_t pixelSize, const bool hasAlpha_)
423 : {
424 15 : Memory& memory = _impl->getMemory(buffer);
425 15 : if (memory.externalFormat == externalFormat)
426 3 : return;
427 :
428 12 : memory.externalFormat = externalFormat;
429 12 : memory.pixelSize = pixelSize;
430 12 : memory.hasAlpha = buffer == Frame::Buffer::depth ? false : hasAlpha_;
431 12 : memory.state = Memory::INVALID;
432 : }
433 :
434 28 : void Image::setInternalFormat(const Frame::Buffer buffer,
435 : const uint32_t internalFormat)
436 : {
437 28 : Memory& memory = _impl->getMemory(buffer);
438 28 : if (memory.internalFormat == internalFormat)
439 15 : return;
440 :
441 13 : memory.internalFormat = internalFormat;
442 13 : allocCompressor(buffer, EQ_COMPRESSOR_INVALID);
443 13 : if (internalFormat == 0)
444 0 : return;
445 : }
446 :
447 0 : uint32_t Image::getInternalFormat(const Frame::Buffer buffer) const
448 : {
449 0 : const Memory& memory = _impl->getMemory(buffer);
450 0 : LBASSERT(memory.internalFormat);
451 0 : return memory.internalFormat;
452 : }
453 :
454 : namespace
455 : {
456 0 : class CompressorFinder : public pression::ConstPluginVisitor
457 : {
458 : public:
459 0 : explicit CompressorFinder(const uint32_t token)
460 0 : : token_(token)
461 : {
462 0 : }
463 :
464 0 : virtual fabric::VisitorResult visit(const pression::Plugin&,
465 : const EqCompressorInfo& info)
466 : {
467 0 : if (info.capabilities & EQ_COMPRESSOR_TRANSFER)
468 0 : return fabric::TRAVERSE_CONTINUE;
469 :
470 0 : if (info.tokenType == token_)
471 0 : result.push_back(info.name);
472 0 : return fabric::TRAVERSE_CONTINUE;
473 : }
474 :
475 : std::vector<uint32_t> result;
476 :
477 : private:
478 : const uint32_t token_;
479 : };
480 : }
481 :
482 0 : std::vector<uint32_t> Image::findCompressors(const Frame::Buffer buffer) const
483 : {
484 0 : CompressorFinder finder(getExternalFormat(buffer));
485 0 : pression::PluginRegistry::getInstance().accept(finder);
486 :
487 0 : LBLOG(LOG_PLUGIN) << "Found " << finder.result.size()
488 0 : << " compressors for token type 0x" << std::hex
489 0 : << getExternalFormat(buffer) << std::dec << std::endl;
490 0 : return finder.result;
491 : }
492 :
493 0 : std::vector<uint32_t> Image::findTransferers(const Frame::Buffer buffer,
494 : const GLEWContext* gl) const
495 : {
496 0 : std::vector<uint32_t> result;
497 0 : const EqCompressorInfos& infos = _impl->findTransferers(buffer, gl);
498 0 : for (EqCompressorInfosCIter i = infos.begin(); i != infos.end(); ++i)
499 0 : result.push_back(i->name);
500 0 : return result;
501 : }
502 :
503 42 : bool Image::hasAlpha() const
504 : {
505 84 : return hasPixelData(Frame::Buffer::color) &&
506 84 : _impl->getMemory(Frame::Buffer::color).hasAlpha;
507 : }
508 :
509 7 : void Image::setAlphaUsage(const bool enabled)
510 : {
511 7 : if (_impl->ignoreAlpha != enabled)
512 7 : return;
513 :
514 0 : _impl->ignoreAlpha = !enabled;
515 0 : _impl->color.memory.compressedData = pression::CompressorResult();
516 0 : _impl->depth.memory.compressedData = pression::CompressorResult();
517 : }
518 :
519 14 : void Image::setQuality(const Frame::Buffer buffer, const float quality)
520 : {
521 14 : Attachment& attachment = _impl->getAttachment(buffer);
522 14 : if (attachment.quality == quality)
523 14 : return;
524 :
525 0 : attachment.quality = quality;
526 0 : if (quality >= 1.f)
527 0 : attachment.active = PLUGIN_FULL;
528 : else
529 : {
530 0 : attachment.active = PLUGIN_LOSSY;
531 0 : attachment.compressor[PLUGIN_LOSSY].clear();
532 0 : attachment.decompressor[PLUGIN_LOSSY].clear();
533 0 : attachment.downloader[PLUGIN_LOSSY].clear();
534 : }
535 : }
536 :
537 0 : float Image::getQuality(const Frame::Buffer buffer) const
538 : {
539 0 : return _impl->getAttachment(buffer).quality;
540 : }
541 :
542 0 : bool Image::hasTextureData(const Frame::Buffer buffer) const
543 : {
544 0 : return getTexture(buffer).isValid();
545 : }
546 :
547 0 : const util::Texture& Image::getTexture(const Frame::Buffer buffer) const
548 : {
549 0 : return _impl->getAttachment(buffer).texture;
550 : }
551 :
552 93 : const uint8_t* Image::getPixelPointer(const Frame::Buffer buffer) const
553 : {
554 93 : LBASSERT(hasPixelData(buffer));
555 93 : return reinterpret_cast<const uint8_t*>(_impl->getMemory(buffer).pixels);
556 : }
557 :
558 12 : uint8_t* Image::getPixelPointer(const Frame::Buffer buffer)
559 : {
560 12 : LBASSERT(hasPixelData(buffer));
561 12 : return reinterpret_cast<uint8_t*>(_impl->getMemory(buffer).pixels);
562 : }
563 :
564 85 : const PixelData& Image::getPixelData(const Frame::Buffer buffer) const
565 : {
566 85 : LBASSERT(hasPixelData(buffer));
567 85 : return _impl->getMemory(buffer);
568 : }
569 :
570 1 : bool Image::upload(const Frame::Buffer buffer, util::Texture* texture,
571 : const Vector2i& position, util::ObjectManager& om) const
572 : {
573 : // freed by deleteGLObjects, e.g., called from Pipe::flushFrames()
574 : pression::Uploader* uploader =
575 1 : om.obtainEqUploader(_getCompressorKey(buffer));
576 1 : const PixelData& pixelData = getPixelData(buffer);
577 1 : const uint32_t externalFormat = pixelData.externalFormat;
578 1 : const uint32_t internalFormat = pixelData.internalFormat;
579 1 : const uint64_t flags = EQ_COMPRESSOR_TRANSFER | EQ_COMPRESSOR_DATA_2D |
580 0 : (texture ? texture->getCompressorTarget()
581 1 : : EQ_COMPRESSOR_USE_FRAMEBUFFER);
582 1 : const GLEWContext* const gl = om.glewGetContext();
583 :
584 1 : if (!uploader->supports(externalFormat, internalFormat, flags, gl))
585 1 : uploader->setup(externalFormat, internalFormat, flags, gl);
586 :
587 1 : if (!uploader->isGood(gl))
588 : {
589 0 : LBWARN << "No upload plugin for " << std::hex << externalFormat
590 0 : << " -> " << internalFormat << std::dec << " upload"
591 0 : << std::endl;
592 0 : return false;
593 : }
594 :
595 1 : PixelViewport pvp = getPixelViewport();
596 1 : pvp.x = position.x() + pvp.x;
597 1 : pvp.y = position.y() + pvp.y;
598 1 : if (texture)
599 0 : texture->init(internalFormat, _impl->pvp.w, _impl->pvp.h);
600 :
601 : uint64_t inDims[4], outDims[4];
602 1 : pixelData.pvp.convertToPlugin(inDims);
603 1 : pvp.convertToPlugin(outDims);
604 1 : uploader->upload(pixelData.pixels, inDims, flags, outDims,
605 1 : texture ? texture->getName() : 0, gl);
606 1 : return true;
607 : }
608 :
609 : //---------------------------------------------------------------------------
610 : // asynchronous readback
611 : //---------------------------------------------------------------------------
612 : // TODO: 2.0 API: rename to readback and return Future
613 1 : bool Image::startReadback(const Frame::Buffer buffers, const PixelViewport& pvp,
614 : const RenderContext& context, const Zoom& zoom,
615 : util::ObjectManager& glObjects)
616 : {
617 1 : LBLOG(LOG_ASSEMBLY) << "startReadback " << pvp << ", buffers " << buffers
618 1 : << std::endl;
619 :
620 1 : _impl->pvp = pvp;
621 1 : _impl->context = context;
622 1 : _impl->color.memory.state = Memory::INVALID;
623 1 : _impl->depth.memory.state = Memory::INVALID;
624 :
625 2 : bool needFinish = (buffers & Frame::Buffer::color) &&
626 2 : _startReadback(Frame::Buffer::color, zoom, glObjects);
627 :
628 1 : if ((buffers & Frame::Buffer::depth) &&
629 0 : _startReadback(Frame::Buffer::depth, zoom, glObjects))
630 : {
631 0 : needFinish = true;
632 : }
633 :
634 1 : _impl->pvp.x = 0;
635 1 : _impl->pvp.y = 0;
636 1 : return needFinish;
637 : }
638 :
639 1 : bool Image::_startReadback(const Frame::Buffer buffer, const Zoom& zoom,
640 : util::ObjectManager& glObjects)
641 : {
642 1 : Attachment& attachment = _impl->getAttachment(buffer);
643 1 : attachment.memory.compressedData = pression::CompressorResult();
644 :
645 1 : if (_impl->type == Frame::TYPE_TEXTURE)
646 : {
647 0 : LBASSERTINFO(zoom == Zoom::NONE,
648 : "Texture readback zoom not "
649 : << "implemented, zoom happens during compositing");
650 0 : util::Texture& texture = attachment.texture;
651 0 : texture.setGLEWContext(glObjects.glewGetContext());
652 0 : texture.copyFromFrameBuffer(getInternalFormat(buffer), _impl->pvp);
653 0 : texture.setGLEWContext(0);
654 0 : return false;
655 : }
656 :
657 1 : attachment.zoom = zoom;
658 1 : if (zoom == Zoom::NONE) // normal framebuffer readback
659 1 : return startReadback(buffer, 0, glObjects.glewGetContext());
660 :
661 : // else copy to texture, draw zoomed quad into FBO, (read FBO texture)
662 0 : return _readbackZoom(buffer, glObjects);
663 : }
664 :
665 1 : bool Image::startReadback(const Frame::Buffer buffer,
666 : const util::Texture* texture, const GLEWContext* gl)
667 : {
668 1 : Attachment& attachment = _impl->getAttachment(buffer);
669 1 : pression::Downloader& downloader = attachment.downloader[attachment.active];
670 1 : Memory& memory = attachment.memory;
671 1 : const uint32_t inputToken = memory.internalFormat;
672 :
673 1 : uint32_t flags = EQ_COMPRESSOR_TRANSFER | EQ_COMPRESSOR_DATA_2D |
674 0 : (texture ? texture->getCompressorTarget()
675 1 : : EQ_COMPRESSOR_USE_FRAMEBUFFER);
676 1 : const bool noAlpha = _impl->ignoreAlpha && buffer == Frame::Buffer::color;
677 :
678 1 : if (!downloader.supports(inputToken, noAlpha, flags))
679 1 : downloader.setup(inputToken, attachment.quality, noAlpha, flags, gl);
680 :
681 1 : if (!downloader.isGood())
682 : {
683 0 : LBWARN << "Download plugin initialization failed using input 0x"
684 0 : << std::hex << inputToken << std::dec << std::endl;
685 0 : return false;
686 : }
687 :
688 : // get the pixel type produced by the downloader
689 1 : const EqCompressorInfo& info = downloader.getInfo();
690 1 : const bool alpha = (info.capabilities & EQ_COMPRESSOR_IGNORE_ALPHA) == 0;
691 1 : _setExternalFormat(buffer, info.outputTokenType, info.outputTokenSize,
692 1 : alpha);
693 1 : attachment.memory.state = Memory::DOWNLOAD;
694 :
695 1 : if (!memory.hasAlpha)
696 0 : flags |= EQ_COMPRESSOR_IGNORE_ALPHA;
697 :
698 1 : uint64_t outDims[4] = {0};
699 1 : if (texture)
700 : {
701 0 : LBASSERT(texture->isValid());
702 0 : const uint64_t inDims[4] = {0ull, uint64_t(texture->getWidth()), 0ull,
703 0 : uint64_t(texture->getHeight())};
704 0 : if (downloader.start(&memory.pixels, inDims, flags, outDims,
705 : texture->getName(), gl))
706 : {
707 0 : return true;
708 : }
709 : }
710 : else
711 : {
712 : uint64_t inDims[4];
713 1 : _impl->pvp.convertToPlugin(inDims);
714 1 : if (downloader.start(&memory.pixels, inDims, flags, outDims, 0, gl))
715 1 : return true;
716 : }
717 :
718 0 : memory.pvp.convertFromPlugin(outDims);
719 0 : attachment.memory.state = Memory::VALID;
720 0 : return false;
721 : }
722 :
723 1 : void Image::finishReadback(const GLEWContext* context)
724 : {
725 1 : LBASSERT(context);
726 1 : LBLOG(LOG_ASSEMBLY) << "finishReadback" << std::endl;
727 :
728 1 : _finishReadback(Frame::Buffer::color, context);
729 1 : _finishReadback(Frame::Buffer::depth, context);
730 :
731 : #ifndef NDEBUG
732 1 : if (getenv("EQ_DUMP_IMAGES"))
733 : {
734 0 : static a_int32_t counter;
735 0 : std::ostringstream stringstream;
736 :
737 0 : stringstream << "Image_" << std::setfill('0') << std::setw(5)
738 0 : << ++counter;
739 0 : writeImages(stringstream.str());
740 : }
741 : #endif
742 1 : }
743 :
744 2 : void Image::_finishReadback(const Frame::Buffer buffer,
745 : const GLEWContext* context)
746 : {
747 2 : if (_impl->type == Frame::TYPE_TEXTURE)
748 1 : return;
749 :
750 2 : Attachment& attachment = _impl->getAttachment(buffer);
751 2 : Memory& memory = attachment.memory;
752 2 : if (memory.state != Memory::DOWNLOAD)
753 1 : return;
754 :
755 1 : pression::Downloader& downloader = attachment.downloader[attachment.active];
756 1 : const uint32_t inputToken = memory.internalFormat;
757 1 : const bool alpha = _impl->ignoreAlpha && buffer == Frame::Buffer::color;
758 : uint32_t flags =
759 1 : EQ_COMPRESSOR_TRANSFER | EQ_COMPRESSOR_DATA_2D |
760 1 : (attachment.zoom == Zoom::NONE ? EQ_COMPRESSOR_USE_FRAMEBUFFER
761 1 : : EQ_COMPRESSOR_USE_TEXTURE_RECT);
762 :
763 1 : if (!downloader.supports(inputToken, alpha, flags))
764 : {
765 0 : LBWARN << "Download plugin initialization failed" << std::endl;
766 0 : attachment.memory.state = Memory::INVALID;
767 0 : return;
768 : }
769 :
770 1 : if (memory.hasAlpha && buffer == Frame::Buffer::color)
771 1 : _impl->hasPremultipliedAlpha = true;
772 :
773 1 : flags |= (memory.hasAlpha ? 0 : EQ_COMPRESSOR_IGNORE_ALPHA);
774 :
775 1 : uint64_t outDims[4] = {0};
776 : uint64_t inDims[4];
777 1 : PixelViewport pvp = _impl->pvp;
778 1 : if (attachment.zoom != Zoom::NONE)
779 : {
780 0 : pvp.apply(attachment.zoom);
781 0 : pvp.x = 0;
782 0 : pvp.y = 0;
783 : }
784 1 : _impl->pvp.convertToPlugin(inDims);
785 :
786 1 : downloader.finish(&memory.pixels, inDims, flags, outDims, context);
787 1 : memory.pvp.convertFromPlugin(outDims);
788 1 : memory.state = Memory::VALID;
789 : }
790 :
791 0 : bool Image::_readbackZoom(const Frame::Buffer buffer, util::ObjectManager& om)
792 : {
793 0 : LBASSERT(om.supportsEqTexture());
794 0 : LBASSERT(om.supportsEqFrameBufferObject());
795 :
796 0 : const Attachment& attachment = _impl->getAttachment(buffer);
797 0 : PixelViewport pvp = _impl->pvp;
798 0 : pvp.apply(attachment.zoom);
799 0 : if (!pvp.hasArea())
800 0 : return false;
801 :
802 : // copy frame buffer to texture
803 0 : const uint32_t inputToken = attachment.memory.internalFormat;
804 0 : const void* bufferKey = _getBufferKey(buffer);
805 : util::Texture* texture =
806 0 : om.obtainEqTexture(bufferKey, GL_TEXTURE_RECTANGLE_ARB);
807 0 : texture->copyFromFrameBuffer(inputToken, _impl->pvp);
808 :
809 : // draw zoomed quad into FBO
810 : // uses the same FBO for color and depth, with masking.
811 0 : const void* fboKey = _getBufferKey(Frame::Buffer::color);
812 0 : util::FrameBufferObject* fbo = om.getEqFrameBufferObject(fboKey);
813 :
814 0 : if (fbo)
815 : {
816 0 : LBCHECK(fbo->resize(pvp.w, pvp.h));
817 : }
818 : else
819 : {
820 0 : fbo = om.newEqFrameBufferObject(fboKey);
821 0 : LBCHECK(fbo->init(pvp.w, pvp.h, inputToken, 24, 0));
822 : }
823 0 : fbo->bind();
824 0 : texture->bind();
825 :
826 0 : if (buffer == Frame::Buffer::color)
827 0 : glDepthMask(false);
828 : else
829 : {
830 0 : LBASSERT(buffer == Frame::Buffer::depth)
831 0 : glColorMask(false, false, false, false);
832 : }
833 :
834 0 : glDisable(GL_LIGHTING);
835 0 : glEnable(GL_TEXTURE_RECTANGLE_ARB);
836 0 : texture->applyZoomFilter(FILTER_LINEAR);
837 0 : glColor3f(1.0f, 1.0f, 1.0f);
838 :
839 0 : glBegin(GL_QUADS);
840 0 : glTexCoord2f(0.0f, 0.0f);
841 0 : glVertex3f(0, 0, 0.0f);
842 :
843 0 : glTexCoord2f(static_cast<float>(_impl->pvp.w), 0.0f);
844 0 : glVertex3f(static_cast<float>(pvp.w), 0, 0.0f);
845 :
846 0 : glTexCoord2f(static_cast<float>(_impl->pvp.w),
847 0 : static_cast<float>(_impl->pvp.h));
848 0 : glVertex3f(static_cast<float>(pvp.w), static_cast<float>(pvp.h), 0.0f);
849 :
850 0 : glTexCoord2f(0.0f, static_cast<float>(_impl->pvp.h));
851 0 : glVertex3f(0, static_cast<float>(pvp.h), 0.0f);
852 0 : glEnd();
853 :
854 : // restore state
855 0 : glDisable(GL_TEXTURE_RECTANGLE_ARB);
856 : // TODO channel->bindFramebuffer()
857 0 : glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0);
858 0 : fbo->unbind();
859 :
860 0 : const util::Texture* zoomedTexture = 0;
861 0 : if (buffer == Frame::Buffer::color)
862 : {
863 0 : glDepthMask(true);
864 0 : zoomedTexture = fbo->getColorTextures().front();
865 : }
866 : else
867 : {
868 0 : const ColorMask colorMask; // TODO = channel->getDrawBufferMask();
869 0 : glColorMask(colorMask.red, colorMask.green, colorMask.blue, true);
870 0 : zoomedTexture = &fbo->getDepthTexture();
871 : }
872 0 : LBASSERT(zoomedTexture->isValid());
873 0 : LBLOG(LOG_ASSEMBLY) << "Scale " << _impl->pvp << " -> " << pvp << std::endl;
874 :
875 : // BUG TODO: this is a bug in case of color and depth buffers read-back, as
876 : // _impl->pvp will be incorrect for the depth buffer!
877 : //
878 : // This should be done separately for color an depth buffers!
879 0 : _impl->pvp = pvp;
880 :
881 0 : LBLOG(LOG_ASSEMBLY) << "Read texture " << getPixelDataSize(buffer)
882 0 : << std::endl;
883 0 : return startReadback(buffer, zoomedTexture, om.glewGetContext());
884 : }
885 :
886 31 : void Image::setPixelViewport(const PixelViewport& pvp)
887 : {
888 31 : _impl->pvp = pvp;
889 31 : _impl->color.memory.state = Memory::INVALID;
890 31 : _impl->depth.memory.state = Memory::INVALID;
891 31 : _impl->color.memory.compressedData = pression::CompressorResult();
892 32 : _impl->depth.memory.compressedData = pression::CompressorResult();
893 32 : }
894 :
895 12 : void Image::clearPixelData(const Frame::Buffer buffer)
896 : {
897 12 : Memory& memory = _impl->getAttachment(buffer).memory;
898 12 : memory.pvp = _impl->pvp;
899 12 : const ssize_t size = getPixelDataSize(buffer);
900 12 : if (size == 0)
901 0 : return;
902 :
903 12 : validatePixelData(buffer);
904 :
905 12 : switch (memory.externalFormat)
906 : {
907 : case EQ_COMPRESSOR_DATATYPE_DEPTH_UNSIGNED_INT:
908 3 : memset(memory.pixels, 0xFF, size);
909 3 : break;
910 :
911 : case EQ_COMPRESSOR_DATATYPE_RGBA:
912 : case EQ_COMPRESSOR_DATATYPE_BGRA:
913 : {
914 9 : uint8_t* data = reinterpret_cast<uint8_t*>(memory.pixels);
915 : #ifdef Darwin
916 : const unsigned char pixel[4] = {0, 0, 0, 255};
917 : memset_pattern4(data, &pixel, size);
918 : #else
919 9 : lunchbox::setZero(data, size);
920 59 : #pragma omp parallel for
921 50 : for (ssize_t i = 3; i < size; i += 4)
922 4658806 : data[i] = 255;
923 : #endif
924 9 : break;
925 : }
926 : default:
927 0 : LBWARN << "Unknown external format " << memory.externalFormat
928 0 : << ", initializing to 0" << std::endl;
929 0 : lunchbox::setZero(memory.pixels, size);
930 0 : break;
931 : }
932 : }
933 :
934 38 : void Image::validatePixelData(const Frame::Buffer buffer)
935 : {
936 38 : Memory& memory = _impl->getAttachment(buffer).memory;
937 38 : memory.useLocalBuffer();
938 38 : memory.state = Memory::VALID;
939 38 : memory.compressedData = pression::CompressorResult();
940 38 : }
941 :
942 12 : void Image::setPixelData(const Frame::Buffer buffer, const PixelData& pixels)
943 : {
944 12 : Memory& memory = _impl->getMemory(buffer);
945 12 : memory.externalFormat = pixels.externalFormat;
946 12 : memory.internalFormat = pixels.internalFormat;
947 12 : memory.pixelSize = pixels.pixelSize;
948 12 : memory.pvp = pixels.pvp;
949 12 : memory.state = Memory::INVALID;
950 12 : memory.compressedData = pression::CompressorResult();
951 12 : memory.hasAlpha = false;
952 :
953 : const EqCompressorInfos& transferrers =
954 12 : _impl->findTransferers(buffer, 0 /*GLEW context*/);
955 12 : if (transferrers.empty())
956 0 : LBWARN << "No upload engines found for given pixel data" << std::endl;
957 : else
958 : {
959 12 : memory.hasAlpha =
960 12 : transferrers.front().capabilities & EQ_COMPRESSOR_IGNORE_ALPHA;
961 : #ifndef NDEBUG
962 72 : for (EqCompressorInfosCIter i = transferrers.begin();
963 48 : i != transferrers.end(); ++i)
964 : {
965 12 : LBASSERTINFO(memory.hasAlpha ==
966 : bool(i->capabilities & EQ_COMPRESSOR_IGNORE_ALPHA),
967 : "Uploaders don't agree on alpha state of external "
968 : << "format: " << transferrers.front()
969 : << " != " << *i);
970 : }
971 : #endif
972 : }
973 :
974 12 : const uint32_t size = getPixelDataSize(buffer);
975 12 : LBASSERT(size > 0);
976 12 : if (size == 0)
977 0 : return;
978 :
979 12 : if (pixels.compressedData.compressor <= EQ_COMPRESSOR_NONE)
980 : {
981 12 : validatePixelData(buffer); // alloc memory for pixels
982 :
983 12 : if (pixels.pixels)
984 : {
985 0 : memcpy(memory.pixels, pixels.pixels, size);
986 0 : memory.state = Memory::VALID;
987 : }
988 : else
989 : // no data in pixels, clear image buffer
990 12 : clearPixelData(buffer);
991 :
992 12 : return;
993 : }
994 :
995 0 : LBASSERT(!pixels.compressedData.chunks.empty());
996 0 : LBASSERT(pixels.compressedData.compressor != EQ_COMPRESSOR_AUTO);
997 :
998 0 : Attachment& attachment = _impl->getAttachment(buffer);
999 0 : if (!attachment.decompressor->setup(pixels.compressedData.compressor))
1000 : {
1001 0 : LBASSERTINFO(false,
1002 : "Can't allocate decompressor "
1003 : << pixels.compressedData.compressor
1004 : << ", mismatched compression plugin installation?");
1005 0 : return;
1006 : }
1007 :
1008 0 : const EqCompressorInfo& info = attachment.decompressor->getInfo();
1009 0 : LBASSERTINFO(info.name == pixels.compressedData.compressor, info);
1010 :
1011 0 : if (memory.externalFormat != info.outputTokenType)
1012 : {
1013 : // decompressor output differs from compressor input
1014 0 : memory.externalFormat = info.outputTokenType;
1015 0 : memory.pixelSize = info.outputTokenSize;
1016 : }
1017 0 : validatePixelData(buffer); // alloc memory for pixels
1018 :
1019 : uint64_t outDims[4];
1020 0 : memory.pvp.convertToPlugin(outDims);
1021 :
1022 0 : attachment.decompressor->decompress(pixels.compressedData, memory.pixels,
1023 0 : outDims, pixels.compressorFlags);
1024 : }
1025 :
1026 : /** Find and activate a compression engine */
1027 13 : bool Image::allocCompressor(const Frame::Buffer buffer, const uint32_t name)
1028 : {
1029 13 : Attachment& attachment = _impl->getAttachment(buffer);
1030 13 : pression::Compressor& compressor = attachment.compressor[attachment.active];
1031 13 : if (name <= EQ_COMPRESSOR_NONE)
1032 : {
1033 13 : attachment.memory.compressedData = pression::CompressorResult();
1034 13 : compressor.clear();
1035 13 : return true;
1036 : }
1037 :
1038 0 : if (compressor.uses(name))
1039 0 : return true;
1040 :
1041 0 : attachment.memory.compressedData = pression::CompressorResult();
1042 0 : compressor.setup(name);
1043 0 : LBLOG(LOG_PLUGIN) << "Instantiated compressor of type 0x" << std::hex
1044 0 : << name << std::dec << std::endl;
1045 0 : return compressor.isGood();
1046 : }
1047 :
1048 : /** Find and activate a compression engine */
1049 0 : bool Image::allocDownloader(const Frame::Buffer buffer, const uint32_t name,
1050 : const GLEWContext* gl)
1051 : {
1052 0 : LBASSERT(name > EQ_COMPRESSOR_NONE)
1053 0 : LBASSERT(gl);
1054 :
1055 0 : Attachment& attachment = _impl->getAttachment(buffer);
1056 0 : pression::Downloader& downloader = attachment.downloader[attachment.active];
1057 :
1058 0 : if (name <= EQ_COMPRESSOR_NONE)
1059 : {
1060 0 : downloader.clear();
1061 0 : _setExternalFormat(buffer, EQ_COMPRESSOR_DATATYPE_NONE, 0, true);
1062 0 : return false;
1063 : }
1064 :
1065 0 : if (downloader.uses(name))
1066 0 : return true;
1067 :
1068 0 : if (!downloader.setup(name, gl))
1069 0 : return false;
1070 :
1071 0 : const EqCompressorInfo& info = downloader.getInfo();
1072 0 : attachment.memory.internalFormat = info.tokenType;
1073 0 : _setExternalFormat(buffer, info.outputTokenType, info.outputTokenSize,
1074 0 : !(info.capabilities & EQ_COMPRESSOR_IGNORE_ALPHA));
1075 0 : return true;
1076 : }
1077 :
1078 1 : uint32_t Image::getDownloaderName(const Frame::Buffer buffer) const
1079 : {
1080 1 : const Attachment& attachment = _impl->getAttachment(buffer);
1081 : const pression::Downloader& downloader =
1082 1 : attachment.downloader[attachment.active];
1083 1 : if (downloader.isGood())
1084 1 : return downloader.getInfo().name;
1085 0 : return EQ_COMPRESSOR_INVALID;
1086 : }
1087 :
1088 14 : void Image::useCompressor(const Frame::Buffer buffer, const uint32_t name)
1089 : {
1090 14 : _impl->getMemory(buffer).compressorName = name;
1091 14 : }
1092 :
1093 0 : const PixelData& Image::compressPixelData(const Frame::Buffer buffer)
1094 : {
1095 0 : LBASSERT(getPixelDataSize(buffer) > 0);
1096 :
1097 0 : Attachment& attachment = _impl->getAttachment(buffer);
1098 0 : Memory& memory = attachment.memory;
1099 0 : if (memory.compressedData.isCompressed() ||
1100 0 : memory.compressorName == EQ_COMPRESSOR_NONE)
1101 : {
1102 0 : LBASSERT(memory.compressorName != EQ_COMPRESSOR_AUTO);
1103 0 : return memory;
1104 : }
1105 :
1106 0 : pression::Compressor& compressor = attachment.compressor[attachment.active];
1107 :
1108 0 : if (!compressor.isGood() ||
1109 0 : compressor.getInfo().tokenType != getExternalFormat(buffer) ||
1110 0 : memory.compressorName == EQ_COMPRESSOR_AUTO)
1111 : {
1112 0 : if (memory.compressorName == EQ_COMPRESSOR_AUTO)
1113 : {
1114 0 : const uint32_t tokenType = getExternalFormat(buffer);
1115 : const float downloadQuality =
1116 0 : attachment.downloader[attachment.active].getInfo().quality;
1117 0 : const float quality = attachment.quality / downloadQuality;
1118 :
1119 0 : compressor.setup(tokenType, quality, _impl->ignoreAlpha);
1120 : }
1121 : else
1122 0 : compressor.setup(memory.compressorName);
1123 :
1124 0 : if (!compressor.isGood())
1125 : {
1126 0 : LBWARN << "No compressor found for token type 0x" << std::hex
1127 0 : << getExternalFormat(buffer) << std::dec << std::endl;
1128 0 : compressor.clear();
1129 : }
1130 : }
1131 :
1132 0 : memory.compressedData.compressor = compressor.getInfo().name;
1133 0 : LBASSERT(memory.compressedData.compressor != EQ_COMPRESSOR_AUTO);
1134 0 : LBASSERT(memory.compressedData.compressor != EQ_COMPRESSOR_INVALID);
1135 0 : if (memory.compressedData.compressor == EQ_COMPRESSOR_NONE)
1136 0 : return memory;
1137 :
1138 0 : memory.compressorFlags = EQ_COMPRESSOR_DATA_2D;
1139 0 : if (_impl->ignoreAlpha && memory.hasAlpha)
1140 : {
1141 0 : LBASSERT(buffer == Frame::Buffer::color);
1142 0 : memory.compressorFlags |= EQ_COMPRESSOR_IGNORE_ALPHA;
1143 : }
1144 :
1145 : uint64_t inDims[4];
1146 0 : memory.pvp.convertToPlugin(inDims);
1147 0 : compressor.compress(memory.pixels, inDims, memory.compressorFlags);
1148 0 : memory.compressedData = compressor.getResult();
1149 0 : return memory;
1150 : }
1151 :
1152 : //---------------------------------------------------------------------------
1153 : // File IO
1154 : //---------------------------------------------------------------------------
1155 3 : bool Image::writeImages(const std::string& filenameTemplate) const
1156 : {
1157 15 : return (writeImage(filenameTemplate + "_color.rgb", Frame::Buffer::color) &&
1158 12 : writeImage(filenameTemplate + "_depth.rgb", Frame::Buffer::depth));
1159 : }
1160 :
1161 : namespace
1162 : {
1163 : #define SWAP_SHORT(v) (v = (v & 0xff) << 8 | (v & 0xff00) >> 8)
1164 : #define SWAP_INT(v) \
1165 : (v = (v & 0xff) << 24 | (v & 0xff00) << 8 | (v & 0xff0000) >> 8 | \
1166 : (v & 0xff000000) >> 24)
1167 :
1168 : #ifdef _WIN32
1169 : #pragma pack(1)
1170 : #endif
1171 : /** @cond IGNORE */
1172 : struct RGBHeader
1173 : {
1174 23 : RGBHeader()
1175 23 : {
1176 23 : memset(this, 0, sizeof(RGBHeader));
1177 23 : magic = 474;
1178 23 : bytesPerChannel = 1;
1179 23 : nDimensions = 3;
1180 23 : maxValue = 255;
1181 23 : }
1182 :
1183 : /**
1184 : * Convert to and from big endian by swapping bytes on little endian
1185 : * machines.
1186 : */
1187 36 : void convert()
1188 : {
1189 : #if defined(__i386__) || defined(__amd64__) || defined(__ia64) || \
1190 : defined(__x86_64) || defined(_WIN32)
1191 36 : SWAP_SHORT(magic);
1192 36 : SWAP_SHORT(nDimensions);
1193 36 : SWAP_SHORT(width);
1194 36 : SWAP_SHORT(height);
1195 36 : SWAP_SHORT(depth);
1196 36 : SWAP_INT(minValue);
1197 36 : SWAP_INT(maxValue);
1198 36 : SWAP_INT(colorMode);
1199 : #endif
1200 36 : }
1201 :
1202 : unsigned short magic;
1203 : char compression;
1204 : char bytesPerChannel;
1205 : unsigned short nDimensions;
1206 : unsigned short width;
1207 : unsigned short height;
1208 : unsigned short depth;
1209 : unsigned minValue;
1210 : unsigned maxValue;
1211 : char unused[4];
1212 : char filename[80];
1213 : unsigned colorMode;
1214 : char fill[404];
1215 : }
1216 : /** @endcond */
1217 : #ifndef _WIN32
1218 : __attribute__((packed))
1219 : #endif
1220 : ;
1221 :
1222 4608000 : void put32f(std::ostream& os, const char* ptr)
1223 : {
1224 : // cppcheck-suppress invalidPointerCast
1225 4608000 : const float& value = *reinterpret_cast<const float*>(ptr);
1226 4608000 : const uint8_t byte = uint8_t(value * 255.f);
1227 4608000 : os.write((const char*)&byte, 1);
1228 4608000 : }
1229 2304000 : void put16f(std::ostream& os, const char* ptr)
1230 : {
1231 2304000 : const uint16_t& value = *reinterpret_cast<const uint16_t*>(ptr);
1232 2304000 : const float f = half_to_float(value);
1233 2304000 : put32f(os, (const char*)&f);
1234 2304000 : }
1235 : }
1236 :
1237 11 : bool Image::writeImage(const std::string& filename,
1238 : const Frame::Buffer buffer) const
1239 : {
1240 11 : const Memory& memory = _impl->getMemory(buffer);
1241 :
1242 11 : const PixelViewport& pvp = memory.pvp;
1243 11 : const size_t nPixels = pvp.w * pvp.h;
1244 :
1245 11 : if (nPixels == 0 || memory.state != Memory::VALID)
1246 2 : return false;
1247 :
1248 : const unsigned char* data =
1249 9 : reinterpret_cast<const unsigned char*>(getPixelPointer(buffer));
1250 :
1251 9 : unsigned char* convertedData = nullptr;
1252 :
1253 : // glReadPixels with alpha has ARGB premultiplied format: post-divide alpha
1254 9 : if (_impl->hasPremultipliedAlpha &&
1255 0 : getExternalFormat(buffer) == EQ_COMPRESSOR_DATATYPE_BGRA)
1256 : {
1257 0 : convertedData = new unsigned char[nPixels * 4];
1258 :
1259 0 : const uint32_t* bgraData = reinterpret_cast<const uint32_t*>(data);
1260 0 : uint32_t* bgraConverted = reinterpret_cast<uint32_t*>(convertedData);
1261 0 : for (size_t i = 0; i < nPixels; ++i, ++bgraConverted, ++bgraData)
1262 : {
1263 0 : *bgraConverted = *bgraData;
1264 0 : uint32_t& pixel = *bgraConverted;
1265 0 : const uint32_t alpha = pixel >> 24;
1266 0 : if (alpha != 0)
1267 : {
1268 0 : const uint32_t red = (pixel >> 16) & 0xff;
1269 0 : const uint32_t green = (pixel >> 8) & 0xff;
1270 0 : const uint32_t blue = pixel & 0xff;
1271 0 : *bgraConverted =
1272 0 : ((alpha << 24) | (((255 * red) / alpha) << 16) |
1273 0 : (((255 * green) / alpha) << 8) | ((255 * blue) / alpha));
1274 : }
1275 : }
1276 : }
1277 :
1278 : const bool retVal =
1279 9 : _writeImage(filename, buffer, convertedData ? convertedData : data);
1280 9 : delete[] convertedData;
1281 9 : return retVal;
1282 : }
1283 :
1284 9 : bool Image::_writeImage(const std::string& filename, const Frame::Buffer buffer,
1285 : const unsigned char* data_) const
1286 : {
1287 9 : const Memory& memory = _impl->getMemory(buffer);
1288 9 : const PixelViewport& pvp = memory.pvp;
1289 9 : const size_t nPixels = pvp.w * pvp.h;
1290 :
1291 9 : RGBHeader header;
1292 9 : header.width = pvp.w;
1293 9 : header.height = pvp.h;
1294 :
1295 9 : switch (getExternalFormat(buffer))
1296 : {
1297 : case EQ_COMPRESSOR_DATATYPE_RGB10_A2:
1298 1 : header.maxValue = 1023;
1299 : case EQ_COMPRESSOR_DATATYPE_BGRA:
1300 : case EQ_COMPRESSOR_DATATYPE_BGRA_UINT_8_8_8_8_REV:
1301 : case EQ_COMPRESSOR_DATATYPE_RGBA:
1302 : case EQ_COMPRESSOR_DATATYPE_RGBA_UINT_8_8_8_8_REV:
1303 6 : header.bytesPerChannel = 1;
1304 6 : header.depth = 4;
1305 6 : break;
1306 : case EQ_COMPRESSOR_DATATYPE_BGR:
1307 : case EQ_COMPRESSOR_DATATYPE_RGB:
1308 0 : header.bytesPerChannel = 1;
1309 0 : header.depth = 3;
1310 0 : break;
1311 : case EQ_COMPRESSOR_DATATYPE_BGRA32F:
1312 : case EQ_COMPRESSOR_DATATYPE_RGBA32F:
1313 1 : header.bytesPerChannel = 4;
1314 1 : header.depth = 4;
1315 1 : break;
1316 : case EQ_COMPRESSOR_DATATYPE_BGR32F:
1317 : case EQ_COMPRESSOR_DATATYPE_RGB32F:
1318 0 : header.bytesPerChannel = 4;
1319 0 : header.depth = 3;
1320 0 : break;
1321 : case EQ_COMPRESSOR_DATATYPE_BGRA16F:
1322 : case EQ_COMPRESSOR_DATATYPE_RGBA16F:
1323 1 : header.bytesPerChannel = 2;
1324 1 : header.depth = 4;
1325 1 : break;
1326 : case EQ_COMPRESSOR_DATATYPE_BGR16F:
1327 : case EQ_COMPRESSOR_DATATYPE_RGB16F:
1328 0 : header.bytesPerChannel = 2;
1329 0 : header.depth = 3;
1330 0 : break;
1331 : case EQ_COMPRESSOR_DATATYPE_DEPTH_UNSIGNED_INT:
1332 1 : header.bytesPerChannel = 4;
1333 1 : header.depth = 1;
1334 1 : break;
1335 :
1336 : default:
1337 0 : LBERROR << "Unknown image pixel data type" << std::endl;
1338 0 : return false;
1339 : }
1340 :
1341 9 : if (header.depth == 1) // depth
1342 : {
1343 1 : LBASSERT((header.bytesPerChannel % 4) == 0);
1344 1 : header.depth = 4;
1345 1 : header.bytesPerChannel /= 4;
1346 : }
1347 9 : LBASSERT(header.bytesPerChannel > 0);
1348 :
1349 : // Swap red & blue where needed
1350 9 : bool swapRB = false;
1351 9 : switch (getExternalFormat(buffer))
1352 : {
1353 : case EQ_COMPRESSOR_DATATYPE_RGB10_A2:
1354 : case EQ_COMPRESSOR_DATATYPE_RGBA:
1355 : case EQ_COMPRESSOR_DATATYPE_RGBA_UINT_8_8_8_8_REV:
1356 : case EQ_COMPRESSOR_DATATYPE_RGB:
1357 : case EQ_COMPRESSOR_DATATYPE_RGBA32F:
1358 : case EQ_COMPRESSOR_DATATYPE_RGB32F:
1359 : case EQ_COMPRESSOR_DATATYPE_RGBA16F:
1360 : case EQ_COMPRESSOR_DATATYPE_RGB16F:
1361 8 : swapRB = true;
1362 : }
1363 :
1364 9 : const uint8_t bpc = header.bytesPerChannel;
1365 9 : const uint16_t nChannels = header.depth;
1366 9 : const size_t depth = nChannels * bpc;
1367 :
1368 18 : const boost::filesystem::path path(filename);
1369 : #ifdef EQUALIZER_USE_OPENSCENEGRAPH
1370 9 : if (path.extension() != ".rgb")
1371 : {
1372 0 : osg::ref_ptr<osg::Image> osgImage = new osg::Image();
1373 0 : osgImage->setImage(pvp.w, pvp.h, depth, getExternalFormat(buffer),
1374 : swapRB ? GL_RGBA : GL_BGRA, GL_UNSIGNED_BYTE,
1375 : const_cast<unsigned char*>(data_),
1376 0 : osg::Image::NO_DELETE);
1377 0 : return osgDB::writeImageFile(*osgImage, filename);
1378 : }
1379 : #endif
1380 :
1381 18 : std::ofstream image(filename.c_str(), std::ios::out | std::ios::binary);
1382 9 : if (!image.is_open())
1383 : {
1384 0 : LBERROR << "Can't open " << filename << " for writing" << std::endl;
1385 0 : return false;
1386 : }
1387 :
1388 9 : const size_t nBytes = nPixels * depth;
1389 9 : if (header.bytesPerChannel > 2)
1390 4 : LBWARN << static_cast<int>(header.bytesPerChannel)
1391 4 : << " bytes per channel not supported by RGB spec" << std::endl;
1392 :
1393 9 : strncpy(header.filename, filename.c_str(), 80);
1394 9 : header.convert();
1395 9 : image.write(reinterpret_cast<const char*>(&header), sizeof(header));
1396 9 : header.convert();
1397 :
1398 9 : const char* data = reinterpret_cast<const char*>(data_);
1399 :
1400 : // Each channel is saved separately
1401 9 : if (nChannels == 3 || nChannels == 4)
1402 : {
1403 : // channel one is R or B
1404 9 : if (swapRB)
1405 10278472 : for (size_t j = 0 * bpc; j < nBytes; j += depth)
1406 10278464 : image.write(&data[j], bpc);
1407 : else
1408 1310721 : for (size_t j = 2 * bpc; j < nBytes; j += depth)
1409 1310720 : image.write(&data[j], bpc);
1410 :
1411 : // channel two is G
1412 11589193 : for (size_t j = 1 * bpc; j < nBytes; j += depth)
1413 11589184 : image.write(&data[j], bpc);
1414 :
1415 : // channel three is B or G
1416 9 : if (swapRB)
1417 10278472 : for (size_t j = 2 * bpc; j < nBytes; j += depth)
1418 10278464 : image.write(&data[j], bpc);
1419 : else
1420 1310721 : for (size_t j = 0; j < nBytes; j += depth)
1421 1310720 : image.write(&data[j], bpc);
1422 :
1423 : // channel four is Alpha
1424 9 : if (nChannels == 4)
1425 11589193 : for (size_t j = 3 * bpc; j < nBytes; j += depth)
1426 11589193 : image.write(&data[j], bpc);
1427 : }
1428 : else
1429 : {
1430 0 : for (size_t i = 0; i < nChannels; i += bpc)
1431 0 : for (size_t j = i * bpc; j < nBytes; j += depth)
1432 0 : image.write(&data[j], bpc);
1433 : }
1434 9 : image.close();
1435 :
1436 9 : if (header.bytesPerChannel == 1)
1437 7 : return true;
1438 : // else also write 8bpp version
1439 :
1440 4 : const std::string smallFilename = path.parent_path().string() + "/s_" +
1441 : #if BOOST_FILESYSTEM_VERSION == 3
1442 8 : path.filename().string();
1443 : #else
1444 : path.filename();
1445 : #endif
1446 2 : image.open(smallFilename.c_str(), std::ios::out | std::ios::binary);
1447 2 : if (!image.is_open())
1448 : {
1449 0 : LBERROR << "Can't open " << smallFilename << " for writing"
1450 0 : << std::endl;
1451 0 : return false;
1452 : }
1453 :
1454 2 : header.bytesPerChannel = 1;
1455 2 : header.maxValue = 255;
1456 2 : header.convert();
1457 2 : image.write(reinterpret_cast<const char*>(&header), sizeof(header));
1458 2 : header.convert();
1459 :
1460 2 : LBASSERTINFO(bpc == 2 || bpc == 4, bpc);
1461 2 : const bool twoBPC = bpc == 2;
1462 :
1463 2 : if (nChannels == 3 || nChannels == 4)
1464 : {
1465 : // channel one is R or B
1466 2 : if (swapRB)
1467 1152002 : for (size_t j = 0 * bpc; j < nBytes; j += depth)
1468 1152000 : twoBPC ? put16f(image, &data[j]) : put32f(image, &data[j]);
1469 : else
1470 0 : for (size_t j = 2 * bpc; j < nBytes; j += depth)
1471 0 : twoBPC ? put16f(image, &data[j]) : put32f(image, &data[j]);
1472 :
1473 : // channel two is G
1474 1152002 : for (size_t j = 1 * bpc; j < nBytes; j += depth)
1475 1152000 : twoBPC ? put16f(image, &data[j]) : put32f(image, &data[j]);
1476 :
1477 : // channel three is B or G
1478 2 : if (swapRB)
1479 1152002 : for (size_t j = 2 * bpc; j < nBytes; j += depth)
1480 1152000 : twoBPC ? put16f(image, &data[j]) : put32f(image, &data[j]);
1481 : else
1482 0 : for (size_t j = 0; j < nBytes; j += depth)
1483 0 : twoBPC ? put16f(image, &data[j]) : put32f(image, &data[j]);
1484 :
1485 : // channel four is Alpha
1486 2 : if (nChannels == 4)
1487 1152002 : for (size_t j = 3 * bpc; j < nBytes; j += depth)
1488 1152002 : twoBPC ? put16f(image, &data[j]) : put32f(image, &data[j]);
1489 : }
1490 : else
1491 : {
1492 0 : for (size_t i = 0; i < nChannels; i += bpc)
1493 0 : for (size_t j = i * bpc; j < nBytes; j += depth)
1494 0 : twoBPC ? put16f(image, &data[j]) : put32f(image, &data[j]);
1495 : }
1496 2 : image.close();
1497 :
1498 2 : return true;
1499 : }
1500 :
1501 14 : bool Image::readImage(const std::string& filename, const Frame::Buffer buffer)
1502 : {
1503 28 : lunchbox::MemoryMap image;
1504 14 : const uint8_t* addr = static_cast<const uint8_t*>(image.map(filename));
1505 :
1506 14 : if (!addr)
1507 : {
1508 0 : LBERROR << "Can't open " << filename << " for reading" << std::endl;
1509 0 : return false;
1510 : }
1511 :
1512 14 : const size_t size = image.getSize();
1513 14 : if (size < sizeof(RGBHeader))
1514 : {
1515 0 : LBWARN << "Image " << filename << " too small" << std::endl;
1516 0 : return false;
1517 : }
1518 :
1519 14 : RGBHeader header;
1520 14 : memcpy(&header, addr, sizeof(header));
1521 14 : addr += sizeof(header);
1522 :
1523 14 : header.convert();
1524 :
1525 14 : if (header.magic != 474)
1526 : {
1527 0 : LBERROR << "Bad magic number " << filename << std::endl;
1528 0 : return false;
1529 : }
1530 14 : if (header.width == 0 || header.height == 0)
1531 : {
1532 0 : LBERROR << "Zero-sized image " << filename << std::endl;
1533 0 : return false;
1534 : }
1535 14 : if (header.compression != 0)
1536 : {
1537 0 : LBERROR << "Unsupported compression " << filename << std::endl;
1538 0 : return false;
1539 : }
1540 :
1541 14 : const unsigned nChannels = header.depth;
1542 :
1543 28 : if (header.nDimensions != 3 || header.minValue != 0 ||
1544 29 : (header.maxValue != 255 && header.maxValue != 1023) ||
1545 28 : header.colorMode != 0 ||
1546 11 : (buffer == Frame::Buffer::color && nChannels != 3 && nChannels != 4) ||
1547 3 : (buffer == Frame::Buffer::depth && nChannels != 4))
1548 : {
1549 0 : LBERROR << "Unsupported image type " << filename << std::endl;
1550 0 : return false;
1551 : }
1552 :
1553 16 : if ((header.bytesPerChannel != 1 || nChannels == 1) &&
1554 2 : header.maxValue != 255)
1555 : {
1556 0 : LBERROR << "Unsupported value range " << header.maxValue << std::endl;
1557 0 : return false;
1558 : }
1559 :
1560 14 : const uint8_t bpc = header.bytesPerChannel;
1561 14 : const size_t depth = nChannels * bpc;
1562 14 : const size_t nPixels = header.width * header.height;
1563 14 : const size_t nComponents = nPixels * nChannels;
1564 14 : const size_t nBytes = nComponents * bpc;
1565 :
1566 14 : if (size < sizeof(RGBHeader) + nBytes)
1567 : {
1568 0 : LBERROR << "Image " << filename << " too small" << std::endl;
1569 0 : return false;
1570 : }
1571 14 : LBASSERTINFO(size == sizeof(RGBHeader) + nBytes,
1572 : "delta " << size - sizeof(RGBHeader) - nBytes);
1573 :
1574 14 : switch (buffer)
1575 : {
1576 : case Frame::Buffer::depth:
1577 3 : if (header.bytesPerChannel != 1)
1578 : {
1579 0 : LBERROR << "Unsupported channel depth "
1580 0 : << static_cast<int>(header.bytesPerChannel) << std::endl;
1581 0 : return false;
1582 : }
1583 : _setExternalFormat(Frame::Buffer::depth,
1584 3 : EQ_COMPRESSOR_DATATYPE_DEPTH_UNSIGNED_INT, 4, false);
1585 3 : setInternalFormat(Frame::Buffer::depth, EQ_COMPRESSOR_DATATYPE_DEPTH);
1586 3 : break;
1587 :
1588 : case Frame::Buffer::color:
1589 11 : switch (header.bytesPerChannel)
1590 : {
1591 : case 1:
1592 9 : if (header.maxValue == 1023)
1593 : {
1594 1 : LBASSERT(nChannels == 4);
1595 : _setExternalFormat(Frame::Buffer::color,
1596 1 : EQ_COMPRESSOR_DATATYPE_RGB10_A2, 4, true);
1597 : setInternalFormat(Frame::Buffer::color,
1598 1 : EQ_COMPRESSOR_DATATYPE_RGB10_A2);
1599 : }
1600 : else
1601 : {
1602 8 : if (nChannels == 4)
1603 : _setExternalFormat(Frame::Buffer::color,
1604 8 : EQ_COMPRESSOR_DATATYPE_RGBA, 4, true);
1605 : else
1606 : {
1607 0 : LBASSERT(nChannels == 3);
1608 : _setExternalFormat(Frame::Buffer::color,
1609 : EQ_COMPRESSOR_DATATYPE_RGB, nChannels,
1610 0 : false);
1611 : }
1612 : setInternalFormat(Frame::Buffer::color,
1613 8 : EQ_COMPRESSOR_DATATYPE_RGBA);
1614 : }
1615 9 : break;
1616 :
1617 : case 2:
1618 1 : if (nChannels == 4)
1619 : _setExternalFormat(Frame::Buffer::color,
1620 1 : EQ_COMPRESSOR_DATATYPE_RGBA16F, 8, true);
1621 : else
1622 : {
1623 0 : LBASSERT(nChannels == 3);
1624 0 : _setExternalFormat(Frame::Buffer::color,
1625 : EQ_COMPRESSOR_DATATYPE_RGB16F, nChannels * 2,
1626 0 : false);
1627 : }
1628 :
1629 : setInternalFormat(Frame::Buffer::color,
1630 1 : EQ_COMPRESSOR_DATATYPE_RGBA16F);
1631 1 : break;
1632 :
1633 : case 4:
1634 1 : if (nChannels == 4)
1635 : _setExternalFormat(Frame::Buffer::color,
1636 1 : EQ_COMPRESSOR_DATATYPE_RGBA32F, 16, true);
1637 : else
1638 : {
1639 0 : LBASSERT(nChannels == 3);
1640 0 : _setExternalFormat(Frame::Buffer::color,
1641 : EQ_COMPRESSOR_DATATYPE_RGBA32F,
1642 0 : nChannels * 4, false);
1643 : }
1644 : setInternalFormat(Frame::Buffer::color,
1645 1 : EQ_COMPRESSOR_DATATYPE_RGBA32F);
1646 1 : break;
1647 :
1648 : default:
1649 0 : LBERROR << "Unsupported channel depth "
1650 0 : << static_cast<int>(header.bytesPerChannel) << std::endl;
1651 0 : return false;
1652 : }
1653 11 : break;
1654 :
1655 : default:
1656 0 : LBUNREACHABLE;
1657 : }
1658 14 : Memory& memory = _impl->getMemory(buffer);
1659 14 : const PixelViewport pvp(0, 0, header.width, header.height);
1660 14 : if (pvp != _impl->pvp)
1661 : {
1662 10 : setPixelViewport(pvp);
1663 : }
1664 :
1665 14 : if (memory.pvp != pvp)
1666 : {
1667 13 : memory.pvp = pvp;
1668 13 : memory.state = Memory::INVALID;
1669 : }
1670 14 : validatePixelData(buffer);
1671 :
1672 14 : uint8_t* data = reinterpret_cast<uint8_t*>(memory.pixels);
1673 14 : LBASSERTINFO(nBytes <= getPixelDataSize(buffer),
1674 : nBytes << " > " << getPixelDataSize(buffer));
1675 : // Each channel is saved separately
1676 14 : switch (bpc)
1677 : {
1678 : case 1:
1679 60 : for (size_t i = 0; i < nChannels; ++i)
1680 75909424 : for (size_t j = i; j < nComponents; j += nChannels)
1681 : {
1682 75909376 : data[j] = *addr;
1683 75909376 : ++addr;
1684 : }
1685 12 : break;
1686 :
1687 : case 2:
1688 5 : for (size_t i = 0; i < nChannels; ++i)
1689 2304004 : for (size_t j = i; j < nComponents; j += nChannels)
1690 : {
1691 4608000 : reinterpret_cast<uint16_t*>(data)[j] =
1692 2304000 : *reinterpret_cast<const uint16_t*>(addr);
1693 2304000 : addr += bpc;
1694 : }
1695 1 : break;
1696 :
1697 : case 4:
1698 5 : for (size_t i = 0; i < nChannels; ++i)
1699 2304004 : for (size_t j = i; j < nComponents; j += nChannels)
1700 : {
1701 4608000 : reinterpret_cast<uint32_t*>(data)[j] =
1702 2304000 : *reinterpret_cast<const uint32_t*>(addr);
1703 2304000 : addr += bpc;
1704 : }
1705 1 : break;
1706 :
1707 : default:
1708 0 : for (size_t i = 0; i < depth; i += bpc)
1709 0 : for (size_t j = i * bpc; j < nBytes; j += depth)
1710 : {
1711 0 : memcpy(&data[j], addr, bpc);
1712 0 : addr += bpc;
1713 : }
1714 0 : break;
1715 : }
1716 14 : return true;
1717 : }
1718 :
1719 18 : uint32_t Image::getExternalFormat(const Frame::Buffer buffer) const
1720 : {
1721 18 : return _impl->getMemory(buffer).externalFormat;
1722 : }
1723 :
1724 42 : uint32_t Image::getPixelSize(const Frame::Buffer buffer) const
1725 : {
1726 42 : return _impl->getMemory(buffer).pixelSize;
1727 : }
1728 :
1729 7 : void Image::setStorageType(const Frame::Type type)
1730 : {
1731 7 : _impl->type = type;
1732 7 : }
1733 :
1734 64 : Frame::Type Image::getStorageType() const
1735 : {
1736 64 : return _impl->type;
1737 : }
1738 :
1739 131 : const PixelViewport& Image::getPixelViewport() const
1740 : {
1741 131 : return _impl->pvp;
1742 : }
1743 :
1744 0 : void Image::setZoom(const Zoom& zoom)
1745 : {
1746 0 : _impl->zoom = zoom;
1747 0 : }
1748 :
1749 64 : const Zoom& Image::getZoom() const
1750 : {
1751 64 : return _impl->zoom;
1752 : }
1753 :
1754 13 : void Image::setContext(const RenderContext& context)
1755 : {
1756 13 : _impl->context = context;
1757 13 : }
1758 :
1759 65 : const RenderContext& Image::getContext() const
1760 : {
1761 65 : return _impl->context;
1762 : }
1763 :
1764 540 : bool Image::hasPixelData(const Frame::Buffer buffer) const
1765 : {
1766 540 : return _impl->getMemory(buffer).state == Memory::VALID;
1767 : }
1768 :
1769 4 : bool Image::hasAsyncReadback(const Frame::Buffer buffer) const
1770 : {
1771 4 : return _impl->getMemory(buffer).state == Memory::DOWNLOAD;
1772 : }
1773 :
1774 3 : bool Image::hasAsyncReadback() const
1775 : {
1776 4 : return hasAsyncReadback(Frame::Buffer::color) ||
1777 4 : hasAsyncReadback(Frame::Buffer::depth);
1778 : }
1779 :
1780 0 : bool Image::getAlphaUsage() const
1781 : {
1782 0 : return !_impl->ignoreAlpha;
1783 : }
1784 :
1785 1 : void Image::setOffset(int32_t x, int32_t y)
1786 : {
1787 1 : _impl->pvp.x = x;
1788 1 : _impl->pvp.y = y;
1789 1 : }
1790 :
1791 0 : co::DataOStream& operator<<(co::DataOStream& os, const Image& image)
1792 : {
1793 0 : os << image._impl->color << image._impl->context << image._impl->depth
1794 0 : << image._impl->hasPremultipliedAlpha << image._impl->ignoreAlpha
1795 0 : << image._impl->pvp << image._impl->type << image._impl->zoom;
1796 0 : return os;
1797 : }
1798 :
1799 0 : co::DataIStream& operator>>(co::DataIStream& is, Image& image)
1800 : {
1801 0 : is >> image._impl->color >> image._impl->context >> image._impl->depth >>
1802 0 : image._impl->hasPremultipliedAlpha >> image._impl->ignoreAlpha >>
1803 0 : image._impl->pvp >> image._impl->type >> image._impl->zoom;
1804 0 : return is;
1805 : }
1806 30 : }
|