Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions indra/llappearanceutility/llappappearanceutility.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,7 @@ bool LLAppAppearanceUtility::init()
LL_DEBUGS() << "BakeSize: " << mBakeTextureSize << LL_ENDL;

// Initialize classes.
// Values taken from settings.xml.
const bool USE_TEXTURE_NEW_BYTE_RANGE=true;
const S32 TEXTURE_REVERSE_BYTE_RANGE=50;
LLImage::initClass(USE_TEXTURE_NEW_BYTE_RANGE, TEXTURE_REVERSE_BYTE_RANGE);
LLImage::initClass();
const bool SKIP_ANALYZE_ALPHA=true;

LLTranslationBridge::ptr_t trans = std::make_shared<LLPassthroughTranslationBridge>();
Expand Down
12 changes: 10 additions & 2 deletions indra/llimage/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ add_library(llimage STATIC)
target_sources(
llimage
PRIVATE
alj2clayout.cpp
llimagebmp.cpp
llimage.cpp
llimagedimensionsinfo.cpp
Expand All @@ -25,6 +26,7 @@ target_sources(
PUBLIC
FILE_SET HEADERS
FILES
alj2clayout.h
llimage.h
llimagebmp.h
llimagedimensionsinfo.h
Expand Down Expand Up @@ -67,6 +69,12 @@ if(AL_BUILD_TESTS)
LIBRARIES llmath llcommon
)

# The locks images are read and written under, over the library itself.
al_add_test_binary(llimage_tests PROJECT llimage SOURCES tests/llimage_test.cpp LIBRARIES llimage)
# The library itself: the locks images are read and written under, and the
# J2C codestream layout and the byte targets it gives.
al_add_test_binary(
llimage_tests
PROJECT llimage
SOURCES tests/llimage_test.cpp tests/alj2clayout_test.cpp
LIBRARIES llimage ll::openjpeg
)
endif()
204 changes: 204 additions & 0 deletions indra/llimage/alj2clayout.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/**
* @file alj2clayout.cpp
* @brief ALJ2CLayout: a JPEG 2000 codestream's layout, read from its headers
*
* $LicenseInfo:firstyear=2026&license=viewerlgpl$
* Alchemy Viewer Source Code
* Copyright (C) 2026, Rye <rye@alchemyviewer.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
* $/LicenseInfo$
*/

#include "linden_common.h"

#include "alj2clayout.h"

#include "llimage.h"

namespace
{
constexpr U8 MARKER = 0xFF;
constexpr U8 SOC = 0x4F;
constexpr U8 SIZ = 0x51;
constexpr U8 COD = 0x52;
constexpr U8 SOT = 0x90;

// The marker, Lsot, Isot, Psot, TPsot and TNsot.
constexpr S32 SOT_FIELDS_SIZE = 12;
// SOT's fields and the SOD that ends the shortest tile-part header.
constexpr S32 MIN_TILE_PART_SIZE = SOT_FIELDS_SIZE + 2;
constexpr S32 EOC_SIZE = 2;

constexpr S32 SIZ_MIN_BODY = 38;
constexpr S32 COD_MIN_BODY = 10;

// The fraction of a codestream that decodes at each discard level within 45 dB PSNR of
// the whole codestream's decode at that level, by the texture's longer side, for nine in
// ten Second Life textures of each size whether single or multi-layer. The quality
// layers' byte tiers are fixed, so they take a larger share of a smaller file.
struct StreamingFractions
{
S32 mMaxSide;
F64 mFraction[MAX_DISCARD_LEVEL + 1];
};
constexpr StreamingFractions STREAMING_FRACTIONS[] = {
{ 128, { 1.0, 0.90, 0.75, 0.60, 0.45, 0.45 } },
{ 256, { 1.0, 0.75, 0.50, 0.30, 0.14, 0.10 } },
{ 512, { 1.0, 0.70, 0.45, 0.20, 0.10, 0.06 } },
{ 1024, { 1.0, 0.65, 0.40, 0.12, 0.08, 0.04 } },
{ S32_MAX, { 1.0, 0.65, 0.30, 0.12, 0.05, 0.03 } },
};

F64 streamingFraction(S32 side, S32 discard)
{
for (const StreamingFractions& row : STREAMING_FRACTIONS)
{
if (side <= row.mMaxSide)
{
return row.mFraction[discard];
}
}
return 1.0;
}

U32 read16(const U8* p)
{
return (U32(p[0]) << 8) | U32(p[1]);
}

U32 read32(const U8* p)
{
return (U32(p[0]) << 24) | (U32(p[1]) << 16) | (U32(p[2]) << 8) | U32(p[3]);
}

U64 ceilDiv(U64 numerator, U64 denominator)
{
return (numerator + denominator - 1) / denominator;
}
}

// static
ALJ2CLayout ALJ2CLayout::parse(const U8* data, S32 size)
{
if (!data || size < 4 || data[0] != MARKER || data[1] != SOC)
{
return {};
}

ALJ2CLayout layout;
bool have_siz = false;
bool have_cod = false;
S32 pos = 2;
while (pos + 4 <= size && data[pos + 1] != SOT)
{
if (data[pos] != MARKER)
{
return {};
}

// The segment length counts itself but not the marker.
const S32 length = S32(read16(data + pos + 2));
if (length < 2 || pos + 2 + length > size)
{
return {};
}

const U8* body = data + pos + 4;
const S32 body_size = length - 2;
const U8 marker = data[pos + 1];
if (marker == SIZ)
{
if (body_size < SIZ_MIN_BODY)
{
return {};
}
const U64 x = read32(body + 2);
const U64 y = read32(body + 6);
const U64 x_origin = read32(body + 10);
const U64 y_origin = read32(body + 14);
const U64 tile_width = read32(body + 18);
const U64 tile_height = read32(body + 22);
const U64 tile_x_origin = read32(body + 26);
const U64 tile_y_origin = read32(body + 30);
if (x <= x_origin || y <= y_origin || x - x_origin > U16_MAX || y - y_origin > U16_MAX
|| !tile_width || !tile_height || x <= tile_x_origin || y <= tile_y_origin)
{
return {};
}
const U64 tiles = ceilDiv(x - tile_x_origin, tile_width) * ceilDiv(y - tile_y_origin, tile_height);
layout.mWidth = S32(x - x_origin);
layout.mHeight = S32(y - y_origin);
layout.mComponents = S32(read16(body + 34));
layout.mTiles = S32(llmin(tiles, U64(S32_MAX)));
have_siz = true;
}
else if (marker == COD)
{
if (body_size < COD_MIN_BODY)
{
return {};
}
layout.mProgression = body[1];
layout.mLayers = S32(read16(body + 2));
layout.mLevels = body[5];
have_cod = true;
}
pos += 2 + length;
}

if (!have_siz || !have_cod || pos + SOT_FIELDS_SIZE > size || data[pos] != MARKER || data[pos + 1] != SOT)
{
return {};
}

layout.mHeaderLength = pos;

const U32 tile_part_length = read32(data + pos + 6);
const U8 tile_parts = data[pos + 11];
if (layout.mTiles == 1 && tile_parts == 1 && tile_part_length >= MIN_TILE_PART_SIZE
&& tile_part_length <= U32(MAX_IMAGE_DATA_SIZE))
{
layout.mLength = pos + S32(tile_part_length) + EOC_SIZE;
}
return layout;
}

S32 ALJ2CLayout::calcDataSize(S32 discard) const
{
if (!isValid())
{
return 0;
}
discard = llclamp(discard, 0, MAX_DISCARD_LEVEL);
const S32 bytes = S32(F64(mLength) * streamingFraction(llmax(mWidth, mHeight), discard));
return llclamp(bytes, llmin(FIRST_PACKET_SIZE, mLength), mLength);
}

S32 ALJ2CLayout::calcDiscardLevelBytes(S32 bytes) const
{
if (!isValid())
{
return MAX_DISCARD_LEVEL;
}
for (S32 discard = 0; discard < MAX_DISCARD_LEVEL; ++discard)
{
if (bytes >= calcDataSize(discard))
{
return discard;
}
}
return MAX_DISCARD_LEVEL;
}
70 changes: 70 additions & 0 deletions indra/llimage/alj2clayout.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @file alj2clayout.h
* @brief ALJ2CLayout: a JPEG 2000 codestream's layout, read from its headers
*
* $LicenseInfo:firstyear=2026&license=viewerlgpl$
* Alchemy Viewer Source Code
* Copyright (C) 2026, Rye <rye@alchemyviewer.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
* $/LicenseInfo$
*/

#pragma once

#include "stdtypes.h"

// What a JPEG 2000 codestream's main header (SIZ, COD) and first tile-part header (SOT) say
// about it, and the bytes from its start that a decode at each discard level needs.
//
// The total length is known only for a codestream of one tile in one tile-part, the shape
// every Second Life texture has: the first SOT's Psot then runs to the EOC.
struct ALJ2CLayout
{
enum EProgression : S32
{
LRCP = 0,
RLCP = 1,
RPCL = 2,
PCRL = 3,
CPRL = 4,
};

S32 mWidth = 0;
S32 mHeight = 0;
S32 mComponents = 0;
S32 mTiles = 0;
S32 mProgression = -1;
S32 mLayers = 0;
// Decomposition levels; the codestream holds mLevels + 1 resolutions.
S32 mLevels = 0;
// Offset of the first SOT marker, which ends the main header.
S32 mHeaderLength = 0;
// The whole codestream, EOC included. 0 when the headers do not fix it.
S32 mLength = 0;

// Reads a codestream's headers from its first bytes. The layout is invalid when they
// stop short of the first SOT's fields or are not a codestream.
static ALJ2CLayout parse(const U8* data, S32 size);

bool isValid() const { return mLength > 0; }

// Bytes from the start of the codestream that decode at `discard` within 45 dB PSNR of
// a decode at that level from the whole of it.
S32 calcDataSize(S32 discard) const;

// The finest discard level the first `bytes` of the codestream decode.
S32 calcDiscardLevelBytes(S32 bytes) const;
};
8 changes: 3 additions & 5 deletions indra/llimage/llimage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -588,14 +588,10 @@ static void bilinear_scale(const U8 *src, U32 srcW, U32 srcH, U32 srcCh, U32 src

//static
thread_local std::string LLImage::sLastThreadErrorMessage;
bool LLImage::sUseNewByteRange = false;
S32 LLImage::sMinimalReverseByteRangePercent = 75;

//static
void LLImage::initClass(bool use_new_byte_range, S32 minimal_reverse_byte_range_percent)
void LLImage::initClass()
{
sUseNewByteRange = use_new_byte_range;
sMinimalReverseByteRangePercent = minimal_reverse_byte_range_percent;
}

//static
Expand Down Expand Up @@ -2503,6 +2499,7 @@ void LLImageFormatted::setData(U8 *data, S32 size)
setDataAndSize(data, size); // Access private LLImageBase members

sGlobalFormattedMemory += getDataSize();
onDataChanged();
}
}

Expand All @@ -2523,6 +2520,7 @@ void LLImageFormatted::appendData(U8 *data, S32 size)
reallocateData(newsize);
memcpy(getData() + cursize, data, size);
ll_aligned_free_16(data);
onDataChanged();
}
}
}
Expand Down
10 changes: 4 additions & 6 deletions indra/llimage/llimage.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,19 +100,14 @@ typedef enum e_image_codec
class LLImage
{
public:
static void initClass(bool use_new_byte_range = false, S32 minimal_reverse_byte_range_percent = 75);
static void initClass();
static void cleanupClass();

static const std::string& getLastThreadError();
static void setLastError(const std::string& message);

static bool useNewByteRange() { return sUseNewByteRange; }
static S32 getReverseByteRangePercent() { return sMinimalReverseByteRangePercent; }

protected:
static thread_local std::string sLastThreadErrorMessage;
static bool sUseNewByteRange;
static S32 sMinimalReverseByteRangePercent;
};

//============================================================================
Expand Down Expand Up @@ -475,6 +470,9 @@ class LLImageFormatted : public LLImageBase
protected:
bool copyData(U8 *data, S32 size); // calls updateData()

// Called with the data lock held once setData or appendData has changed the data.
virtual void onDataChanged() {}

protected:
S8 mCodec;
S8 mDecoding;
Expand Down
Loading
Loading