普通视图

发现新文章,点击刷新页面。
昨天以前首页

ROG XBOX 掌机 X 体验:是 Win 掌机的最优解,但不是游戏掌机的

作者 肖钦鹏
2025年10月23日 14:37

在 ROG XBOX 掌机 X 刚发布的时候,我们曾打趣地说:

任天堂 Switch 2 杀手来了!

现在体验过一段时间 ROG XBOX 掌机 X 后,我想这话至少说对了一半——也可以说,可惜只说对了一半。

XBOX 硬件做对了所有事

从游戏机硬件的角度上看,ROG XBOX 掌机 X 在现阶段已经无可挑剔——

  • 7英寸 IPS 120Hz 高刷屏
  • AMD Ryzen™ AI Z2 Extreme 处理器
  • 24GB LPDDR5X 运行内存
  • 1TB SSD 存储空间
  • 80WHrs 4芯锂电池
  • 机身尺寸为 29.0 × 12.1 × 2.75 cm
  • 机身重量为 715g
  • 支持 Wi-Fi 6E / 蓝牙5.2
  • 配备 3.5mm 复合音频接口 / UHS-II microSD 卡槽 / 双 USB-C 高速接口(分别为 10Gbps / 40Gbps,均支持 DP 2.1和PD 3.0 100W)

纸面上的参数配置已经是游戏掌机的天花板,但更重要的是 XBOX 为其定制的这套手柄,几乎是把充盈饱满且反馈感十足的 XBOX 手柄,搬到了掌机上来。这与索尼的 PlayStation Portal 串流游戏机不谋而合,都是试图在移动设备上还原主机端的操控体验——而且确实做到了。

ROG XBOX 掌机 X 的屏幕只有 7 英寸,很好控制了机身大小。尽管手柄握把硕大,实际长度却几乎跟 Switch 2 一致,只是厚度略宽。换言之,如果你的随身包能放下 Switch 2,那么稍微腾挪一下,大概率也能把 ROG XBOX 掌机 X 塞进去。

得益于良好的配重设计,这台掌机拿在手里并不会觉得沉,反倒是由于掌心有了良好的支撑,即便是玩一些高强度的动作游戏也不容易觉得酸,这点比 Switch 2 要强得多,相信这也是未来所有高性能掌机——譬如传闻中的索尼 PS6 掌机,会考虑的路线。

性能表现方面,ROG XBOX 掌机 X 的 SoC Z2 Extreme 在 3D Mark 的 Time Spy 测试中跑出了 3664 分,相较 Z1 Extreme 约有 10%-15% 左右的性能提升,尤其是在低功耗下表现更佳。插电状态下,可以打开 TDP 35W 的 TM h re w qurbo 模式,换取更强劲的性能——可以说,这台掌机的上限和下限都比前代有所提升。

更重要的是实际游戏表现——

在高性能和帧生成的加持下,ROG XBOX 掌机 X 几乎可以在中低画质下,运行所有主流的 3A 游戏,就连对配置要求较高的《黑神话 悟空》《怪物猎人 荒野》也不在话下,开启帧生成可以在中画质 1080P 稳定跑到 40 帧。而前几年的 3A 大作譬如《赛博朋克 2077》《荒野大镖客 救赎 2》等画质标杆,更是能跑出 100 帧以上的流畅帧数。

续航,是最让我惊喜的部分——ROG XBOX 掌机 X 配备了 80WHrs 的大电池,几乎是 Switch 2 的四倍,即便是在高功率下玩游戏,也能跑到 2-3 小时的续航。

至于《空洞骑士 丝之歌》《黑帝斯 2》这样的独立游戏,只需要 13W 的低功耗就能流畅运行,足足可以玩上 5-6 小时,非常安逸。

值得一提的是,ROG XBOX 掌机 X 的散热和噪声控制也达到了同类产品的高水准——高负载下没噪音,持续运行不烫手,这些基础体验,对游戏机来说也相当加分。

可以说,硬件层面,ROG XBOX 掌机 X 几乎做对了所有事,也证明了 Windows 掌机的硬件天花板,事实上已经足够高了。

但 Windows 还不是游戏机的最优解

如果说 XBOX 是这台掌机的长板,那么 Windows 就是它无法回避的短板。

ROG XBOX 掌机 X 运行完整版 Windows 11 系统,但支持 XBOX 全屏模式——这是微软专为游戏场景定制的新界面。

在解锁登录界面进入系统后,有一个全屏运行的 XBOX 界面,对手柄进行了专门适配,并且会自动抓取安装在主机上所有平台的游戏,打开游戏后跳转到对应平台运行。

玩家还可以通过 XBOX 键快速切换任务,并一键唤出紧凑界面的 GameBar,打开奥创中心、进行快速设置或者截图录屏等操作。

另一个好消息是,ROG XBOX 掌机 X 终于有了像样的系统电源管理(基于 Modern Standby 改进的超级待机模式),按下电源键能让主机和手柄都稳定进入休眠,再按一下就能快速启动,待机一整晚也可以马上回到游戏,非常方便。待机功耗表现也很不错,一晚上掉电不到 5%。

但除此之外,这套系统和 Windows 并无太大差别,这意味着玩家可以享受到 Windows 系统出色的兼容性,但也要容忍 Windows 带来的各种各样的冗余和 bug——大部分情况下,重启就能解决问题。

对于一台游戏机来说,需要频繁重启算不上什么好事。

运行完整版 Windows 还有一大问题,就是在游戏之外的体验上,譬如——游戏的启动响应、下载速度、操作效率等,这些无关游戏但关乎体验的地方,都达不到专用游戏主机的水准。

快捷键就是一个典型例子。

由于这台掌机没有键盘,于是很多快捷键没办法映射到键盘上,而新增的 XBOX 按键也没有在系统层面上打通快捷指令,于是就会遇到各种按键打架的情况——

譬如在 Steam 大屏幕模式下,XBOX 按键既是菜单键又能唤出 GameBar 键,结果就是按键冲突,要打开菜单只能触屏;又譬如,在 PC 运行游戏时,有好几组原生快捷键都能截图,可在 XBOX 掌机上,甚至没法在 GameBar 里设置截图快捷键,因为没有键盘。

当然,这些问题都可以通过给 Windows 系统装软件、打补丁来解决,这对 Win 掌机玩家来说已经是家常便饭。只是每当需要我耐着性子来折腾时,ROG XBOX 掌机 X 就不再是游戏机,只是一台用起来不太利索的电脑。

ROG 和 XBOX 的合作,理应通过试图完善的硬件和优化的界面,去弥补 Windows 作为游戏平台的先天不足——他们做了很多示范,但还远远不够。

作为一台 6000 元档的旗舰游戏机,ROG XBOX 掌机 X 解决了 Win 掌机能效比差、操作繁琐的问题,可仍然是上手门槛颇高的游戏硬件。

同价位产品没解决的问题,官方下场的 XBOX 掌机也没拿出让人信服的解决方案;而与 Switch 2、Steam Deck 甚至 ROG Ally 相比,不算便宜的售价将会是一道不小的门槛。

给 Win 掌机下定论,总是充满矛盾的,而这份矛盾在 ROG XBOX 掌机 X 上达到了一个峰值——

作为一个评测者,我很喜欢 XBOX 掌机在软硬件层面带来的各色解决方案,这确实给未来的高性能掌机做了示范;但作为一名玩家,XBOX 掌机距离我心目中的成熟游戏机仍有差距。

这是手感性能俱佳的游戏硬件,也是最自由最便利的游戏平台,却搭载了最臃肿最繁琐的操作系统——这是现阶段 Windows 高性能掌机的最优解,但它依然不是那台最好的游戏掌机。

说到底,PC 是通用设备,而游戏机是专业设备,游戏机的本质,是打开就能玩。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


去 Apple Store 修手机 - 肘子的 Swift 周报 #107

作者 Fatbobman
2025年10月20日 22:00

父亲的 iPhone 16 突然无法充电。预约后,我前往 Apple Store 送修。工作人员确认问题后,为我提供了一部 iPhone 14 作为备用机,并协助完成数据转移。十二天后(期间正好赶上一个长假),设备维修完成——更换了 Type-C 接口,同时还免费更换了一块新电池。体验一如既往地令人满意。

高通收购 Arduino:历史的轮回 - 肘子的 Swift 周报 #106

作者 Fatbobman
2025年10月13日 22:00

上周,高通宣布收购知名开源硬件平台 Arduino,并同步发布首款搭载自家芯片的 Arduino UNO Q。与经典版本不同,UNO Q 采用了“双脑”架构——由运行 Linux 的 Qualcomm Dragonwing 处理器负责高性能计算,同时保留 STM32 微控制器以执行实时控制任务。这种设计无疑强大,却也悄然偏离了 Arduino 一直以来“简单、低成本、易上手”的初心。

Sora 2:好模型,但未必是好生意 - 肘子的 Swift 周报 #105

作者 Fatbobman
2025年10月6日 22:00

一周前,OpenAI 发布了 Sora 2 模型,并同步推出了带有社交平台属性的 Sora 应用。目前,用户仅能通过 iOS 应用使用该模型生成视频。无论在视觉细节、人物形象、环境纹理,还是声画同步方面,Sora 2 相较早期版本都有显著提升。

苹果正在为系统级支持 MCP 做准备 - 肘子的 Swift 周报 #104

作者 Fatbobman
2025年9月29日 22:00

根据 9TO5Mac 的报道,苹果正在为其生态系统添加 MCP(Model Context Protocol)支持,以实现智能体 AI 功能。其实现路径与我们在周报 #077 中的设想十分吻合:通过开发者熟悉的 App Intents 框架进行系统级集成,既保持了苹果一贯追求的“可控、安全、完整”用户体验,又巧妙规避了让普通用户直接面对复杂 MCP 配置的门槛。

RISC-V from Scratch: Building a Virtual Machine

作者 yukang
2025年9月23日 08:54

I’ve always wanted to learn RISC-V. A few days ago, I finally got my hands dirty with it.

This post will guide you through the process of building a simple RISC-V VM from the ground up, using Rust as our implementation language.

Understanding the Core Concepts

Before writing any code, I need to grasp the fundamentals of RISC-V.

  • RISC vs. CISC: RISC (Reduced Instruction Set Computing) architectures use a small, highly optimized set of instructions. This is in contrast to CISC (Complex Instruction Set Computing), which has a large number of complex instructions. RISC-V’s simplicity makes it ideal for building a VM.
  • Modular Architecture: RISC-V has a base instruction set (RV32I for 32-bit systems) and optional extensions like M (for multiplication) or F (for floating-point). We’ll focus on the RV32I base to keep things simple.
  • The Three Pillars: At its core, a CPU (and thus our VM) consists of three main components:
    • Registers: A small set of high-speed memory locations used for calculations. RISC-V has 32 general-purpose registers (x0-x31).
    • Memory: A much larger space for storing program code and data.
    • Program Counter (PC): A special register that holds the memory address of the next instruction to be executed.

We can get all the details of RISC-V instructions from RISC-V Technical Specifications.

The VM’s Core Logic

Our VM is essentially a program that emulates a real CPU’s behavior. The core of our VM is the instruction loop, which follows a simple fetch-decode-execute cycle.

  1. Fetch: Read the 32-bit instruction from the memory address pointed to by the PC.
  2. Decode: Parse the instruction’s binary code to determine its type and what operation to perform.
  3. Execute: Perform the operation (e.g., an addition) and update the relevant registers or memory.

Here’s a simplified Rust code snippet to illustrate the VM structure and the run loop:

pub struct VM {    x_registers: [u32; 32],    pc: u32,    memory: Vec<u8>,}impl VM {    pub fn run(&mut self) {        loop {            // 1. Fetch the instruction            let instruction = self.fetch_instruction();            // 2. Decode            let decoded_instruction = self.decode(instruction);            // 3. Execute            self.execute_instruction(decoded_instruction);            // 4. Increment the PC            self.pc += 4;        }    }}

The fetch instruction turns out to be very simple, we just load 4 bytes in little-endian format into a u32 integer:

/// Fetch 32-bit instruction from memory at current PCfn fetch_instruction(&self) -> Option<u32> {    let pc = self.pc as usize;    if pc + 4 > self.memory.len() {        return None;    }    // RISC-V uses little-endian byte order    let instruction = u32::from_le_bytes([        self.memory[pc],        self.memory[pc + 1],        self.memory[pc + 2],        self.memory[pc + 3],    ]);    Some(instruction)}

Then we need to decode the integer into a RISC-V instruction. Here’s how we decode IType and RType instructions. The specifications for these two types are:

/// Decode 32-bit instruction into structured formatfn decode(&self, code: u32) -> Option<Instruction> {    let opcode = code & 0x7f;    match opcode {        0x13 => {            // I-type instruction (ADDI, etc.)            let rd = ((code >> 7) & 0x1f) as usize;            let rs1 = ((code >> 15) & 0x1f) as usize;            let funct3 = (code >> 12) & 0x7;            let imm = (code as i32) >> 20; // Sign-extended            Some(Instruction::IType {                rd,                rs1,                imm,                funct3,            })        }        0x33 => {            // R-type instruction (ADD, SUB, etc.)            let rd = ((code >> 7) & 0x1f) as usize;            let rs1 = ((code >> 15) & 0x1f) as usize;            let rs2 = ((code >> 20) & 0x1f) as usize;            let funct3 = (code >> 12) & 0x7;            let funct7 = (code >> 25) & 0x7f;            Some(Instruction::RType {                rd,                rs1,                rs2,                funct3,                funct7,            })        }        _ => None, // Unsupported opcode    }}

Then we want to execute the instruction, just following the specification. For demonstration purposes, we return the execution debug string as a result:

/// Execute decoded instructionfn execute(&mut self, instruction_type: Instruction) -> Result<String, String> {    match instruction_type {        Instruction::IType {            rd,            rs1,            imm,            funct3,        } => {            match funct3 {                0x0 => {                    // ADDI - Add immediate                    self.write_register(rd, self.x_registers[rs1] + imm as u32);                    Ok(format!(                        "ADDI x{}, x{}, {} -> x{} = {}",                        rd, rs1, imm, rd, self.x_registers[rd]                    ))                }                _ => Err(format!("Unsupported I-type funct3: {:#x}", funct3)),            }        }        Instruction::RType {            rd,            rs1,            rs2,            funct3,            funct7,        } => {            match (funct3, funct7) {                (0x0, 0x00) => {                    // ADD - Add registers                    let result = self.x_registers[rs1] + self.x_registers[rs2];                    self.write_register(rd, result);                    Ok(format!(                        "ADD x{}, x{}, x{} -> x{} = {}",                        rd, rs1, rs2, rd, self.x_registers[rd]                    ))                }                (0x0, 0x20) => {                    // SUB - Subtract registers                    let result = self.x_registers[rs1] - self.x_registers[rs2];                    self.write_register(rd, result);                    Ok(format!(                        "SUB x{}, x{}, x{} -> x{} = {}",                        rd, rs1, rs2, rd, self.x_registers[rd]                    ))                }                _ => Err(format!(                    "Unsupported R-type instruction: funct3={:#x}, funct7={:#x}",                    funct3, funct7                )),            }        }    }}

The simplest VM code is available at: riscv-vm-v0

From Rust to RISC-V binary

Now we need to write more complex assembly code for testing our VM, but we don’t want to write assembly code by hand.

To test our VM, we will write Rust code then use cross-compile toolchains to compile it into RISC-V executable files.

  1. Prepare the Environment: Install the riscv32imac-unknown-none-elf target toolchain. This is a bare-metal target, meaning it doesn’t rely on any operating system.
rustup target add riscv32imac-unknown-none-elf

Next, you’ll need a RISC-V linker. You can get this from the official RISC-V GNU toolchain.

# On Linux or macOSsudo apt-get install gcc-riscv64-unknown-elf# Alternatively, on macOSbrew install riscv-gnu-toolchain

Note: The gcc-riscv64-unknown-elf package includes both 32-bit and 64-bit tools.

  1. Write “Bare-Metal” Rust: Our Rust program must be written for a “bare-metal” environment, meaning you cannot use the standard library and must provide your own entry point and panic handler.
#[unsafe(no_mangle)]pub extern "C" fn _start() {    let mut sum = 0;    for i in 1..=10 {        sum += i;    }    // Store the result (which should be 55) in a known memory location.    let result_ptr = 0x1000 as *mut u32;    unsafe {        *result_ptr = sum;    }}#[panic_handler]fn panic(_info: &PanicInfo) -> ! {    loop {}}
  1. Cross-Compile: Use cargo with the specific target and a linker script to build the executable. We need to add options for Cargo in .cargo/config.toml
[target.riscv32imac-unknown-none-elf]rustflags = ["-C", "link-arg=-Tlink.ld"]

The content for link.ld is as follows. It tells the linker the layout of the binary file generated. Notice that we specify the entry point at address 0x80:

OUTPUT_ARCH(riscv)ENTRY(_start)SECTIONS {    . = 0x80;    .text : {        *(.text.boot)        *(.text)    }    .rodata : {        *(.rodata)    }    .data : {        *(.data)    }    .bss : {        *(.bss)    }}

Then we can build the program to a binary:

cargo build --release --target riscv32imac-unknown-none-elf
  1. Disassemble and check the binary code: We can use the tool riscv64-unknown-elf-objdump to double-check the generated binary file:
riscv64-unknown-elf-objdump -d ./demo/target/riscv32imac-unknown-none-elf/release/demo./demo/target/riscv32imac-unknown-none-elf/release/demo:     file format elf32-littleriscvDisassembly of section .text._start:00000080 <_start>:  80:   4501                    li      a0,0  82:   4605                    li      a2,1  84:   45ad                    li      a1,11  86:   4729                    li      a4,10  88:   00e61763                bne     a2,a4,96 <_start+0x16>  8c:   46a9                    li      a3,10  8e:   9532                    add     a0,a0,a2  90:   00e61863                bne     a2,a4,a0 <_start+0x20>  94:   a809                    j       a6 <_start+0x26>  96:   00160693                addi    a3,a2,1  9a:   9532                    add     a0,a0,a2  9c:   00e60563                beq     a2,a4,a6 <_start+0x26>  a0:   8636                    mv      a2,a3  a2:   feb6e3e3                bltu    a3,a1,88 <_start+0x8>  a6:   6585                    lui     a1,0x1  a8:   c188                    sw      a0,0(a1)  aa:   8082                    ret

The complete cross-compile Rust code is available at: riscv-demo

Using the VM to Execute Binary

The first problem is how do we parse the executable file? It turns out there is a crate called elf that can help us parse the header of an ELF file. We extract the interested parts from the header and record the base_mem so that we can convert virtual address to physical address. Of course, we also load the code into memory:

pub fn new_from_elf(elf_data: &[u8]) -> Self {    let mut memory = vec![0u8; MEM_SIZE];    let elf = ElfBytes::<elf::endian::AnyEndian>::minimal_parse(elf_data)        .expect("Failed to parse ELF file");    // Get the program entry point    let entry_point = elf.ehdr.e_entry as u32;    // Iterate through program headers, load PT_LOAD type segments    for segment in elf.segments().expect("Failed to get segments") {        if segment.p_type == PT_LOAD {            let virt_addr = segment.p_vaddr as usize;            let file_size = segment.p_filesz as usize;            let mem_size = segment.p_memsz as usize;            let file_offset = segment.p_offset as usize;            // Address translation: virtual address -> physical address            let phys_addr = virt_addr - entry_point as usize;            // Check memory boundaries            if phys_addr + mem_size > MEM_SIZE {                panic!(                    "Segment is too large for the allocated memory. vaddr: {:#x}, mem_size: {:#x}",                    virt_addr, mem_size                );            }            // Copy data from ELF file to memory            if file_size > 0 {                let segment_data = &elf_data[file_offset..file_offset + file_size];                memory[phys_addr..phys_addr + file_size].copy_from_slice(segment_data);            }        }    }    let mut vm = VM {        x_registers: [0; 32],        // Set directly to entry_point to match the linker script        pc: entry_point,        memory,        mem_base: entry_point,    };    vm.x_registers[0] = 0;    vm}

What’s left is that we need to extend our VM to support all the instruction formats used in this binary file, including li, bne, beq, etc.

There are 16-bit compressed instructions, so we can’t always increment the PC by 4; sometimes we only need to increment it by 2 for shorter ones.

Another interesting thing is that some of them are conditional jump instructions, so we need to get the return new_pc from the execution of the instruction.

So now we need to update the core logic of fetch and execution of instructions:

// Check the lowest 2 bits to determine instruction lengthif first_half & 0x3 != 0x3 {    // 16-bit compressed instruction    pc_increment = 2;    new_pc = self.execute_compressed_instruction(first_half);} else {    // 32-bit instruction    pc_increment = 4;    if physical_pc.saturating_add(3) >= self.memory.len() {        break;    }    let second_half = u16::from_le_bytes([        self.memory[physical_pc + 2],        self.memory[physical_pc + 3],    ]);    let instruction = (second_half as u32) << 16 | (first_half as u32);    if instruction == 0 {        break;    }    new_pc = self.execute_instruction(instruction);}

The complete new VM which can run compiled RISC-V binary files is available at: riscv-vm

References

❌
❌