Merge branch 'master' into medusa
3
CHANGES
|
@ -35,6 +35,7 @@ Emulation fixes:
|
|||
- GB MBC: Fix MBC1 RAM enable bit selection
|
||||
- GB MBC: Fix MBC2 bit selection
|
||||
- GB Video: Fix state after skipping BIOS (fixes mgba.io/i/1715 and mgba.io/i/1716)
|
||||
- GB Video: Always initialize palette
|
||||
- GBA: Fix timing advancing too quickly in rare cases
|
||||
- GBA BIOS: Implement dummy sound driver calls
|
||||
- GBA BIOS: Improve HLE BIOS timing
|
||||
|
@ -46,6 +47,7 @@ Emulation fixes:
|
|||
- GBA Video: Latch scanline at end of Hblank (fixes mgba.io/i/1319)
|
||||
- GBA Video: Fix Hblank timing
|
||||
- GBA Video: Fix invalid read in mode 4 mosaic
|
||||
- GBA Video: Fix color of disabled screen
|
||||
- SM83: Emulate HALT bug
|
||||
Other fixes:
|
||||
- All: Improve export headers (fixes mgba.io/i/1738)
|
||||
|
@ -55,6 +57,7 @@ Other fixes:
|
|||
- Core: Ensure ELF regions can be written before trying
|
||||
- Debugger: Don't skip undefined instructions when debugger attached
|
||||
- FFmpeg: Fix some small memory leaks
|
||||
- FFmpeg: Fix encoding of time base
|
||||
- GB Core: Fix extracting SRAM when none is present
|
||||
- GBA Savedata: Fix extracting save when not yet configured in-game
|
||||
- Qt: Force OpenGL paint engine creation thread (fixes mgba.io/i/1642)
|
||||
|
|
|
@ -60,6 +60,7 @@ if(NOT LIBMGBA_ONLY)
|
|||
set(BUILD_PERF OFF CACHE BOOL "Build performance profiling tool")
|
||||
set(BUILD_TEST OFF CACHE BOOL "Build testing harness")
|
||||
set(BUILD_SUITE OFF CACHE BOOL "Build test suite")
|
||||
set(BUILD_CINEMA OFF CACHE BOOL "Build video tests suite")
|
||||
set(BUILD_EXAMPLE OFF CACHE BOOL "Build example frontends")
|
||||
set(BUILD_PYTHON OFF CACHE BOOL "Build Python bindings")
|
||||
set(BUILD_STATIC OFF CACHE BOOL "Build a static library")
|
||||
|
@ -1250,6 +1251,7 @@ if(NOT QUIET AND NOT LIBMGBA_ONLY)
|
|||
message(STATUS " Profiling: ${BUILD_PERF}")
|
||||
message(STATUS " Test harness: ${BUILD_TEST}")
|
||||
message(STATUS " Test suite: ${BUILD_SUITE}")
|
||||
message(STATUS " Video test suite: ${BUILD_CINEMA}")
|
||||
message(STATUS " Python bindings: ${BUILD_PYTHON}")
|
||||
message(STATUS " Examples: ${BUILD_EXAMPLE}")
|
||||
message(STATUS "Cores:")
|
||||
|
|
Before Width: | Height: | Size: 466 B |
Before Width: | Height: | Size: 560 B After Width: | Height: | Size: 702 B |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.2 KiB |
|
@ -10,6 +10,8 @@
|
|||
|
||||
CXX_GUARD_START
|
||||
|
||||
#include <mgba/core/core.h>
|
||||
|
||||
#include <mgba-util/circle-buffer.h>
|
||||
|
||||
#define mVL_MAX_CHANNELS 32
|
||||
|
@ -115,7 +117,7 @@ void mVideoLogContextSetOutput(struct mVideoLogContext*, struct VFile*);
|
|||
void mVideoLogContextWriteHeader(struct mVideoLogContext*, struct mCore* core);
|
||||
|
||||
bool mVideoLogContextLoad(struct mVideoLogContext*, struct VFile*);
|
||||
void mVideoLogContextDestroy(struct mCore* core, struct mVideoLogContext*);
|
||||
void mVideoLogContextDestroy(struct mCore* core, struct mVideoLogContext*, bool closeVF);
|
||||
|
||||
void mVideoLogContextRewind(struct mVideoLogContext*, struct mCore*);
|
||||
void* mVideoLogContextInitialState(struct mVideoLogContext*, size_t* size);
|
||||
|
@ -128,6 +130,7 @@ void mVideoLoggerInjectVideoRegister(struct mVideoLogger* logger, uint32_t addre
|
|||
void mVideoLoggerInjectPalette(struct mVideoLogger* logger, uint32_t address, uint16_t value);
|
||||
void mVideoLoggerInjectOAM(struct mVideoLogger* logger, uint32_t address, uint16_t value);
|
||||
|
||||
enum mPlatform mVideoLogIsCompatible(struct VFile*);
|
||||
struct mCore* mVideoLogCoreFind(struct VFile*);
|
||||
|
||||
CXX_GUARD_END
|
||||
|
|
|
@ -56,7 +56,7 @@ enum {
|
|||
GBA_COLOR_WHITE = 0x7FFF,
|
||||
#endif
|
||||
#else
|
||||
GBA_COLOR_WHITE = 0x00F8F8F8,
|
||||
GBA_COLOR_WHITE = 0x00FFFFFF,
|
||||
#endif
|
||||
OFFSET_PRIORITY = 30,
|
||||
OFFSET_INDEX = 28,
|
||||
|
|
|
@ -369,6 +369,8 @@ bool FFmpegEncoderOpen(struct FFmpegEncoder* encoder, const char* outfile) {
|
|||
encoder->video->height = encoder->height;
|
||||
encoder->video->time_base = (AVRational) { encoder->frameCycles * encoder->frameskip, encoder->cycles };
|
||||
encoder->video->framerate = (AVRational) { encoder->cycles, encoder->frameCycles * encoder->frameskip };
|
||||
encoder->videoStream->time_base = encoder->video->time_base;
|
||||
encoder->videoStream->avg_frame_rate = encoder->video->framerate;
|
||||
encoder->video->pix_fmt = encoder->pixFormat;
|
||||
encoder->video->gop_size = 60;
|
||||
encoder->video->max_b_frames = 3;
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
#include <mgba/feature/video-logger.h>
|
||||
|
||||
#include <mgba/core/core.h>
|
||||
#include <mgba-util/memory.h>
|
||||
#include <mgba-util/vfs.h>
|
||||
#include <mgba-util/math.h>
|
||||
|
@ -724,7 +723,7 @@ static void _flushBuffer(struct mVideoLogContext* context) {
|
|||
}
|
||||
}
|
||||
|
||||
void mVideoLogContextDestroy(struct mCore* core, struct mVideoLogContext* context) {
|
||||
void mVideoLogContextDestroy(struct mCore* core, struct mVideoLogContext* context, bool closeVF) {
|
||||
if (context->write) {
|
||||
_flushBuffer(context);
|
||||
|
||||
|
@ -752,6 +751,10 @@ void mVideoLogContextDestroy(struct mCore* core, struct mVideoLogContext* contex
|
|||
#endif
|
||||
}
|
||||
|
||||
if (closeVF && context->backing) {
|
||||
context->backing->close(context->backing);
|
||||
}
|
||||
|
||||
free(context);
|
||||
}
|
||||
|
||||
|
@ -1033,7 +1036,7 @@ static ssize_t mVideoLoggerWriteChannel(struct mVideoLogChannel* channel, const
|
|||
return read;
|
||||
}
|
||||
|
||||
struct mCore* mVideoLogCoreFind(struct VFile* vf) {
|
||||
static const struct mVLDescriptor* _mVideoLogDescriptor(struct VFile* vf) {
|
||||
if (!vf) {
|
||||
return NULL;
|
||||
}
|
||||
|
@ -1055,6 +1058,25 @@ struct mCore* mVideoLogCoreFind(struct VFile* vf) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (descriptor->platform == PLATFORM_NONE) {
|
||||
return NULL;
|
||||
}
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
enum mPlatform mVideoLogIsCompatible(struct VFile* vf) {
|
||||
const struct mVLDescriptor* descriptor = _mVideoLogDescriptor(vf);
|
||||
if (descriptor) {
|
||||
return descriptor->platform;
|
||||
}
|
||||
return PLATFORM_NONE;
|
||||
}
|
||||
|
||||
struct mCore* mVideoLogCoreFind(struct VFile* vf) {
|
||||
const struct mVLDescriptor* descriptor = _mVideoLogDescriptor(vf);
|
||||
if (!descriptor) {
|
||||
return NULL;
|
||||
}
|
||||
struct mCore* core = NULL;
|
||||
if (descriptor->open) {
|
||||
core = descriptor->open();
|
||||
|
|
|
@ -1091,7 +1091,7 @@ static bool _GBVLPInit(struct mCore* core) {
|
|||
static void _GBVLPDeinit(struct mCore* core) {
|
||||
struct GBCore* gbcore = (struct GBCore*) core;
|
||||
if (gbcore->logContext) {
|
||||
mVideoLogContextDestroy(core, gbcore->logContext);
|
||||
mVideoLogContextDestroy(core, gbcore->logContext, true);
|
||||
}
|
||||
_GBCoreDeinit(core);
|
||||
}
|
||||
|
@ -1120,7 +1120,7 @@ static bool _GBVLPLoadROM(struct mCore* core, struct VFile* vf) {
|
|||
struct GBCore* gbcore = (struct GBCore*) core;
|
||||
gbcore->logContext = mVideoLogContextCreate(NULL);
|
||||
if (!mVideoLogContextLoad(gbcore->logContext, vf)) {
|
||||
mVideoLogContextDestroy(core, gbcore->logContext);
|
||||
mVideoLogContextDestroy(core, gbcore->logContext, false);
|
||||
gbcore->logContext = NULL;
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -213,6 +213,8 @@ static void GBVideoSoftwareRendererInit(struct GBVideoRenderer* renderer, enum G
|
|||
softwareRenderer->lookup[i] = i;
|
||||
softwareRenderer->lookup[i] = i;
|
||||
}
|
||||
|
||||
memset(softwareRenderer->palette, 0, sizeof(softwareRenderer->palette));
|
||||
}
|
||||
|
||||
static void GBVideoSoftwareRendererDeinit(struct GBVideoRenderer* renderer) {
|
||||
|
|
|
@ -894,6 +894,9 @@ void GBVideoDeserialize(struct GBVideo* video, const struct GBSerializedState* s
|
|||
mTimingSchedule(&video->p->timing, &video->frameEvent, when);
|
||||
}
|
||||
|
||||
video->renderer->deinit(video->renderer);
|
||||
video->renderer->init(video->renderer, video->p->model, video->sgbBorders);
|
||||
|
||||
size_t i;
|
||||
for (i = 0; i < 64; ++i) {
|
||||
LOAD_16LE(video->palette[i], i * 2, state->video.palette);
|
||||
|
@ -905,7 +908,4 @@ void GBVideoDeserialize(struct GBVideo* video, const struct GBSerializedState* s
|
|||
|
||||
_cleanOAM(video, video->ly);
|
||||
GBVideoSwitchBank(video, video->vramCurrentBank);
|
||||
|
||||
video->renderer->deinit(video->renderer);
|
||||
video->renderer->init(video->renderer, video->p->model, video->sgbBorders);
|
||||
}
|
||||
|
|
|
@ -1248,7 +1248,7 @@ static bool _GBAVLPInit(struct mCore* core) {
|
|||
static void _GBAVLPDeinit(struct mCore* core) {
|
||||
struct GBACore* gbacore = (struct GBACore*) core;
|
||||
if (gbacore->logContext) {
|
||||
mVideoLogContextDestroy(core, gbacore->logContext);
|
||||
mVideoLogContextDestroy(core, gbacore->logContext, true);
|
||||
}
|
||||
_GBACoreDeinit(core);
|
||||
}
|
||||
|
@ -1277,7 +1277,7 @@ static bool _GBAVLPLoadROM(struct mCore* core, struct VFile* vf) {
|
|||
struct GBACore* gbacore = (struct GBACore*) core;
|
||||
gbacore->logContext = mVideoLogContextCreate(NULL);
|
||||
if (!mVideoLogContextLoad(gbacore->logContext, vf)) {
|
||||
mVideoLogContextDestroy(core, gbacore->logContext);
|
||||
mVideoLogContextDestroy(core, gbacore->logContext, false);
|
||||
gbacore->logContext = NULL;
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -8,15 +8,17 @@ Output = namedtuple('Output', ['video'])
|
|||
class Tracer(object):
|
||||
def __init__(self, core):
|
||||
self.core = core
|
||||
self.framebuffer = Image(*core.desired_video_dimensions())
|
||||
self.core.set_video_buffer(self.framebuffer)
|
||||
self._video_fifo = []
|
||||
|
||||
def yield_frames(self, skip=0, limit=None):
|
||||
self.framebuffer = Image(*self.core.desired_video_dimensions())
|
||||
self.core.set_video_buffer(self.framebuffer)
|
||||
self.core.reset()
|
||||
skip = (skip or 0) + 1
|
||||
while skip > 0:
|
||||
frame = self.core.frame_counter
|
||||
self.framebuffer = Image(*self.core.desired_video_dimensions())
|
||||
self.core.set_video_buffer(self.framebuffer)
|
||||
self.core.run_frame()
|
||||
skip -= 1
|
||||
while frame <= self.core.frame_counter and limit != 0:
|
||||
|
|
|
@ -3,7 +3,6 @@ import os.path
|
|||
import mgba.core
|
||||
import mgba.image
|
||||
import cinema.movie
|
||||
import itertools
|
||||
import glob
|
||||
import re
|
||||
from copy import deepcopy
|
||||
|
@ -73,7 +72,7 @@ class VideoTest(CinemaTest):
|
|||
self.tracer = cinema.movie.Tracer(self.core)
|
||||
|
||||
def generate_frames(self):
|
||||
for i, frame in zip(itertools.count(), self.tracer.video(**self.output_settings())):
|
||||
for i, frame in enumerate(self.tracer.video(**self.output_settings())):
|
||||
try:
|
||||
baseline = VideoFrame.load(os.path.join(self.path, self.BASELINE % i))
|
||||
yield baseline, frame, VideoFrame.diff(baseline, frame)
|
||||
|
@ -85,7 +84,7 @@ class VideoTest(CinemaTest):
|
|||
assert not any(any(diffs[0].image.convert("L").point(bool).getdata()) for diffs in self.diffs)
|
||||
|
||||
def generate_baseline(self):
|
||||
for i, frame in zip(itertools.count(), self.tracer.video(**self.output_settings())):
|
||||
for i, frame in enumerate(self.tracer.video(**self.output_settings())):
|
||||
frame.save(os.path.join(self.path, self.BASELINE % i))
|
||||
|
||||
|
||||
|
|
|
@ -859,9 +859,8 @@ void CoreController::endVideoLog(bool closeVf) {
|
|||
}
|
||||
|
||||
Interrupter interrupter(this);
|
||||
mVideoLogContextDestroy(m_threadContext.core, m_vl);
|
||||
if (m_vlVf && closeVf) {
|
||||
m_vlVf->close(m_vlVf);
|
||||
mVideoLogContextDestroy(m_threadContext.core, m_vl, closeVf);
|
||||
if (closeVf) {
|
||||
m_vlVf = nullptr;
|
||||
}
|
||||
m_vl = nullptr;
|
||||
|
|
|
@ -1512,11 +1512,6 @@ Game Boy Advance 是任天堂有限公司(Nintendo Co., Ltd.)的注册商标
|
|||
<source>Add CodeBreaker</source>
|
||||
<translation>添加 CodeBreaker</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../CheatsView.cpp" line="74"/>
|
||||
<source>Add GameShark</source>
|
||||
<translation>添加 GameShark</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../CheatsView.cpp" line="80"/>
|
||||
<source>Add GameGenie</source>
|
||||
|
@ -1562,7 +1557,7 @@ Game Boy Advance 是任天堂有限公司(Nintendo Co., Ltd.)的注册商标
|
|||
<message>
|
||||
<location filename="../CoreManager.cpp" line="86"/>
|
||||
<source>Could not load game. Are you sure it's in the correct format?</source>
|
||||
<translation>无法载入游戏。请确认游戏格式是否无误。</translation>
|
||||
<translation>无法载入游戏。是否确认游戏格式无误?</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
|
@ -1696,8 +1691,8 @@ Game Boy Advance 是任天堂有限公司(Nintendo Co., Ltd.)的注册商标
|
|||
</message>
|
||||
<message>
|
||||
<location filename="../GIFView.cpp" line="81"/>
|
||||
<source>Graphics Interchange Format (*.gif);;Animated Portable Network Graphics (*.png *.apng)"</source>
|
||||
<translation>图形交换格式 (*.gif);;动画便携式网络图形 (*.png *.apng)"</translation>
|
||||
<source>Graphics Interchange Format (*.gif);;Animated Portable Network Graphics (*.png *.apng)"</source>
|
||||
<translation>图形交换格式 (*.gif);;动画便携式网络图形 (*.png *.apng)"</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
|
@ -3947,7 +3942,7 @@ Game Boy Advance 是任天堂有限公司(Nintendo Co., Ltd.)的注册商标
|
|||
<location filename="../Window.cpp" line="1191"/>
|
||||
<location filename="../Window.cpp" line="1196"/>
|
||||
<source>State &%1</source>
|
||||
<translation>即时存档 1(&1)</translation>
|
||||
<translation>即时存档 (&%1)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../Window.cpp" line="1203"/>
|
||||
|
@ -5318,21 +5313,11 @@ Game Boy Advance 是任天堂有限公司(Nintendo Co., Ltd.)的注册商标
|
|||
<source>MKV</source>
|
||||
<translation>MKV</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../VideoView.ui" line="251"/>
|
||||
<source>WebM</source>
|
||||
<translation>WebM</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../VideoView.ui" line="256"/>
|
||||
<source>AVI</source>
|
||||
<translation>AVI</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../VideoView.ui" line="21"/>
|
||||
<source>MP4</source>
|
||||
<translation>MP4</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../VideoView.ui" line="273"/>
|
||||
<source>h.264</source>
|
||||
|
|
|
@ -37,3 +37,10 @@ if(BUILD_SUITE)
|
|||
add_test(${TEST_NAME} test-${TEST_NAME})
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
if(BUILD_CINEMA)
|
||||
enable_testing()
|
||||
add_executable(${BINARY_NAME}-cinema ${CMAKE_CURRENT_SOURCE_DIR}/cinema-main.c)
|
||||
target_link_libraries(${BINARY_NAME}-cinema ${BINARY_NAME} ${PLATFORM_LIBRARY})
|
||||
set_target_properties(${BINARY_NAME}-cinema PROPERTIES COMPILE_DEFINITIONS "${OS_DEFINES};${FEATURE_DEFINES};${FUNCTION_DEFINES}")
|
||||
endif()
|
||||
|
|
|
@ -0,0 +1,850 @@
|
|||
/* Copyright (c) 2013-2020 Jeffrey Pfau
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
#include <mgba/core/config.h>
|
||||
#include <mgba/core/core.h>
|
||||
#include <mgba/core/log.h>
|
||||
#include <mgba/core/version.h>
|
||||
#include <mgba/feature/commandline.h>
|
||||
#include <mgba/feature/video-logger.h>
|
||||
|
||||
#include <mgba-util/png-io.h>
|
||||
#include <mgba-util/table.h>
|
||||
#include <mgba-util/vector.h>
|
||||
#include <mgba-util/vfs.h>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#include <mgba-util/platform/windows/getopt.h>
|
||||
#else
|
||||
#include <getopt.h>
|
||||
#endif
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#define MAX_TEST 200
|
||||
|
||||
static const struct option longOpts[] = {
|
||||
{ "base", required_argument, 0, 'b' },
|
||||
{ "diffs", no_argument, 0, 'd' },
|
||||
{ "help", no_argument, 0, 'h' },
|
||||
{ "dry-run", no_argument, 0, 'n' },
|
||||
{ "outdir", required_argument, 0, 'o' },
|
||||
{ "quiet", no_argument, 0, 'q' },
|
||||
{ "rebaseline", no_argument, 0, 'r' },
|
||||
{ "verbose", no_argument, 0, 'v' },
|
||||
{ "version", no_argument, 0, '\0' },
|
||||
{ 0, 0, 0, 0 }
|
||||
};
|
||||
|
||||
static const char shortOpts[] = "b:dhno:qrv";
|
||||
|
||||
enum CInemaStatus {
|
||||
CI_PASS,
|
||||
CI_FAIL,
|
||||
CI_XPASS,
|
||||
CI_XFAIL,
|
||||
CI_ERROR,
|
||||
CI_SKIP
|
||||
};
|
||||
|
||||
struct CInemaTest {
|
||||
char directory[MAX_TEST];
|
||||
char filename[MAX_TEST];
|
||||
char name[MAX_TEST];
|
||||
enum CInemaStatus status;
|
||||
unsigned failedFrames;
|
||||
uint64_t failedPixels;
|
||||
unsigned totalFrames;
|
||||
uint64_t totalDistance;
|
||||
uint64_t totalPixels;
|
||||
};
|
||||
|
||||
struct CInemaImage {
|
||||
void* data;
|
||||
unsigned width;
|
||||
unsigned height;
|
||||
unsigned stride;
|
||||
};
|
||||
|
||||
DECLARE_VECTOR(CInemaTestList, struct CInemaTest)
|
||||
DEFINE_VECTOR(CInemaTestList, struct CInemaTest)
|
||||
|
||||
DECLARE_VECTOR(ImageList, void*)
|
||||
DEFINE_VECTOR(ImageList, void*)
|
||||
|
||||
static bool showVersion = false;
|
||||
static bool showUsage = false;
|
||||
static char base[PATH_MAX] = {0};
|
||||
static char outdir[PATH_MAX] = {'.'};
|
||||
static bool dryRun = false;
|
||||
static bool diffs = false;
|
||||
static bool rebaseline = false;
|
||||
static int verbosity = 0;
|
||||
|
||||
bool CInemaTestInit(struct CInemaTest*, const char* directory, const char* filename);
|
||||
void CInemaTestRun(struct CInemaTest*, struct Table* configTree);
|
||||
|
||||
bool CInemaConfigGetUInt(struct Table* configTree, const char* testName, const char* key, unsigned* value);
|
||||
void CInemaConfigLoad(struct Table* configTree, const char* testName, struct mCore* core);
|
||||
|
||||
static void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args);
|
||||
|
||||
ATTRIBUTE_FORMAT(printf, 2, 3) void CIlog(int minlevel, const char* format, ...) {
|
||||
if (verbosity < minlevel) {
|
||||
return;
|
||||
}
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vprintf(format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
ATTRIBUTE_FORMAT(printf, 2, 3) void CIerr(int minlevel, const char* format, ...) {
|
||||
if (verbosity < minlevel) {
|
||||
return;
|
||||
}
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vfprintf(stderr, format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
static bool parseCInemaArgs(int argc, char* const* argv) {
|
||||
int ch;
|
||||
int index = 0;
|
||||
while ((ch = getopt_long(argc, argv, shortOpts, longOpts, &index)) != -1) {
|
||||
const struct option* opt = &longOpts[index];
|
||||
switch (ch) {
|
||||
case '\0':
|
||||
if (strcmp(opt->name, "version") == 0) {
|
||||
showVersion = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'b':
|
||||
strncpy(base, optarg, sizeof(base));
|
||||
// TODO: Verify path exists
|
||||
break;
|
||||
case 'd':
|
||||
diffs = true;
|
||||
break;
|
||||
case 'h':
|
||||
showUsage = true;
|
||||
break;
|
||||
case 'n':
|
||||
dryRun = true;
|
||||
break;
|
||||
case 'o':
|
||||
strncpy(outdir, optarg, sizeof(outdir));
|
||||
// TODO: Make directory
|
||||
break;
|
||||
case 'q':
|
||||
--verbosity;
|
||||
break;
|
||||
case 'r':
|
||||
rebaseline = true;
|
||||
break;
|
||||
case 'v':
|
||||
++verbosity;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void usageCInema(const char* arg0) {
|
||||
printf("usage: %s [-dhnqv] [-b BASE] [-o DIR] [--version] [test...]\n", arg0);
|
||||
puts(" -b, --base [BASE] Path to the CInema base directory");
|
||||
puts(" -d, --diffs Output image diffs from failures");
|
||||
puts(" -h, --help Print this usage and exit");
|
||||
puts(" -n, --dry-run List all collected tests instead of running them");
|
||||
puts(" -o, --output [DIR] Path to output applicable results");
|
||||
puts(" -q, --quiet Decrease log verbosity (can be repeated)");
|
||||
puts(" -r, --rebaseline Rewrite the baseline for failing tests");
|
||||
puts(" -v, --verbose Increase log verbosity (can be repeated)");
|
||||
puts(" --version Print version and exit");
|
||||
}
|
||||
|
||||
static bool determineBase(int argc, char* const* argv) {
|
||||
// TODO: Better dynamic detection
|
||||
separatePath(__FILE__, base, NULL, NULL);
|
||||
strncat(base, PATH_SEP ".." PATH_SEP ".." PATH_SEP ".." PATH_SEP "cinema", sizeof(base) - strlen(base) - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool collectTests(struct CInemaTestList* tests, const char* path) {
|
||||
CIerr(2, "Considering path %s\n", path);
|
||||
struct VDir* dir = VDirOpen(path);
|
||||
if (!dir) {
|
||||
return false;
|
||||
}
|
||||
struct VDirEntry* entry = dir->listNext(dir);
|
||||
while (entry) {
|
||||
char subpath[PATH_MAX];
|
||||
snprintf(subpath, sizeof(subpath), "%s" PATH_SEP "%s", path, entry->name(entry));
|
||||
if (entry->type(entry) == VFS_DIRECTORY && strncmp(entry->name(entry), ".", 2) != 0 && strncmp(entry->name(entry), "..", 3) != 0) {
|
||||
if (!collectTests(tests, subpath)) {
|
||||
dir->close(dir);
|
||||
return false;
|
||||
}
|
||||
} else if (entry->type(entry) == VFS_FILE && strncmp(entry->name(entry), "test.", 5) == 0) {
|
||||
CIerr(3, "Found potential test %s\n", subpath);
|
||||
struct VFile* vf = dir->openFile(dir, entry->name(entry), O_RDONLY);
|
||||
if (vf) {
|
||||
if (mCoreIsCompatible(vf) != PLATFORM_NONE || mVideoLogIsCompatible(vf) != PLATFORM_NONE) {
|
||||
struct CInemaTest* test = CInemaTestListAppend(tests);
|
||||
if (!CInemaTestInit(test, path, entry->name(entry))) {
|
||||
CIerr(3, "Failed to create test\n");
|
||||
CInemaTestListResize(tests, -1);
|
||||
} else {
|
||||
CIerr(2, "Found test %s\n", test->name);
|
||||
}
|
||||
} else {
|
||||
CIerr(3, "Not a compatible file\n");
|
||||
}
|
||||
vf->close(vf);
|
||||
} else {
|
||||
CIerr(3, "Failed to open file\n");
|
||||
}
|
||||
}
|
||||
entry = dir->listNext(dir);
|
||||
}
|
||||
dir->close(dir);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int _compareNames(const void* a, const void* b) {
|
||||
const struct CInemaTest* ta = a;
|
||||
const struct CInemaTest* tb = b;
|
||||
|
||||
return strncmp(ta->name, tb->name, sizeof(ta->name));
|
||||
}
|
||||
|
||||
static void reduceTestList(struct CInemaTestList* tests) {
|
||||
qsort(CInemaTestListGetPointer(tests, 0), CInemaTestListSize(tests), sizeof(struct CInemaTest), _compareNames);
|
||||
|
||||
size_t i;
|
||||
for (i = 1; i < CInemaTestListSize(tests);) {
|
||||
struct CInemaTest* cur = CInemaTestListGetPointer(tests, i);
|
||||
struct CInemaTest* prev = CInemaTestListGetPointer(tests, i - 1);
|
||||
if (strncmp(cur->name, prev->name, sizeof(cur->name)) != 0) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
CInemaTestListShift(tests, i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void testToPath(const char* testName, char* path) {
|
||||
strncpy(path, base, PATH_MAX);
|
||||
|
||||
bool dotSeen = true;
|
||||
size_t i;
|
||||
for (i = strlen(path); testName[0] && i < PATH_MAX; ++testName) {
|
||||
if (testName[0] == '.') {
|
||||
dotSeen = true;
|
||||
} else {
|
||||
if (dotSeen) {
|
||||
strncpy(&path[i], PATH_SEP, PATH_MAX - i);
|
||||
i += strlen(PATH_SEP);
|
||||
dotSeen = false;
|
||||
if (!i) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
path[i] = testName[0];
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _loadConfigTree(struct Table* configTree, const char* testName) {
|
||||
char key[MAX_TEST];
|
||||
strncpy(key, testName, sizeof(key) - 1);
|
||||
|
||||
struct mCoreConfig* config;
|
||||
while (!(config = HashTableLookup(configTree, key))) {
|
||||
char path[PATH_MAX];
|
||||
config = malloc(sizeof(*config));
|
||||
mCoreConfigInit(config, "cinema");
|
||||
testToPath(key, path);
|
||||
strncat(path, PATH_SEP, sizeof(path) - 1);
|
||||
strncat(path, "config.ini", sizeof(path) - 1);
|
||||
mCoreConfigLoadPath(config, path);
|
||||
HashTableInsert(configTree, key, config);
|
||||
char* pos = strrchr(key, '.');
|
||||
if (pos) {
|
||||
pos[0] = '\0';
|
||||
} else if (key[0]) {
|
||||
key[0] = '\0';
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _unloadConfigTree(const char* key, void* value, void* user) {
|
||||
UNUSED(key);
|
||||
UNUSED(user);
|
||||
mCoreConfigDeinit(value);
|
||||
}
|
||||
|
||||
static const char* _lookupValue(struct Table* configTree, const char* testName, const char* key) {
|
||||
_loadConfigTree(configTree, testName);
|
||||
|
||||
char testKey[MAX_TEST];
|
||||
strncpy(testKey, testName, sizeof(testKey) - 1);
|
||||
|
||||
struct mCoreConfig* config;
|
||||
while (true) {
|
||||
config = HashTableLookup(configTree, testKey);
|
||||
if (!config) {
|
||||
continue;
|
||||
}
|
||||
const char* str = ConfigurationGetValue(&config->configTable, "testinfo", key);
|
||||
if (str) {
|
||||
return str;
|
||||
}
|
||||
char* pos = strrchr(testKey, '.');
|
||||
if (pos) {
|
||||
pos[0] = '\0';
|
||||
} else if (testKey[0]) {
|
||||
testKey[0] = '\0';
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool CInemaConfigGetUInt(struct Table* configTree, const char* testName, const char* key, unsigned* out) {
|
||||
const char* charValue = _lookupValue(configTree, testName, key);
|
||||
if (!charValue) {
|
||||
return false;
|
||||
}
|
||||
char* end;
|
||||
unsigned long value = strtoul(charValue, &end, 10);
|
||||
if (*end) {
|
||||
return false;
|
||||
}
|
||||
*out = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
void CInemaConfigLoad(struct Table* configTree, const char* testName, struct mCore* core) {
|
||||
_loadConfigTree(configTree, testName);
|
||||
|
||||
char testKey[MAX_TEST] = {0};
|
||||
char* keyEnd = testKey;
|
||||
const char* pos;
|
||||
while (true) {
|
||||
pos = strchr(testName, '.');
|
||||
size_t maxlen = sizeof(testKey) - (keyEnd - testKey) - 1;
|
||||
size_t len;
|
||||
if (pos) {
|
||||
len = pos - testName;
|
||||
} else {
|
||||
len = strlen(testName);
|
||||
}
|
||||
if (len > maxlen) {
|
||||
len = maxlen;
|
||||
}
|
||||
strncpy(keyEnd, testName, len);
|
||||
keyEnd += len;
|
||||
|
||||
struct mCoreConfig* config = HashTableLookup(configTree, testKey);
|
||||
if (config) {
|
||||
core->loadConfig(core, config);
|
||||
}
|
||||
if (!pos) {
|
||||
break;
|
||||
}
|
||||
testName = pos + 1;
|
||||
keyEnd[0] = '.';
|
||||
++keyEnd;
|
||||
}
|
||||
}
|
||||
|
||||
bool CInemaTestInit(struct CInemaTest* test, const char* directory, const char* filename) {
|
||||
if (strncmp(base, directory, strlen(base)) != 0) {
|
||||
return false;
|
||||
}
|
||||
memset(test, 0, sizeof(*test));
|
||||
strncpy(test->directory, directory, sizeof(test->directory) - 1);
|
||||
strncpy(test->filename, filename, sizeof(test->filename) - 1);
|
||||
directory += strlen(base) + 1;
|
||||
strncpy(test->name, directory, sizeof(test->name) - 1);
|
||||
char* str = strstr(test->name, PATH_SEP);
|
||||
while (str) {
|
||||
str[0] = '.';
|
||||
str = strstr(str, PATH_SEP);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _loadBaseline(struct VDir* dir, struct CInemaImage* image, size_t frame, enum CInemaStatus* status) {
|
||||
char baselineName[32];
|
||||
snprintf(baselineName, sizeof(baselineName), "baseline_%04" PRIz "u.png", frame);
|
||||
struct VFile* baselineVF = dir->openFile(dir, baselineName, O_RDONLY);
|
||||
if (!baselineVF) {
|
||||
if (*status == CI_PASS) {
|
||||
*status = CI_FAIL;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
png_structp png = PNGReadOpen(baselineVF, 0);
|
||||
png_infop info = png_create_info_struct(png);
|
||||
png_infop end = png_create_info_struct(png);
|
||||
if (!png || !info || !end || !PNGReadHeader(png, info)) {
|
||||
PNGReadClose(png, info, end);
|
||||
baselineVF->close(baselineVF);
|
||||
CIerr(1, "Failed to load %s\n", baselineName);
|
||||
*status = CI_ERROR;
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned pwidth = png_get_image_width(png, info);
|
||||
unsigned pheight = png_get_image_height(png, info);
|
||||
if (pheight != image->height || pwidth != image->width) {
|
||||
PNGReadClose(png, info, end);
|
||||
baselineVF->close(baselineVF);
|
||||
CIlog(1, "Size mismatch for %s, expected %ux%u, got %ux%u\n", baselineName, pwidth, pheight, image->width, image->height);
|
||||
if (*status == CI_PASS) {
|
||||
*status = CI_FAIL;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
image->data = malloc(pwidth * pheight * BYTES_PER_PIXEL);
|
||||
if (!image->data) {
|
||||
CIerr(1, "Failed to allocate baseline buffer\n");
|
||||
*status = CI_ERROR;
|
||||
PNGReadClose(png, info, end);
|
||||
baselineVF->close(baselineVF);
|
||||
return false;
|
||||
}
|
||||
if (!PNGReadPixels(png, info, image->data, pwidth, pheight, pwidth) || !PNGReadFooter(png, end)) {
|
||||
CIerr(1, "Failed to read %s\n", baselineName);
|
||||
*status = CI_ERROR;
|
||||
free(image->data);
|
||||
return false;
|
||||
}
|
||||
PNGReadClose(png, info, end);
|
||||
baselineVF->close(baselineVF);
|
||||
image->stride = pwidth;
|
||||
return true;
|
||||
}
|
||||
|
||||
static struct VDir* _makeOutDir(const char* testName) {
|
||||
char path[PATH_MAX] = {0};
|
||||
strncpy(path, outdir, sizeof(path) - 1);
|
||||
char* pathEnd = path + strlen(path);
|
||||
const char* pos;
|
||||
while (true) {
|
||||
pathEnd[0] = PATH_SEP[0];
|
||||
++pathEnd;
|
||||
pos = strchr(testName, '.');
|
||||
size_t maxlen = sizeof(path) - (pathEnd - path) - 1;
|
||||
size_t len;
|
||||
if (pos) {
|
||||
len = pos - testName;
|
||||
} else {
|
||||
len = strlen(testName);
|
||||
}
|
||||
if (len > maxlen) {
|
||||
len = maxlen;
|
||||
}
|
||||
strncpy(pathEnd, testName, len);
|
||||
pathEnd += len;
|
||||
|
||||
mkdir(path, 0777);
|
||||
|
||||
if (!pos) {
|
||||
break;
|
||||
}
|
||||
testName = pos + 1;
|
||||
}
|
||||
return VDirOpen(path);
|
||||
}
|
||||
|
||||
static void _writeImage(struct VFile* vf, const struct CInemaImage* image) {
|
||||
png_structp png = PNGWriteOpen(vf);
|
||||
png_infop info = PNGWriteHeader(png, image->width, image->height);
|
||||
if (!PNGWritePixels(png, image->width, image->height, image->stride, image->data)) {
|
||||
CIerr(0, "Could not write output image\n");
|
||||
}
|
||||
PNGWriteClose(png, info);
|
||||
|
||||
vf->close(vf);
|
||||
}
|
||||
|
||||
static void _writeDiff(const char* testName, const struct CInemaImage* image, size_t frame, const char* type) {
|
||||
struct VDir* dir = _makeOutDir(testName);
|
||||
if (!dir) {
|
||||
CIerr(0, "Could not open directory for %s\n", testName);
|
||||
return;
|
||||
}
|
||||
char name[32];
|
||||
snprintf(name, sizeof(name), "%s_%04" PRIz "u.png", type, frame);
|
||||
struct VFile* vf = dir->openFile(dir, name, O_CREAT | O_TRUNC | O_WRONLY);
|
||||
if (!vf) {
|
||||
CIerr(0, "Could not open output file %s\n", name);
|
||||
dir->close(dir);
|
||||
return;
|
||||
}
|
||||
_writeImage(vf, image);
|
||||
dir->close(dir);
|
||||
}
|
||||
|
||||
static void _writeBaseline(struct VDir* dir, const struct CInemaImage* image, size_t frame) {
|
||||
char baselineName[32];
|
||||
snprintf(baselineName, sizeof(baselineName), "baseline_%04" PRIz "u.png", frame);
|
||||
struct VFile* baselineVF = dir->openFile(dir, baselineName, O_CREAT | O_TRUNC | O_WRONLY);
|
||||
if (baselineVF) {
|
||||
_writeImage(baselineVF, image);
|
||||
} else {
|
||||
CIerr(0, "Could not open output file %s\n", baselineName);
|
||||
}
|
||||
}
|
||||
|
||||
void CInemaTestRun(struct CInemaTest* test, struct Table* configTree) {
|
||||
unsigned ignore = 0;
|
||||
CInemaConfigGetUInt(configTree, test->name, "ignore", &ignore);
|
||||
if (ignore) {
|
||||
test->status = CI_SKIP;
|
||||
return;
|
||||
}
|
||||
|
||||
struct VDir* dir = VDirOpen(test->directory);
|
||||
if (!dir) {
|
||||
CIerr(0, "Failed to open test directory\n");
|
||||
test->status = CI_ERROR;
|
||||
return;
|
||||
}
|
||||
struct VFile* rom = dir->openFile(dir, test->filename, O_RDONLY);
|
||||
if (!rom) {
|
||||
CIerr(0, "Failed to open test\n");
|
||||
test->status = CI_ERROR;
|
||||
return;
|
||||
}
|
||||
struct mCore* core = mCoreFindVF(rom);
|
||||
if (!core) {
|
||||
CIerr(0, "Failed to load test\n");
|
||||
test->status = CI_ERROR;
|
||||
rom->close(rom);
|
||||
return;
|
||||
}
|
||||
if (!core->init(core)) {
|
||||
CIerr(0, "Failed to init test\n");
|
||||
test->status = CI_ERROR;
|
||||
core->deinit(core);
|
||||
return;
|
||||
}
|
||||
struct CInemaImage image;
|
||||
core->desiredVideoDimensions(core, &image.width, &image.height);
|
||||
ssize_t bufferSize = image.width * image.height * BYTES_PER_PIXEL;
|
||||
image.data = malloc(bufferSize);
|
||||
image.stride = image.width;
|
||||
if (!image.data) {
|
||||
CIerr(0, "Failed to allocate video buffer\n");
|
||||
test->status = CI_ERROR;
|
||||
core->deinit(core);
|
||||
}
|
||||
core->setVideoBuffer(core, image.data, image.stride);
|
||||
mCoreConfigInit(&core->config, "cinema");
|
||||
|
||||
unsigned limit = 9999;
|
||||
unsigned skip = 0;
|
||||
unsigned fail = 0;
|
||||
|
||||
CInemaConfigGetUInt(configTree, test->name, "frames", &limit);
|
||||
CInemaConfigGetUInt(configTree, test->name, "skip", &skip);
|
||||
CInemaConfigGetUInt(configTree, test->name, "fail", &fail);
|
||||
CInemaConfigLoad(configTree, test->name, core);
|
||||
|
||||
core->loadROM(core, rom);
|
||||
core->rtc.override = RTC_FAKE_EPOCH;
|
||||
core->rtc.value = 1200000000;
|
||||
core->reset(core);
|
||||
|
||||
test->status = CI_PASS;
|
||||
|
||||
unsigned minFrame = core->frameCounter(core);
|
||||
size_t frame;
|
||||
for (frame = 0; frame < skip; ++frame) {
|
||||
core->runFrame(core);
|
||||
}
|
||||
for (frame = 0; limit; ++frame, --limit) {
|
||||
core->runFrame(core);
|
||||
++test->totalFrames;
|
||||
unsigned frameCounter = core->frameCounter(core);
|
||||
if (frameCounter <= minFrame) {
|
||||
break;
|
||||
}
|
||||
CIlog(3, "Test frame: %u\n", frameCounter);
|
||||
core->desiredVideoDimensions(core, &image.width, &image.height);
|
||||
uint8_t* diff = NULL;
|
||||
struct CInemaImage expected = {
|
||||
.data = NULL,
|
||||
.width = image.width,
|
||||
.height = image.height,
|
||||
.stride = image.width,
|
||||
};
|
||||
if (_loadBaseline(dir, &expected, frame, &test->status)) {
|
||||
uint8_t* testPixels = image.data;
|
||||
uint8_t* expectPixels = expected.data;
|
||||
size_t x;
|
||||
size_t y;
|
||||
int max = 0;
|
||||
bool failed = false;
|
||||
for (y = 0; y < image.height; ++y) {
|
||||
for (x = 0; x < image.width; ++x) {
|
||||
size_t pix = expected.stride * y + x;
|
||||
size_t tpix = image.stride * y + x;
|
||||
int testR = testPixels[tpix * 4 + 0];
|
||||
int testG = testPixels[tpix * 4 + 1];
|
||||
int testB = testPixels[tpix * 4 + 2];
|
||||
int expectR = expectPixels[pix * 4 + 0];
|
||||
int expectG = expectPixels[pix * 4 + 1];
|
||||
int expectB = expectPixels[pix * 4 + 2];
|
||||
int r = expectR - testR;
|
||||
int g = expectG - testG;
|
||||
int b = expectB - testB;
|
||||
if (r | g | b) {
|
||||
failed = true;
|
||||
if (diffs && !diff) {
|
||||
diff = calloc(expected.width * expected.height, BYTES_PER_PIXEL);
|
||||
}
|
||||
CIlog(3, "Frame %u failed at pixel %" PRIz "ux%" PRIz "u with diff %i,%i,%i (expected %02x%02x%02x, got %02x%02x%02x)\n",
|
||||
frameCounter, x, y, r, g, b,
|
||||
expectR, expectG, expectB,
|
||||
testR, testG, testB);
|
||||
test->status = CI_FAIL;
|
||||
if (r < 0) {
|
||||
r = -r;
|
||||
}
|
||||
if (g < 0) {
|
||||
g = -g;
|
||||
}
|
||||
if (b < 0) {
|
||||
b = -b;
|
||||
}
|
||||
|
||||
if (diff) {
|
||||
if (r > max) {
|
||||
max = r;
|
||||
}
|
||||
if (g > max) {
|
||||
max = g;
|
||||
}
|
||||
if (b > max) {
|
||||
max = b;
|
||||
}
|
||||
diff[pix * 4 + 0] = r;
|
||||
diff[pix * 4 + 1] = g;
|
||||
diff[pix * 4 + 2] = b;
|
||||
}
|
||||
|
||||
test->totalDistance += r + g + b;
|
||||
++test->failedPixels;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failed) {
|
||||
++test->failedFrames;
|
||||
}
|
||||
test->totalPixels += image.height * image.width;
|
||||
if (rebaseline && failed) {
|
||||
_writeBaseline(dir, &image, frame);
|
||||
}
|
||||
if (diff) {
|
||||
if (failed) {
|
||||
struct CInemaImage outdiff = {
|
||||
.data = diff,
|
||||
.width = image.width,
|
||||
.height = image.height,
|
||||
.stride = image.width,
|
||||
};
|
||||
|
||||
_writeDiff(test->name, &image, frame, "result");
|
||||
_writeDiff(test->name, &expected, frame, "expected");
|
||||
_writeDiff(test->name, &outdiff, frame, "diff");
|
||||
|
||||
for (y = 0; y < outdiff.height; ++y) {
|
||||
for (x = 0; x < outdiff.width; ++x) {
|
||||
size_t pix = outdiff.stride * y + x;
|
||||
diff[pix * 4 + 0] = diff[pix * 4 + 0] * 255 / max;
|
||||
diff[pix * 4 + 1] = diff[pix * 4 + 1] * 255 / max;
|
||||
diff[pix * 4 + 2] = diff[pix * 4 + 2] * 255 / max;
|
||||
}
|
||||
}
|
||||
_writeDiff(test->name, &outdiff, frame, "normalized");
|
||||
}
|
||||
free(diff);
|
||||
}
|
||||
free(expected.data);
|
||||
} else if (test->status == CI_ERROR) {
|
||||
break;
|
||||
} else if (rebaseline) {
|
||||
_writeBaseline(dir, &image, frame);
|
||||
}
|
||||
}
|
||||
|
||||
if (fail) {
|
||||
if (test->status == CI_FAIL) {
|
||||
test->status = CI_XFAIL;
|
||||
} else if (test->status == CI_PASS) {
|
||||
test->status = CI_XPASS;
|
||||
}
|
||||
}
|
||||
|
||||
free(image.data);
|
||||
mCoreConfigDeinit(&core->config);
|
||||
core->deinit(core);
|
||||
dir->close(dir);
|
||||
}
|
||||
|
||||
void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args) {
|
||||
UNUSED(log);
|
||||
if (verbosity < 0) {
|
||||
return;
|
||||
}
|
||||
int mask = mLOG_FATAL;
|
||||
if (verbosity >= 1) {
|
||||
mask |= mLOG_ERROR;
|
||||
}
|
||||
if (verbosity >= 2) {
|
||||
mask |= mLOG_WARN;
|
||||
}
|
||||
if (verbosity >= 4) {
|
||||
mask |= mLOG_INFO;
|
||||
}
|
||||
if (verbosity >= 5) {
|
||||
mask |= mLOG_ALL;
|
||||
}
|
||||
if (!(mask & level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
char buffer[256];
|
||||
vsnprintf(buffer, sizeof(buffer), format, args);
|
||||
CIerr(0, "[%s] %s\n", mLogCategoryName(category), buffer);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
int status = 0;
|
||||
if (!parseCInemaArgs(argc, argv)) {
|
||||
status = 1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (showVersion) {
|
||||
version(argv[0]);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (showUsage) {
|
||||
usageCInema(argv[0]);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
argc -= optind;
|
||||
argv += optind;
|
||||
|
||||
if (!base[0] && !determineBase(argc, argv)) {
|
||||
CIlog(0, "Could not determine CInema test base. Please specify manually.");
|
||||
status = 1;
|
||||
goto cleanup;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
char* rbase = realpath(base, NULL);
|
||||
strncpy(base, rbase, PATH_MAX);
|
||||
free(rbase);
|
||||
#endif
|
||||
|
||||
struct CInemaTestList tests;
|
||||
CInemaTestListInit(&tests, 0);
|
||||
|
||||
struct mLogger logger = { .log = _log };
|
||||
mLogSetDefaultLogger(&logger);
|
||||
|
||||
if (argc > 0) {
|
||||
size_t i;
|
||||
for (i = 0; i < (size_t) argc; ++i) {
|
||||
char path[PATH_MAX + 1] = {0};
|
||||
testToPath(argv[i], path);
|
||||
|
||||
if (!collectTests(&tests, path)) {
|
||||
status = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (!collectTests(&tests, base)) {
|
||||
status = 1;
|
||||
}
|
||||
|
||||
if (CInemaTestListSize(&tests) == 0) {
|
||||
CIlog(1, "No tests found.");
|
||||
status = 1;
|
||||
} else {
|
||||
reduceTestList(&tests);
|
||||
}
|
||||
|
||||
struct Table configTree;
|
||||
HashTableInit(&configTree, 0, free);
|
||||
|
||||
size_t i;
|
||||
for (i = 0; i < CInemaTestListSize(&tests); ++i) {
|
||||
struct CInemaTest* test = CInemaTestListGetPointer(&tests, i);
|
||||
if (dryRun) {
|
||||
CIlog(-1, "%s\n", test->name);
|
||||
} else {
|
||||
CIlog(1, "%s: ", test->name);
|
||||
fflush(stdout);
|
||||
CInemaTestRun(test, &configTree);
|
||||
switch (test->status) {
|
||||
case CI_PASS:
|
||||
CIlog(1, "pass\n");
|
||||
break;
|
||||
case CI_FAIL:
|
||||
status = 1;
|
||||
CIlog(1, "fail\n");
|
||||
break;
|
||||
case CI_XPASS:
|
||||
CIlog(1, "xpass\n");
|
||||
break;
|
||||
case CI_XFAIL:
|
||||
CIlog(1, "xfail\n");
|
||||
break;
|
||||
case CI_SKIP:
|
||||
CIlog(1, "skip\n");
|
||||
break;
|
||||
case CI_ERROR:
|
||||
status = 1;
|
||||
CIlog(1, "error");
|
||||
break;
|
||||
}
|
||||
if (test->failedFrames) {
|
||||
CIlog(2, "\tfailed frames: %u/%u (%1.3g%%)\n", test->failedFrames, test->totalFrames, test->failedFrames / (test->totalFrames * 0.01));
|
||||
CIlog(2, "\tfailed pixels: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->failedPixels, test->totalPixels, test->failedPixels / (test->totalPixels * 0.01));
|
||||
CIlog(2, "\tdistance: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->totalDistance, test->totalPixels * 765, test->totalDistance / (test->totalPixels * 7.65));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HashTableEnumerate(&configTree, _unloadConfigTree, NULL);
|
||||
HashTableDeinit(&configTree);
|
||||
CInemaTestListDeinit(&tests);
|
||||
|
||||
cleanup:
|
||||
return status;
|
||||
}
|