Commit 8bc67ec2 authored by Henrik Gramner's avatar Henrik Gramner Committed by Janne Grunau

Checkasm: assembly testing and benchmarking tool

It provides the following features:
 * verify correctness by comparing output to the C version.
 * detect failure to save and restore clobbered callee-saved registers.
 * detect 32-bit parameters being used as if they were 64-bit in x86-64
   (the upper halves are not guaranteed to be zero - but in practice
   they very often are, which makes those bugs hard to spot otherwise).
 * easy benchmarking.

Compile by running 'make checkasm'.
Execute by running 'tests/checkasm/checkasm'.

Optional arguments are '--bench' to run benchmarks for all functions,
'--bench=<pattern>' to run benchmarks for all functions that starts with
<pattern>, and '<integer>' to seed the PRNG for reproducible results.

Contains unit tests for most h264pred functions to get started, more tests
can be added afterwards using those as a reference.

Loosely based on code from x264. Currently only supports x86 and x86-64,
but additional architectures shouldn't be too much of an obstacle to add.

Note that functions with floating point parameters or floating point
return values are not supported. Some compiler-specific features or
preprocessor hacks would likely be required to add support for that.
Signed-off-by: 's avatarJanne Grunau <janne-libav@jannau.net>
parent 79626865
......@@ -46,6 +46,7 @@
/libavutil/avconfig.h
/tests/audiogen
/tests/base64
/tests/checkasm/checkasm
/tests/data/
/tests/pixfmts.mak
/tests/rotozoom
......
......@@ -173,5 +173,7 @@ testclean:
-include $(wildcard tests/*.d)
include $(SRC_PATH)/tests/checkasm/Makefile
.PHONY: fate* lcov lcov-reset
.INTERMEDIATE: coverage.info
# libavcodec tests
AVCODECOBJS-$(CONFIG_H264PRED) += h264pred.o
CHECKASMOBJS-$(CONFIG_AVCODEC) += $(AVCODECOBJS-yes)
-include $(SRC_PATH)/tests/checkasm/$(ARCH)/Makefile
CHECKASMOBJS += $(CHECKASMOBJS-yes) checkasm.o
CHECKASMOBJS := $(sort $(CHECKASMOBJS:%=tests/checkasm/%))
-include $(CHECKASMOBJS:.o=.d)
CHECKASMDIRS := $(sort $(dir $(CHECKASMOBJS)))
$(CHECKASMOBJS): | $(CHECKASMDIRS)
OBJDIRS += $(CHECKASMDIRS)
# We rely on function pointers intentionally declared without specified argument types.
tests/checkasm/%.o: CFLAGS := $(CFLAGS:-Wstrict-prototypes=-Wno-strict-prototypes)
CHECKASM := tests/checkasm/checkasm$(EXESUF)
$(CHECKASM): $(EXEOBJS) $(CHECKASMOBJS) $(FF_DEP_LIBS)
$(LD) $(LDFLAGS) $(LDEXEFLAGS) $(LD_O) $(CHECKASMOBJS) $(FF_EXTRALIBS)
checkasm: $(CHECKASM)
clean:: checkasmclean
checkasmclean:
$(RM) $(CHECKASM) $(CLEANSUFFIXES:%=tests/checkasm/%) $(CLEANSUFFIXES:%=tests/checkasm/$(ARCH)/%)
.PHONY: checkasm
This diff is collapsed.
/*
* Assembly testing and benchmarking tool
* Copyright (c) 2015 Henrik Gramner
* Copyright (c) 2008 Loren Merritt
*
* This file is part of Libav.
*
* Libav 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.
*
* Libav 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 Libav; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef CHECKASM_H
#define CHECKASM_H
#include <stdint.h>
#include "config.h"
#include "libavutil/avstring.h"
#include "libavutil/lfg.h"
#include "libavutil/timer.h"
void checkasm_check_h264pred(void);
intptr_t (*checkasm_check_func(intptr_t (*func)(), const char *name, ...))() av_printf_format(2, 3);
int checkasm_bench_func(void);
void checkasm_fail_func(const char *msg, ...) av_printf_format(1, 2);
void checkasm_update_bench(int iterations, uint64_t cycles);
void checkasm_report(const char *name, ...) av_printf_format(1, 2);
extern AVLFG checkasm_lfg;
#define rnd() av_lfg_get(&checkasm_lfg)
static av_unused intptr_t (*func_ref)();
static av_unused intptr_t (*func_new)();
#define BENCH_RUNS 1000 /* Trade-off between accuracy and speed */
/* Decide whether or not the specified function needs to be tested */
#define check_func(func, ...) ((func_new = (intptr_t (*)())func) &&\
(func_ref = checkasm_check_func(func_new, __VA_ARGS__)))
/* Indicate that the current test has failed */
#define fail() checkasm_fail_func("%s:%d", av_basename(__FILE__), __LINE__)
/* Print the test outcome */
#define report(...) checkasm_report("" __VA_ARGS__)
/* Call the reference function */
#define call_ref(...) func_ref(__VA_ARGS__)
#if ARCH_X86 && HAVE_YASM
/* Verifies that clobbered callee-saved registers are properly saved and restored */
intptr_t checkasm_checked_call(intptr_t (*func)(), ...);
#endif
/* Call the function */
#if ARCH_X86_64 && HAVE_YASM
/* Evil hack: detect incorrect assumptions that 32-bit ints are zero-extended to 64-bit.
* This is done by clobbering the stack with junk around the stack pointer and calling the
* assembly function through x264_checkasm_call with added dummy arguments which forces all
* real arguments to be passed on the stack and not in registers. For 32-bit arguments the
* upper half of the 64-bit register locations on the stack will now contain junk which will
* cause misbehaving functions to either produce incorrect output or segfault. Note that
* even though this works extremely well in practice, it's technically not guaranteed
* and false negatives is theoretically possible, but there can never be any false positives.
*/
void checkasm_stack_clobber(uint64_t clobber, ...);
#define CLOB (UINT64_C(0xdeadbeefdeadbeef))
#define call_new(...) (checkasm_stack_clobber(CLOB,CLOB,CLOB,CLOB,CLOB,CLOB,CLOB,CLOB,CLOB,CLOB,CLOB,\
CLOB,CLOB,CLOB,CLOB,CLOB,CLOB,CLOB,CLOB,CLOB,CLOB),\
checkasm_checked_call(func_new, 0, 0, 0, 0, 0, __VA_ARGS__))
#elif ARCH_X86_32 && HAVE_YASM
#define call_new(...) checkasm_checked_call(func_new, __VA_ARGS__)
#else
#define call_new(...) func_new(__VA_ARGS__)
#endif
/* Benchmark the function */
#ifdef AV_READ_TIME
#define bench_new(...)\
do {\
if (checkasm_bench_func()) {\
intptr_t (*tfunc)() = func_new;\
uint64_t tsum = 0;\
int ti, tcount = 0;\
for (ti = 0; ti < BENCH_RUNS; ti++) {\
uint64_t t = AV_READ_TIME();\
tfunc(__VA_ARGS__);\
tfunc(__VA_ARGS__);\
tfunc(__VA_ARGS__);\
tfunc(__VA_ARGS__);\
t = AV_READ_TIME() - t;\
if (t*tcount <= tsum*4 && ti > 0) {\
tsum += t;\
tcount++;\
}\
}\
checkasm_update_bench(tcount, tsum);\
}\
} while (0)
#else
#define bench_new(...)
#endif
#endif
/*
* Copyright (c) 2015 Henrik Gramner
*
* This file is part of Libav.
*
* Libav 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.
*
* Libav 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 Libav; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <string.h>
#include "checkasm.h"
#include "libavcodec/avcodec.h"
#include "libavcodec/h264pred.h"
#include "libavutil/common.h"
#include "libavutil/intreadwrite.h"
static const int codec_ids[4] = { AV_CODEC_ID_H264, AV_CODEC_ID_VP8, AV_CODEC_ID_RV40, AV_CODEC_ID_SVQ3 };
static const char * const pred4x4_modes[4][15] = {
{ /* H264 */
[VERT_PRED ] = "vertical",
[HOR_PRED ] = "horizontal",
[DC_PRED ] = "dc",
[DIAG_DOWN_LEFT_PRED ] = "down_left",
[DIAG_DOWN_RIGHT_PRED] = "down_right",
[VERT_RIGHT_PRED ] = "vertical_right",
[HOR_DOWN_PRED ] = "horizontal_right",
[VERT_LEFT_PRED ] = "vertical_left",
[HOR_UP_PRED ] = "horizontal_up",
[LEFT_DC_PRED ] = "left_dc",
[TOP_DC_PRED ] = "top_dc",
[DC_128_PRED ] = "dc_128",
},
{ /* VP8 */
[VERT_PRED ] = "vertical_vp8",
[HOR_PRED ] = "horizontal_vp8",
[VERT_LEFT_PRED] = "vertical_left_vp8",
[TM_VP8_PRED ] = "tm_vp8",
[DC_127_PRED ] = "dc_127_vp8",
[DC_129_PRED ] = "dc_129_vp8",
},
{ /* RV40 */
[DIAG_DOWN_LEFT_PRED ] = "down_left_rv40",
[VERT_LEFT_PRED ] = "vertical_left_rv40",
[HOR_UP_PRED ] = "horizontal_up_rv40",
[DIAG_DOWN_LEFT_PRED_RV40_NODOWN] = "down_left_nodown_rv40",
[HOR_UP_PRED_RV40_NODOWN ] = "horizontal_up_nodown_rv40",
[VERT_LEFT_PRED_RV40_NODOWN ] = "vertical_left_nodown_rv40",
},
{ /* SVQ3 */
[DIAG_DOWN_LEFT_PRED] = "down_left_svq3",
},
};
static const char * const pred8x8_modes[4][11] = {
{ /* H264 */
[DC_PRED8x8 ] = "dc",
[HOR_PRED8x8 ] = "horizontal",
[VERT_PRED8x8 ] = "vertical",
[PLANE_PRED8x8 ] = "plane",
[LEFT_DC_PRED8x8 ] = "left_dc",
[TOP_DC_PRED8x8 ] = "top_dc",
[DC_128_PRED8x8 ] = "dc_128",
[ALZHEIMER_DC_L0T_PRED8x8] = "mad_cow_dc_l0t",
[ALZHEIMER_DC_0LT_PRED8x8] = "mad_cow_dc_0lt",
[ALZHEIMER_DC_L00_PRED8x8] = "mad_cow_dc_l00",
[ALZHEIMER_DC_0L0_PRED8x8] = "mad_cow_dc_0l0",
},
{ /* VP8 */
[PLANE_PRED8x8 ] = "tm_vp8",
[DC_127_PRED8x8] = "dc_127_vp8",
[DC_129_PRED8x8] = "dc_129_vp8",
},
{ /* RV40 */
[DC_PRED8x8 ] = "dc_rv40",
[LEFT_DC_PRED8x8] = "left_dc_rv40",
[TOP_DC_PRED8x8 ] = "top_dc_rv40",
},
{ /* SVQ3 */
},
};
static const char * const pred16x16_modes[4][9] = {
{ /* H264 */
[DC_PRED8x8 ] = "dc",
[HOR_PRED8x8 ] = "horizontal",
[VERT_PRED8x8 ] = "vertical",
[PLANE_PRED8x8 ] = "plane",
[LEFT_DC_PRED8x8] = "left_dc",
[TOP_DC_PRED8x8 ] = "top_dc",
[DC_128_PRED8x8 ] = "dc_128",
},
{ /* VP8 */
[PLANE_PRED8x8 ] = "tm_vp8",
[DC_127_PRED8x8] = "dc_127_vp8",
[DC_129_PRED8x8] = "dc_129_vp8",
},
{ /* RV40 */
[PLANE_PRED8x8] = "plane_rv40",
},
{ /* SVQ3 */
[PLANE_PRED8x8] = "plane_svq3",
},
};
static const uint32_t pixel_mask[3] = { 0xffffffff, 0x01ff01ff, 0x03ff03ff };
#define SIZEOF_PIXEL ((bit_depth + 7) / 8)
#define BUF_SIZE (3*16*17)
#define check_pred_func(func, name, mode_name)\
(mode_name && ((codec_ids[codec] == AV_CODEC_ID_H264) ?\
check_func(func, "pred%s_%s_%d", name, mode_name, bit_depth) :\
check_func(func, "pred%s_%s", name, mode_name)))
#define randomize_buffers()\
do {\
uint32_t mask = pixel_mask[bit_depth-8];\
int i;\
for (i = 0; i < BUF_SIZE; i += 4) {\
uint32_t r = rnd() & mask;\
AV_WN32A(buf0+i, r);\
AV_WN32A(buf1+i, r);\
}\
} while (0)
#define src0 (buf0 + 4*16) /* Offset to allow room for top and left */
#define src1 (buf1 + 4*16)
static void check_pred4x4(H264PredContext *h, uint8_t *buf0, uint8_t *buf1,
int codec, int chroma_format, int bit_depth)
{
if (chroma_format == 1) {
uint8_t *topright = buf0 + 2*16;
int pred_mode;
for (pred_mode = 0; pred_mode < 15; pred_mode++) {
if (check_pred_func(h->pred4x4[pred_mode], "4x4", pred4x4_modes[codec][pred_mode])) {
randomize_buffers();
call_ref(src0, topright, (ptrdiff_t)12*SIZEOF_PIXEL);
call_new(src1, topright, (ptrdiff_t)12*SIZEOF_PIXEL);
if (memcmp(buf0, buf1, BUF_SIZE))
fail();
bench_new(src1, topright, (ptrdiff_t)12*SIZEOF_PIXEL);
}
}
}
}
static void check_pred8x8(H264PredContext *h, uint8_t *buf0, uint8_t *buf1,
int codec, int chroma_format, int bit_depth)
{
int pred_mode;
for (pred_mode = 0; pred_mode < 11; pred_mode++) {
if (check_pred_func(h->pred8x8[pred_mode], (chroma_format == 2) ? "8x16" : "8x8",
pred8x8_modes[codec][pred_mode])) {
randomize_buffers();
call_ref(src0, (ptrdiff_t)24*SIZEOF_PIXEL);
call_new(src1, (ptrdiff_t)24*SIZEOF_PIXEL);
if (memcmp(buf0, buf1, BUF_SIZE))
fail();
bench_new(src1, (ptrdiff_t)24*SIZEOF_PIXEL);
}
}
}
static void check_pred16x16(H264PredContext *h, uint8_t *buf0, uint8_t *buf1,
int codec, int chroma_format, int bit_depth)
{
if (chroma_format == 1) {
int pred_mode;
for (pred_mode = 0; pred_mode < 9; pred_mode++) {
if (check_pred_func(h->pred16x16[pred_mode], "16x16", pred16x16_modes[codec][pred_mode])) {
randomize_buffers();
call_ref(src0, (ptrdiff_t)48);
call_new(src1, (ptrdiff_t)48);
if (memcmp(buf0, buf1, BUF_SIZE))
fail();
bench_new(src1, (ptrdiff_t)48);
}
}
}
}
static void check_pred8x8l(H264PredContext *h, uint8_t *buf0, uint8_t *buf1,
int codec, int chroma_format, int bit_depth)
{
if (chroma_format == 1 && codec_ids[codec] == AV_CODEC_ID_H264) {
int pred_mode;
for (pred_mode = 0; pred_mode < 12; pred_mode++) {
if (check_pred_func(h->pred8x8l[pred_mode], "8x8l", pred4x4_modes[codec][pred_mode])) {
int neighbors;
for (neighbors = 0; neighbors <= 0xc000; neighbors += 0x4000) {
int has_topleft = neighbors & 0x8000;
int has_topright = neighbors & 0x4000;
if ((pred_mode == DIAG_DOWN_RIGHT_PRED || pred_mode == VERT_RIGHT_PRED) && !has_topleft)
continue; /* Those aren't allowed according to the spec */
randomize_buffers();
call_ref(src0, has_topleft, has_topright, (ptrdiff_t)24*SIZEOF_PIXEL);
call_new(src1, has_topleft, has_topright, (ptrdiff_t)24*SIZEOF_PIXEL);
if (memcmp(buf0, buf1, BUF_SIZE))
fail();
bench_new(src1, has_topleft, has_topright, (ptrdiff_t)24*SIZEOF_PIXEL);
}
}
}
}
}
/* TODO: Add tests for H.264 lossless H/V prediction */
void checkasm_check_h264pred(void)
{
static const struct {
void (*func)(H264PredContext*, uint8_t*, uint8_t*, int, int, int);
const char *name;
} tests[] = {
{ check_pred4x4, "pred4x4" },
{ check_pred8x8, "pred8x8" },
{ check_pred16x16, "pred16x16" },
{ check_pred8x8l, "pred8x8l" },
};
DECLARE_ALIGNED(16, uint8_t, buf0)[BUF_SIZE];
DECLARE_ALIGNED(16, uint8_t, buf1)[BUF_SIZE];
H264PredContext h;
int test, codec, chroma_format, bit_depth;
for (test = 0; test < FF_ARRAY_ELEMS(tests); test++) {
for (codec = 0; codec < 4; codec++) {
int codec_id = codec_ids[codec];
for (bit_depth = 8; bit_depth <= (codec_id == AV_CODEC_ID_H264 ? 10 : 8); bit_depth++)
for (chroma_format = 1; chroma_format <= (codec_id == AV_CODEC_ID_H264 ? 2 : 1); chroma_format++) {
ff_h264_pred_init(&h, codec_id, bit_depth, chroma_format);
tests[test].func(&h, buf0, buf1, codec, chroma_format, bit_depth);
}
}
report("%s", tests[test].name);
}
}
CHECKASMOBJS-$(HAVE_YASM) += x86/checkasm.o
tests/checkasm/x86/%.o: tests/checkasm/x86/%.asm
$(DEPYASM) $(YASMFLAGS) -I $(<D)/ -M -o $@ $< > $(@:.o=.d)
$(YASM) $(YASMFLAGS) -I $(<D)/ -o $@ $<
-$(STRIP) $(STRIPFLAGS) $@
;*****************************************************************************
;* Assembly testing and benchmarking tool
;* Copyright (c) 2008 Loren Merritt
;* Copyright (c) 2012 Henrik Gramner
;*
;* This file is part of Libav.
;*
;* Libav 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.
;*
;* Libav 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., 51 Franklin Street, Fifth Floor, Boston, MA 02111, USA.
;*****************************************************************************
%define private_prefix checkasm
%include "libavutil/x86/x86inc.asm"
SECTION_RODATA
error_message: db "failed to preserve register", 0
%if ARCH_X86_64
; just random numbers to reduce the chance of incidental match
ALIGN 16
x6: dq 0x1a1b2550a612b48c,0x79445c159ce79064
x7: dq 0x2eed899d5a28ddcd,0x86b2536fcd8cf636
x8: dq 0xb0856806085e7943,0x3f2bf84fc0fcca4e
x9: dq 0xacbd382dcf5b8de2,0xd229e1f5b281303f
x10: dq 0x71aeaff20b095fd9,0xab63e2e11fa38ed9
x11: dq 0x89b0c0765892729a,0x77d410d5c42c882d
x12: dq 0xc45ea11a955d8dd5,0x24b3c1d2a024048b
x13: dq 0x2e8ec680de14b47c,0xdd7b8919edd42786
x14: dq 0x135ce6888fa02cbf,0x11e53e2b2ac655ef
x15: dq 0x011ff554472a7a10,0x6de8f4c914c334d5
n7: dq 0x21f86d66c8ca00ce
n8: dq 0x75b6ba21077c48ad
n9: dq 0xed56bb2dcb3c7736
n10: dq 0x8bda43d3fd1a7e06
n11: dq 0xb64a9c9e5d318408
n12: dq 0xdf9a54b303f1d3a3
n13: dq 0x4a75479abd64e097
n14: dq 0x249214109d5d1c88
%endif
SECTION .text
cextern fail_func
; max number of args used by any asm function.
; (max_args % 4) must equal 3 for stack alignment
%define max_args 15
%if ARCH_X86_64
;-----------------------------------------------------------------------------
; int checkasm_stack_clobber(uint64_t clobber, ...)
;-----------------------------------------------------------------------------
cglobal stack_clobber, 1,2
; Clobber the stack with junk below the stack pointer
%define size (max_args+6)*8
SUB rsp, size
mov r1, size-8
.loop:
mov [rsp+r1], r0
sub r1, 8
jge .loop
ADD rsp, size
RET
%if WIN64
%assign free_regs 7
%else
%assign free_regs 9
%endif
;-----------------------------------------------------------------------------
; intptr_t checkasm_checked_call(intptr_t (*func)(), ...)
;-----------------------------------------------------------------------------
INIT_XMM
cglobal checked_call, 2,15,16,max_args*8+8
mov r6, r0
; All arguments have been pushed on the stack instead of registers in order to
; test for incorrect assumptions that 32-bit ints are zero-extended to 64-bit.
mov r0, r6mp
mov r1, r7mp
mov r2, r8mp
mov r3, r9mp
%if UNIX64
mov r4, r10mp
mov r5, r11mp
%assign i 6
%rep max_args-6
mov r9, [rsp+stack_offset+(i+1)*8]
mov [rsp+(i-6)*8], r9
%assign i i+1
%endrep
%else
%assign i 4
%rep max_args-4
mov r9, [rsp+stack_offset+(i+7)*8]
mov [rsp+i*8], r9
%assign i i+1
%endrep
%endif
%if WIN64
%assign i 6
%rep 16-6
mova m %+ i, [x %+ i]
%assign i i+1
%endrep
%endif
%assign i 14
%rep 15-free_regs
mov r %+ i, [n %+ i]
%assign i i-1
%endrep
call r6
%assign i 14
%rep 15-free_regs
xor r %+ i, [n %+ i]
or r14, r %+ i
%assign i i-1
%endrep
%if WIN64
%assign i 6
%rep 16-6
pxor m %+ i, [x %+ i]
por m6, m %+ i
%assign i i+1
%endrep
packsswb m6, m6
movq r5, m6
or r14, r5
%endif
jz .ok
mov r9, rax
lea r0, [error_message]
call fail_func
mov rax, r9
.ok:
RET
%else
; just random numbers to reduce the chance of incidental match
%define n3 dword 0x6549315c
%define n4 dword 0xe02f3e23
%define n5 dword 0xb78d0d1d
%define n6 dword 0x33627ba7
;-----------------------------------------------------------------------------
; intptr_t checkasm_checked_call(intptr_t (*func)(), ...)
;-----------------------------------------------------------------------------
cglobal checked_call, 1,7
mov r3, n3
mov r4, n4
mov r5, n5
mov r6, n6
%rep max_args
PUSH dword [esp+20+max_args*4]
%endrep
call r0
xor r3, n3
xor r4, n4
xor r5, n5
xor r6, n6
or r3, r4
or r5, r6
or r3, r5
jz .ok
mov r3, eax
lea r0, [error_message]
mov [esp], r0
call fail_func
mov eax, r3
.ok:
add esp, max_args*4
REP_RET
%endif ; ARCH_X86_64
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment