// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Ownable} from "solady/src/auth/Ownable.sol"; import {Base64} from "solady/src/utils/Base64.sol"; import {IColorgasmArtworkSource, IColorgasmBlobStorage, IColorgasmRenderer} from "./interfaces/IColorgasm.sol"; /// @title COLORGASM onchain renderer, v3 — single-pass assembly /// /// @notice Builds the whole token document in ONE buffer: the output is sized /// exactly, allocated once, and every byte is written into its final /// place and never moved again. v1 and v2 both built the image, then a /// base64 of it, then the JSON, then sometimes a base64 of that — four /// full-size buffers, and EVM memory is billed with the square of how /// far it stretches. /// /// Two storage shapes, chosen per deployment: /// /// PRE-ENCODED (`preEncoded = true`, the cheapest to render) — the /// bytes in storage are already the base64 of the image, so rendering /// is a straight `extcodecopy` of each chunk into the output. No /// encoding onchain at all. Costs 33% more to store, since base64 is /// 4 bytes per 3. /// /// RAW (`preEncoded = false`) — the bytes in storage are the image /// itself and the renderer base64s them. Because ColorgasmStorage caps /// chunks at a multiple of 3, each chunk encodes independently and the /// results concatenate exactly, so this still needs only one scratch /// chunk of working memory rather than a copy of the whole image. /// Cheapest to store; slightly more expensive to render. /// /// Both produce byte-identical documents. Measure both for your own /// sizes with `npm run ceiling` before committing to either. /// /// @dev Reads the artwork pointer (a storage key) and the metadata fragment /// from the extension, then the chunk pointers from the blob storage. /// Nothing here is token-aware beyond that, so it is a plain /// `setRenderer()` swap. contract ColorgasmRendererV3 is Ownable, IColorgasmRenderer { error NothingStored(); error ChunkNotAligned(uint256 index); /// @notice The redeem extension: artwork pointers and metadata fragments. IColorgasmArtworkSource public immutable source; /// @notice Where the artwork bytes live. IColorgasmBlobStorage public immutable blobs; /// @notice MIME type used for the image data URI. string public imageMime; /// @notice True when stored bytes are already base64 of the image. bool public immutable preEncoded; /// @notice When true the JSON document is base64-encoded (maximum /// compatibility); when false it is served as UTF-8, which is /// roughly half the gas and what makes large tokens renderable. bool public base64Json; constructor(address source_, address blobs_, bool preEncoded_) { _initializeOwner(msg.sender); source = IColorgasmArtworkSource(source_); blobs = IColorgasmBlobStorage(blobs_); preEncoded = preEncoded_; imageMime = "image/avif"; base64Json = true; } function setImageMime(string calldata imageMime_) external onlyOwner { imageMime = imageMime_; } /// @notice Switch the JSON document between base64 and plain UTF-8. function setBase64Json(bool base64Json_) external onlyOwner { base64Json = base64Json_; } /// @inheritdoc IColorgasmRenderer function tokenURI(address, uint256, uint256 originalTokenId) external view override returns (string memory) { if (base64Json) { bytes memory json = _assemble(originalTokenId, ""); return string(abi.encodePacked("data:application/json;base64,", Base64.encode(json))); } // The cheap path: the prefix is reserved at the front of the same // buffer, so the finished document is never copied again. return string(_assemble(originalTokenId, "data:application/json;utf8,")); } /// @notice Size in bytes of the document `tokenURI` will return, without /// building it. Useful to check a token against an RPC's response /// limits before anyone tries to load it. function renderedSize(uint256 originalTokenId) external view returns (uint256) { (, uint256 imageChars) = _imageLayout(originalTokenId); bytes memory meta = source.metadataOf(originalTokenId); uint256 metaLen = base64Json ? meta.length : _escapeForUri(meta).length; uint256 json = _headLength(metaLen) + imageChars + 2; return base64Json ? 28 + 4 * ((json + 2) / 3) : 26 + json; } // -------------------------------------------------------------- internal -- /// @dev `{` + fragment + `,` + `"image":"data:;base64,` function _headLength(uint256 metaLen) internal view returns (uint256) { // "{" + meta + ","(only when meta is present) // + '"image":"data:' (14) + mime + ';base64,' (8) return 1 + metaLen + (metaLen == 0 ? 0 : 1) + 14 + bytes(imageMime).length + 8; } /// @dev Chunk pointers plus how many base64 characters they will produce. function _imageLayout(uint256 originalTokenId) internal view returns (address[] memory pointers, uint256 imageChars) { pointers = blobs.chunksOf(source.artworkKeyOf(originalTokenId)); uint256 n = pointers.length; if (n == 0) revert NothingStored(); uint256 dataLen; for (uint256 i; i < n; ++i) { uint256 size = pointers[i].code.length - 1; // Every chunk but the last must be a multiple of 3, otherwise // encoding chunk by chunk would insert padding mid-image. if (!preEncoded && i + 1 < n && size % 3 != 0) revert ChunkNotAligned(i); dataLen += size; } imageChars = preEncoded ? dataLen : 4 * ((dataLen + 2) / 3); } /// @dev Build `{,"image":"data:;base64,"}` into a /// single exactly-sized buffer. function _assemble(uint256 originalTokenId, bytes memory prefix) internal view returns (bytes memory out) { (address[] memory pointers, uint256 imageChars) = _imageLayout(originalTokenId); bytes memory meta = source.metadataOf(originalTokenId); if (!base64Json) meta = _escapeForUri(meta); bytes memory head = meta.length == 0 ? abi.encodePacked("{", '"image":"data:', imageMime, ";base64,") : abi.encodePacked("{", meta, ",", '"image":"data:', imageMime, ";base64,"); uint256 total = prefix.length + head.length + imageChars + 2; assembly { out := mload(0x40) mstore(out, total) // Zero the final partial word so no stale memory can leak into the // string, then reserve the whole thing. let end := add(add(out, 0x20), total) mstore(end, 0) mstore(0x40, add(out, and(add(add(total, 0x20), 0x3f), not(0x1f)))) } uint256 dst; assembly { dst := add(out, 0x20) } dst = _copy(dst, prefix); dst = _copy(dst, head); uint256 n = pointers.length; for (uint256 i; i < n; ++i) { address pointer = pointers[i]; if (preEncoded) { // Straight copy: the stored bytes are already base64. assembly { let size := sub(extcodesize(pointer), 1) extcodecopy(pointer, dst, 1, size) dst := add(dst, size) } } else { // Encode this chunk on its own and copy the result in. The // scratch and the encoder's output are both released again by // restoring the free-memory pointer, so working memory stays at // one chunk however many there are. uint256 mark; assembly { mark := mload(0x40) } bytes memory raw; assembly { let size := sub(extcodesize(pointer), 1) raw := mload(0x40) mstore(raw, size) extcodecopy(pointer, add(raw, 0x20), 1, size) mstore(0x40, add(raw, and(add(add(size, 0x20), 0x3f), not(0x1f)))) } dst = _copy(dst, bytes(Base64.encode(raw))); assembly { mstore(0x40, mark) } } } _copy(dst, bytes('"}')); } /// @dev Copy `data` to `dst`, returning the position just past it. function _copy(uint256 dst, bytes memory data) internal pure returns (uint256) { assembly { let len := mload(data) let src := add(data, 0x20) for { let o := 0 } lt(o, len) { o := add(o, 0x20) } { mstore(add(dst, o), mload(add(src, o))) } dst := add(dst, len) } return dst; } /// @dev Percent-encode what would break a raw data URI. Every COLORGASM /// name contains a `#`, which starts the fragment and would truncate /// the document at the name; `%` starts an escape. Applied only to the /// metadata fragment — base64 output is URI-safe throughout. function _escapeForUri(bytes memory input) internal pure returns (bytes memory) { uint256 extra; for (uint256 i; i < input.length; ++i) { uint8 c = uint8(input[i]); if (c == 0x25 || c == 0x23 || c < 0x21 || c > 0x7e) extra += 2; } if (extra == 0) return input; bytes memory out = new bytes(input.length + extra); bytes16 hexDigits = "0123456789ABCDEF"; uint256 j; for (uint256 i; i < input.length; ++i) { uint8 c = uint8(input[i]); if (c == 0x25 || c == 0x23 || c < 0x21 || c > 0x7e) { out[j++] = "%"; out[j++] = hexDigits[c >> 4]; out[j++] = hexDigits[c & 0x0f]; } else { out[j++] = input[i]; } } return out; } }