nissy-core

The "engine" of nissy, including the H48 optimal solver.
git clone https://git.tronto.net/nissy-core
Download | Log | Files | Refs | README | LICENSE

solve.h (17864B)


      1 #define H48_STARTING_MOVES 4
      2 
      3 #if H48_STARTING_MOVES == 3
      4 #define H48_STARTING_CUBES 3240 /* Number of 3-move sequences */
      5 #elif H48_STARTING_MOVES == 4
      6 #define H48_STARTING_CUBES 43254 /* Number of 4-move sequences */
      7 #endif
      8 
      9 #define H48_SORT_TASKS_MIN_DEPTH 16
     10 #define H48_LOG_PROGRESS_MIN_DEPTH 15
     11 
     12 typedef struct {
     13 	cube_t cube;
     14 	uint8_t moves[H48_STARTING_MOVES];
     15 	int64_t rank;
     16 	uint64_t tmask[H48_STARTING_MOVES];
     17 } solve_h48_task_t;
     18 
     19 typedef struct {
     20 	cube_t start_cube;
     21 	cube_t cube;
     22 	cube_t inverse;
     23 	int8_t target_depth;
     24 	solution_moves_t *solution_moves;
     25 	solution_settings_t *solution_settings;
     26 	const uint64_t *tmask;
     27 	solution_list_t *solution_list;
     28 	int8_t lb_normal;
     29 	int8_t lb_inverse;
     30 	bool use_lb_normal;
     31 	bool use_lb_inverse;
     32 	uint8_t h;
     33 	uint8_t base;
     34 	const uint32_t *cocsepdata;
     35 	const unsigned char *h48data;
     36 	const unsigned char *h48data_fallback_eoesep;
     37 	uint64_t movemask_normal;
     38 	uint64_t movemask_inverse;
     39 	uint64_t nodes_visited;
     40 	uint64_t table_fallbacks;
     41 	uint64_t table_lookups;
     42 	int8_t threads;
     43 	int ntasks;
     44 	solve_h48_task_t *tasks;
     45 	int thread_id;
     46 	wrapthread_define_struct_mutex_t(*solutions_mutex);
     47 	wrapthread_atomic int *status;
     48 	wrapthread_atomic bool thread_done;
     49 } dfsarg_solve_h48_t;
     50 
     51 typedef struct {
     52 	cube_t cube;
     53 	int8_t nmoves;
     54 	uint8_t moves[H48_STARTING_MOVES];
     55 	int8_t minmoves;
     56 	int8_t maxmoves;
     57 	int8_t *shortest_sol;
     58 	uint64_t tmask[H48_STARTING_MOVES];
     59 } dfsarg_solve_h48_maketasks_t;
     60 
     61 STATIC long long solve_h48_dispatch(oriented_cube_t, const char *, unsigned,
     62     unsigned, unsigned, unsigned, unsigned, unsigned, unsigned long long,
     63     const unsigned char *, unsigned, char *,
     64     long long [static NISSY_SIZE_SOLVE_STATS], int (*)(void *), void *);
     65 STATIC_INLINE bool solve_h48_stop(dfsarg_solve_h48_t [static 1]);
     66 STATIC int64_t solve_h48_maketasks(
     67     dfsarg_solve_h48_t [static 1], dfsarg_solve_h48_maketasks_t [static 1],
     68     solve_h48_task_t [static H48_STARTING_CUBES], int [static 1]);
     69 STATIC void *solve_h48_runthread(void *);
     70 STATIC int64_t solve_h48_dfs(dfsarg_solve_h48_t [static 1]);
     71 STATIC void solve_h48_log_solutions(solution_list_t [static 1], size_t);
     72 STATIC int solve_h48_compare_tasks(const void *, const void *);
     73 STATIC int64_t solve_h48(oriented_cube_t, uint8_t, uint8_t, uint64_t, uint8_t,
     74     uint8_t, uint64_t, const unsigned char *, size_t, char *,
     75     long long [static NISSY_SIZE_SOLVE_STATS], int (*)(void *), void *);
     76 
     77 STATIC long long solve_h48_dispatch(
     78 	oriented_cube_t oc,
     79         const char *solver,
     80         unsigned nissflag,
     81         unsigned minmoves,
     82         unsigned maxmoves,
     83         unsigned maxsols,
     84         unsigned optimal,
     85         unsigned threads,
     86         unsigned long long data_size,
     87         const unsigned char *data,
     88         unsigned sols_size,
     89         char *sols,
     90         long long stats[static NISSY_SIZE_SOLVE_STATS],
     91         int (*poll_status)(void *),
     92         void *poll_status_data
     93 )
     94 {
     95 	uint8_t h;
     96 	long long err;
     97 
     98 	err = parse_h48h(solver, &h);
     99 	if (err != NISSY_OK)
    100 		return err;
    101 
    102 	return solve_h48(oc, minmoves, maxmoves, maxsols, optimal, threads,
    103 	    data_size, data, sols_size, sols, stats,
    104 	    poll_status, poll_status_data);
    105 }
    106 
    107 STATIC_INLINE bool
    108 solve_h48_stop(dfsarg_solve_h48_t arg[static 1])
    109 {
    110 	uint32_t data, data_inv;
    111 	int64_t coord;
    112 	int8_t target, nh, n;
    113 	uint8_t pval, pval_min, pval_eoesep;
    114 
    115 	arg->movemask_normal = arg->movemask_inverse = MM18_ALLMOVES;
    116 	arg->nodes_visited++;
    117 
    118 	n = arg->solution_moves->nmoves + arg->solution_moves->npremoves;
    119 	target = arg->target_depth - n;
    120 
    121 	/* We'll never get a bound higher than base + 3 */
    122 	if (arg->base + 3 <= target)
    123 		return false;
    124 
    125 	/* Preliminary probing using last computed bound, if possible */
    126 
    127 	if ((arg->use_lb_normal && arg->lb_normal > target) ||
    128 	    (arg->use_lb_inverse && arg->lb_inverse > target))
    129 		return true;
    130 
    131 	/* Preliminary corner probing */
    132 
    133 	if (get_h48_cdata(arg->cube, arg->cocsepdata, &data) > target ||
    134 	    get_h48_cdata(arg->inverse, arg->cocsepdata, &data_inv) > target)
    135 		return true;
    136 
    137 	/* Inverse probing */
    138 
    139 	if (!arg->use_lb_inverse) {
    140 		arg->table_lookups++;
    141 		arg->use_lb_inverse = true;
    142 		coord = coord_h48_edges(
    143 		    arg->inverse, COCLASS(data_inv), TTREP(data_inv), arg->h);
    144 		pval = get_h48_pval_and_min(arg->h48data, coord, &pval_min);
    145 
    146 		if (pval == 0) {
    147 			arg->table_fallbacks++;
    148 
    149 			pval_eoesep = get_eoesep_pval_cube(
    150 			    arg->h48data_fallback_eoesep, arg->inverse);
    151 			pval = MAX(pval_min, pval_eoesep);
    152 		} else {
    153 			pval += arg->base;
    154 		}
    155 
    156 		arg->lb_inverse = pval;
    157 	}
    158 
    159 	if (arg->lb_inverse > target)
    160 		return true;
    161 	nh = arg->lb_inverse == target;
    162 	arg->movemask_normal = nh * MM18_NOHALFTURNS + (1-nh) * MM18_ALLMOVES;
    163 
    164 	/* Normal probing */
    165 
    166 	if (!arg->use_lb_normal) {
    167 		arg->table_lookups++;
    168 		arg->use_lb_normal = true;
    169 		coord = coord_h48_edges(
    170 		    arg->cube, COCLASS(data), TTREP(data), arg->h);
    171 		pval = get_h48_pval_and_min(arg->h48data, coord, &pval_min);
    172 
    173 		if (pval == 0) {
    174 			arg->table_fallbacks++;
    175 
    176 			pval_eoesep = get_eoesep_pval_cube(
    177 			    arg->h48data_fallback_eoesep, arg->cube);
    178 			pval = MAX(pval_min, pval_eoesep);
    179 		} else {
    180 			pval += arg->base;
    181 		}
    182 
    183 		arg->lb_normal = pval;
    184 	}
    185 
    186 	if (arg->lb_normal > target)
    187 		return true;
    188 	nh = arg->lb_normal == target;
    189 	arg->movemask_inverse = nh * MM18_NOHALFTURNS + (1-nh) * MM18_ALLMOVES;
    190 
    191 	return false;
    192 }
    193 
    194 STATIC int64_t
    195 solve_h48_dfs(dfsarg_solve_h48_t arg[static 1])
    196 {
    197 	int64_t ret, n;
    198 	uint8_t m, nm, lbn, lbi, t;
    199 	uint64_t mm_normal, mm_inverse;
    200 	bool ulbi, ulbn;
    201 	cube_t backup_cube, backup_inverse;
    202 
    203 	nm = arg->solution_moves->nmoves + arg->solution_moves->npremoves;
    204 	if (equal(arg->cube, SOLVED_CUBE)) {
    205 		if (arg->target_depth != nm)
    206 			return 0;
    207 		wrapthread_mutex_lock(arg->solutions_mutex);
    208 		ret = appendsolution(arg->solution_moves, H48_STARTING_MOVES,
    209 		    arg->tmask, arg->solution_settings, arg->solution_list);
    210 		wrapthread_mutex_unlock(arg->solutions_mutex);
    211 		return ret;
    212 	}
    213 
    214 	if (solve_h48_stop(arg))
    215 		return 0;
    216 
    217 	t = arg->solution_list->shortest_sol + arg->solution_settings->optimal;
    218 	if (nm + 1 > MIN(t, arg->target_depth) ||
    219 	    arg->solution_list->nsols >= arg->solution_settings->maxsolutions)
    220 		return 0;
    221 
    222 	backup_cube = arg->cube;
    223 	backup_inverse = arg->inverse;
    224 	lbn = arg->lb_normal;
    225 	lbi = arg->lb_inverse;
    226 	ulbn = arg->use_lb_normal;
    227 	ulbi = arg->use_lb_inverse;
    228 
    229 	ret = 0;
    230 	mm_normal = arg->movemask_normal;
    231 	if (arg->solution_moves->nmoves > 0) {
    232 		m = arg->solution_moves->moves[arg->solution_moves->nmoves-1];
    233 		mm_normal &= allowedmask[movebase(m)];
    234 	}
    235 	mm_inverse = arg->movemask_inverse;
    236 	if (arg->solution_moves->npremoves > 0) {
    237 		m = arg->solution_moves->premoves[arg->solution_moves->npremoves-1];
    238 		mm_inverse &= allowedmask[movebase(m)];
    239 	}
    240 	if (popcount_u32(mm_normal) <= popcount_u32(mm_inverse)) {
    241 		arg->solution_moves->nmoves++;
    242 		for (m = 0; m < 18; m++) {
    243 			if (!(mm_normal & MM_SINGLE(m)))
    244 				continue;
    245 			arg->solution_moves->moves[
    246 			    arg->solution_moves->nmoves-1] = m;
    247 			arg->cube = move(backup_cube, m);
    248 			arg->inverse = premove(backup_inverse, m);
    249 			arg->lb_inverse = lbi;
    250 			arg->use_lb_normal = false;
    251 			arg->use_lb_inverse = ulbi && m % 3 == 1;
    252 			n = solve_h48_dfs(arg);
    253 			if (n < 0)
    254 				return n;
    255 			ret += n;
    256 		}
    257 		arg->solution_moves->nmoves--;
    258 	} else {
    259 		arg->solution_moves->npremoves++;
    260 		for (m = 0; m < 18; m++) {
    261 			if(!(mm_inverse & MM_SINGLE(m)))
    262 				continue;
    263 			arg->solution_moves->premoves[
    264 			    arg->solution_moves->npremoves-1] = m;
    265 			arg->inverse = move(backup_inverse, m);
    266 			arg->cube = premove(backup_cube, m);
    267 			arg->lb_normal = lbn;
    268 			arg->use_lb_inverse = false;
    269 			arg->use_lb_normal = ulbn && m % 3 == 1;
    270 			n = solve_h48_dfs(arg);
    271 			if (n < 0)
    272 				return n;
    273 			ret += n;
    274 		}
    275 		arg->solution_moves->npremoves--;
    276 	}
    277 
    278 	arg->cube = backup_cube;
    279 	arg->inverse = backup_inverse;
    280 
    281 	return ret;
    282 }
    283 
    284 STATIC void *
    285 solve_h48_runthread(void *arg)
    286 {
    287 	int i, j;
    288 	uint8_t lastmove;
    289 	int64_t nprev;
    290 	dfsarg_solve_h48_t *dfsarg;
    291 
    292 	dfsarg = (dfsarg_solve_h48_t *)arg;
    293 
    294 	nprev = 0;
    295 	for (i = dfsarg->thread_id; i < dfsarg->ntasks; i += dfsarg->threads) {
    296 		if (*dfsarg->status == NISSY_STATUS_STOP)
    297 			goto solve_h48_runthread_end;
    298 		while (*dfsarg->status == NISSY_STATUS_PAUSE)
    299 			msleep(BASE_SLEEP_TIME);
    300 
    301 		solution_moves_reset(dfsarg->solution_moves);
    302 		memcpy(dfsarg->solution_moves->moves,
    303 		    dfsarg->tasks[i].moves, H48_STARTING_MOVES);
    304 		dfsarg->solution_moves->nmoves = H48_STARTING_MOVES;
    305 
    306 		dfsarg->cube = dfsarg->start_cube;
    307 		for (j = 0; j < H48_STARTING_MOVES; j++)
    308 			dfsarg->cube =
    309 			   move(dfsarg->cube, dfsarg->tasks[i].moves[j]);
    310 		dfsarg->inverse = inverse(dfsarg->cube);
    311 
    312 		dfsarg->lb_normal = 0;
    313 		dfsarg->lb_inverse = 0;
    314 		dfsarg->use_lb_normal = false;
    315 		dfsarg->use_lb_inverse = false;
    316 		dfsarg->movemask_normal = MM18_ALLMOVES;
    317 		dfsarg->movemask_inverse = MM18_ALLMOVES;
    318 		dfsarg->tmask = dfsarg->tasks[i].tmask;
    319 
    320 		solve_h48_dfs(dfsarg);
    321 
    322 		/*
    323 		We compute the "rank" of each taks, which is used in the next
    324 		step of the IDFS to sort them. This heuristically leads us
    325 		faster to a solution.  The rank is computed by taking the
    326 		number of nodes visited, adjusted by a factor of about
    327 		sqrt(2)/sqrt(3) because there are more sequences starting with
    328 		U, R and F than with D, L and B, as we don't allow e.g. both
    329 		U D and D U, but only U D.
    330 		This trick was suggested by Chen Shuang, the implementation is
    331 		inspired by Andrew Skalski's vcube.
    332 		*/
    333 		lastmove = dfsarg->tasks[i].moves[H48_STARTING_MOVES-1];
    334 		dfsarg->tasks[i].rank = (dfsarg->nodes_visited - nprev) *
    335 		    (movebase(lastmove) % 2 == 0 ? 47525 : 58206);
    336 		nprev = dfsarg->nodes_visited;
    337 	}
    338 
    339 solve_h48_runthread_end:
    340 	dfsarg->thread_done = true;
    341 	return NULL;
    342 }
    343 
    344 STATIC int64_t
    345 solve_h48_maketasks(
    346 	dfsarg_solve_h48_t solve_arg[static 1],
    347 	dfsarg_solve_h48_maketasks_t mtarg[static 1],
    348 	solve_h48_task_t tasks[static H48_STARTING_CUBES],
    349 	int ntasks[static 1]
    350 )
    351 {
    352 	int r;
    353 	int64_t appret;
    354 	uint8_t m, t;
    355 	uint64_t mm;
    356 	cube_t backup_cube;
    357 	solution_moves_t moves;
    358 
    359 	if (equal(mtarg->cube, SOLVED_CUBE)) {
    360 		if (mtarg->nmoves > mtarg->maxmoves ||
    361 		    mtarg->nmoves < mtarg->minmoves ||
    362 		    solutions_done(solve_arg->solution_list,
    363 		        solve_arg->solution_settings, mtarg->nmoves))
    364 			return NISSY_OK;
    365 
    366 		solution_moves_reset(&moves);
    367 		moves.nmoves = mtarg->nmoves;
    368 		memcpy(moves.moves, mtarg->moves, mtarg->nmoves);
    369 
    370 		appret = appendsolution(&moves, mtarg->nmoves, mtarg->tmask,
    371 		    solve_arg->solution_settings, solve_arg->solution_list);
    372 		return appret < 0 ? appret : NISSY_OK;
    373 	}
    374 
    375 	if (mtarg->nmoves == H48_STARTING_MOVES) {
    376 		tasks[*ntasks].cube = mtarg->cube;
    377 		memcpy(tasks[*ntasks].moves, mtarg->moves,
    378 		    H48_STARTING_MOVES * sizeof(uint8_t));
    379 		memcpy(tasks[*ntasks].tmask, mtarg->tmask,
    380 		    H48_STARTING_MOVES * sizeof(uint64_t));
    381 		(*ntasks)++;
    382 		return NISSY_OK;
    383 	}
    384 
    385 	if (mtarg->nmoves == 0) {
    386 		mm = MM18_ALLMOVES;
    387 	} else {
    388 		m = mtarg->moves[mtarg->nmoves-1];
    389 		mm = allowedmask[movebase(m)];
    390 	}
    391 
    392 	mtarg->tmask[mtarg->nmoves] = symmetry_mask(mtarg->cube);
    393 
    394 	mtarg->nmoves++;
    395 	backup_cube = mtarg->cube;
    396 	for (m = 0; m < 18; m++) {
    397 		if (!(mm & MM_SINGLE(m)))
    398 			continue;
    399 
    400 		mtarg->moves[mtarg->nmoves-1] = m;
    401 		mtarg->cube = move(backup_cube, m);
    402 		r = solve_h48_maketasks(solve_arg, mtarg, tasks, ntasks);
    403 		if (r < 0)
    404 			return r;
    405 
    406 		/* Avoid symmetry-equivalent moves from the starting cube */
    407 		for (t = 0; t < NTRANS; t++)
    408 			if (mtarg->tmask[mtarg->nmoves-1] & TM_SINGLE(t))
    409 				mm &= ~MM_SINGLE(transform_move(m, t));
    410 	}
    411 	mtarg->nmoves--;
    412 	mtarg->cube = backup_cube;
    413 
    414 	return NISSY_OK;
    415 }
    416 
    417 STATIC void
    418 solve_h48_log_solutions(solution_list_t s[static 1], size_t e)
    419 {
    420 	size_t i;
    421 	char b;
    422 	while (e != s->used) {
    423 		LOG("[h48 solve] Found solution: ");
    424 		for (i = e; s->buf[i] != '\n' && s->buf[i] != '\0'; i++) ;
    425 		b = s->buf[i];
    426 		s->buf[i] = '\0';
    427 		LOG("%s\n", s->buf + e);
    428 		s->buf[i] = b;
    429 		e = i + 1;
    430 	}
    431 }
    432 
    433 STATIC int
    434 solve_h48_compare_tasks(const void *x, const void *y)
    435 {
    436 	int64_t nodes_x = ((solve_h48_task_t *)x)->rank;
    437 	int64_t nodes_y = ((solve_h48_task_t *)y)->rank;
    438 
    439 	/* Same as returning nodes_y - nodes_x, but avoids overflow */
    440 	return (nodes_x < nodes_y) - (nodes_x > nodes_y);
    441 }
    442 
    443 STATIC int64_t
    444 solve_h48(
    445 	oriented_cube_t oc,
    446 	uint8_t minmoves,
    447 	uint8_t maxmoves,
    448 	uint64_t maxsolutions,
    449 	uint8_t optimal,
    450 	uint8_t threads,
    451 	uint64_t data_size,
    452 	const unsigned char *data,
    453 	size_t solutions_size,
    454 	char *solutions,
    455 	long long stats[static NISSY_SIZE_SOLVE_STATS],
    456 	int (*poll_status)(void *),
    457 	void *poll_status_data
    458 )
    459 {
    460 	int i, ntasks;
    461 	bool td;
    462 	wrapthread_atomic int status, prev_status;
    463 	size_t lastused;
    464 	int8_t d;
    465 	dfsarg_solve_h48_t arg[THREADS];
    466 	solve_h48_task_t tasks[H48_STARTING_CUBES];
    467 	dfsarg_solve_h48_maketasks_t mtarg;
    468 	long double fallback_rate, lookups_per_node;
    469 	uint64_t offset;
    470 	uint64_t nodes_visited, table_lookups, table_fallbacks;
    471 	tableinfo_t info, fbinfo2;
    472 	const uint32_t *cocsepdata;
    473 	const unsigned char *h48data;
    474 	const unsigned char *eoesep;
    475 	solution_moves_t solution_moves[THREADS];
    476 	solution_settings_t settings;
    477 	solution_list_t sollist;
    478 	wrapthread_define_var_thread_t(thread[THREADS]);
    479 	wrapthread_define_var_mutex_t(solutions_mutex);
    480 
    481 	if (!solution_list_init(&sollist, solutions_size, solutions))
    482 		goto solve_h48_error_solutions_buffer;
    483 
    484 	if (readtableinfo_n(data_size, data, 2, &info) != NISSY_OK)
    485 		goto solve_h48_error_data;
    486 
    487 	cocsepdata = (uint32_t *)(data + INFOSIZE);
    488 	h48data = data + COCSEP_FULLSIZE + INFOSIZE;
    489 
    490 	/* Read additional eoesep table */
    491 	offset = info.next;
    492 	if (readtableinfo_n(data_size, data, 3, &fbinfo2)
    493 	    != NISSY_OK)
    494 		goto solve_h48_error_data;
    495 
    496 	/* Some heuristic check to see that it is eoesep */
    497 	if (fbinfo2.bits != 4 || fbinfo2.type != TABLETYPE_SPECIAL)
    498 		goto solve_h48_error_data;
    499 	eoesep = h48data + offset;
    500 
    501 	settings = (solution_settings_t) {
    502 		.unniss = true,
    503 		.maxmoves = maxmoves,
    504 		.maxsolutions = maxsolutions,
    505 		.optimal = optimal,
    506 		.orientation = oc.orientation,
    507 	};
    508 
    509 	for (i = 0; i < threads; i++) {
    510 		arg[i] = (dfsarg_solve_h48_t) {
    511 			.start_cube = oc.cube,
    512 			.cube = oc.cube,
    513 			.h = info.h48h,
    514 			.base = info.base,
    515 			.cocsepdata = cocsepdata,
    516 			.h48data = h48data,
    517 			.h48data_fallback_eoesep = eoesep,
    518 			.solution_moves = &solution_moves[i],
    519 			.solution_settings = &settings,
    520 			.solution_list = &sollist,
    521 			.nodes_visited = 0,
    522 			.table_fallbacks = 0,
    523 			.table_lookups = 0,
    524 			.threads = threads,
    525 			.thread_id = i,
    526 			.solutions_mutex = &solutions_mutex,
    527 			.status = &status,
    528 		};
    529 
    530 	}
    531 
    532 	wrapthread_mutex_init(&solutions_mutex, NULL);
    533 
    534 	mtarg = (dfsarg_solve_h48_maketasks_t) {
    535 		.cube = oc.cube,
    536 		.nmoves = 0,
    537 		.minmoves = minmoves,
    538 		.maxmoves = maxmoves,
    539 	};
    540 	ntasks = 0;
    541 	solve_h48_maketasks(&arg[0], &mtarg, tasks, &ntasks);
    542 	if (ntasks < 0)
    543 		goto solve_h48_error_solutions_buffer;
    544 	if (solutions_done(&sollist, &settings,
    545 	    MAX(minmoves, H48_STARTING_MOVES)))
    546 		goto solve_h48_done;
    547 
    548 	for (i = 0; i < threads; i++) {
    549 		arg[i].ntasks = ntasks;
    550 		arg[i].tasks = tasks;
    551 	}
    552 
    553 	LOG("[H48 solve] Prepared %d tasks\n", ntasks);
    554 
    555 	solve_h48_log_solutions(&sollist, 0);
    556 	lastused = sollist.used;
    557 	status = poll_status == NULL ? NISSY_STATUS_RUN :
    558 	    poll_status(poll_status_data);
    559 	if (!NISSY_CANSLEEP) {
    560 		LOG("[solve h48] Pause / Stop / Resume functionality won't "
    561 		    "be available on this system (can't sleep()).\n");
    562 	}
    563 	for (
    564 	    d = MAX(minmoves, H48_STARTING_MOVES + 1);
    565 	    !(solutions_done(&sollist, &settings, d)) &&
    566 	        status != NISSY_STATUS_STOP;
    567 	    d++
    568 	) {
    569 		if (d >= H48_LOG_PROGRESS_MIN_DEPTH) {
    570 			LOG("[H48 solve] Found %" PRIu64 " solutions, "
    571 			    "searching at depth %" PRId8 "\n",
    572 			    sollist.nsols, d);
    573 		}
    574 
    575 		if (d >= H48_SORT_TASKS_MIN_DEPTH)
    576 			qsort(tasks, ntasks, sizeof(solve_h48_task_t),
    577 			    solve_h48_compare_tasks);
    578 
    579 		for (i = 0; i < threads; i++) {
    580 			arg[i].target_depth = d;
    581 			arg[i].thread_done = false;
    582 			wrapthread_create(
    583 			    &thread[i], NULL, solve_h48_runthread, &arg[i]);
    584 		}
    585 
    586 		/* Log solutions and handle pause / stop / resume */
    587 		if (poll_status != NULL && d >= 15 && NISSY_CANSLEEP) {
    588 			td = false;
    589 			while (!td && status != NISSY_STATUS_STOP) {
    590 				msleep(BASE_SLEEP_TIME);
    591 
    592 				wrapthread_mutex_lock(&solutions_mutex);
    593 				solve_h48_log_solutions(&sollist, lastused);
    594 				lastused = sollist.used;
    595 				wrapthread_mutex_unlock(&solutions_mutex);
    596 
    597 				prev_status = status;
    598 				status = poll_status(poll_status_data);
    599 				if (status != prev_status) {
    600 					if (status == NISSY_STATUS_PAUSE)
    601 						LOG("[H48 solve] Paused\n");
    602 					if (status == NISSY_STATUS_RUN)
    603 						LOG("[H48 solve] Resumed\n");
    604 				}
    605 
    606 				for (td = true, i = 0; i < threads; i++)
    607 					td = td && arg[i].thread_done;
    608 			}
    609 		}
    610 
    611 		for (i = 0; i < threads; i++)
    612 			wrapthread_join(thread[i], NULL);
    613 
    614 		solve_h48_log_solutions(&sollist, lastused);
    615 		lastused = sollist.used;
    616 	}
    617 
    618 	if (status == NISSY_STATUS_STOP) {
    619 		LOG("[H48 solve] Received stop request, ending solution "
    620 		    "search early.\n");
    621 	}
    622 
    623 solve_h48_done:
    624 	nodes_visited = table_lookups = table_fallbacks = 0;
    625 	for (i = 0; i < threads; i++) {
    626 		nodes_visited += arg[i].nodes_visited;
    627 		table_fallbacks += arg[i].table_fallbacks;
    628 		table_lookups += arg[i].table_lookups;
    629 	}
    630 
    631 	stats[0] = nodes_visited;
    632 	stats[1] = table_lookups;
    633 	stats[2] = table_fallbacks;
    634 	lookups_per_node = table_lookups / (long double)nodes_visited;
    635 	fallback_rate = nodes_visited == 0 ? 0.0 :
    636 	    (table_fallbacks * 100) / (long double)table_lookups;
    637 	LOG("[H48 solve] Nodes visited: %" PRId64 "\n", nodes_visited);
    638 	LOG("[H48 solve] Lookups: %" PRId64 " (%.3Lf per node)\n",
    639 	    table_lookups, lookups_per_node);
    640 	LOG("[H48 solve] Table fallbacks: %" PRId64 " (%.3Lf%%)\n",
    641 	    table_fallbacks, fallback_rate);
    642 
    643 	return sollist.nsols;
    644 
    645 solve_h48_error_data:
    646 	LOG("[H48 solve] Error reading data table\n");
    647 	return NISSY_ERROR_DATA;
    648 
    649 solve_h48_error_solutions_buffer:
    650 	return NISSY_ERROR_BUFFER_SIZE;
    651 }