Line data Source code
1 0 : /*
2 :
3 : OOTextureLoader.m
4 :
5 :
6 : Copyright (C) 2007-2014 Jens Ayton
7 :
8 : Permission is hereby granted, free of charge, to any person obtaining a copy
9 : of this software and associated documentation files (the "Software"), to deal
10 : in the Software without restriction, including without limitation the rights
11 : to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 : copies of the Software, and to permit persons to whom the Software is
13 : furnished to do so, subject to the following conditions:
14 :
15 : The above copyright notice and this permission notice shall be included in all
16 : copies or substantial portions of the Software.
17 :
18 : THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 : IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 : FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 : AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 : LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 : OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 : SOFTWARE.
25 :
26 : */
27 :
28 : #import "OOPNGTextureLoader.h"
29 : #import "OOTextureLoader.h"
30 : #import "OOFunctionAttributes.h"
31 : #import "OOCollectionExtractors.h"
32 : #import "OOMaths.h"
33 : #import "Universe.h"
34 : #import "OOTextureScaling.h"
35 : #import "OOPixMapChannelOperations.h"
36 : #import "OOConvertCubeMapToLatLong.h"
37 : #include <stdlib.h>
38 : #import "ResourceManager.h"
39 : #import "OOOpenGLExtensionManager.h"
40 : #import "OODebugStandards.h"
41 :
42 :
43 0 : #define DUMP_CONVERTED_CUBE_MAPS 0
44 :
45 0 : enum
46 : {
47 : // Thresholds for reduced-detail texture shrinking in different circumstances.
48 : kNeverShrinkThreshold = UINT32_MAX,
49 : kDefaultShrinkThreshold = 512,
50 : kExtraShrinkThreshold = 128,
51 : kExtraShrinkMaxSize = 256,
52 : kCubeShrinkThreshold = 256
53 : };
54 :
55 :
56 0 : static unsigned sGLMaxSize;
57 0 : static uint32_t sUserMaxSize;
58 0 : static BOOL sReducedDetail;
59 0 : static BOOL sHaveNPOTTextures = NO; // TODO: support "true" non-power-of-two textures.
60 0 : static BOOL sHaveSetUp = NO;
61 :
62 :
63 : @interface OOTextureLoader (OOPrivate)
64 :
65 0 : + (void)setUp;
66 :
67 0 : - (void)applySettings;
68 0 : - (void)getDesiredWidth:(OOPixMapDimension *)outDesiredWidth andHeight:(OOPixMapDimension *)outDesiredHeight;
69 :
70 :
71 : @end
72 :
73 :
74 : @implementation OOTextureLoader
75 :
76 : + (id)loaderWithPath:(NSString *)inPath options:(uint32_t)options
77 : {
78 : NSString *extension = nil;
79 : id result = nil;
80 :
81 : if (EXPECT_NOT(inPath == nil)) return nil;
82 : if (EXPECT_NOT(!sHaveSetUp)) [self setUp];
83 :
84 : // Get reduced detail setting (every time, in case it changes; we don't want to call through to Universe on the loading thread in case the implementation becomes non-trivial).
85 : sReducedDetail = [UNIVERSE reducedDetail];
86 :
87 : // Get a suitable loader. FIXME -- this should sniff the data instead of relying on extensions.
88 : extension = [[inPath pathExtension] lowercaseString];
89 : if ([extension isEqualToString:@"png"])
90 : {
91 : result = [[[OOPNGTextureLoader alloc] initWithPath:inPath options:options] autorelease];
92 : }
93 : else
94 : {
95 : OOLog(@"texture.load.unknownType", @"Can't use %@ as a texture - extension \"%@\" does not identify a known type.", inPath, extension);
96 : }
97 :
98 : if (result != nil)
99 : {
100 : if (![[OOAsyncWorkManager sharedAsyncWorkManager] addTask:result priority:kOOAsyncPriorityMedium]) result = nil;
101 : }
102 :
103 : return result;
104 : }
105 :
106 :
107 : + (id)loaderWithTextureSpecifier:(id)specifier extraOptions:(uint32_t)extraOptions folder:(NSString *)folder
108 : {
109 : NSString *name = nil;
110 : NSString *path = nil;
111 : uint32_t options = 0;
112 :
113 : if (!OOInterpretTextureSpecifier(specifier, &name, &options, NULL, NULL, NO)) return nil;
114 : options |= extraOptions;
115 : path = [ResourceManager pathForFileNamed:name inFolder:folder];
116 : if (path == nil)
117 : {
118 : if (!(options & kOOTextureNoFNFMessage))
119 : {
120 : OOLogWARN(kOOLogFileNotFound, @"Could not find texture file \"%@\".", name);
121 : OOStandardsError(@"Texture file not found");
122 : }
123 : return nil;
124 : }
125 :
126 : return [self loaderWithPath:path options:options];
127 : }
128 :
129 :
130 : - (id)initWithPath:(NSString *)inPath options:(uint32_t)options
131 : {
132 : self = [super init];
133 : if (self == nil) return nil;
134 :
135 : _path = [inPath copy];
136 : if (EXPECT_NOT(_path == nil))
137 : {
138 : [self release];
139 : return nil;
140 : }
141 :
142 : _options = options;
143 :
144 : _maxSize = MIN(sUserMaxSize, sGLMaxSize);
145 :
146 : _generateMipMaps = (options & kOOTextureMinFilterMask) == kOOTextureMinFilterMipMap;
147 : _avoidShrinking = (options & kOOTextureNoShrink) != 0;
148 : _noScalingWhatsoever = (options & kOOTextureNeverScale) != 0;
149 : if (_avoidShrinking || _noScalingWhatsoever)
150 : {
151 : _shrinkThreshold = kNeverShrinkThreshold;
152 : }
153 : else if (options & kOOTextureExtraShrink)
154 : {
155 : _shrinkThreshold = kExtraShrinkThreshold;
156 : _maxSize = MIN(_maxSize, (uint32_t)kExtraShrinkMaxSize);
157 : }
158 : else {
159 : _shrinkThreshold = kDefaultShrinkThreshold;
160 : }
161 : #if OO_TEXTURE_CUBE_MAP
162 : _allowCubeMap = (options & kOOTextureAllowCubeMap) != 0;
163 : #endif
164 :
165 : if (options & kOOTextureExtractChannelMask)
166 : {
167 : _extractChannel = YES;
168 : switch (options & kOOTextureExtractChannelMask)
169 : {
170 : case kOOTextureExtractChannelR:
171 : _extractChannelIndex = 0;
172 : break;
173 :
174 : case kOOTextureExtractChannelG:
175 : _extractChannelIndex = 1;
176 : break;
177 :
178 : case kOOTextureExtractChannelB:
179 : _extractChannelIndex = 2;
180 : break;
181 :
182 : case kOOTextureExtractChannelA:
183 : _extractChannelIndex = 3;
184 : break;
185 :
186 : default:
187 : OOLogERR(@"texture.load.unknownExtractChannelMask", @"Unknown texture extract channel mask (0x%.4X). This is an internal error, please report it.", options & kOOTextureExtractChannelMask);
188 : _extractChannel = NO;
189 : }
190 : }
191 :
192 : return self;
193 : }
194 :
195 :
196 0 : - (void)dealloc
197 : {
198 : [_path autorelease];
199 : _path = nil;
200 : free(_data);
201 : _data = NULL;
202 :
203 : [super dealloc];
204 : }
205 :
206 :
207 0 : - (NSString *)descriptionComponents
208 : {
209 : NSString *state = nil;
210 :
211 : if (_ready)
212 : {
213 : if (_data != NULL) state = @"ready";
214 : else state = @"failed";
215 : }
216 : else
217 : {
218 : state = @"loading";
219 : #if INSTRUMENT_TEXTURE_LOADING
220 : if (debugHasLoaded) state = @"loaded";
221 : #endif
222 : }
223 :
224 : return [NSString stringWithFormat:@"{%@ -- %@}", _path, state];
225 : }
226 :
227 :
228 0 : - (NSString *)shortDescriptionComponents
229 : {
230 : return [_path lastPathComponent];
231 : }
232 :
233 :
234 : - (NSString *)path
235 : {
236 : return _path;
237 : }
238 :
239 :
240 : - (BOOL)isReady
241 : {
242 : return _ready;
243 : }
244 :
245 :
246 : - (BOOL) getResult:(OOPixMap *)result
247 : format:(OOTextureDataFormat *)outFormat
248 : originalWidth:(uint32_t *)outWidth
249 : originalHeight:(uint32_t *)outHeight
250 : {
251 : NSParameterAssert(result != NULL && outFormat != NULL);
252 :
253 : BOOL OK = YES;
254 :
255 : if (!_ready)
256 : {
257 : [[OOAsyncWorkManager sharedAsyncWorkManager] waitForTaskToComplete:self];
258 : }
259 : if (_data == NULL) OK = NO;
260 :
261 : if (OK)
262 : {
263 : *result = OOMakePixMap(_data, _width, _height, OOTextureComponentsForFormat(_format), 0, 0);
264 : _data = NULL;
265 : *outFormat = _format;
266 : OK = OOIsValidPixMap(*result);
267 : if (outWidth != NULL) *outWidth = _originalWidth;
268 : if (outHeight != NULL) *outHeight = _originalHeight;
269 : }
270 :
271 : if (!OK)
272 : {
273 : *result = kOONullPixMap;
274 : *outFormat = kOOTextureDataInvalid;
275 : }
276 :
277 : return OK;
278 : }
279 :
280 :
281 : - (NSString *) cacheKey
282 : {
283 : return [NSString stringWithFormat:@"%@:0x%.4X", [[self path] lastPathComponent], _options];
284 : }
285 :
286 :
287 : - (void)loadTexture
288 : {
289 : OOLogGenericSubclassResponsibility();
290 : }
291 :
292 :
293 0 : + (void)setUp
294 : {
295 : // Load two maximum sizes - graphics hardware limit and user-specified limit.
296 : GLint maxSize;
297 : OOGL(glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxSize));
298 : sGLMaxSize = MAX(maxSize, 64);
299 : OOLog(@"texture.load.rescale.maxSize", @"GL maximum texture size: %u", sGLMaxSize);
300 :
301 : // Why 0x80000000? Because it's the biggest number OORoundUpToPowerOf2() can handle.
302 : sUserMaxSize = [[NSUserDefaults standardUserDefaults] oo_unsignedIntForKey:@"max-texture-size" defaultValue:0x80000000];
303 : if (sUserMaxSize < 0x80000000) OOLog(@"texture.load.rescale.maxSize", @"User maximum texture size: %u", sUserMaxSize);
304 : sUserMaxSize = OORoundUpToPowerOf2_32(sUserMaxSize);
305 : sUserMaxSize = MAX(sUserMaxSize, 64U);
306 :
307 :
308 : sHaveSetUp = YES;
309 : }
310 :
311 :
312 : /*** Methods performed on the loader thread. ***/
313 :
314 0 : - (void)performAsyncTask
315 : {
316 : @try
317 : {
318 : OOLog(@"texture.load.asyncLoad", @"Loading texture %@", [_path lastPathComponent]);
319 :
320 : [self loadTexture];
321 :
322 : // Catch an error I've seen but not diagnosed yet.
323 : if (_data != NULL && OOTextureComponentsForFormat(_format) == 0)
324 : {
325 : OOLog(@"texture.load.failed.internalError", @"Texture loader internal error for %@: data is non-null but data format is invalid (%u).", _path, _format);
326 : free(_data);
327 : _data = NULL;
328 : }
329 :
330 : if (_data != NULL) [self applySettings];
331 :
332 : OOLog(@"texture.load.asyncLoad.done", @"%@", @"Loading complete.");
333 : }
334 : @catch (NSException *exception)
335 : {
336 : OOLog(@"texture.load.asyncLoad.exception", @"***** Exception loading texture %@: %@ (%@).", _path, [exception name], [exception reason]);
337 :
338 : // Be sure to signal load failure.
339 : free(_data);
340 : _data = NULL;
341 : }
342 : }
343 :
344 :
345 0 : - (void) generateMipMapsForCubeMap
346 : {
347 : // Generate mip maps for each cube face.
348 : NSParameterAssert(_data != NULL);
349 :
350 : uint8_t components = OOTextureComponentsForFormat(_format);
351 : size_t srcSideSize = _width * _width * components; // Space for one side without mip-maps.
352 : size_t newSideSize = srcSideSize * 4 / 3; // Space for one side with mip-maps.
353 : newSideSize = (newSideSize + 15) & ~15; // Round up to multiple of 16 bytes.
354 : size_t newSize = newSideSize * 6; // Space for all six sides.
355 :
356 : void *newData = malloc(newSize);
357 : if (EXPECT_NOT(newData == NULL))
358 : {
359 : _generateMipMaps = NO;
360 : _options = (_options & ~kOOTextureMinFilterMask) | kOOTextureMinFilterLinear;
361 : return;
362 : }
363 :
364 : unsigned i;
365 : for (i = 0; i < 6; i++)
366 : {
367 : void *srcBytes = ((uint8_t *)_data) + srcSideSize * i;
368 : void *dstBytes = ((uint8_t *)newData) + newSideSize * i;
369 :
370 : memcpy(dstBytes, srcBytes, srcSideSize);
371 : OOGenerateMipMaps(dstBytes, _width, _width, _format);
372 : }
373 :
374 : free(_data);
375 : _data = newData;
376 : }
377 :
378 :
379 0 : - (void)applySettings
380 : {
381 : OOPixMapDimension desiredWidth, desiredHeight;
382 : BOOL rescale;
383 : size_t newSize;
384 : uint8_t components;
385 : OOPixMap pixMap;
386 :
387 : components = OOTextureComponentsForFormat(_format);
388 :
389 : // Apply defaults.
390 : if (_originalWidth == 0) _originalWidth = _width;
391 : if (_originalHeight == 0) _originalHeight = _height;
392 : if (_rowBytes == 0) _rowBytes = _width * components;
393 :
394 : pixMap = OOMakePixMap(_data, _width, _height, components, _rowBytes, 0);
395 :
396 : if (_extractChannel)
397 : {
398 : if (OOExtractPixMapChannel(&pixMap, _extractChannelIndex, NO))
399 : {
400 : _format = kOOTextureDataGrayscale;
401 : components = 1;
402 : }
403 : else
404 : {
405 : OOLogWARN(@"texture.load.extractChannel.invalid", @"Cannot extract channel from texture \"%@\"", [_path lastPathComponent]);
406 : }
407 : }
408 :
409 : [self getDesiredWidth:&desiredWidth andHeight:&desiredHeight];
410 :
411 : if (_isCubeMap && !OOCubeMapsAvailable())
412 : {
413 : OOPixMapToRGBA(&pixMap);
414 : desiredHeight = MIN(desiredWidth * 2, 512U);
415 : if (sReducedDetail && desiredHeight > kCubeShrinkThreshold) desiredHeight /= 2;
416 : desiredWidth = desiredHeight * 2;
417 :
418 : OOPixMap converted = OOConvertCubeMapToLatLong(pixMap, desiredHeight, _generateMipMaps);
419 : OOFreePixMap(&pixMap);
420 : pixMap = converted;
421 : _isCubeMap = NO;
422 :
423 : #if DUMP_CONVERTED_CUBE_MAPS
424 : OODumpPixMap(pixMap, [NSString stringWithFormat:@"converted cube map %@", [[_path lastPathComponent] stringByDeletingPathExtension]]);
425 : #endif
426 : }
427 :
428 : // Rescale if needed.
429 : rescale = (_width != desiredWidth || _height != desiredHeight);
430 : if (rescale)
431 : {
432 : BOOL leaveSpaceForMipMaps = _generateMipMaps;
433 : #if OO_TEXTURE_CUBE_MAP
434 : if (_isCubeMap) leaveSpaceForMipMaps = NO;
435 : #endif
436 :
437 : OOLog(@"texture.load.rescale", @"Rescaling texture \"%@\" from %u x %u to %u x %u.", [_path lastPathComponent], pixMap.width, pixMap.height, desiredWidth, desiredHeight);
438 :
439 : pixMap = OOScalePixMap(pixMap, desiredWidth, desiredHeight, leaveSpaceForMipMaps);
440 : if (EXPECT_NOT(!OOIsValidPixMap(pixMap))) return;
441 :
442 : _data = pixMap.pixels;
443 : _width = pixMap.width;
444 : _height = pixMap.height;
445 : _rowBytes = pixMap.rowBytes;
446 : }
447 :
448 : #if OO_TEXTURE_CUBE_MAP
449 : if (_isCubeMap)
450 : {
451 : if (_generateMipMaps)
452 : {
453 : [self generateMipMapsForCubeMap];
454 : }
455 : return;
456 : }
457 : #endif
458 :
459 : // Generate mip maps if needed.
460 : if (_generateMipMaps)
461 : {
462 : // Make space if needed.
463 : newSize = desiredWidth * components * desiredHeight;
464 : newSize = (newSize * 4) / 3;
465 : // +1 to fix overrun valgrind spotted - CIM
466 : _generateMipMaps = OOExpandPixMap(&pixMap, newSize+1);
467 :
468 : _data = pixMap.pixels;
469 : _width = pixMap.width;
470 : _height = pixMap.height;
471 : _rowBytes = pixMap.rowBytes;
472 : }
473 : if (_generateMipMaps)
474 : {
475 : OOGenerateMipMaps(_data, _width, _height, _format);
476 : }
477 :
478 : // All done.
479 : }
480 :
481 :
482 0 : - (void)getDesiredWidth:(OOPixMapDimension *)outDesiredWidth andHeight:(OOPixMapDimension *)outDesiredHeight
483 : {
484 : OOPixMapDimension desiredWidth, desiredHeight;
485 :
486 : // Work out appropriate final size for textures.
487 : if (!_noScalingWhatsoever)
488 : {
489 : // Cube maps are six times as high as they are wide, and we need to preserve that.
490 : if (_allowCubeMap && _height == _width * 6)
491 : {
492 : _isCubeMap = YES;
493 :
494 : desiredWidth = OORoundUpToPowerOf2_PixMap((2 * _width) / 3);
495 : desiredWidth = MIN(desiredWidth, sGLMaxSize / 8);
496 : if (sReducedDetail)
497 : {
498 : if (256 < desiredWidth) desiredWidth /= 2;
499 : }
500 : desiredWidth = MIN(desiredWidth, sUserMaxSize / 4);
501 :
502 : desiredHeight = desiredWidth * 6;
503 : }
504 : else
505 : {
506 : if (!sHaveNPOTTextures)
507 : {
508 : // Round to nearest power of two. NOTE: this is duplicated in OOTextureVerifierStage.m.
509 : desiredWidth = OORoundUpToPowerOf2_PixMap((2 * _width) / 3);
510 : desiredHeight = OORoundUpToPowerOf2_PixMap((2 * _height) / 3);
511 : }
512 : else
513 : {
514 : desiredWidth = _width;
515 : desiredHeight = _height;
516 : }
517 :
518 : desiredWidth = MIN(desiredWidth, sGLMaxSize);
519 : desiredHeight = MIN(desiredHeight, sGLMaxSize);
520 :
521 : if (!_avoidShrinking)
522 : {
523 : if (sReducedDetail)
524 : {
525 : if (_shrinkThreshold < desiredWidth) desiredWidth /= 2;
526 : if (_shrinkThreshold < desiredHeight) desiredHeight /= 2;
527 : }
528 :
529 : desiredWidth = MIN(desiredWidth, _maxSize);
530 : desiredHeight = MIN(desiredHeight, _maxSize);
531 : }
532 : }
533 : }
534 : else
535 : {
536 : desiredWidth = _width;
537 : desiredHeight = _height;
538 : }
539 :
540 : if (outDesiredWidth != NULL) *outDesiredWidth = desiredWidth;
541 : if (outDesiredHeight != NULL) *outDesiredHeight = desiredHeight;
542 : }
543 :
544 :
545 0 : - (void) completeAsyncTask
546 : {
547 : _ready = YES;
548 : }
549 :
550 : @end
|