Using a shader is actually likely to be the most effecient route but feel free to test all methods; it could depend on what you're actually drawing. Of course, you may also want to consider that a render texture would be needed anyway if you wanted to apply the shader to the entire window at once as opposed to on an object by object basis.
The other option you mentioned (using a smaller render texture) is a viable option. In fact, I think, the first thing I added to GitHub was a class that can do it automatically for you!
https://github.com/Hapaxia/PXL8I'm not sure what you mean by "scaling the window up"...
Another option that you seem to have overlooked (this only really works if you're only using images - won't work with vector graphics) is to simply just scale up everything. Remember to turn off smoothing for textures and consider reducing/zooming the view to match so you can simply pretend it's the smaller size but it will draw everything larger.
It's possible that by "scaling the window up", you might mean that you have noticed that things can stretch when a window is resized. This is because the view is not changed. However, you can simply set the view manually.
For this, just set the view to the window size divided by the scale size. Something like this, maybe:
const unsigned int pixelSize{ 2u };
const sf::Vector2u windowSize{ 800u, 600u };
const sf::View view(sf::Vector2f(windowSize / pixelSize), sf::Vector2f(windowSize / pixelSize / 2u));
sf::RenderWindow window(sf::VideoMode(windowSize.x, windowSize.y), "");
window.setView(view);