kmssink: Messes up scaling when output has non-square pixels
TL;DR: When the output has non-square pixels, kmssink will compensate for that by either cropping (instead of scaling) the video, or picking a drm render mode src size that is larger than the video frames, causing drm to refuse (and gst then falls back to just rendering pixels 1:1).
This is an issue I noticed while running on an Orange Pi SBC with a HDMI screen that reported incorrect dimensions, but I can reproduce it on a regular machine by forcing a 16:9 resolution on a 4:3 screen as well. I've had a look at the code and I suspect that the way this is handled might be all wrong, but I do not have enough familiarity with the code to really tell where the problem is and what the fix would be, so I'll just share my observations here and hope someone else will pick it up.
The original problem was:
-
Rendering a 1920x1080 video to an 1920x1080 (16:9) HDMI screen with playbin3.
-
The screen reports a size of 300x260mm (10.4:9), which would mean pixels are not square, but have a 1.54:1 pixel ratio (which
gst_video_calculate_device_ratio
rounds to 5:3). -
gst_kms_sink_calculate_display_ratio
, which seems to be intended to compensate for the non-square pixels, then decides the sink size should become 3200x1080. I'm not entirely sure what this number means, I think that if the video would be stretched up to this value, and then proportionally scaled back down to the 1920x1080 output, then I b believe it would be displayed correctly (with black bars above and below) - in the case the physical size of the display was really correct. -
However, the sink size is not used like this, but instead is used to set the "SRC" size of the DRM plane mode (iow, which pixels to take from the video to optionally scale and render into the given DST size). Since the video only has 1920x1080 pixels, the kernel refuses to set this up and
drmModeSetPlane
fails. If you enable drm kernel debug output (echo 0x04 | sudo tee /sys/module/drm/parameters/debug
), the kernel complains about this:[drm:drm_framebuffer_check_src_coords] Invalid source coordinates 3200.000000x1080.000000+0.000000+0.000000 (fb 1920x1088)
(note that the fb 1920x1088 is not the output fb size, but the input memory buffer that contains video)
-
Because the modeset fails, gstreamer unsets can_scale and retries, which causes the video to be rendered pixel-for-pixel to the output making it fullscreen (since video and output resolution are the same). Even though this is what I wanted, gstreamer is not behaving correctly in the face of the specified physical size.
-
When rendering a second video with the same kmssink, the negotiation is now down with can_scale=false, which means that kmssink advertises its non-square pixels, causing playbin to add a software scaling step to (correctly) add black horizontal bars and the output is correct (but needlessly slow, since drm could have handled scaling).
To reproduce this issue on a regular PC, I:
- Passed the
video=DP-1:1280x720
option on the kernel commandline. This forced my 4:3 monitor into a 16:9 resolution, i.e. non-square pixels - Played a test video:
sudo GST_DEBUG=kmssink:5 gst-launch-1.0 -v videotestsrc ! video/x-raw,width=1920,height=1080 ! kmssink connector-id=317
(addedsudo
to allow DRM access, passed a connector-id to use the right monitor, value taken fromdrm_info
, I used 1920x1080 video instead of 1280x720 to trigger this if. - As before, this sets the sink size too large (2560x1080) and makes the drm plane fail (
kernel: [drm:drm_framebuffer_check_src_coords [drm]] Invalid source coordinates 2560.000000x1080.000000+0.000000+0.000000 (fb 1920x1080)
), falling back to 1:1 pixel rendering, which shows only the top left 1280x720 pixels (since the display resolution is smaller than the video resolution). - Gstreamer output: gst-output.txt
The issue can also manifest itself differently depending on the video resolution:
sudo GST_DEBUG=kmssink:5 gst-launch-1.0 -v videotestsrc ! video/x-raw,width=1280,height=720 ! kmssink connector-id=317
- This ends up in this if, setting the sink resolution to 1280x540. Again, if the video would be stretched to this value and then proportionally scaled to the fill the output (which would not need scaling in this case), the output would be ok. But instead of that, the output shows black bars top and bottom, but the actual video is cropped (only showing 540 of the 720 lines), which is also incorrect. I didn't get output of this run.
And also here, with can_scale=false and a videoscale element, the non-square pixels are compensated for properly, showing the image with horizontal bars as expected: sudo GST_DEBUG=kmssink:5 gst-launch-1.0 -v videotestsrc ! video/x-raw,width=1920,height=1080 ! videoscale ! kmssink connector-id=317 can_scale=false
As I said, I am not entirely sure how this is supposed to work, but I have the feeling that gst_kms_sink_calculate_display_ratio()
is not doing the right thing. Instead of modifying the SRC rectangle for rendering, I think the non-square-pixel compensation should be modifying the DST rectangle instead. i.e. always render the full src, but possible compress it to add bars to the output). This is also what is done currently when the video and output ratio do not match (through gst_video_sink_center_rect in gst_kms_sink_show_frame
)
Looking at the commit history, it seems that this code was already (broken?) like this when it was first introduced in commit 1aee6cd by @vjaquez.