Files
LookingGlass/client/transports/Network/video/pyrowave.c
2026-11-06 14:37:38 +11:00

325 lines
11 KiB
C

/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program 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 General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/*
* The packet layout and block ordering validated here are adapted from the
* PyroWave bitstream specification and reference parser at commit
* 89f7e47d4abbf650c91fae766728af866c5e32a0.
*
* Copyright (c) 2025 Hans-Kristian Arntzen
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
#include "pyrowave.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#define NETWORK_PYROWAVE_HEADER_SIZE 8U
#define NETWORK_PYROWAVE_DECOMPOSITION_LEVELS 5U
#define NETWORK_PYROWAVE_ALIGNMENT \
(1U << NETWORK_PYROWAVE_DECOMPOSITION_LEVELS)
#define NETWORK_PYROWAVE_MINIMUM_SIZE \
(4U << NETWORK_PYROWAVE_DECOMPOSITION_LEVELS)
#define NETWORK_PYROWAVE_SEQUENCE_MASK 7U
static uint32_t networkPyroWaveReadU32(const uint8_t * data)
{
return (uint32_t)data[0] |
(uint32_t)data[1] << 8 |
(uint32_t)data[2] << 16 |
(uint32_t)data[3] << 24;
}
static uint16_t networkPyroWaveReadU16(const uint8_t * data)
{
return (uint16_t)((uint16_t)data[0] | (uint16_t)data[1] << 8);
}
static unsigned networkPyroWaveBitCount(uint32_t value)
{
unsigned count = 0;
while (value)
{
count += value & 1U;
value >>= 1;
}
return count;
}
static bool networkPyroWavePacketValid(
const uint8_t * packet, size_t packetSize, uint16_t ballot)
{
const unsigned groups = networkPyroWaveBitCount(ballot);
const size_t controlBase = NETWORK_PYROWAVE_HEADER_SIZE;
const size_t scaleBase = controlBase + groups * sizeof(uint16_t);
const size_t planeBase = scaleBase + groups;
if (planeBase > packetSize)
return false;
size_t planeOffset = planeBase;
unsigned signs = 0;
for (unsigned group = 0; group < 16U; ++group)
{
if (!(ballot & (UINT32_C(1) << group)))
continue;
const unsigned compact = networkPyroWaveBitCount(
ballot & ((UINT32_C(1) << group) - 1U));
const uint16_t control = networkPyroWaveReadU16(
packet + controlBase + compact * sizeof(uint16_t));
const uint8_t qBits = packet[scaleBase + compact] & 15U;
for (unsigned subblock = 0; subblock < 8U; ++subblock)
{
const unsigned planes = qBits +
((control >> (2U * subblock)) & 3U);
if (planes > packetSize - planeOffset)
return false;
uint8_t nonzero = 0;
for (unsigned plane = 0; plane < planes; ++plane)
nonzero |= packet[planeOffset++];
signs += networkPyroWaveBitCount(nonzero);
}
}
const size_t signBytes = (signs + 7U) / 8U;
if (signBytes > packetSize - planeOffset)
return false;
/* payload_words rounds the packet to a u32 boundary. No other trailing
* payload is defined by version 1. */
return packetSize - planeOffset - signBytes < sizeof(uint32_t);
}
static uint32_t networkPyroWaveAlign(uint32_t value, uint32_t alignment)
{
return (value + alignment - 1U) & ~(alignment - 1U);
}
static bool networkPyroWaveBlockCapacity(
const LGNetVideoStreamConfig * config, uint32_t * capacity)
{
if (!config || !capacity)
return false;
uint32_t width = networkPyroWaveAlign(
config->width, NETWORK_PYROWAVE_ALIGNMENT);
uint32_t height = networkPyroWaveAlign(
config->height, NETWORK_PYROWAVE_ALIGNMENT);
if (width < NETWORK_PYROWAVE_MINIMUM_SIZE)
width = NETWORK_PYROWAVE_MINIMUM_SIZE;
if (height < NETWORK_PYROWAVE_MINIMUM_SIZE)
height = NETWORK_PYROWAVE_MINIMUM_SIZE;
uint64_t blocks = 0;
for (int level = NETWORK_PYROWAVE_DECOMPOSITION_LEVELS - 1;
level >= 0; --level)
{
const uint32_t levelWidth = width >> (level + 1);
const uint32_t levelHeight = height >> (level + 1);
const uint32_t blocksWide = (levelWidth + 31U) / 32U;
const uint32_t blocksHigh = (levelHeight + 31U) / 32U;
const uint32_t bands =
level == NETWORK_PYROWAVE_DECOMPOSITION_LEVELS - 1 ? 4U : 3U;
uint32_t components = 3U;
if (!level && config->chromaSubsampling == LG_NET_VIDEO_CHROMA_420)
components = 1U;
blocks += (uint64_t)blocksWide * blocksHigh * bands * components;
}
if (!blocks || blocks > LG_NET_VIDEO_MAX_BLOCKS)
return false;
*capacity = (uint32_t)blocks;
return true;
}
bool networkPyroWaveProfileValid(const LGNetVideoStreamConfig * config)
{
if (!config || !lgNetVideoStreamConfigValid(config) ||
config->codec != LG_NET_VIDEO_CODEC_PYROWAVE ||
config->codecVersion < LG_NET_VIDEO_PYROWAVE_CODEC_VERSION_MIN ||
config->codecVersion > LG_NET_VIDEO_PYROWAVE_CODEC_VERSION_MAX)
return false;
if (config->flags & LG_NET_VIDEO_STREAM_RESERVED_ALPHA)
return false;
const bool hdr =
(config->flags & LG_NET_VIDEO_STREAM_HDR) != 0;
if (hdr ?
(config->colorPrimaries != LG_NET_COLOR_PRIMARIES_BT2020 ||
config->colorTransfer != LG_NET_COLOR_TRANSFER_PQ ||
config->colorMatrix != LG_NET_COLOR_MATRIX_BT2020) :
(config->colorPrimaries != LG_NET_COLOR_PRIMARIES_BT709 ||
config->colorTransfer != LG_NET_COLOR_TRANSFER_BT709 ||
config->colorMatrix != LG_NET_COLOR_MATRIX_BT709))
return false;
uint32_t capacity;
return networkPyroWaveBlockCapacity(config, &capacity);
}
static bool networkPyroWaveSequenceProfile(
const LGNetVideoStreamConfig * config, uint32_t word)
{
const uint32_t chroma = (word >> 26) & 1U;
const uint32_t primaries = (word >> 27) & 1U;
const uint32_t transfer = (word >> 28) & 1U;
const uint32_t transform = (word >> 29) & 1U;
const uint32_t range = (word >> 30) & 1U;
const uint32_t wireChroma =
config->chromaSubsampling == LG_NET_VIDEO_CHROMA_444 ? 1U : 0U;
const uint32_t wirePrimaries =
config->colorPrimaries == LG_NET_COLOR_PRIMARIES_BT2020 ? 1U : 0U;
const uint32_t wireTransfer =
config->colorTransfer == LG_NET_COLOR_TRANSFER_PQ ? 1U : 0U;
const uint32_t wireTransform =
config->colorMatrix == LG_NET_COLOR_MATRIX_BT2020 ? 1U : 0U;
const uint32_t wireRange =
config->colorRange == LG_NET_COLOR_RANGE_LIMITED ? 1U : 0U;
return chroma == wireChroma &&
primaries == wirePrimaries &&
transfer == wireTransfer &&
transform == wireTransform &&
range == wireRange;
}
NetworkPyroWaveResult networkPyroWaveValidate(
const LGNetVideoStreamConfig * config, const void * data, size_t size,
uint32_t expectedBlocks, uint8_t * sequence)
{
if (!networkPyroWaveProfileValid(config))
return NETWORK_PYROWAVE_UNSUPPORTED;
if (!data || size < NETWORK_PYROWAVE_HEADER_SIZE || !expectedBlocks)
return NETWORK_PYROWAVE_INVALID;
uint32_t capacity;
if (!networkPyroWaveBlockCapacity(config, &capacity) ||
expectedBlocks > capacity)
return NETWORK_PYROWAVE_INVALID;
const size_t seenSize = ((size_t)capacity + 7U) / 8U;
uint8_t * seen = calloc(1, seenSize);
if (!seen)
return NETWORK_PYROWAVE_NO_MEMORY;
const uint8_t * bytes = data;
size_t remaining = size;
uint32_t uniqueBlocks = 0;
uint32_t declaredBlocks = 0;
uint8_t frameSequence = 0;
bool haveSequence = false;
bool haveHeader = false;
bool valid = true;
while (valid && remaining >= NETWORK_PYROWAVE_HEADER_SIZE)
{
const uint32_t first = networkPyroWaveReadU32(bytes);
const uint32_t second = networkPyroWaveReadU32(bytes + 4U);
const uint8_t packetSequence =
(uint8_t)((first >> 28) & NETWORK_PYROWAVE_SEQUENCE_MASK);
const bool extended = (first >> 31) != 0;
if (haveSequence && packetSequence != frameSequence)
{
valid = false;
break;
}
haveSequence = true;
frameSequence = packetSequence;
if (extended)
{
const uint32_t code = (second >> 24) & 3U;
const uint32_t width = (first & UINT32_C(0x00003FFF)) + 1U;
const uint32_t height = ((first >> 14) & UINT32_C(0x00003FFF)) + 1U;
const uint32_t blocks = second & UINT32_C(0x00FFFFFF);
if (code || width != config->width || height != config->height ||
!blocks || blocks > capacity ||
!networkPyroWaveSequenceProfile(config, second) ||
(haveHeader && blocks != declaredBlocks))
{
valid = false;
break;
}
declaredBlocks = blocks;
haveHeader = true;
bytes += NETWORK_PYROWAVE_HEADER_SIZE;
remaining -= NETWORK_PYROWAVE_HEADER_SIZE;
continue;
}
const uint32_t ballot = first & UINT32_C(0x0000FFFF);
const uint32_t payloadWords = (first >> 16) & UINT32_C(0x00000FFF);
const uint32_t blockIndex = second >> 8;
const size_t packetSize = (size_t)payloadWords * sizeof(uint32_t);
if (!ballot || payloadWords < 2U || packetSize > remaining ||
blockIndex >= capacity ||
!networkPyroWavePacketValid(bytes, packetSize, (uint16_t)ballot))
{
valid = false;
break;
}
const uint8_t mask = (uint8_t)(1U << (blockIndex & 7U));
if (!(seen[blockIndex >> 3] & mask))
{
seen[blockIndex >> 3] |= mask;
++uniqueBlocks;
}
bytes += packetSize;
remaining -= packetSize;
}
valid = valid && !remaining && haveHeader && haveSequence &&
declaredBlocks == uniqueBlocks && expectedBlocks == uniqueBlocks;
free(seen);
if (!valid)
return NETWORK_PYROWAVE_INVALID;
if (sequence)
*sequence = frameSequence;
return NETWORK_PYROWAVE_OK;
}