Use spelling projection, which displays units of eye space in pixels:
glViewport(0,0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, width, 0, height, -1, 1);
Update due to question update:
Pixel matching in pixels can be used with perspective projection, but only with a certain restriction: the textured square must be coplanar to the perspective truncation of the near / far plane.
? glFrustum(left, right, bottom, top, near, far) Z = XY [, ] × [, ] NDC xy [-1, 1] ² NDC xy [-1, 1] ² , ,
y(x) = to_lower_bound + (x - from_lower_bound) * (to_upper_bound - to_lower_bound) / (from_upper_bound - from_lower_bound)
, , - , Z =/= /Z.