Mike Sherwin wrote: ↑Sat Nov 05, 2022 2:39 amWell my generator is pseudo legal and I know no other way than to make the move, call InCheck and then unmake the move for the last ply. If that is stupid

then please explain.
The 'stupid' qualification only referred to the case where you would make and unmake moves without doing anything in between. Testing for check would be a valid excuse for doing it. But it would be a very inefficient method for testing move legality. And that could be justifiably be held against the method for move generation. Most moves can very well be tested for legality without making them.
E.g. moves of a non-royal piece can only be illegal when that piece was pinned. (Or when you were in check to begin with, but that should be handled by a special-case move generator.) To be pinned such pieces have to be aligned with the friendly King. Most pieces won't be so aligned, and it is quite cheap to test for (e.g. if(aligned[fromSqr-kingSqr]) ... ), and failing the test excludes that the move can be illegal. And thus removes the necessity for making and unmaking them just for check testing.
And you don't even have to do that test for every individual move: you can do it for all moves of the piece at once, as these all have the same fromSqr. So you could do it in the move generator before generating moves for that piece, complete the test by looking for a pinner in the few cases that is is aligned with its King, and fall back on a special case of move generation only for moves along the pin ray when they are actually pinned, instead of generating all their moves.
This is basically what Qperft does. Except instead of subjecting all the pieces it generates moves for to the alignment test, it subjects all opponent sliders to such a test. (There are only 5 of those, while it could have 16 pieces of its own.) Again alignment is not common, but if it occurs it tests for a single piece on the ray to King, and then that piece is pinned and exempted from normal move generation, and uses the on-ray move generator instead.
None of the above is a 'perft trick'; it is just a method for making a speedy move generator from which an engine benefits just as well.
The above optimizations do not apply to King moves; actually Qperft does make all King moves, even in 'bulk counting' mode, to test whether they are legal by an IsSquareAttacked() call on the toSqr after the move. In principle this is still wasteful. Making the move before doing the IsSquareAttacked test only makes a difference when the King would have 'stepped into its own shadow', and this is only possible when it was in check to begin with. Which probably is known, but even if not, fully making the move could simply be replaced by temporarily taking the King off board during the test, so that it cannot block any slider attacks on the square behind it. Even worse, doing tests for all King destinations separately is wasteful. Because once you found out a slider attacks one such square, it usually attacks more. And the same test could have revealed that.
E.g. you could subject every enemy piece an alignment test with the
king neighborhood through a lookup in bulkAligned[sqr-kingSqr], which would give you a mask where each bit corresponds to a square adjacent to King, set to 1 if the piece is aligned with it. You could have a second set of bits in the same word to indicate whether these are distant attacks. For those you would have to vet the attack for not being blocked. But usually finding a blocker would block all attacks of that piece into the King neighborhood. The masks with the unblocked attacks for all pieces could be ORed together to get the illegal King destinations, and then you only generate King moves to the remaining squares.
Again, this would not be a 'perft trick', but just an optimization of the move generator.