Skip to content

Commit 7c52354

Browse files
007hacky007TalhaMudo
authored andcommitted
Picture options for the HD engine: colour tint, console gamma, edge smoothing, sharpening (#43)
Four optional picture settings, all off by default, in a new Picture tab of the launcher's Settings window: - Colour (TINT=less|none|cool): removes the yellow-green tint of the PC version's screen filter. A hook before its SetVector call (0x5c6766, NA 0x5c6756) makes the tint colour grey, and the 1x256 blue ramp the game refills every frame (pointer 0x86f8ac) is rewritten on upload. - Gamma (CONSOLEGAMMA=1): the Xbox 360 version's brightness curve over the finished frame (ThirteenAG's X360GammaApprox, MIT notice in picture.m). - Edge smoothing (EDGEAA=1): an FXAA-style filter on geometric edges only, found from the scene's depth; textures and the HUD stay sharp. - Sharpening (SHARPEN=1..100): CAS-style adaptive sharpening of the 3D scene, not the HUD. The original game never had these, so they belong to the HD engine: in the original engine visual.c, picture.m and their call sites in metal.m / com.c compile to nothing and the variables are ignored. The launcher enables the tab only with HD chosen, sets the variables only when starting the HD engine, and Restore Original Game resets them. tools/check.sh passes the four variables through.
1 parent 1eac0e6 commit 7c52354

14 files changed

Lines changed: 521 additions & 7 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,10 @@ with `swiftc`): it finds and checks `SPEED2.EXE`, offers full screen, opens the
287287
logs folder and manages saves (**Saves…**: the career profiles, backups, ZIP
288288
export and import; the current saves are backed up before an import replaces
289289
them; tests/save_manager.sh). **Settings…** (Cmd-,) holds the options on tabs:
290-
Original or HD graphics and the HUD on **Graphics**; on **Controls**, the game
290+
Original or HD graphics and the HUD on **Graphics**; on **Picture**, optional
291+
settings of the HD engine only (the PC version's colour tint, console gamma, edge
292+
smoothing, sharpening; `TINT` / `CONSOLEGAMMA` / `EDGEAA` / `SHARPEN`, all off by
293+
default; the Original engine ignores them); on **Controls**, the game
291294
controller on or off, its stick dead zone and the XtendedInput layout
292295
(`NFSU2_PAD` / `NFSU2_PAD_DEADZONE` / `NFSU2_XTINPUT`, which still win when set by
293296
hand); on **More**, **Make Copy…** builds a portable

‎launcher/Game.swift‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ enum Prefs {
8383
OriginalValue(\.wideHUD, false),
8484
OriginalValue(\.frameRate, 120),
8585
OriginalValue(\.fpsCounter, false),
86+
OriginalValue(\.picture, PictureOptions.original),
8687
]
8788
}
8889

@@ -96,6 +97,40 @@ struct OriginalValue {
9697
}
9798
}
9899

100+
/// Settings › Picture: optional picture settings of the HD engine, each passed to it as
101+
/// an environment variable (runtime/visual.c, runtime/picture.m; the original engine
102+
/// ignores them). The first choice of every option is the original game and sets nothing.
103+
struct PictureOption {
104+
let key: String // launcher default: the chosen index
105+
let title: String
106+
let description: String
107+
let env: String
108+
let choices: [(label: String, value: String)]
109+
}
110+
111+
enum PictureOptions {
112+
static let all: [PictureOption] = [
113+
PictureOption(key: "LauncherColourTint", title: "Colour",
114+
description: "The yellow-green tint the PC version lays over the city. No tint keeps grey grey, Cool is a little bluer. The HUD and menus keep their colours.",
115+
env: "TINT",
116+
choices: [("As on PC (original)", ""), ("Less tint", "less"), ("No tint", "none"), ("Cool", "cool")]),
117+
PictureOption(key: "LauncherGamma", title: "Gamma",
118+
description: "Console: the deeper, darker brightness curve of the Xbox 360 version.",
119+
env: "CONSOLEGAMMA",
120+
choices: [("As on PC (original)", ""), ("Console", "1")]),
121+
PictureOption(key: "LauncherEdgeSmoothing", title: "Edge smoothing",
122+
description: "Softens the jagged outlines of cars, buildings, poles and wires. Textures and the HUD stay sharp.",
123+
env: "EDGEAA",
124+
choices: [("Off (original)", ""), ("On", "1")]),
125+
PictureOption(key: "LauncherSharpening", title: "Sharpening",
126+
description: "Crisper textures in the city. The HUD is left alone.",
127+
env: "SHARPEN",
128+
choices: [("Off (original)", ""), ("Low", "50"), ("High", "100")]),
129+
]
130+
/// Every option at its first choice: the original game.
131+
static var original: [Int] { all.map { _ in 0 } }
132+
}
133+
99134
enum Locator {
100135
static func exeIn(_ dir: URL) -> URL? {
101136
for n in SupportedExe.names {

‎launcher/Model.swift‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ final class LauncherModel: ObservableObject {
2929
@Published var stickDeadzone: Double { didSet { defaults.set(stickDeadzone, forKey: Prefs.stickDeadzone) } }
3030
private var setPad = false // NFSU2_PAD / NFSU2_PAD_DEADZONE were set by us
3131
@Published var xtendedInput: Bool { didSet { defaults.set(xtendedInput, forKey: Prefs.xtendedInput) } }
32+
/// Settings › Picture: PictureOptions.all[i] -> the chosen index (0 = the original game).
33+
@Published var picture: [Int] { didSet { for (o, i) in zip(PictureOptions.all, picture) { defaults.set(i, forKey: o.key) } } }
34+
/// Picture variables that were set by hand when the launcher started: those win.
35+
private let pictureFromEnv = Set(PictureOptions.all.map { $0.env }
36+
.filter { ProcessInfo.processInfo.environment[$0] != nil })
3237
private let xtendedInputFromEnv = ProcessInfo.processInfo.environment["NFSU2_XTINPUT"] != nil
3338
@Published var job: Job? = nil
3439
@Published var notice: String? = nil
@@ -66,6 +71,8 @@ final class LauncherModel: ObservableObject {
6671
controller = defaults.bool(forKey: Prefs.controller)
6772
stickDeadzone = min(0.5, max(0, defaults.double(forKey: Prefs.stickDeadzone)))
6873
xtendedInput = defaults.bool(forKey: Prefs.xtendedInput)
74+
let d = defaults
75+
picture = PictureOptions.all.map { o in min(max(d.integer(forKey: o.key), 0), o.choices.count - 1) }
6976
}
7077

7178
/// runtime/xtinput.c has the North American v1.2 executable's addresses only.
@@ -156,6 +163,11 @@ final class LauncherModel: ObservableObject {
156163
}
157164
var e = engine
158165
if !hasEngine(e) { e = (e == .hd) ? .original : .hd }
166+
// Picture options: HD engine only (the original engine ignores them anyway).
167+
// Ours are set again on every Play; a variable set by hand still wins.
168+
for (o, i) in zip(PictureOptions.all, picture) where !pictureFromEnv.contains(o.env) {
169+
if e == .hd && i > 0 { setenv(o.env, o.choices[i].value, 1) } else { unsetenv(o.env) }
170+
}
159171
let path = Paths.engine(loc.build, hd: e == .hd).path
160172
// A child process, not exec(): macOS attributes the child's file access to
161173
// this launcher (the "responsible" process), so folder permissions the

‎launcher/Settings.swift‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ enum SettingsTab: String, CaseIterable {
2424
case graphics, picture, controls, online, game, mods, more
2525

2626
/// The tabs that have rows, in this order.
27-
static let shown: [SettingsTab] = [.graphics, .controls, .more]
27+
static let shown: [SettingsTab] = [.graphics, .picture, .controls, .more]
2828

2929
var title: String { rawValue.capitalized }
3030
var icon: String { // SF Symbols available on macOS 11
@@ -118,6 +118,34 @@ struct GraphicsSettings: View {
118118
}
119119
}
120120

121+
/// HD engine only: the original engine is the game's 2004 picture.
122+
struct PictureSettings: View {
123+
@ObservedObject var model: LauncherModel
124+
private var active: Bool { model.engine == .hd && model.hasEngine(.hd) }
125+
var body: some View {
126+
VStack(alignment: .leading, spacing: 12) {
127+
Card("Picture") {
128+
Text(!model.hasEngine(.hd)
129+
? "These need the HD engine, which is not in this app."
130+
: active ? "HD engine only. (original) is the game as it was on PC."
131+
: "HD engine only: choose HD in Graphics to use them. The Original engine always shows the game as it was on PC.")
132+
.font(.system(size: 11)).foregroundColor(active ? Theme.dim : Theme.accent)
133+
.fixedSize(horizontal: false, vertical: true)
134+
ForEach(0..<PictureOptions.all.count, id: \.self) { i in
135+
let o = PictureOptions.all[i]
136+
SettingRow(title: o.title, description: o.description) {
137+
Picker("", selection: $model.picture[i]) {
138+
ForEach(0..<o.choices.count, id: \.self) { k in Text(o.choices[k].label).tag(k) }
139+
}
140+
.pickerStyle(MenuPickerStyle()).labelsHidden().fixedSize()
141+
}
142+
.disabled(!active).opacity(active ? 1 : 0.5)
143+
}
144+
}
145+
}
146+
}
147+
}
148+
121149
struct ControlsSettings: View {
122150
@ObservedObject var model: LauncherModel
123151
var body: some View {
@@ -191,9 +219,10 @@ struct SettingsView: View {
191219
@ViewBuilder private var page: some View {
192220
switch tab {
193221
case .graphics: GraphicsSettings(model: model)
222+
case .picture: PictureSettings(model: model)
194223
case .controls: ControlsSettings(model: model)
195224
case .more: MoreSettings(model: model)
196-
case .picture, .online, .game, .mods: EmptyView() // no rows yet
225+
case .online, .game, .mods: EmptyView() // no rows yet
197226
}
198227
}
199228

‎link.sh‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ case "${BUILD:-eu}" in
4242
*) echo "BUILD must be eu or na"; exit 1 ;;
4343
esac
4444

45-
CSRC="mem dispatch genexec win32 win32_ext com debug res fileio overrides d3dx_hle vsemu enhance perflog wsfov gheap sndlead xpatch highfps xtinput"
46-
MSRC="metal audio bundle gamepad"
45+
CSRC="mem dispatch genexec win32 win32_ext com debug res fileio overrides d3dx_hle vsemu enhance perflog wsfov gheap sndlead xpatch highfps xtinput visual"
46+
MSRC="metal audio bundle gamepad picture"
4747
FW="-lm -framework Cocoa -framework Metal -framework QuartzCore -framework ImageIO -framework CoreGraphics -framework CoreServices -framework AudioToolbox -framework GameController -framework CoreText"
4848
SHARDS="$SOBJ/shard_*.o $SOBJ/fntab.o $SOBJ/stubs.o"
4949

‎run.sh‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ fi
2323
# HD_GSEP=1 (G-buffer in a separate single-sampled pass), HD_MBFIX=1 (the scene stays
2424
# multisampled during motion blur), HD_RT_FAR / HD_RT_BMIN (ray limits; off by default).
2525
# Details: runtime/enhance.c, runtime/metal.m (sections of the same name)
26+
# Picture options, HD engine only (launcher Settings > Picture; unset = the original game):
27+
# TINT=less|none|cool the screen filter's yellow-green tint: half / none / none + cooler
28+
# CONSOLEGAMMA=1 the Xbox 360 version's brightness curve
29+
# EDGEAA=1 edge smoothing (post-process) of the 3D scene, not the HUD
30+
# SHARPEN=1..100 contrast adaptive sharpening of the 3D scene, not the HUD
31+
# Details: runtime/visual.c (TINT), runtime/picture.m (the others)
2632
# In game, Cmd+Shift+D: a PNG of that frame + the draw list (~/Library/Logs/NFSU2/
2733
# or the PERFLOG directory). Measurement recording while playing: tools/session.sh [hd].
2834
MODE="${1:-window}"

‎runtime/build_na.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#define GA_0x005BA11Du 0x005ba10du /* function 005ba070 + 0xad */
1616
#define GA_0x005BA1DDu 0x005ba1cdu /* function 005ba1b0 + 0x2d */
1717
#define GA_0x005BA559u 0x005ba549u /* function 005ba4c0 + 0x99 */
18+
#define GA_0x005c6766u 0x005c6756u /* function 005c6530 + 0x236 */
1819
#define GA_0x005c7ffeu 0x005c7feeu /* function 005c7ed0 + 0x12e */
1920
#define GA_0x005c804eu 0x005c803eu /* function 005c7ed0 + 0x17e */
2021
#define GA_0x005cbe54u 0x005cbe44u /* function 005cbdd0 + 0x84 */
@@ -90,6 +91,7 @@
9091
#define GA_0x0086549cu 0x0086549cu /* operand pair */
9192
#define GA_0x008654a4u 0x008654a4u /* operand pair */
9293
#define GA_0x00865558u 0x00865558u /* operand pair */
94+
#define GA_0x0086f8acu 0x0086f8acu /* operand pair */
9395
#define GA_0x00870818u 0x00870818u /* operand pair */
9496
#define GA_0x00890080u 0x00890080u /* operand pair */
9597
#define GA_0x008a44ecu 0x008a44ecu /* operand pair */

‎runtime/com.c‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4028,9 +4028,21 @@ int com_call(CPU *c, uint32_t magic){
40284028
if (is_cube2)
40294029
metal_upload_texture_face(mid, level, (face2 < 6u) ? face2 : 0u, g_mem+buf,
40304030
metal_level_pitch(w,level,fmt), w, h, fmt);
4031+
#if NFS_ENHANCED
4032+
else {
4033+
/* visual.c: TINT (HD engine) rewrites the screen filter's colour ramp on the way */
4034+
const void *visual_tex_upload(uint32_t, const void *, uint32_t, uint32_t, uint32_t, uint32_t, void *, uint32_t);
4035+
static uint8_t vtmp[1024];
4036+
uint32_t pitch = metal_level_pitch(w,level,fmt);
4037+
const void *src = level ? (const void *)(g_mem+buf)
4038+
: visual_tex_upload(self, g_mem+buf, w, h, fmt, pitch, vtmp, sizeof vtmp);
4039+
metal_upload_texture(mid, level, src, pitch, w, h, fmt);
4040+
}
4041+
#else
40314042
else
40324043
metal_upload_texture(mid, level, g_mem+buf,
40334044
metal_level_pitch(w,level,fmt), w, h, fmt);
4045+
#endif
40344046
}
40354047
if (ENVC("LOCKLOG") && w==640 && h==480){
40364048
static int nl=0;
@@ -4853,6 +4865,8 @@ void draw_stats_save(uint64_t *a, uint64_t *b){ g_ptype=a; g_stride=b; }
48534865
void com_report(void){
48544866
fprintf(stderr,"\n=== COM / D3D9 KATMANI ===\n");
48554867
{ void ws_report(void); ws_report(); } /* wsfov.c */
4868+
{ void visual_report(void); visual_report(); } /* visual.c */
4869+
{ void pic_report(void); pic_report(); } /* picture.m */
48564870
{ void gamepad_report(void); gamepad_report(); } /* gamepad.m */
48574871
{ void xpatch_report(void); xpatch_report(); }
48584872
{ void highfps_report(void); highfps_report(); } /* LOCAL PATCH: highfps.c */
@@ -5064,3 +5078,4 @@ static void fill_caps(uint32_t p){
50645078
MR32(p+0xC4)=vs; MR32(p+0xC8)=256; MR32(p+0xCC)=ps; }
50655079
MRF32(p+0xD0)=8.0f; MR32(p+0xD4)=0x00000051;
50665080
}
5081+

‎runtime/debug.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ void guest_hook(CPU *c, uint32_t ip){
9696
{ int ws_hook(CPU *, uint32_t); if (ws_hook(c, ip)) return; } /* wsfov.c */
9797
{ int sndlead_hook(CPU *, uint32_t); if (sndlead_hook(c, ip)) return; } /* sndlead.c */
9898
{ int highfps_hook(CPU *, uint32_t); if (highfps_hook(c, ip)) return; } /* highfps.c */
99+
{ int visual_hook(CPU *, uint32_t); if (visual_hook(c, ip)) return; } /* visual.c */
99100
if (ip == GA(0x0065ae35u)){
100101
uint32_t a1=MR32(c->esp+4u), a2=MR32(c->esp+8u), a3=MR32(c->esp+12u);
101102
if (!g_rec_esp0) g_rec_esp0 = c->esp;

‎runtime/metal.m‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,7 @@ static int gpuw_in(long f){
592592
static int abt_off(const char *name){
593593
return g_abt_cur_off && g_abt_name && !strcmp(g_abt_name, name);
594594
}
595+
int metal_abt_off(const char *name){ return abt_off(name); } /* for the other runtime files */
595596
/* called at the start of encoding: mode (-1 = outside A/B, 0 = on, 1 = off);
596597
* *cnt = whether the frame's GPU time counts */
597598
static int abt_frame(long f, int *cnt){
@@ -4728,6 +4729,36 @@ static long scr_redirect_plan(int rb, id<MTLTexture> scr, unsigned *outT){
47284729
#endif
47294730
}
47304731

4732+
/* ---- PICTURE OPTIONS (picture.m) -------------------------------------------
4733+
* EDGEAA / SHARPEN work on the 3D scene before the HUD: the frame's last draw
4734+
* to the screen that covers it completely and reads a render target of the
4735+
* screen's size is the copy of the finished scene (in a race: ps 22 after the
4736+
* post-process chain); the HUD comes after it. That draw reads picture.m's
4737+
* output instead of the target; EDGEAA also reads the scene's depth (the
4738+
* implicit Z surface, still the scene's then). A frame without such a draw
4739+
* (2D menus) is left alone. CONSOLEGAMMA runs over the finished frame (end of encoding).
4740+
* ABTEST=PICTURE turns both off in the measurement's off slices.
4741+
* HD engine only: the original engine draws the game's picture as it was. */
4742+
#if NFS_ENHANCED /* HD engine only */
4743+
int pic_pre_on(void);
4744+
id<MTLTexture> pic_pre(id<MTLDevice>, id<MTLCommandBuffer>, id<MTLTexture>, id<MTLTexture>);
4745+
static long pic_plan(int rb, id<MTLTexture> scr){
4746+
if (rb < 0 || !scr || !pic_pre_on() || abt_off("PICTURE")) return -1;
4747+
const MDraw *D = g_dbuf[rb];
4748+
for (long k = (long)g_dn[rb] - 1; k >= 0; k--){
4749+
const MDraw *x = &D[k];
4750+
if (x->target || x->is_clear || x->is_blit || x->is_up || !x->count) continue;
4751+
unsigned t = x->tex;
4752+
if (!t || t >= MAXTEX || !g_tex[t]) continue;
4753+
id<MTLTexture> T = g_tex[t];
4754+
if (T.textureType != MTLTextureType2D || !(T.usage & MTLTextureUsageRenderTarget) ||
4755+
T.width != scr.width || T.height != scr.height) continue;
4756+
if (draw_covers(x, scr.width, scr.height)) return k;
4757+
}
4758+
return -1;
4759+
}
4760+
#endif
4761+
47314762
/* ---- HUD ANCHORING (HUDPOS=1) ---------------------------------------------
47324763
* The game lays its HUD out for 640x480; on a wider screen HUDFIX squeezes it
47334764
* into the centered 4:3 area (round gauges stay round). With HUDPOS the
@@ -6891,6 +6922,10 @@ + (unsigned long long)_d1.tv_nsec - (unsigned long long)_d0.tv_nsec);
68916922
/* see POST-PROCESS CHAIN: fbrole[i] 1 = chain start, 2 = continuation */
68926923
static uint8_t fbrole[MAXD];
68936924
int nfb = rb >= 0 ? fb_plan(rb, fbrole, g_dn[rb]) : 0;
6925+
#if NFS_ENHANCED
6926+
long pic_at = pic_plan(rb, dtex); /* see PICTURE OPTIONS */
6927+
id<MTLTexture> pic_tex = nil;
6928+
#endif
68946929
int fbv_pend = 0; unsigned fbv_s = 0, fbv_e = 0; /* FBZINCIR=2: verification pending */
68956930
#if NFS_ENHANCED
68966931
int hd_scene_open = 1; /* main scene up to the first copy from the screen */
@@ -7247,6 +7282,16 @@ + (unsigned long long)_d1.tv_nsec - (unsigned long long)_d0.tv_nsec);
72477282
}
72487283
continue;
72497284
}
7285+
#if NFS_ENHANCED
7286+
if ((long)i == pic_at){
7287+
/* PICTURE OPTIONS: the scene is complete, the copy to the screen follows */
7288+
if (enc){ int pe = hd_ms_end(enc, rb, i, c_ms, c_gb, c_att);
7289+
[enc endEncoding]; enc = nil;
7290+
if (pe) hd_ms_fallback(cb, dtex, pe); }
7291+
cur_t = 0xFFFFFFFFu; cur_z = 0xFFFFFFFFu;
7292+
pic_tex = pic_pre(g_dev, cb, g_tex[dr->tex], g_ztex[1]); /* the scene's Z: the implicit surface */
7293+
}
7294+
#endif
72507295
if (nfb && fbrole[i] == 1){
72517296
unsigned e = i; while (e + 1 < g_dn[rb] && fbrole[e + 1] == 2) e++;
72527297
#if NFS_ENHANCED
@@ -7656,6 +7701,9 @@ + (unsigned long long)_d1.tv_nsec - (unsigned long long)_d0.tv_nsec);
76567701
| MTLTextureUsageShaderRead /* SSR reads depth */
76577702
#endif
76587703
;
7704+
#if NFS_ENHANCED
7705+
if (pic_pre_on()) zd.usage |= MTLTextureUsageShaderRead; /* EDGEAA: picture.m reads it */
7706+
#endif
76597707
zd.storageMode = MTLStorageModePrivate;
76607708
g_ztex[zid] = zt = [g_dev newTextureWithDescriptor:zd];
76617709
atomic_store(&g_zlive[zid], 1);
@@ -7937,6 +7985,9 @@ + (unsigned long long)_d1.tv_nsec - (unsigned long long)_d0.tv_nsec);
79377985
freed_use(tt, 0);
79387986
id<MTLTexture> txn = (tt && tt < MAXTEX && g_tex[tt])
79397987
? g_tex[tt] : g_white;
7988+
#if NFS_ENHANCED
7989+
if (q4 == 0 && pic_tex && (long)i == pic_at){ txn = pic_tex; c_tex[0] = ~0u; }
7990+
#endif
79407991
[enc setFragmentTexture:txn atIndex:(NSUInteger)q4];
79417992
} }
79427993
atomic_fetch_add(&g_draw_n, 1ull);
@@ -7991,6 +8042,12 @@ + (unsigned long long)_d1.tv_nsec - (unsigned long long)_d0.tv_nsec);
79918042
[cb renderCommandEncoderWithDescriptor:rp];
79928043
[e2 endEncoding];
79938044
}
8045+
/* picture.m: CONSOLEGAMMA over the finished frame (HUD included);
8046+
* ABTEST=PICTURE measures the picture passes' cost */
8047+
#if NFS_ENHANCED
8048+
{ void pic_gamma(id<MTLDevice>, id<MTLCommandBuffer>, id<MTLTexture>);
8049+
if (!abt_off("PICTURE")) pic_gamma(g_dev, cb, dtex); }
8050+
#endif
79948051
/* PERFLOG counters must be taken BEFORE the buffer is GIVEN BACK to the game:
79958052
* afterwards the game thread zeroes and refills the same buffer.
79968053
* It used to be read after present; when the drawable wait got

0 commit comments

Comments
 (0)