| 1 |
|
| 2 |
|
| 3 |
#include "ZMBVEncoder.hh" |
| 4 |
|
| 5 |
#include "FrameSource.hh" |
| 6 |
#include "PixelOperations.hh" |
| 7 |
|
| 8 |
#include "cstd.hh" |
| 9 |
#include "endian.hh" |
| 10 |
#include "narrow.hh" |
| 11 |
#include "unreachable.hh" |
| 12 |
|
| 13 |
#include <algorithm> |
| 14 |
#include <array> |
| 15 |
#include <bit> |
| 16 |
#include <cassert> |
| 17 |
#include <cmath> |
| 18 |
#include <cstdint> |
| 19 |
#include <cstdlib> |
| 20 |
#include <cstring> |
| 21 |
#include <tuple> |
| 22 |
|
| 23 |
namespace openmsx { |
| 24 |
|
| 25 |
static constexpr uint8_t DBZV_VERSION_HIGH = 0; |
| 26 |
static constexpr uint8_t DBZV_VERSION_LOW = 1; |
| 27 |
static constexpr uint8_t COMPRESSION_ZLIB = 1; |
| 28 |
static constexpr unsigned MAX_VECTOR = 16; |
| 29 |
static constexpr unsigned BLOCK_WIDTH = MAX_VECTOR; |
| 30 |
static constexpr unsigned BLOCK_HEIGHT = MAX_VECTOR; |
| 31 |
static constexpr unsigned FLAG_KEYFRAME = 0x01; |
| 32 |
|
| 33 |
struct CodecVector { |
| 34 |
int8_t x; |
| 35 |
int8_t y; |
| 36 |
}; |
| 37 |
|
| 38 |
static constexpr unsigned VECTOR_TAB_SIZE = |
| 39 |
1 + |
| 40 |
8 * MAX_VECTOR + |
| 41 |
MAX_VECTOR * MAX_VECTOR - 2 * MAX_VECTOR; |
| 42 |
|
| 43 |
static constexpr auto vectorTable = [] { |
| 44 |
std::array<CodecVector, VECTOR_TAB_SIZE> result = {}; |
| 45 |
|
| 46 |
unsigned p = 0; |
| 47 |
|
| 48 |
result[p] = {.x = 0, .y = 0}; |
| 49 |
p += 1; |
| 50 |
|
| 51 |
for (int i = 1; i <= int(MAX_VECTOR); ++i, p += 8) { |
| 52 |
result[p + 0] = {.x = int8_t( i), .y = int8_t( 0)}; |
| 53 |
result[p + 1] = {.x = int8_t(-i), .y = int8_t( 0)}; |
| 54 |
result[p + 2] = {.x = int8_t( 0), .y = int8_t( i)}; |
| 55 |
result[p + 3] = {.x = int8_t( 0), .y = int8_t(-i)}; |
| 56 |
result[p + 4] = {.x = int8_t( i), .y = int8_t( i)}; |
| 57 |
result[p + 5] = {.x = int8_t(-i), .y = int8_t( i)}; |
| 58 |
result[p + 6] = {.x = int8_t( i), .y = int8_t(-i)}; |
| 59 |
result[p + 7] = {.x = int8_t(-i), .y = int8_t(-i)}; |
| 60 |
} |
| 61 |
|
| 62 |
for (int y = 1; y <= int(MAX_VECTOR / 2); ++y) { |
| 63 |
for (int x = 1; x <= int(MAX_VECTOR / 2); ++x) { |
| 64 |
if (x == y) continue; |
| 65 |
result[p + 0] = {.x = int8_t( x), .y = int8_t( y)}; |
| 66 |
result[p + 1] = {.x = int8_t(-x), .y = int8_t( y)}; |
| 67 |
result[p + 2] = {.x = int8_t( x), .y = int8_t(-y)}; |
| 68 |
result[p + 3] = {.x = int8_t(-x), .y = int8_t(-y)}; |
| 69 |
p += 4; |
| 70 |
} |
| 71 |
} |
| 72 |
assert(p == VECTOR_TAB_SIZE); |
| 73 |
|
| 74 |
|
| 75 |
auto compare = [](const CodecVector& l, const CodecVector& r) { |
| 76 |
auto cost = [](const CodecVector& v) { |
| 77 |
auto c = cstd::sqrt(double(v.x * v.x + v.y * v.y)); |
| 78 |
if ((v.x == 0) || (v.y == 0)) { |
| 79 |
|
| 80 |
c *= 1.0; |
| 81 |
} else if (cstd::abs(v.x) == cstd::abs(v.y)) { |
| 82 |
|
| 83 |
c *= 2.0; |
| 84 |
} else { |
| 85 |
|
| 86 |
c *= 4.0; |
| 87 |
} |
| 88 |
return c; |
| 89 |
}; |
| 90 |
return std::tuple(cost(l), l.x, l.y) < |
| 91 |
std::tuple(cost(r), r.x, r.y); |
| 92 |
}; |
| 93 |
std::ranges::sort(result, compare); |
| 94 |
|
| 95 |
return result; |
| 96 |
}(); |
| 97 |
|
| 98 |
struct KeyframeHeader { |
| 99 |
uint8_t high_version; |
| 100 |
uint8_t low_version; |
| 101 |
uint8_t compression; |
| 102 |
uint8_t format; |
| 103 |
uint8_t blockWidth; |
| 104 |
uint8_t blockHeight; |
| 105 |
}; |
| 106 |
|
| 107 |
|
| 108 |
static inline void writePixel( |
| 109 |
unsigned pixel, Endian::L32& dest) |
| 110 |
{ |
| 111 |
PixelOperations pixelOps; |
| 112 |
unsigned r = pixelOps.red(pixel); |
| 113 |
unsigned g = pixelOps.green(pixel); |
| 114 |
unsigned b = pixelOps.blue(pixel); |
| 115 |
dest = (r << 16) | (g << 8) | b; |
| 116 |
} |
| 117 |
|
| 118 |
|
| 119 |
ZMBVEncoder::ZMBVEncoder(unsigned width_, unsigned height_) |
| 120 |
: width(width_) |
| 121 |
, height(height_) |
| 122 |
{ |
| 123 |
setupBuffers(); |
| 124 |
memset(&zstream, 0, sizeof(zstream)); |
| 125 |
deflateInit(&zstream, 6); |
| 126 |
|
| 127 |
|
| 128 |
|
| 129 |
|
| 130 |
|
| 131 |
|
| 132 |
|
| 133 |
|
| 134 |
|
| 135 |
|
| 136 |
|
| 137 |
|
| 138 |
|
| 139 |
|
| 140 |
|
| 141 |
|
| 142 |
|
| 143 |
|
| 144 |
} |
| 145 |
|
| 146 |
void ZMBVEncoder::setupBuffers() |
| 147 |
{ |
| 148 |
static constexpr size_t pixelSize = sizeof(Pixel); |
| 149 |
|
| 150 |
pitch = width + 2 * MAX_VECTOR; |
| 151 |
auto bufSize = (height + 2 * MAX_VECTOR) * pitch * pixelSize + 2048; |
| 152 |
|
| 153 |
oldFrame.resize(bufSize); |
| 154 |
newFrame.resize(bufSize); |
| 155 |
std::ranges::fill(std::span{oldFrame}, 0); |
| 156 |
std::ranges::fill(std::span{newFrame}, 0); |
| 157 |
work.resize(bufSize); |
| 158 |
outputSize = neededSize(); |
| 159 |
output.resize(outputSize); |
| 160 |
|
| 161 |
assert((width % BLOCK_WIDTH ) == 0); |
| 162 |
assert((height % BLOCK_HEIGHT) == 0); |
| 163 |
size_t xBlocks = width / BLOCK_WIDTH; |
| 164 |
size_t yBlocks = height / BLOCK_HEIGHT; |
| 165 |
blockOffsets.resize(xBlocks * yBlocks); |
| 166 |
for (auto y : xrange(yBlocks)) { |
| 167 |
for (auto x : xrange(xBlocks)) { |
| 168 |
blockOffsets[y * xBlocks + x] = |
| 169 |
((y * BLOCK_HEIGHT) + MAX_VECTOR) * pitch + |
| 170 |
(x * BLOCK_WIDTH) + MAX_VECTOR; |
| 171 |
} |
| 172 |
} |
| 173 |
} |
| 174 |
|
| 175 |
unsigned ZMBVEncoder::neededSize() const |
| 176 |
{ |
| 177 |
static constexpr unsigned pixelSize = sizeof(Pixel); |
| 178 |
unsigned f = pixelSize * width * height + 2 * (1 + (width / 8)) * (1 + (height / 8)) + 1024; |
| 179 |
return f + f / 1000; |
| 180 |
} |
| 181 |
|
| 182 |
unsigned ZMBVEncoder::possibleBlock(int vx, int vy, size_t offset) |
| 183 |
{ |
| 184 |
int ret = 0; |
| 185 |
const auto* pOld = &(std::bit_cast<const Pixel*>(oldFrame.data()))[offset + (vy * pitch) + vx]; |
| 186 |
const auto* pNew = &(std::bit_cast<const Pixel*>(newFrame.data()))[offset]; |
| 187 |
for (unsigned y = 0; y < BLOCK_HEIGHT; y += 4) { |
| 188 |
for (unsigned x = 0; x < BLOCK_WIDTH; x += 4) { |
| 189 |
if (pOld[x] != pNew[x]) ++ret; |
| 190 |
} |
| 191 |
pOld += pitch * 4; |
| 192 |
pNew += pitch * 4; |
| 193 |
} |
| 194 |
return ret; |
| 195 |
} |
| 196 |
|
| 197 |
unsigned ZMBVEncoder::compareBlock(int vx, int vy, size_t offset) |
| 198 |
{ |
| 199 |
int ret = 0; |
| 200 |
const auto* pOld = &(std::bit_cast<const Pixel*>(oldFrame.data()))[offset + (vy * pitch) + vx]; |
| 201 |
const auto* pNew = &(std::bit_cast<const Pixel*>(newFrame.data()))[offset]; |
| 202 |
repeat(BLOCK_HEIGHT, [&] { |
| 203 |
for (auto x : xrange(BLOCK_WIDTH)) { |
| 204 |
if (pOld[x] != pNew[x]) ++ret; |
| 205 |
} |
| 206 |
pOld += pitch; |
| 207 |
pNew += pitch; |
| 208 |
}); |
| 209 |
return ret; |
| 210 |
} |
| 211 |
|
| 212 |
void ZMBVEncoder::addXorBlock(int vx, int vy, size_t offset, unsigned& workUsed) |
| 213 |
{ |
| 214 |
using LE_P = typename Endian::Little<Pixel>::type; |
| 215 |
|
| 216 |
const auto* pOld = &(std::bit_cast<const Pixel*>(oldFrame.data()))[offset + (vy * pitch) + vx]; |
| 217 |
const auto* pNew = &(std::bit_cast<const Pixel*>(newFrame.data()))[offset]; |
| 218 |
repeat(BLOCK_HEIGHT, [&] { |
| 219 |
for (auto x : xrange(BLOCK_WIDTH)) { |
| 220 |
auto pXor = pNew[x] ^ pOld[x]; |
| 221 |
writePixel(pXor, *std::bit_cast<LE_P*>(&work[workUsed])); |
| 222 |
workUsed += sizeof(Pixel); |
| 223 |
} |
| 224 |
pOld += pitch; |
| 225 |
pNew += pitch; |
| 226 |
}); |
| 227 |
} |
| 228 |
|
| 229 |
void ZMBVEncoder::addXorFrame(unsigned& workUsed) |
| 230 |
{ |
| 231 |
auto* vectors = std::bit_cast<int8_t*>(&work[workUsed]); |
| 232 |
|
| 233 |
unsigned xBlocks = width / BLOCK_WIDTH; |
| 234 |
unsigned yBlocks = height / BLOCK_HEIGHT; |
| 235 |
unsigned blockCount = xBlocks * yBlocks; |
| 236 |
|
| 237 |
|
| 238 |
workUsed = (workUsed + blockCount * 2 + 3) & ~3; |
| 239 |
|
| 240 |
int bestVx = 0; |
| 241 |
int bestVy = 0; |
| 242 |
for (auto b : xrange(blockCount)) { |
| 243 |
auto offset = blockOffsets[b]; |
| 244 |
|
| 245 |
unsigned bestChange = compareBlock(bestVx, bestVy, offset); |
| 246 |
if (bestChange >= 4) { |
| 247 |
int possibles = 64; |
| 248 |
for (const auto& v : vectorTable) { |
| 249 |
if (possibleBlock(v.x, v.y, offset) < 4) { |
| 250 |
if (auto testChange = compareBlock(v.x, v.y, offset); |
| 251 |
testChange < bestChange) { |
| 252 |
bestChange = testChange; |
| 253 |
bestVx = narrow<int>(v.x); |
| 254 |
bestVy = narrow<int>(v.y); |
| 255 |
if (bestChange < 4) break; |
| 256 |
} |
| 257 |
--possibles; |
| 258 |
if (possibles == 0) break; |
| 259 |
} |
| 260 |
} |
| 261 |
} |
| 262 |
vectors[b * 2 + 0] = narrow<int8_t>(bestVx << 1); |
| 263 |
vectors[b * 2 + 1] = narrow<int8_t>(bestVy << 1); |
| 264 |
if (bestChange) { |
| 265 |
vectors[b * 2 + 0] |= 1; |
| 266 |
addXorBlock(bestVx, bestVy, offset, workUsed); |
| 267 |
} |
| 268 |
} |
| 269 |
} |
| 270 |
|
| 271 |
void ZMBVEncoder::addFullFrame(unsigned& workUsed) |
| 272 |
{ |
| 273 |
using LE_P = typename Endian::Little<Pixel>::type; |
| 274 |
static constexpr size_t pixelSize = sizeof(Pixel); |
| 275 |
|
| 276 |
auto* readFrame = |
| 277 |
&newFrame[pixelSize * (MAX_VECTOR + MAX_VECTOR * pitch)]; |
| 278 |
repeat(height, [&] { |
| 279 |
const auto* pixelsIn = std::bit_cast<const Pixel*>(readFrame); |
| 280 |
auto* pixelsOut = std::bit_cast<LE_P*>(&work[workUsed]); |
| 281 |
for (auto x : xrange(width)) { |
| 282 |
writePixel(pixelsIn[x], pixelsOut[x]); |
| 283 |
} |
| 284 |
readFrame += pitch * sizeof(Pixel); |
| 285 |
workUsed += narrow<unsigned>(width * sizeof(Pixel)); |
| 286 |
}); |
| 287 |
} |
| 288 |
|
| 289 |
const ZMBVEncoder::Pixel* ZMBVEncoder::getScaledLine(const FrameSource* frame, unsigned y, Pixel* workBuf) const |
| 290 |
{ |
| 291 |
switch (height) { |
| 292 |
case 240: |
| 293 |
return frame->getLinePtr320_240(y, std::span<uint32_t, 320>(workBuf, 320)).data(); |
| 294 |
case 480: |
| 295 |
return frame->getLinePtr640_480(y, std::span<uint32_t, 640>(workBuf, 640)).data(); |
| 296 |
case 720: |
| 297 |
return frame->getLinePtr960_720(y, std::span<uint32_t, 960>(workBuf, 960)).data(); |
| 298 |
default: |
| 299 |
UNREACHABLE; |
| 300 |
} |
| 301 |
} |
| 302 |
|
| 303 |
std::span<const uint8_t> ZMBVEncoder::compressFrame(bool keyFrame, const FrameSource* frame) |
| 304 |
{ |
| 305 |
std::swap(newFrame, oldFrame); |
| 306 |
|
| 307 |
|
| 308 |
unsigned workUsed = 0; |
| 309 |
unsigned writeDone = 1; |
| 310 |
uint8_t* writeBuf = output.data(); |
| 311 |
|
| 312 |
output[0] = 0; |
| 313 |
if (keyFrame) { |
| 314 |
static const uint8_t ZMBV_FORMAT_32BPP = 8; |
| 315 |
|
| 316 |
output[0] |= FLAG_KEYFRAME; |
| 317 |
auto* header = std::bit_cast<KeyframeHeader*>( |
| 318 |
writeBuf + writeDone); |
| 319 |
header->high_version = DBZV_VERSION_HIGH; |
| 320 |
header->low_version = DBZV_VERSION_LOW; |
| 321 |
header->compression = COMPRESSION_ZLIB; |
| 322 |
header->format = ZMBV_FORMAT_32BPP; |
| 323 |
header->blockWidth = BLOCK_WIDTH; |
| 324 |
header->blockHeight = BLOCK_HEIGHT; |
| 325 |
writeDone += sizeof(KeyframeHeader); |
| 326 |
deflateReset(&zstream); |
| 327 |
} |
| 328 |
|
| 329 |
|
| 330 |
static constexpr size_t pixelSize = sizeof(Pixel); |
| 331 |
auto linePitch = pitch * pixelSize; |
| 332 |
auto lineWidth = size_t(width) * pixelSize; |
| 333 |
uint8_t* dest = |
| 334 |
&newFrame[pixelSize * (MAX_VECTOR + MAX_VECTOR * pitch)]; |
| 335 |
for (auto i : xrange(height)) { |
| 336 |
const auto* scaled = std::bit_cast<const uint8_t*>( |
| 337 |
getScaledLine(frame, i, std::bit_cast<Pixel*>(dest))); |
| 338 |
if (scaled != dest) memcpy(dest, scaled, lineWidth); |
| 339 |
dest += linePitch; |
| 340 |
} |
| 341 |
|
| 342 |
|
| 343 |
if (keyFrame) { |
| 344 |
|
| 345 |
addFullFrame(workUsed); |
| 346 |
} else { |
| 347 |
|
| 348 |
addXorFrame(workUsed); |
| 349 |
} |
| 350 |
|
| 351 |
zstream.next_in = work.data(); |
| 352 |
zstream.avail_in = workUsed; |
| 353 |
zstream.total_in = 0; |
| 354 |
|
| 355 |
zstream.next_out = std::bit_cast<Bytef*>(writeBuf + writeDone); |
| 356 |
zstream.avail_out = outputSize - writeDone; |
| 357 |
zstream.total_out = 0; |
| 358 |
auto r = deflate(&zstream, Z_SYNC_FLUSH); |
| 359 |
assert(r == Z_OK); (void)r; |
| 360 |
|
| 361 |
return {output.data(), writeDone + zstream.total_out}; |
| 362 |
} |
| 363 |
|
| 364 |
} |