1 // Copyright 2009-2021 Intel Corporation
2 // SPDX-License-Identifier: Apache-2.0
3 
4 #include "Texture2D.h"
5 #include <sstream>
6 #include "rkcommon/memory/malloc.h"
7 
8 #include "stb_image.h"
9 
10 namespace ospray {
11 namespace sg {
12 
13 // static helper functions //////////////////////////////////////////////////
14 
15 //
16 // Generic
17 //
18 // Create the childData node given all other texture params
createDataNode()19 void Texture2D::createDataNode()
20 {
21   if (params.depth == 1)
22     createDataNodeType_internal<uint8_t>();
23   else if (params.depth == 2)
24     createDataNodeType_internal<uint16_t>();
25   else if (params.depth == 4)
26     createDataNodeType_internal<float>();
27   else
28     std::cerr << "#osp:sg: INVALID Texture depth " << params.depth << std::endl;
29 }
30 
31 template <typename T>
createDataNodeType_internal()32 void Texture2D::createDataNodeType_internal()
33 {
34   if (params.components == 1)
35     createDataNodeVec_internal<T>();
36   else if (params.components == 2)
37     createDataNodeVec_internal<T, 2>();
38   else if (params.components == 3)
39     createDataNodeVec_internal<T, 3>();
40   else if (params.components == 4)
41     createDataNodeVec_internal<T, 4>();
42   else
43     std::cerr << "#osp:sg: INVALID number of texture components "
44               << params.components << std::endl;
45 }
46 template <typename T, int N>
createDataNodeVec_internal()47 void Texture2D::createDataNodeVec_internal()
48 {
49   using vecT = vec_t<T, N>;
50   // If texture doesn't use all channels(4), setup a strided-data access
51   if (params.colorChannel < 4) {
52     createChildData("data",
53         params.size, // numItems
54         sizeof(vecT) * vec2ul(1, params.size.x), // byteStride
55         (T *)texelData.get() + params.colorChannel,
56         true);
57   } else // RGBA
58     createChildData(
59         "data", params.size, vec2ul(0, 0), (vecT *)texelData.get(), true);
60 }
61 template <typename T>
createDataNodeVec_internal()62 void Texture2D::createDataNodeVec_internal()
63 {
64   createChildData(
65       "data", params.size, vec2ul(0, 0), (T *)texelData.get(), true);
66 }
67 
osprayTextureFormat(int components)68 OSPTextureFormat Texture2D::osprayTextureFormat(int components)
69 {
70   if (params.depth == 1) {
71     if (components == 1)
72       return params.preferLinear ? OSP_TEXTURE_R8 : OSP_TEXTURE_L8;
73     if (components == 2)
74       return params.preferLinear ? OSP_TEXTURE_RA8 : OSP_TEXTURE_LA8;
75     if (components == 3)
76       return params.preferLinear ? OSP_TEXTURE_RGB8 : OSP_TEXTURE_SRGB;
77     if (components == 4)
78       return params.preferLinear ? OSP_TEXTURE_RGBA8 : OSP_TEXTURE_SRGBA;
79   } else if (params.depth == 2) {
80     if (components == 1)
81       return OSP_TEXTURE_R16;
82     if (components == 2)
83       return OSP_TEXTURE_RA16;
84     if (components == 3)
85       return OSP_TEXTURE_RGB16;
86     if (components == 4)
87       return OSP_TEXTURE_RGBA16;
88   } else if (params.depth == 4) {
89     if (components == 1)
90       return OSP_TEXTURE_R32F;
91     if (components == 3)
92       return OSP_TEXTURE_RGB32F;
93     if (components == 4)
94       return OSP_TEXTURE_RGBA32F;
95   }
96 
97   std::cerr << "#osp:sg: INVALID format " << params.depth << ":"
98             << components << std::endl;
99   return OSP_TEXTURE_FORMAT_INVALID;
100 }
101 
102 #ifdef USE_OPENIMAGEIO
103 //
104 // OpenImageIO
105 //
106 OIIO_NAMESPACE_USING
107 template <typename T>
loadTexture_OIIO_readFile(std::unique_ptr<ImageInput> & in)108 void Texture2D::loadTexture_OIIO_readFile(std::unique_ptr<ImageInput> &in)
109 {
110   const ImageSpec &spec = in->spec();
111   const auto typeDesc = TypeDescFromC<T>::value();
112 
113   std::shared_ptr<void> data(new T[params.size.product() * params.components]);
114   T *start = (T *)data.get()
115       + (params.flip ? (params.size.y - 1) * params.size.x * params.components
116                      : 0);
117   const long int stride =
118       (params.flip ? -1 : 1) * params.size.x * sizeof(T) * params.components;
119 
120   bool success =
121       in->read_image(typeDesc, start, AutoStride, stride, AutoStride);
122   if (success) {
123     // Move shared_ptr ownership
124     texelData = data;
125   }
126 }
127 
loadTexture_OIIO(const std::string & fileName)128 void Texture2D::loadTexture_OIIO(const std::string &fileName)
129 {
130   auto in = ImageInput::open(fileName.c_str());
131   if (in) {
132     const ImageSpec &spec = in->spec();
133     const auto typeDesc = spec.format.elementtype();
134 
135     params.size = vec2ul(spec.width, spec.height);
136     params.components = spec.nchannels;
137     params.depth = spec.format.size();
138 
139     if (params.depth == 1)
140       loadTexture_OIIO_readFile<uint8_t>(in);
141     else if (params.depth == 2 && (typeDesc != TypeDesc::FLOAT))
142       loadTexture_OIIO_readFile<uint16_t>(in);
143     else if (params.depth == 4)
144       loadTexture_OIIO_readFile<float>(in);
145     else
146       std::cerr << "#osp:sg: INVALID Texture depth " << params.depth
147                 << std::endl;
148 
149     in->close();
150 #if OIIO_VERSION < 10903 && OIIO_VERSION > 10603
151     ImageInput::destroy(in);
152 #endif
153   }
154 
155   if (!texelData.get()) {
156     std::cerr << "#osp:sg: OpenImageIO failed to load texture '" << fileName
157               << "'" << std::endl;
158   }
159 }
160 
161 #else
162 //
163 // PFM
164 //
loadTexture_PFM_readFile(FILE * file,float scaleFactor)165 void Texture2D::loadTexture_PFM_readFile(FILE *file, float scaleFactor)
166 {
167   size_t size = params.size.product() * params.components;
168   std::shared_ptr<void> data(new float[size]);
169   const size_t dataSize = sizeof(size) * sizeof(float);
170 
171   int rc = fread(data.get(), dataSize, 1, file);
172   if (rc) {
173     // Scale texels by scale factor
174     float *texels = (float *)data.get();
175     for (size_t i = 0; i < params.size.product(); i++)
176       texels[i] *= scaleFactor;
177 
178     // Move shared_ptr ownership
179     texelData = data;
180   }
181 }
182 
loadTexture_PFM(const std::string & fileName)183 void Texture2D::loadTexture_PFM(const std::string &fileName)
184 {
185   FILE *file = nullptr;
186   try {
187     // Note: the PFM file specification does not support comments thus we
188     // don't skip any http://netpbm.sourceforge.net/doc/pfm.html
189     int rc = 0;
190     file = fopen(fileName.c_str(), "rb");
191     if (!file) {
192       throw std::runtime_error(
193           "#ospray_sg: could not open texture file '" + fileName + "'.");
194     }
195     // read format specifier:
196     // PF: color floating point image
197     // Pf: grayscale floating point image
198     char format[2] = {0};
199     if (fscanf(file, "%c%c\n", &format[0], &format[1]) != 2)
200       throw std::runtime_error("could not fscanf");
201 
202     if (format[0] != 'P' || (format[1] != 'F' && format[1] != 'f')) {
203       throw std::runtime_error(
204           "#ospray_sg: invalid pfm texture file, header is not PF or "
205           "Pf");
206     }
207 
208     params.components = 3;
209     if (format[1] == 'f') {
210       params.components = 1;
211     }
212 
213     // read width and height
214     int width = -1;
215     int height = -1;
216     rc = fscanf(file, "%i %i\n", &width, &height);
217     if (rc != 2 || width < 0 || height < 0) {
218       throw std::runtime_error(
219                 "#ospray_sg: could not parse width and height in PF PFM file "
220                 "'" +
221                 fileName +
222                 "'. "
223                 "Please report this bug at ospray.github.io, and include named "
224                 "file to reproduce the error.");
225     }
226 
227     // read scale factor/endiannes
228     float scaleEndian = 0.0;
229     rc = fscanf(file, "%f\n", &scaleEndian);
230 
231     if (rc != 1) {
232       throw std::runtime_error(
233                 "#ospray_sg: could not parse scale factor/endianness in PF "
234                 "PFM file '" +
235                 fileName +
236                 "'. "
237                 "Please report this bug at ospray.github.io, and include named "
238                 "file to reproduce the error.");
239     }
240     if (scaleEndian == 0.0) {
241       throw std::runtime_error(
242           "#ospray_sg: scale factor/endianness in PF PFM file can not be 0");
243     }
244     if (scaleEndian > 0.0) {
245       throw std::runtime_error(
246                 "#ospray_sg: could not parse PF PFM file '" + fileName +
247                 "': currently supporting only little endian formats"
248                 "Please report this bug at ospray.github.io, and include named "
249                 "file to reproduce the error.");
250     }
251 
252     float scaleFactor = std::abs(scaleEndian);
253     params.size = vec2ul(width, height);
254     params.depth = 4; // pfm is always float
255 
256     loadTexture_PFM_readFile(file, scaleFactor);
257 
258     if (!texelData.get())
259       std::cerr << "#osp:sg: INVALID FORMAT PFM " << params.components
260                 << std::endl;
261 
262   } catch (const std::runtime_error &e) {
263     std::cerr << "#osp:sg: INVALID PFM" << std::endl;
264     std::cerr << e.what() << std::endl;
265   }
266 
267   if (file)
268     fclose(file);
269 
270   if (!texelData.get()) {
271     std::cerr << "#osp:sg: PFM failed to load texture '" << fileName << "'"
272               << std::endl;
273   }
274 }
275 
276 //
277 // STBi
278 //
loadTexture_STBi(const std::string & fileName)279 void Texture2D::loadTexture_STBi(const std::string &fileName)
280 {
281   stbi_set_flip_vertically_on_load(params.flip);
282 
283   const bool isHDR = stbi_is_hdr(fileName.c_str());
284   const bool is16b = stbi_is_16_bit(fileName.c_str());
285 
286   void *texels{nullptr};
287   int width, height;
288   if (isHDR)
289     texels = (void *)stbi_loadf(
290         fileName.c_str(), &width, &height, &params.components, 0);
291   else if (is16b)
292     texels = (void *)stbi_load_16(
293         fileName.c_str(), &width, &height, &params.components, 0);
294   else
295     texels = (void *)stbi_load(
296         fileName.c_str(), &width, &height, &params.components, 0);
297 
298   // Set flip on load back to default, STBi maintains a static global.
299   stbi_set_flip_vertically_on_load(0);
300 
301   params.size = vec2ul(width, height);
302   params.depth = isHDR ? 4 : is16b ? 2 : 1;
303 
304   if (texels) {
305     // XXX stbi uses malloc/free override these with our alignedMalloc/Free
306     // (and implement a realloc?) to prevent this memcpy?
307     size_t size = params.size.product() * params.components * params.depth;
308     std::shared_ptr<void> data(new uint8_t[size]);
309     std::memcpy(data.get(), texels, size);
310     texelData = data;
311     stbi_image_free(texels);
312   }
313 
314   if (!texelData.get()) {
315     std::cerr << "#osp:sg: STB_image failed to load texture '" + fileName + "'"
316               << std::endl;
317     std::cerr << "#osp:sg: Rebuilding OSPRay Studio with OpenImageIO "
318               << "support may fix this error." << std::endl;
319   }
320 }
321 #endif
322 
323 // Texture2D UDIM ///////////////////////////////////////////////////////////
324 
325 // Check texture filename for udim pattern.  Then check that each tile file
326 // exists.  Image size/format will be checked on load.  This is a quick check.
checkForUDIM(FileName filename)327 bool Texture2D::checkForUDIM(FileName filename)
328 {
329   std::string fullName = filename.str();
330 
331   // Quick return if texture is already UDIM
332   if (hasUDIM())
333     return true;
334 
335   // Make sure base file even exists
336   std::ifstream f(fullName.c_str());
337   if (!f.good())
338     return false;
339 
340   // See if base tile "1001" is in the filename. If not, it's not a UDIM.
341   auto found = fullName.rfind("1001");
342   if (found == std::string::npos)
343     return false;
344 
345   // Strip off the "1001" and continue searching for other tiles
346   // by checking existing files of the correct pattern.
347   // This will work for most any consistent *1001* naming scheme.
348   // pattern:  lFileName<tileNum>rFileName
349   std::string lFileName = fullName.substr(0, found);
350   std::string rFileName = fullName.substr(found + 4);
351 
352   int vmax = 0;
353   int umax = 0;
354   for (int v = 1; v <= 10; v++)
355     for (int u = 1; u <= 10; u++) {
356       std::string tileNum = std::to_string(1000 + (v - 1) * 10 + u);
357       std::string checkName = lFileName + tileNum + rFileName;
358       std::ifstream f(checkName.c_str());
359       if (f.good()) {
360         udimTile tile(checkName, vec2i(u - 1, v - 1));
361         udim_params.tiles.push_back(tile);
362         vmax = std::max(vmax, v);
363         umax = std::max(umax, u);
364       }
365     }
366 
367   if (umax > 1) {
368     udim_params.dims.y = vmax;
369     udim_params.dims.x = vmax > 1 ? 10 : umax;
370   }
371 
372   return umax > 1;
373 }
374 
loadUDIM_tiles(const FileName & fileName)375 void Texture2D::loadUDIM_tiles(const FileName &fileName)
376 {
377   if (udim_params.tiles.size() < 2) {
378     std::cerr << "#osp:sg: loadUDIM_tiles: not a udim atlas" << std::endl;
379     return;
380   }
381 
382   // Create two temporary working nodes
383   // work contains each loaded tile and builds a texture atlas into main
384   auto atlas = &createChildAs<Texture2D>("udim_main", "texture_2d");
385   auto work = &createChildAs<Texture2D>("udim_work", "texture_2d");
386 
387   // Use the same params as parent texture
388   // but, mark as "loading" textures to skip re-checking udim tiles
389   work->params = params;
390   work->udim_params.loading = true;
391 
392   // Load the first tile to establish tile parameters
393   auto tile = udim_params.tiles.front();
394   work->load(tile.first);
395   udim_params.tiles.pop_front();
396 
397   auto tileSize = work->params.size;
398   auto tileDepth = work->params.depth;
399   auto tileComponents = work->params.components;
400   auto texelSize = tileDepth * tileComponents;
401   auto tileStride = tileSize.x * texelSize;
402 
403   // Allocate space large enough to hold all tiles (all tiles guaranteed to be
404   // of equal size and format)
405   atlas->params = work->params;
406   atlas->udim_params = work->udim_params;
407   atlas->params.size *= udim_params.dims;
408   std::shared_ptr<void> data(
409       new uint8_t[atlas->params.size.product() * texelSize]);
410   atlas->texelData = data;
411   auto atlasStride = atlas->params.size.x * texelSize;
412 
413   // Lambda to copy work tile into atlas
414   auto CopyTile = [&](vec2i origin) {
415     uint8_t *dest = (uint8_t *)data.get() + origin.y * tileSize.y * atlasStride
416         + origin.x * tileStride;
417     uint8_t *src = (uint8_t *)work->texelData.get();
418     for (int y = 0; y < tileSize.y; y++)
419       std::memcpy(dest + y * atlasStride, src + y * tileStride, tileStride);
420   };
421 
422   CopyTile(tile.second);
423 
424   // Load the remaining tiles into the atlas
425   for (const auto &tile : udim_params.tiles) {
426     work->load(tile.first);
427     // XXX TODO, allow different size/format tiles?
428     // This would require pre-loading all tiles and setting atlas to multiple
429     // of the largest size, then scaling all tiles into the atlas.
430     if (work->params.size != tileSize || work->params.depth != tileDepth
431         || work->params.components != tileComponents) {
432       std::cerr
433           << "#osp:sg: udim tile size or format doesn't match, skipping: "
434           << tile.first << std::endl;
435       continue;
436     }
437 
438     CopyTile(tile.second);
439 
440     // Don't keep tiles in the texture cache
441     textureCache.erase(tile.first);
442   }
443 
444   // Copy atlas back to parent
445   params = atlas->params;
446   texelData = atlas->texelData;
447   for (auto &c : atlas->children())
448     add(c.second);
449 
450   // Remove the temporary working nodes
451   remove("udim_work");
452   remove("udim_main");
453 }
454 
455 // Texture2D public methods /////////////////////////////////////////////////
456 
load(const FileName & _fileName,const bool _preferLinear,const bool _nearestFilter,const int _colorChannel)457 void Texture2D::load(const FileName &_fileName,
458     const bool _preferLinear,
459     const bool _nearestFilter,
460     const int _colorChannel)
461 {
462   fileName = _fileName;
463 
464   // Check the cache before creating a new texture
465   if (textureCache.find(fileName) != textureCache.end()) {
466     std::shared_ptr<Texture2D> cache = textureCache[fileName].lock();
467     if (cache) {
468       params = cache->params;
469       udim_params = cache->udim_params;
470       // Copy shared_ptr ownership
471       texelData = cache->texelData;
472     }
473   } else {
474     // Check if fileName indicates a UDIM atlas and load tiles
475     if (!udim_params.loading && checkForUDIM(fileName))
476       loadUDIM_tiles(fileName);
477     else {
478 #ifdef USE_OPENIMAGEIO
479       loadTexture_OIIO(fileName);
480 #else
481       if (_fileName.ext() == "pfm")
482         loadTexture_PFM(fileName);
483       else
484         loadTexture_STBi(fileName);
485 #endif
486     }
487 
488     // Add this texture to the cache
489     if (texelData.get())
490       textureCache[fileName] = this->nodeAs<Texture2D>();
491   }
492 
493   if (texelData.get()) {
494     params.preferLinear = _preferLinear;
495     params.nearestFilter = _nearestFilter;
496     params.colorChannel = _colorChannel;
497 
498     createDataNode();
499 
500     // If the load was successful, populate children
501     if (hasChild("data")) {
502       child("data").setSGNoUI();
503 
504       // If not using all channels, set used components to 1 for texture format
505       auto ospTexFormat =
506           osprayTextureFormat(params.colorChannel < 4 ? 1 : params.components);
507       auto texFilter = params.nearestFilter ? OSP_TEXTURE_FILTER_NEAREST
508                                             : OSP_TEXTURE_FILTER_BILINEAR;
509 
510       createChild("format", "int", (int)ospTexFormat);
511       createChild("filter", "int", (int)texFilter);
512 
513       createChild("filename", "string", fileName);
514       child("filename").setSGOnly();
515 
516       child("format").setMinMax((int)OSP_TEXTURE_RGBA8, (int)OSP_TEXTURE_R16);
517       child("filter").setMinMax(
518           (int)OSP_TEXTURE_FILTER_BILINEAR, (int)OSP_TEXTURE_FILTER_NEAREST);
519     } else
520       std::cerr << "Failed texture " << fileName << std::endl;
521   }
522 }
523 
load(void * memory,const bool _preferLinear,const bool _nearestFilter,const int _colorChannel)524 void Texture2D::load(void *memory,
525     const bool _preferLinear,
526     const bool _nearestFilter,
527     const int _colorChannel)
528 {
529   std::stringstream ss;
530   ss << "memory: " << std::hex << memory;
531   fileName = ss.str();
532 
533   // Check the cache before creating a new texture
534   if (textureCache.find(fileName) != textureCache.end()) {
535     std::shared_ptr<Texture2D> cache = textureCache[fileName].lock();
536     if (cache) {
537       params = cache->params;
538       udim_params = cache->udim_params;
539       // Copy shared_ptr ownership
540       texelData = cache->texelData;
541     }
542   } else {
543     if (memory) {
544       size_t size = params.size.product() * params.components * params.depth;
545       std::shared_ptr<void> data(new uint8_t[size]);
546       std::memcpy(data.get(), memory, size);
547       // Move shared_ptr ownership
548       texelData = data;
549 
550       // Add this texture to the cache
551       textureCache[fileName] = this->nodeAs<Texture2D>();
552     }
553   }
554 
555   if (texelData.get()) {
556     params.preferLinear = _preferLinear;
557     params.nearestFilter = _nearestFilter;
558     params.colorChannel = _colorChannel;
559 
560     createDataNode();
561 
562     // If the load was successful, populate children
563     if (hasChild("data")) {
564       child("data").setSGNoUI();
565 
566       // If not using all channels, set used components to 1 for texture format
567       auto ospTexFormat =
568           osprayTextureFormat(params.colorChannel < 4 ? 1 : params.components);
569       auto texFilter = params.nearestFilter ? OSP_TEXTURE_FILTER_NEAREST
570         : OSP_TEXTURE_FILTER_BILINEAR;
571 
572       createChild("format", "int", (int)ospTexFormat);
573       createChild("filter", "int", (int)texFilter);
574 
575       createChild("filename", "string", fileName);
576       child("filename").setSGOnly();
577 
578       child("format").setMinMax((int)OSP_TEXTURE_RGBA8, (int)OSP_TEXTURE_R16);
579       child("filter").setMinMax(
580           (int)OSP_TEXTURE_FILTER_BILINEAR, (int)OSP_TEXTURE_FILTER_NEAREST);
581     } else
582       std::cerr << "Failed texture " << fileName << std::endl;
583   }
584 }
585 
586 // Texture2D definitions ////////////////////////////////////////////////////
587 
Texture2D()588 Texture2D::Texture2D() : Texture("texture2d") {}
~Texture2D()589 Texture2D::~Texture2D()
590 {
591   textureCache.erase(fileName);
592 }
593 
preCommit()594 void Texture2D::preCommit()
595 {
596   // make sure to call base-class precommit
597   Texture::preCommit();
598 }
599 
postCommit()600 void Texture2D::postCommit()
601 {
602   Texture::postCommit();
603 }
604 
605 OSP_REGISTER_SG_NODE_NAME(Texture2D, texture_2d);
606 
607 std::map<std::string, std::weak_ptr<Texture2D>> Texture2D::textureCache;
608 
609 } // namespace sg
610 } // namespace ospray
611