mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
MOBILEGL_FRAMES_IN_FLIGHT drives both backends and derives the split present credit (default 3 keeps credit 1; the Magma name stays as an alias).
This commit is contained in:
@@ -46,7 +46,7 @@ require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
|
||||
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle'
|
||||
require 'key:MOBILEGL_MAGMA_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
|
||||
require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle'
|
||||
require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting'
|
||||
require 'key:MOBILEGL_FRAMES_IN_FLIGHT' "$plugin_resource_text" 'V2 frames-in-flight setting'
|
||||
require 'key:MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
|
||||
require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle'
|
||||
require 'key:MOBILEGL_ESPRYT_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
|
||||
|
||||
+17
-9
@@ -163,8 +163,15 @@ namespace MobileGL::MG_Config {
|
||||
// draws refused by name), because a driver that accepts invalid SPIR-V may fault the GPU
|
||||
// into device loss. For diagnosing a driver that copes with a specific module only.
|
||||
Bool MagmaAllowInvalidSpirv = false;
|
||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||
Uint32 MagmaFramesInFlight = 3;
|
||||
// MOBILEGL_FRAMES_IN_FLIGHT (P15; MOBILEGL_MAGMA_FRAMESINFLIGHT is its deprecated alias):
|
||||
// how many frames may be queued ahead of the GPU, default 3, both backends.
|
||||
// - Magma: its frame contexts (frame S+1 reuses S+1-N's), clamped to the surface.
|
||||
// - Espryt: after presenting S the backend waits for S+1-N to complete (N = 1 is a full
|
||||
// CPU/GPU sync; N >= 2 is what Android's own queueBuffer throttle already gives).
|
||||
// - inproc / spawn / TCP: the present credit, how many presents the GL thread may run
|
||||
// ahead of the server, is derived from it: max(1, N - 2), so the default 3 keeps the
|
||||
// shipped credit of 1 and N = 4 is credit 2 (one more frame of latency).
|
||||
Uint32 FramesInFlight = 3;
|
||||
// MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER: GL draws and dispatches Magma records into
|
||||
// one command buffer before it submits it and continues on a fresh one. 0 = unbounded.
|
||||
// Drivers back a command buffer with GPU memory that is only returned when the buffer is
|
||||
@@ -700,13 +707,14 @@ namespace MobileGL::MG_Config {
|
||||
// client-filled gPipeInputs block for every verb, and run-ahead is precisely the
|
||||
// arm that stops filling it.
|
||||
Uint32 RunAhead = 1;
|
||||
// MOBILEGL_IPC_PRESENT_CREDIT (P5e, ruling 4 / ID-92): how many presents the client may
|
||||
// have in flight before it waits for a swap to come back. 1 = the client publishes
|
||||
// frame N+1's records while the server applies and swaps frame N - one frame of
|
||||
// overlap, at most one frame of added latency - and the CREDIT, never the ring's bytes,
|
||||
// is what paces a run-ahead client. 2 is a device MEASUREMENT arm: it buys no CPU on a
|
||||
// client that is already CPU-bound and costs a frame of latency, which is why the
|
||||
// default is 1 and not "as deep as the ring".
|
||||
// The present credit (P5e, ruling 4 / ID-92): how many presents the client may have in
|
||||
// flight before it waits for a swap to come back. 1 = the client publishes frame N+1's
|
||||
// records while the server applies and swaps frame N - one frame of overlap, at most one
|
||||
// frame of added latency - and the CREDIT, never the ring's bytes, is what paces a
|
||||
// run-ahead client. Derived from MOBILEGL_FRAMES_IN_FLIGHT (max(1, N - 2)); the
|
||||
// MOBILEGL_IPC_PRESENT_CREDIT override stays for experiments. Credit 2 is a real trade,
|
||||
// not a measurement arm: on FCL inproc (cpuhunt, 2026-10-08) it brought Magma 309 -> 320
|
||||
// and Espryt 299 -> 319 fps, at one frame of added latency.
|
||||
Uint32 PresentCredit = 1;
|
||||
// MOBILEGL_IPC_WAIT_STATS (P15, diagnostic): every 600 presents, log how many times per
|
||||
// frame the run-ahead client waited, by record op (applied/reply), by quiesce reason and
|
||||
|
||||
@@ -377,7 +377,14 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
|
||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
// P15: one backend-neutral knob; the Magma-only spelling stays as a deprecated alias.
|
||||
if (acceptedEnvVariablesMap->count("MOBILEGL_FRAMES_IN_FLIGHT") != 0) {
|
||||
features.FramesInFlight = QueryEnvUint32("MOBILEGL_FRAMES_IN_FLIGHT", 3, 1, 64);
|
||||
} else {
|
||||
features.FramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
if (acceptedEnvVariablesMap->count("MOBILEGL_MAGMA_FRAMESINFLIGHT") != 0)
|
||||
MGLOG_W("Config: MOBILEGL_MAGMA_FRAMESINFLIGHT is deprecated; use MOBILEGL_FRAMES_IN_FLIGHT");
|
||||
}
|
||||
features.MagmaMaxDrawsPerCommandBuffer =
|
||||
QueryEnvUint32("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER", 16384, 0, 1u << 24);
|
||||
features.MagmaDescriptorTrimFrames = QueryEnvUint32("MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES", 120, 0, 1u << 20);
|
||||
@@ -611,7 +618,14 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
// The present credit (ruling 4). 1 is one frame of overlap; 8 is the ceiling because a
|
||||
// deeper queue buys nothing on a CPU-bound client and pays for it in latency. 0 is NOT
|
||||
// admitted: a credit of zero would mean "publish no present at all".
|
||||
ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", 1, 1, 8);
|
||||
// P15: derived from MOBILEGL_FRAMES_IN_FLIGHT (Config.h states the mapping); the explicit
|
||||
// knob stays as an override for experiments.
|
||||
{
|
||||
const Uint32 derived = std::max<Uint32>(1u, std::min<Uint32>(8u, MG_Config::Features.FramesInFlight > 2
|
||||
? MG_Config::Features.FramesInFlight - 2
|
||||
: 1u));
|
||||
ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", derived, 1, 8);
|
||||
}
|
||||
// CONTRACT-P6 D5b's reply bound and P7's cold-start budget (Config.h has the semantics).
|
||||
// Both are floored at 100 ms: a zero bound would declare every live server silent.
|
||||
ipc.ControlTimeoutMs = QueryEnvUint32("MOBILEGL_IPC_CONTROL_TIMEOUT_MS", 5000, 100, 600000);
|
||||
|
||||
@@ -13713,8 +13713,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// makes it reachable), then non-blocking-poll prior frames' fences AFTER to
|
||||
// advance the completed-frame watermark that gates buffer-pool recycling.
|
||||
const Bool canFence = IsBackendContextCurrentOnThisThread() && g_GLESFuncs.glFenceSync;
|
||||
Uint64 presentedSerial = 0;
|
||||
if (canFence) {
|
||||
const Uint64 serial = g_currentFrameSerial.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
presentedSerial = serial;
|
||||
FrameFence& slot = g_frameFenceRing[serial % kFrameFenceRingDepth];
|
||||
if (slot.sync && slot.contextGeneration == g_syncContextGeneration) {
|
||||
// The slot's fence is a ring-depth of serials old and may be another context's:
|
||||
@@ -13772,6 +13774,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// A frame boundary: the next frame's age is asked anew.
|
||||
t_ageAskedSurface = EGL_NO_SURFACE;
|
||||
|
||||
// P15: MOBILEGL_FRAMES_IN_FLIGHT, Espryt's half. Magma bounds the frames the GPU has
|
||||
// queued with its frame contexts (frame S+1 reuses the context of frame S+1-N); this is
|
||||
// the same bound on the ES driver: after presenting S, wait for S+1-N to complete. The
|
||||
// platform's own queueBuffer throttle already waits for S-1, so only N = 1 is stricter
|
||||
// than what Android does by itself; larger N is the same contract stated, not a change.
|
||||
const Uint32 framesInFlight = MG_Config::Features.FramesInFlight;
|
||||
if (presentedSerial != 0 && framesInFlight != 0 &&
|
||||
framesInFlight < static_cast<Uint32>(kFrameFenceRingDepth) && g_GLESFuncs.glClientWaitSync &&
|
||||
presentedSerial + 1 > framesInFlight) {
|
||||
const Uint64 target = presentedSerial + 1 - framesInFlight;
|
||||
const FrameFence& oldest = g_frameFenceRing[target % kFrameFenceRingDepth];
|
||||
if (oldest.sync && oldest.serial == target && oldest.contextGeneration == g_syncContextGeneration) {
|
||||
constexpr Uint64 kFramesInFlightWaitNs = 1000ull * 1000 * 1000;
|
||||
(void)g_GLESFuncs.glClientWaitSync(oldest.sync, GL_SYNC_FLUSH_COMMANDS_BIT, kFramesInFlightWaitNs);
|
||||
}
|
||||
}
|
||||
|
||||
if (canFence) PollFrameFences();
|
||||
|
||||
// After the watermark advanced: retire grown-away ring stores and record the
|
||||
|
||||
@@ -2156,7 +2156,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
{
|
||||
// Desired depth comes from MOBILEGL_MAGMA_FRAMESINFLIGHT, parsed once by ConfigLoader
|
||||
// with a default of 3 when the variable is unset or invalid.
|
||||
Uint32 requestedFramesInFlight = MG_Config::Features.MagmaFramesInFlight;
|
||||
Uint32 requestedFramesInFlight = MG_Config::Features.FramesInFlight;
|
||||
MGLOG_I("MaxFramesInFlight: configured request=%u", requestedFramesInFlight);
|
||||
|
||||
VkSurfaceCapabilitiesKHR surfaceCaps{};
|
||||
|
||||
@@ -36,6 +36,7 @@ namespace {
|
||||
"MOBILEGL_CONFIG_FILE", "MOBILEGL_BACKEND_FILE", "MOBILEGL_BACKEND_TYPE",
|
||||
"MOBILEGL_MAGMA_FRAMESINFLIGHT", "MOBILEGL_TRANSPORT", "MOBILEGL_IPC_CONTROL",
|
||||
"MOBILEGL_IPC_DATA", "MOBILEGL_IPC_ROLE", "MOBILEGL_TEST_ONLY_KEY",
|
||||
"MOBILEGL_FRAMES_IN_FLIGHT", "MOBILEGL_IPC_PRESENT_CREDIT",
|
||||
};
|
||||
|
||||
class ConfigFileTest : public ::testing::Test {
|
||||
@@ -84,7 +85,7 @@ namespace {
|
||||
TEST_F(ConfigFileTest, WithoutFilesOrEnvironmentTheBuiltInDefaultsHold) {
|
||||
Loader::Init();
|
||||
EXPECT_EQ(Config::ActiveBackendType, BackendType::DirectGLES);
|
||||
EXPECT_EQ(Config::Features.MagmaFramesInFlight, 3u);
|
||||
EXPECT_EQ(Config::Features.FramesInFlight, 3u);
|
||||
EXPECT_EQ(Lookup("MOBILEGL_TRANSPORT"), "<unset>");
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
EXPECT_EQ(Config::Transport, Config::TransportMode::Monolith);
|
||||
@@ -92,6 +93,30 @@ namespace {
|
||||
#endif
|
||||
}
|
||||
|
||||
// P15: one frames-in-flight knob for both backends, the Magma spelling as its alias, and the
|
||||
// split arms' present credit derived from it (max(1, N - 2), so the default keeps credit 1).
|
||||
TEST_F(ConfigFileTest, FramesInFlightIsBackendNeutralAndDerivesThePresentCredit) {
|
||||
Loader::Init();
|
||||
EXPECT_EQ(Config::Features.FramesInFlight, 3u);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
EXPECT_EQ(Config::Ipc.PresentCredit, 1u);
|
||||
#endif
|
||||
::setenv("MOBILEGL_FRAMES_IN_FLIGHT", "4", 1);
|
||||
::setenv("MOBILEGL_MAGMA_FRAMESINFLIGHT", "6", 1); // the new name wins over the alias
|
||||
Loader::Init();
|
||||
EXPECT_EQ(Config::Features.FramesInFlight, 4u);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
EXPECT_EQ(Config::Ipc.PresentCredit, 2u);
|
||||
::setenv("MOBILEGL_IPC_PRESENT_CREDIT", "1", 1); // the explicit override stays
|
||||
Loader::Init();
|
||||
EXPECT_EQ(Config::Ipc.PresentCredit, 1u);
|
||||
::unsetenv("MOBILEGL_IPC_PRESENT_CREDIT");
|
||||
#endif
|
||||
::unsetenv("MOBILEGL_FRAMES_IN_FLIGHT");
|
||||
Loader::Init();
|
||||
EXPECT_EQ(Config::Features.FramesInFlight, 6u); // the alias alone still works
|
||||
}
|
||||
|
||||
TEST_F(ConfigFileTest, TheFileFillsWhatTheEnvironmentLeavesUnset) {
|
||||
Write(conf_, "# a comment\n"
|
||||
"\n"
|
||||
@@ -103,7 +128,7 @@ namespace {
|
||||
"garbage line without equals\n"
|
||||
"MOBILEGL_TEST_ONLY_KEY=a=b\n");
|
||||
Loader::Init();
|
||||
EXPECT_EQ(Config::Features.MagmaFramesInFlight, 5u);
|
||||
EXPECT_EQ(Config::Features.FramesInFlight, 5u);
|
||||
EXPECT_EQ(Lookup("MOBILEGL_TRANSPORT"), "spawn");
|
||||
EXPECT_EQ(Lookup("MOBILEGL_IPC_CONTROL"), "unix:@test-endpoint");
|
||||
EXPECT_EQ(Lookup("MOBILEGL_IPC_DATA"), "shm");
|
||||
@@ -121,7 +146,7 @@ namespace {
|
||||
::setenv("MOBILEGL_MAGMA_FRAMESINFLIGHT", "7", 1);
|
||||
::setenv("MOBILEGL_IPC_CONTROL", "unix:@from-env", 1);
|
||||
Loader::Init();
|
||||
EXPECT_EQ(Config::Features.MagmaFramesInFlight, 7u);
|
||||
EXPECT_EQ(Config::Features.FramesInFlight, 7u);
|
||||
EXPECT_EQ(Lookup("MOBILEGL_IPC_CONTROL"), "unix:@from-env");
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
EXPECT_EQ(Config::Ipc.Control, "unix:@from-env");
|
||||
@@ -156,12 +181,12 @@ namespace {
|
||||
::setenv("MOBILEGL_CONFIG_FILE", "", 1);
|
||||
::setenv("MOBILEGL_BACKEND_FILE", "", 1);
|
||||
Loader::Init();
|
||||
EXPECT_EQ(Config::Features.MagmaFramesInFlight, 3u);
|
||||
EXPECT_EQ(Config::Features.FramesInFlight, 3u);
|
||||
EXPECT_EQ(Config::ActiveBackendType, BackendType::DirectGLES);
|
||||
|
||||
::setenv("MOBILEGL_CONFIG_FILE", (dir_ / "absent.conf").string().c_str(), 1);
|
||||
Loader::Init();
|
||||
EXPECT_EQ(Config::Features.MagmaFramesInFlight, 3u);
|
||||
EXPECT_EQ(Config::Features.FramesInFlight, 3u);
|
||||
}
|
||||
|
||||
TEST_F(ConfigFileTest, AServerProcessNeverReadsTheClientsFile) {
|
||||
@@ -169,7 +194,7 @@ namespace {
|
||||
Write(backend_, "DirectVulkan\n");
|
||||
::setenv("MOBILEGL_IPC_ROLE", "server", 1);
|
||||
Loader::Init();
|
||||
EXPECT_EQ(Config::Features.MagmaFramesInFlight, 3u);
|
||||
EXPECT_EQ(Config::Features.FramesInFlight, 3u);
|
||||
EXPECT_EQ(Config::ActiveBackendType, BackendType::DirectGLES);
|
||||
EXPECT_EQ(Lookup("MOBILEGL_MAGMA_FRAMESINFLIGHT"), "<unset>");
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ val pluginRendererConfig = buildJsonValue {
|
||||
toggleable("MOBILEGL_DISABLE_TIMERQUERY", "1", false, RendererConfig.MetaString("mobilegl_disable_timerquery_title"))
|
||||
toggleable("MOBILEGL_MAGMA_DISABLE_SUBGROUP", "1", false, RendererConfig.MetaString("mobilegl_disable_subgroup_title"))
|
||||
toggleable("MOBILEGL_MAGMA_R11G11B10F_FALLBACK", "1", false, RendererConfig.MetaString("mobilegl_magma_r11g11b10f_fallback_title"))
|
||||
customizable("MOBILEGL_MAGMA_FRAMESINFLIGHT", "3", RendererConfig.MetaString("mobilegl_magma_frames_inflight_title"))
|
||||
customizable("MOBILEGL_FRAMES_IN_FLIGHT", "3", RendererConfig.MetaString("mobilegl_frames_inflight_title"))
|
||||
toggleable("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER", "1", false, RendererConfig.MetaString("mobilegl_avoid_sampler_mipmap_min_filter_title"))
|
||||
toggleable("MOBILEGL_COHERENT_AS_FLUSH", "1", false, RendererConfig.MetaString("mobilegl_coherent_as_flush_title"))
|
||||
toggleable("MOBILEGL_RELAXED_SEMANTICS", "1", false, RendererConfig.MetaString("mobilegl_relaxed_semantics_title"))
|
||||
|
||||
@@ -102,8 +102,8 @@
|
||||
android:name="mobilegl_magma_r11g11b10f_fallback_title"
|
||||
android:resource="@string/mobilegl_magma_r11g11b10f_fallback_title" />
|
||||
<meta-data
|
||||
android:name="mobilegl_magma_frames_inflight_title"
|
||||
android:resource="@string/mobilegl_magma_frames_inflight_title" />
|
||||
android:name="mobilegl_frames_inflight_title"
|
||||
android:resource="@string/mobilegl_frames_inflight_title" />
|
||||
<meta-data
|
||||
android:name="mobilegl_avoid_sampler_mipmap_min_filter_title"
|
||||
android:resource="@string/mobilegl_avoid_sampler_mipmap_min_filter_title" />
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<string name="mobilegl_disable_timerquery_title">Disable GPU timer queries</string>
|
||||
<string name="mobilegl_disable_subgroup_title">Disable Vulkan shader subgroups</string>
|
||||
<string name="mobilegl_magma_r11g11b10f_fallback_title">Use Magma R11G11B10F fallback</string>
|
||||
<string name="mobilegl_magma_frames_inflight_title">Magma frames in flight (1-64)</string>
|
||||
<string name="mobilegl_frames_inflight_title">Frames in flight (1-64)</string>
|
||||
<string name="mobilegl_avoid_sampler_mipmap_min_filter_title">Avoid sampler mipmap minification filters</string>
|
||||
<string name="mobilegl_coherent_as_flush_title">Treat explicit-flush persistent maps as coherent</string>
|
||||
<string name="mobilegl_use_angle_title">Use ANGLE GLES libraries</string>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
android:name="mobilegl_magma_r11g11b10f_fallback_title"
|
||||
tools:node="remove" />
|
||||
<meta-data
|
||||
android:name="mobilegl_magma_frames_inflight_title"
|
||||
android:name="mobilegl_frames_inflight_title"
|
||||
tools:node="remove" />
|
||||
<meta-data
|
||||
android:name="mobilegl_avoid_sampler_mipmap_min_filter_title"
|
||||
|
||||
@@ -52,7 +52,8 @@ CMake:`MOBILEGL_BUILD_DISAGGREGATED`(OFF)、`MOBILEGL_BUILD_DISAGGREGATED_
|
||||
| `MOBILEGL_IPC_RING_MB` / `MOBILEGL_IPC_STAGE_MB` | 8 / 32 | `SEG_CMD` / `SEG_STAGE`;stage 的 1/4 是内容分块预算 |
|
||||
| `MOBILEGL_IPC_RUN_AHEAD` | 1 | P5e run-ahead;只是合取式的一半(server 须发布 `kCapRunAheadApply`);`0` 是 A/B 对照 |
|
||||
| `MOBILEGL_IPC_VERB_BARRIER` | 1 | `0` 只作阴性对照,且同时关掉 run-ahead |
|
||||
| `MOBILEGL_IPC_PRESENT_CREDIT` | 1 | 1..8,§14 |
|
||||
| `MOBILEGL_FRAMES_IN_FLIGHT` | 3 | 1..64,两个后端共用(旧名 `MOBILEGL_MAGMA_FRAMESINFLIGHT` 为弃用别名):Magma 的帧上下文数;Espryt 在 present S 后等待帧 S+1-N 完成;split 的 present credit 由它导出为 max(1, N-2)(P15) |
|
||||
| `MOBILEGL_IPC_PRESENT_CREDIT` | 由 `MOBILEGL_FRAMES_IN_FLIGHT` 导出(默认 1) | 1..8,§14;显式设置覆盖导出值,仅供实验 |
|
||||
| `MOBILEGL_IPC_POLL_ESCALATE` | 64 | 未报告 fence 的零超时轮询本地答"未完成",同一 fence 连续第 N 次才往返(P10,`CONTRACT-P10.md` §1);`0` = 每次轮询都往返,是 A/B 对照 |
|
||||
| `MOBILEGL_IPC_BATCH_WAITS` | 1 | lockstep 下值类记录发布即返回;verify 强制 0 |
|
||||
| `MOBILEGL_IPC_EVENT_WAIT_MS` | 2000 | 反向事件遇满环时 server 等 client 排空的整笔预算(§11.7) |
|
||||
|
||||
Reference in New Issue
Block a user