diff --git a/cheat_manager.c b/cheat_manager.c index 99f541a748..23935c21e9 100644 --- a/cheat_manager.c +++ b/cheat_manager.c @@ -27,6 +27,8 @@ #include #include #include +#include +#include #include #ifdef HAVE_CONFIG_H @@ -1193,14 +1195,246 @@ static void cheat_manager_setup_search_meta( } } +/* First address from @idx, stepping by @step, below @limit whose match + * byte is set; @limit when there is none. Clear bytes are scanned over + * a machine word at a time (16, 32 or 64 bytes with SSE2, AVX2 or + * AVX-512BW), since after a search or two nearly all of them are clear. + * An item is judged by its first match byte alone, as the search does, + * so a set byte part-way through an item rounds back to the item's + * start and is looked at there. */ +#if defined(__SSE2__) && (defined(__x86_64__) || defined(__i386__) \ + || defined(_M_X64) || defined(_M_IX86)) +#include +#define CHEAT_SCAN_SSE2 1 +#if defined(_MSC_VER) && !defined(__clang__) +#if _MSC_VER >= 1911 +#define CHEAT_SCAN_WIDE 1 +#define CHEAT_SCAN_TARGET_AVX2 +#define CHEAT_SCAN_TARGET_AVX512 +#endif +#elif defined(__clang__) && (defined(__SCE__) || defined(_MSC_VER)) +/* PlayStation clang and clang-cl declare only the intrinsics of ISAs + * the whole build enables: a wide tier only where the baseline has it. */ +#if defined(__AVX2__) && defined(__AVX512F__) && defined(__AVX512BW__) +#define CHEAT_SCAN_WIDE 1 +#define CHEAT_SCAN_TARGET_AVX2 +#define CHEAT_SCAN_TARGET_AVX512 +#endif +#elif (defined(__clang__) && __clang_major__ >= 4) \ + || (defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 5) +#define CHEAT_SCAN_WIDE 1 +#define CHEAT_SCAN_TARGET_AVX2 __attribute__((target("avx2"))) +#define CHEAT_SCAN_TARGET_AVX512 __attribute__((target("avx512f,avx512bw"))) +#endif +#endif + +#ifdef CHEAT_SCAN_WIDE +#include + +/* Index of the first set byte in [i, limit), or limit. */ +CHEAT_SCAN_TARGET_AVX2 +static unsigned cheat_scan_avx2(const uint8_t *m, unsigned i, unsigned limit) +{ + while (limit - i >= 32) + { + unsigned zero = (unsigned)_mm256_movemask_epi8(_mm256_cmpeq_epi8( + _mm256_loadu_si256((const __m256i*)(m + i)), + _mm256_setzero_si256())); + if (zero != 0xffffffffu) + return i + (unsigned)compat_ctz(~zero); + i += 32; + } + return i; +} + +CHEAT_SCAN_TARGET_AVX512 +static unsigned cheat_scan_avx512(const uint8_t *m, unsigned i, unsigned limit) +{ + while (limit - i >= 64) + { + __mmask64 set = _mm512_test_epi8_mask( + _mm512_loadu_si512((const void*)(m + i)), + _mm512_set1_epi8((char)0xff)); + if (set) + return i + (unsigned)compat_ctz_u64((uint64_t)set); + i += 64; + } + return i; +} + +#ifdef CHEAT_SCAN_TEST_HOOKS +/* -1 picks by CPU; otherwise caps the tier for a test. */ +int cheat_scan_tier_cap = -1; +#endif + +/* 0 = SSE2, 1 = AVX2, 2 = AVX-512BW; looked up on the first search, + * which runs on the main thread like every search. */ +static int cheat_scan_tier(void) +{ + static int tier = -1; + if (tier < 0) + { + uint64_t cpu = cpu_features_get(); + tier = (cpu & RETRO_SIMD_AVX512) ? 2 : (cpu & RETRO_SIMD_AVX2) ? 1 : 0; + } +#ifdef CHEAT_SCAN_TEST_HOOKS + if (cheat_scan_tier_cap >= 0 && tier > cheat_scan_tier_cap) + return cheat_scan_tier_cap; +#endif + return tier; +} +#endif + +static unsigned cheat_skip_unmatched(const uint8_t *m, unsigned idx, + unsigned limit, unsigned step) +{ + unsigned i = idx; + for (;;) + { + if (i >= limit) + return limit; +#ifdef CHEAT_SCAN_WIDE + switch (cheat_scan_tier()) + { + case 2: + i = cheat_scan_avx512(m, i, limit); + break; + case 1: + i = cheat_scan_avx2(m, i, limit); + break; + default: + break; + } +#endif +#ifdef CHEAT_SCAN_SSE2 + while (limit - i >= 16) + { + unsigned zero = (unsigned)_mm_movemask_epi8(_mm_cmpeq_epi8( + _mm_loadu_si128((const __m128i*)(m + i)), + _mm_setzero_si128())); + if (zero != 0xffffu) + { + i += (unsigned)compat_ctz(~zero & 0xffffu); + break; + } + i += 16; + } +#else + while (limit - i >= sizeof(uint32_t)) + { + uint32_t w; + memcpy(&w, m + i, sizeof(w)); + if (w) + break; + i += sizeof(uint32_t); + } +#endif + while (i < limit && !m[i]) + i++; + if (i >= limit) + return limit; + /* Back to the start of the item holding byte i. */ + i -= (i - idx) % step; + if (m[i]) + return i; + i += step; + } +} + +/* An item's value at byte, 16- or 32-bit size in the given order. */ +static INLINE unsigned cheat_read_item(const unsigned char *p, + unsigned bytes, bool big_endian) +{ + switch (bytes) + { + case 2: + return big_endian ? ((unsigned)p[0] << 8) | p[1] + : p[0] | ((unsigned)p[1] << 8); + case 4: + return big_endian + ? ((unsigned)p[0] << 24) | ((unsigned)p[1] << 16) + | ((unsigned)p[2] << 8) | p[3] + : p[0] | ((unsigned)p[1] << 8) + | ((unsigned)p[2] << 16) | ((unsigned)p[3] << 24); + default: + break; + } + return p[0]; +} + +/* One region's items at byte size or wider, where an item is one + * match: the search type is chosen once, outside the loop. Same rule as + * the general path - an item still in the running that fails the test + * has its match bytes cleared and the count lowered. */ +#define CHEAT_SEARCH_ITEMS(COND) \ + for (;; idx += bytes) \ + { \ + unsigned cs, ps; \ + if (idx >= end || end - idx < bytes) \ + break; \ + if (!matches[idx]) \ + { \ + idx = cheat_skip_unmatched(matches, idx, end - bytes + 1, bytes); \ + if (idx >= end || end - idx < bytes) \ + break; \ + } \ + cs = cheat_read_item(curr + (idx - start), bytes, big_endian); \ + ps = cheat_read_item(prev + idx, bytes, big_endian); \ + (void)ps; /* unread by an exact-value search */ \ + if (!(COND)) \ + { \ + switch (bytes) \ + { \ + case 4: \ + matches[idx + 3] = 0; \ + matches[idx + 2] = 0; \ + /* fall through */ \ + case 2: \ + matches[idx + 1] = 0; \ + /* fall through */ \ + default: \ + matches[idx] = 0; \ + break; \ + } \ + if (num > 0) \ + num--; \ + } \ + } + +static void cheat_search_whole_items(enum cheat_search_type type, + const unsigned char *curr, unsigned start, unsigned end, + unsigned bytes, bool big_endian, uint8_t *matches, + const unsigned char *prev, unsigned *num_matches, + unsigned exact, unsigned plus, unsigned minus) +{ + unsigned idx = (start + bytes - 1) / bytes * bytes; + /* Counted in a local: stores through the uint8_t match array may + * alias anything, so the count would be reloaded every item. */ + unsigned num = *num_matches; + switch (type) + { + case CHEAT_SEARCH_TYPE_EXACT: CHEAT_SEARCH_ITEMS(cs == exact) break; + case CHEAT_SEARCH_TYPE_LT: CHEAT_SEARCH_ITEMS(cs < ps) break; + case CHEAT_SEARCH_TYPE_GT: CHEAT_SEARCH_ITEMS(cs > ps) break; + case CHEAT_SEARCH_TYPE_LTE: CHEAT_SEARCH_ITEMS(cs <= ps) break; + case CHEAT_SEARCH_TYPE_GTE: CHEAT_SEARCH_ITEMS(cs >= ps) break; + case CHEAT_SEARCH_TYPE_EQ: CHEAT_SEARCH_ITEMS(cs == ps) break; + case CHEAT_SEARCH_TYPE_NEQ: CHEAT_SEARCH_ITEMS(cs != ps) break; + case CHEAT_SEARCH_TYPE_EQPLUS: CHEAT_SEARCH_ITEMS(cs == ps + plus) break; + case CHEAT_SEARCH_TYPE_EQMINUS: CHEAT_SEARCH_ITEMS(cs == ps - minus) break; + } + *num_matches = num; +} + static int cheat_manager_search(enum cheat_search_type search_type) { size_t _len; char msg[100]; cheat_manager_t *cheat_st = &cheat_manager_state; - cheat_region_t region = { NULL, 0, 0 }; unsigned char *curr = NULL; unsigned char *prev = cheat_st->prev_memory_buf; + unsigned int r = 0; + unsigned int region_start = 0; unsigned int idx = 0; unsigned int curr_val = 0; unsigned int prev_val = 0; @@ -1223,16 +1457,46 @@ static int cheat_manager_search(enum cheat_search_type search_type) cheat_manager_setup_search_meta(cheat_st->search_bit_size, &bytes_per_item, &mask, &bits); - /* little endian FF000000 = 256 */ - for (idx = 0; idx < cheat_st->total_memory_size; idx = idx + bytes_per_item) + /* Region by region: an item is searched only where it lies whole + * inside one region, at addresses that are multiples of its size. + * An item whose match byte is clear was ruled out by an earlier + * search and cannot match again, so runs of them are stepped over + * by cheat_skip_unmatched rather than visited one at a time; after + * the first search or two that is nearly all of memory. + * little endian FF000000 = 256 */ + for (r = 0, region_start = 0; r < cheat_st->num_memory_buffers; + region_start += cheat_st->memory_size_list[r], r++) { + unsigned region_end = region_start + cheat_st->memory_size_list[r]; + curr = cheat_st->memory_buf_list[r]; + offset = region_start; + idx = (region_start + bytes_per_item - 1) + / bytes_per_item * bytes_per_item; + + if (bits == 8) + { + cheat_search_whole_items(search_type, curr, region_start, + region_end, bytes_per_item, cheat_st->big_endian, + cheat_st->matches, prev, &cheat_st->num_matches, + cheat_st->search_exact_value, + cheat_st->search_eqplus_value, + cheat_st->search_eqminus_value); + continue; + } + + for (;; idx += bytes_per_item) + { unsigned byte_part; - if (!cheat_region_seek(®ion, idx, bytes_per_item)) - continue; - curr = region.base; - offset = region.start; - + if (region_end - idx < bytes_per_item || idx >= region_end) + break; + if (!cheat_st->matches[idx]) + { + idx = cheat_skip_unmatched(cheat_st->matches, idx, + region_end - bytes_per_item + 1, bytes_per_item); + if (idx >= region_end || region_end - idx < bytes_per_item) + break; + } switch (bytes_per_item) { case 2: @@ -1244,12 +1508,18 @@ static int cheat_manager_search(enum cheat_search_type search_type) *(prev + idx) + (*(prev + idx + 1) * 256); break; case 4: + /* Unsigned shifts: a top byte of 0x80 or more times 2^24 in + * int overflowed, which is undefined. Same value. */ curr_val = cheat_st->big_endian ? - (*(curr + idx - offset) * 256 * 256 * 256) + (*(curr + idx + 1 - offset) * 256 * 256) + (*(curr + idx + 2 - offset) * 256) + *(curr + idx + 3 - offset) : - *(curr + idx - offset) + (*(curr + idx + 1 - offset) * 256) + (*(curr + idx + 2 - offset) * 256 * 256) + (*(curr + idx + 3 - offset) * 256 * 256 * 256); + ((unsigned)curr[idx - offset] << 24) | ((unsigned)curr[idx + 1 - offset] << 16) + | ((unsigned)curr[idx + 2 - offset] << 8) | curr[idx + 3 - offset] : + curr[idx - offset] | ((unsigned)curr[idx + 1 - offset] << 8) + | ((unsigned)curr[idx + 2 - offset] << 16) | ((unsigned)curr[idx + 3 - offset] << 24); prev_val = cheat_st->big_endian ? - (*(prev + idx) * 256 * 256 * 256) + (*(prev + idx + 1) * 256 * 256) + (*(prev + idx + 2) * 256) + *(prev + idx + 3) : - *(prev + idx) + (*(prev + idx + 1) * 256) + (*(prev + idx + 2) * 256 * 256) + (*(prev + idx + 3) * 256 * 256 * 256); + ((unsigned)prev[idx] << 24) | ((unsigned)prev[idx + 1] << 16) + | ((unsigned)prev[idx + 2] << 8) | prev[idx + 3] : + prev[idx] | ((unsigned)prev[idx + 1] << 8) + | ((unsigned)prev[idx + 2] << 16) | ((unsigned)prev[idx + 3] << 24); break; case 1: default: @@ -1315,6 +1585,7 @@ static int cheat_manager_search(enum cheat_search_type search_type) } } } + } } offset = 0; diff --git a/samples/cheats/cheat_regions/cheat_regions_test.c b/samples/cheats/cheat_regions/cheat_regions_test.c index 74e87439dd..872f93f11a 100644 --- a/samples/cheats/cheat_regions/cheat_regions_test.c +++ b/samples/cheats/cheat_regions/cheat_regions_test.c @@ -30,6 +30,9 @@ bool core_get_system_info(struct retro_system_info *info) { (void)info; return f bool core_set_cheat(retro_ctx_cheat_info_t *info) { (void)info; return true; } bool core_reset_cheat(void) { return true; } retro_time_t cpu_features_get_time_usec(void) { return 0; } +/* No SIMD reported: the search scans with its baseline path here; + * samples/cheats/cheat_search runs the wider ones. */ +uint64_t cpu_features_get(void) { return 0; } bool input_set_rumble_state(unsigned port, enum retro_rumble_effect e, uint16_t strength) { (void)port; (void)e; (void)strength; return true; } config_file_t *config_file_new_from_path_to_string(const char *p) { (void)p; return NULL; } diff --git a/samples/cheats/cheat_search/Makefile b/samples/cheats/cheat_search/Makefile new file mode 100644 index 0000000000..dc7a3440fc --- /dev/null +++ b/samples/cheats/cheat_search/Makefile @@ -0,0 +1,45 @@ +include ../../sample_objdir.mk + +TARGET := cheat_search_test + +REPO_ROOT := ../../.. +LRC := $(REPO_ROOT)/libretro-common + +SOURCES := cheat_search_test.c \ + $(REPO_ROOT)/cheat_manager.c \ + $(LRC)/compat/compat_strl.c \ + $(LRC)/string/stdstring.c \ + $(LRC)/encodings/encoding_utf.c \ + $(LRC)/features/features_cpu.c + +# cheat_manager.h reaches setting_list.h as ../setting_list.h +CFLAGS += -Wall -std=gnu99 -g1 -O0 -DRARCH_INTERNAL -DHAVE_CHEATS -DCHEAT_SCAN_TEST_HOOKS \ + -I$(REPO_ROOT) -I$(REPO_ROOT)/menu -I$(LRC)/include + +OBJS := $(call sample_objs,$(SOURCES:.c=.o)) + +ifneq ($(SANITIZER),) + CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS) + LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS) +endif + +all: $(TARGET) + +%.o: %.c + $(CC) -c -o $@ $< $(CFLAGS) + +$(SAMPLE_OBJDIR)/%.o: $$(call sample_src,$$*).c + @mkdir -p $(@D) + $(CC) -c -o $@ $< $(CFLAGS) + +$(TARGET): $(OBJS) + $(CC) -o $@ $^ $(LDFLAGS) + +check: $(TARGET) + ./$(TARGET) + +clean: + rm -rf $(SAMPLE_OBJDIR) + rm -f $(TARGET) $(TARGET).exe $(OBJS) + +.PHONY: all check clean diff --git a/samples/cheats/cheat_search/cheat_search_test.c b/samples/cheats/cheat_search/cheat_search_test.c new file mode 100644 index 0000000000..068c43205c --- /dev/null +++ b/samples/cheats/cheat_search/cheat_search_test.c @@ -0,0 +1,328 @@ +/* cheat_manager.c's memory search against the algorithm it replaced, + * kept here as the reference: random core memory maps (regions of + * sizes that are not multiples of 2 or 4), every item size (1, 2 and 4 + * bits, bytes, 16- and 32-bit words), both byte orders, every search + * type, the item size changed between searches without starting over, + * chained searches with memory changing between them - in runs, + * scattered, or not at all - and after each search the match bytes, + * the match count and the saved copy of memory must agree exactly. + * features_cpu.c is linked for real and the whole run repeats with the + * match-byte scan capped at each tier (SSE2, AVX2, AVX-512BW), so every + * one this CPU has is checked. */ + +#include +#include +#include +#include + +#include "cheat_manager.h" +#include "runloop.h" +#include "core.h" +#include "msg_hash.h" +#include "verbosity.h" + +/* What cheat_manager.c calls outside itself */ +static runloop_state_t test_runloop; +runloop_state_t *runloop_state_get_ptr(void) { return &test_runloop; } +void runloop_msg_queue_push(const char *msg, size_t len, unsigned prio, + unsigned duration, bool flush, char *title, + enum message_queue_icon icon, enum message_queue_category category) +{ (void)msg; (void)len; (void)prio; (void)duration; (void)flush; + (void)title; (void)icon; (void)category; } +void runloop_frame_work_set(unsigned work, bool on) { (void)work; (void)on; } +const char *msg_hash_to_str(enum msg_hash_enums msg) { (void)msg; return ""; } +bool core_get_memory(retro_ctx_memory_info_t *info) { (void)info; return false; } +bool core_get_system_info(struct retro_system_info *info) { (void)info; return false; } +bool core_set_cheat(retro_ctx_cheat_info_t *info) { (void)info; return true; } +bool core_reset_cheat(void) { return true; } +bool input_set_rumble_state(unsigned port, enum retro_rumble_effect e, + uint16_t strength) { (void)port; (void)e; (void)strength; return true; } +config_file_t *config_file_new_from_path_to_string(const char *p) { (void)p; return NULL; } +config_file_t *config_file_new_alloc(void) { return NULL; } +config_file_t *config_file_new_with_callback(const char *p, + config_file_cb_t *cb) { (void)p; (void)cb; return NULL; } +void config_file_free(config_file_t *c) { (void)c; } +bool config_file_write(config_file_t *c, const char *p, bool s) +{ (void)c; (void)p; (void)s; return false; } +size_t config_set_int(config_file_t *c, const char *k, int v) { (void)c; (void)k; (void)v; return 0; } +size_t config_set_uint(config_file_t *c, const char *k, unsigned v) { (void)c; (void)k; (void)v; return 0; } +void config_set_string(config_file_t *c, const char *k, const char *v) { (void)c; (void)k; (void)v; } +int filestream_delete(const char *p) { (void)p; return -1; } +size_t fill_pathname_join(char *o, const char *d, const char *p, size_t n) +{ (void)d; (void)p; if (n) *o = '\0'; return 0; } +const char *path_basename_nocompression(const char *p) { return p; } +bool path_is_valid(const char *p) { (void)p; return false; } +bool path_mkdir(const char *p) { (void)p; return false; } + + +void RARCH_LOG(const char *fmt, ...) { (void)fmt; } + +static unsigned failures; +#ifdef CHEAT_SCAN_TEST_HOOKS +extern int cheat_scan_tier_cap; +#endif +static uint32_t rng = 0xC0FFEEu; +static unsigned rnd(void) { rng = rng * 1103515245u + 12345u; return rng >> 8; } + +#define MAX_REGIONS 6 +static unsigned char *mem[MAX_REGIONS]; +static unsigned mem_len[MAX_REGIONS]; +static unsigned nregions; +static rarch_memory_descriptor_t descs[MAX_REGIONS]; + +static void map_memory(unsigned scale) +{ + unsigned i; + nregions = 1 + rnd() % MAX_REGIONS; + for (i = 0; i < nregions; i++) + { + unsigned j; + mem_len[i] = 1 + rnd() % scale; + mem[i] = (unsigned char*)malloc(mem_len[i]); + for (j = 0; j < mem_len[i]; j++) + mem[i][j] = (unsigned char)((rnd() & 3) ? rnd() % 4 : rnd()); + memset(&descs[i], 0, sizeof(descs[i])); + descs[i].core.flags = RETRO_MEMDESC_SYSTEM_RAM; + descs[i].core.ptr = mem[i]; + descs[i].core.len = mem_len[i]; + } + test_runloop.system.mmaps.descriptors = descs; + test_runloop.system.mmaps.num_descriptors = nregions; +} + +static void unmap_memory(void) +{ + unsigned i; + for (i = 0; i < nregions; i++) + free(mem[i]); +} + +/* The previous search, item by item through a region lookup per item. */ +static bool ref_seek(unsigned address, unsigned size, unsigned char **base, + unsigned *start) +{ + unsigned i, s = 0; + cheat_manager_t *st = &cheat_manager_state; + for (i = 0; i < st->num_memory_buffers; i++) + { + unsigned len = st->memory_size_list[i]; + if (address - s < len) + { + if (size > len - (address - s)) + return false; + *base = st->memory_buf_list[i]; + *start = s; + return true; + } + s += len; + } + return false; +} + +static void ref_search(enum cheat_search_type type, uint8_t *matches, + unsigned *num_matches, uint8_t *prev) +{ + cheat_manager_t *st = &cheat_manager_state; + unsigned bytes_per_item = 1, bits = 8, mask = 0xFF, idx, i, off = 0; + switch (st->search_bit_size) + { + case 0: bits = 1; mask = 0x01; break; + case 1: bits = 2; mask = 0x03; break; + case 2: bits = 4; mask = 0x0F; break; + case 3: break; + case 4: bytes_per_item = 2; mask = 0xFFFF; break; + case 5: bytes_per_item = 4; mask = 0xFFFFFFFF; break; + } + for (idx = 0; idx < st->total_memory_size; idx += bytes_per_item) + { + unsigned char *curr; + unsigned offset, curr_val, prev_val, byte_part; + if (!ref_seek(idx, bytes_per_item, &curr, &offset)) + continue; + switch (bytes_per_item) + { + case 2: + curr_val = st->big_endian + ? (curr[idx - offset] * 256) + curr[idx + 1 - offset] + : curr[idx - offset] + (curr[idx + 1 - offset] * 256); + prev_val = st->big_endian + ? (prev[idx] * 256) + prev[idx + 1] + : prev[idx] + (prev[idx + 1] * 256); + break; + case 4: + curr_val = st->big_endian + ? ((unsigned)curr[idx - offset] << 24) | (curr[idx + 1 - offset] << 16) + | (curr[idx + 2 - offset] << 8) | curr[idx + 3 - offset] + : curr[idx - offset] | (curr[idx + 1 - offset] << 8) + | (curr[idx + 2 - offset] << 16) | ((unsigned)curr[idx + 3 - offset] << 24); + prev_val = st->big_endian + ? ((unsigned)prev[idx] << 24) | (prev[idx + 1] << 16) + | (prev[idx + 2] << 8) | prev[idx + 3] + : prev[idx] | (prev[idx + 1] << 8) + | (prev[idx + 2] << 16) | ((unsigned)prev[idx + 3] << 24); + break; + default: + curr_val = curr[idx - offset]; + prev_val = prev[idx]; + break; + } + for (byte_part = 0; byte_part < 8 / bits; byte_part++) + { + unsigned cs = (curr_val >> (byte_part * bits)) & mask; + unsigned ps = (prev_val >> (byte_part * bits)) & mask; + unsigned pm = (bits < 8) ? (matches[idx] & (mask << (byte_part * bits))) + : matches[idx]; + bool m = false; + if (!pm) + continue; + switch (type) + { + case CHEAT_SEARCH_TYPE_EXACT: m = cs == st->search_exact_value; break; + case CHEAT_SEARCH_TYPE_LT: m = cs < ps; break; + case CHEAT_SEARCH_TYPE_GT: m = cs > ps; break; + case CHEAT_SEARCH_TYPE_LTE: m = cs <= ps; break; + case CHEAT_SEARCH_TYPE_GTE: m = cs >= ps; break; + case CHEAT_SEARCH_TYPE_EQ: m = cs == ps; break; + case CHEAT_SEARCH_TYPE_NEQ: m = cs != ps; break; + case CHEAT_SEARCH_TYPE_EQPLUS: m = cs == ps + st->search_eqplus_value; break; + case CHEAT_SEARCH_TYPE_EQMINUS: m = cs == ps - st->search_eqminus_value; break; + } + if (!m) + { + if (bits < 8) + matches[idx] &= (~(mask << (byte_part * bits))) & 0xFF; + else + memset(matches + idx, 0, bytes_per_item); + if (*num_matches > 0) + (*num_matches)--; + } + } + } + for (i = 0; i < st->num_memory_buffers; i++) + { + memcpy(prev + off, st->memory_buf_list[i], st->memory_size_list[i]); + off += st->memory_size_list[i]; + } +} + +static void run_search(enum cheat_search_type type, rarch_setting_t *s) +{ + switch (type) + { + case CHEAT_SEARCH_TYPE_EXACT: cheat_manager_search_exact(s, 0, false); break; + case CHEAT_SEARCH_TYPE_LT: cheat_manager_search_lt(s, 0, false); break; + case CHEAT_SEARCH_TYPE_GT: cheat_manager_search_gt(s, 0, false); break; + case CHEAT_SEARCH_TYPE_LTE: cheat_manager_search_lte(s, 0, false); break; + case CHEAT_SEARCH_TYPE_GTE: cheat_manager_search_gte(s, 0, false); break; + case CHEAT_SEARCH_TYPE_EQ: cheat_manager_search_eq(s, 0, false); break; + case CHEAT_SEARCH_TYPE_NEQ: cheat_manager_search_neq(s, 0, false); break; + case CHEAT_SEARCH_TYPE_EQPLUS: cheat_manager_search_eqplus(s, 0, false); break; + case CHEAT_SEARCH_TYPE_EQMINUS: cheat_manager_search_eqminus(s, 0, false); break; + } +} + +/* Changes a few bytes, a run, or nothing. */ +static void mutate(void) +{ + unsigned r = rnd() % nregions, k, n; + switch (rnd() % 3) + { + case 0: + n = rnd() % 4; + for (k = 0; k < n; k++) + mem[r][rnd() % mem_len[r]] += (unsigned char)(1 + rnd() % 3); + break; + case 1: + { + unsigned at = rnd() % mem_len[r], len = 1 + rnd() % 40; + for (k = at; k < at + len && k < mem_len[r]; k++) + mem[r][k] = (unsigned char)(mem[r][k] - 1); + break; + } + default: + break; + } +} + +int main(void) +{ + static const enum cheat_search_type types[] = { + CHEAT_SEARCH_TYPE_EXACT, CHEAT_SEARCH_TYPE_LT, CHEAT_SEARCH_TYPE_GT, + CHEAT_SEARCH_TYPE_LTE, CHEAT_SEARCH_TYPE_GTE, CHEAT_SEARCH_TYPE_EQ, + CHEAT_SEARCH_TYPE_NEQ, CHEAT_SEARCH_TYPE_EQPLUS, CHEAT_SEARCH_TYPE_EQMINUS + }; + unsigned round, searches = 0; + int tier; + rarch_setting_t setting; + memset(&setting, 0, sizeof(setting)); + + for (tier = 0; tier <= 2; tier++) + for (round = 0; round < 600; round++) + { + cheat_manager_t *st = &cheat_manager_state; + unsigned total, step, scale = (round % 10 == 0) ? 3000 : 70; + uint8_t *m_ref, *p_ref; + unsigned n_ref; + +#ifdef CHEAT_SCAN_TEST_HOOKS + cheat_scan_tier_cap = tier; +#endif + map_memory(scale); + st->search_bit_size = rnd() % 6; + cheat_manager_initialize_memory(&setting, 0, false); + st->big_endian = (rnd() & 1) != 0; + total = st->total_memory_size; + if (!st->matches || !st->prev_memory_buf) + { + printf("FAIL round %u: search not initialised\n", round); + failures++; + unmap_memory(); + continue; + } + m_ref = (uint8_t*)malloc(total); + p_ref = (uint8_t*)malloc(total); + memcpy(m_ref, st->matches, total); + memcpy(p_ref, st->prev_memory_buf, total); + n_ref = st->num_matches; + + for (step = 0; step < 8; step++) + { + enum cheat_search_type t = types[rnd() % 9]; + /* The size can change in the menu without the search being + * started again; match bytes cleared at one size are then read + * at another. */ + if (rnd() % 8 == 0) + st->search_bit_size = rnd() % 6; + st->search_exact_value = rnd() % 4; + st->search_eqplus_value = 1 + rnd() % 2; + st->search_eqminus_value = 1 + rnd() % 2; + mutate(); + ref_search(t, m_ref, &n_ref, p_ref); + run_search(t, &setting); + searches++; + if ( st->num_matches != n_ref + || memcmp(st->matches, m_ref, total) + || memcmp(st->prev_memory_buf, p_ref, total)) + { + printf("FAIL tier %d round %u step %u: type %d, bit size %u, %s, %u bytes " + "in %u regions: %u matches, reference %u\n", + tier, round, step, (int)t, st->search_bit_size, + st->big_endian ? "big-endian" : "little-endian", + total, nregions, st->num_matches, n_ref); + failures++; + break; + } + } + free(m_ref); + free(p_ref); + unmap_memory(); + } + + if (failures) + { + printf("cheat_search: %u failing rounds\n", failures); + return 1; + } + printf("cheat_search: %u searches agree with the reference\n", searches); + return 0; +}