第 11 章:渲染与坐标——画面对了,点击也必须对
远程显示有两条互逆路径:像素从设备坐标变换到窗口;输入从窗口坐标反算回设备。只实现前者会得到“看起来正确、点哪里都偏”的系统。
一、Screen 的双重身份
sc_screen 同时是:
- 视频
frame_sink:接收解码帧,更新 SDL texture; - UI owner:持有 window、renderer、尺寸与方向;
- input boundary:拥有
input_manager,把 SDL 事件转给控制层。
它把“用户看到的几何”与“输入使用的几何”放在同一个对象中,避免两套缩放参数漂移。初始化入口见 sc_screen_init()。
二、视频帧如何跨线程到 UI
decoder 线程调用 screen frame sink 时:
- mutex 下把 AVFrame 引用放入单槽 frame buffer;
- 若此前无待处理帧,投递
SC_EVENT_NEW_FRAME; - 主线程收到事件后消费“当前最新帧”;
- 更新 YUV texture;
- 计算目标矩形并 render present。
SDL window/renderer/texture 的实际操作始终在主线程,后台线程只碰 frame buffer 与 SDL user event。
三、三种 fit 策略
compute_content_rect() 根据窗口和内容大小生成 screen->rect:
letterbox:保持比例,居中,可能有黑边;stretched:填满窗口,比例可能改变;unscaled:按原像素大小居中,超出部分裁切。
断开图标不会被放大;视频可放大。算法见 screen.c。目标 rect 是渲染与点击反算共同的几何真相。
四、HiDPI:逻辑坐标与物理像素
SDL 窗口事件通常使用逻辑尺寸,renderer 面向物理像素。绘制前代码读取 SDL_GetWindowPixelDensity(),把 rect 乘以 scale,并把起点对齐整数物理像素,避免高 DPI 下的模糊半像素采样。
对应实现见 sc_screen_render()。输入事件仍使用逻辑窗口坐标,所以反算必须基于未乘 density 的 screen->rect。
五、八种显示方向
方向不只有 0/90/180/270,还包括四种 mirror 组合。渲染时:
- 90/270 交换目标宽高并绕中心旋转;
- mirror 使用 SDL horizontal flip;
- 0 度走普通
SDL_RenderTexture快路径。
方向是桌面显示变换,不必改变设备编码方向。它允许用户仅旋转/镜像本地视图。
六、窗口坐标如何回到 frame
sc_screen_convert_window_to_frame_coords() 分两步:
6.1 去除窗口布局
frame_x = (window_x - rect.x) * content_width / rect.width
frame_y = (window_y - rect.y) * content_height / rect.height这一步移除 black bar、缩放和居中偏移。
6.2 逆方向变换
再按 8 种 orientation 交换、翻转 x/y。例如显示旋转 90° 时,窗口中的 (x, y) 要还原为 (y, width - x)。
只有渲染和输入共享同一 orientation/rect/content size,这个逆变换才成立。
七、frame 坐标为何还不是最终 display 坐标
控制协议里的 Position 不只发送 x/y,还发送编码画面 width/height。服务端 PositionMapper 根据当前 capture 变换,把 frame 坐标映射到真实 display 或 virtual display:
- capture 可能 crop;
- encoder 为满足 alignment 可能缩放;
- 设备旋转可能改变源方向;
- 虚拟显示尺寸可能与桌面窗口协商变化。
因此路径是:
设备端映射入口在 Controller.getEventPointAndDisplayId()。
八、动态尺寸与 resize 反馈环
普通镜像时设备旋转会带来新 session size,screen 通常调整窗口以保持合适比例。flex_display 模式则反过来:用户调整窗口,客户端发送 RESIZE_DISPLAY,设备改变虚拟显示,随后视频 session 又返回新尺寸。
如果没有抑制机制,会形成:窗口 resize → display resize → 新 frame size → 自动窗口 resize → 再次 display resize。
scrcpy 用两层标记打断:
- 客户端
resize_tracker记录自己因 frame size 发起的窗口 resize,3 秒内匹配的 SDL resize 事件忽略; - session meta 的
client_resized告诉 screen 不要因这次设备变化再做自动 resize。
逻辑见 sc_screen_on_resize() 和 frame sink 保存的 prevent_auto_resize 标志。
九、连续 resize 的平台 workaround
Windows 与 macOS 在拖拽窗口期间可能阻塞主 event loop,普通 resize event 不能及时处理。源码注册 SDL event watcher,在事件过滤回调中处理窗口变化,见 CONTINUOUS_RESIZING_WORKAROUND。
这不是美化体验的补丁:flex display 需要持续把新尺寸发往设备,否则窗口与远端画面会长时间不同步。
十、断开后的窗口为何暂时保留
设备断开后,如果窗口已打开,scrcpy 可加载 disconnected 图标并维持一个短事件循环,让用户看到明确状态,而不是窗口瞬间消失。期间仍处理 expose 与 quit,但输入 manager 标记 disconnected,不再把动作发给设备。
对应逻辑见 sc_screen_handle_disconnection()。这也是 UI 状态与 transport 状态解耦的例子。
十一、本章小结
screen 不只是渲染器,它维护一套可逆几何:content rect、DPI、方向、镜像和动态 session。像素前向变换与输入逆变换共享同一状态,resize 又用 tracker 与协议标志避免反馈振荡。下一章研究另外两类输出:为什么录像接在压缩包层,而 V4L2 接在解码帧层。