Files
nxnative-easy/dm/host/front_mac.m
T
seeddsandv0lumehi a6787033eb Mac, in fullscreen: a screen whose refresh rate is not a multiple of the game's frame rate switches to one that is (144 → 120 Hz), from PR #79
60 fps on a 144 Hz screen cannot be paced evenly: the display link counts a tick on the refresh nearest each 1/60 s
slot (link_range's virtual ticks), so frames stay up two and three refreshes in turn (13.9 / 20.8 ms) and Apple's HUD
shows the frame interval jumping between them. link_sync now tells the front end the link's rate (60, 30, or 120
with DM_PRESENT_LINK=2; front_screen_match), and in fullscreen front_mac.m switches a screen whose mode is not a
multiple of it to the mode of the same size with the highest rate that is, with kCGConfigureForAppOnly, so macOS
switches it back when the host quits; leaving fullscreen or that screen switches it back at once. A window keeps
the screen's rate and the virtual ticks: the switch changes the whole screen for every app and most external
screens go black for a second or more. DM_SCREEN_MATCH=0 keeps the rate in fullscreen too.

Changes to the PR: the switch only in fullscreen, with the way back on leaving it (the PR switched in a window
too); no switch without a window (the windowless host's main thread runs the guest); the rate to match is
atomic; the link takes a rate change only once its object exists, so one made while its thread starts is not lost,
and sets the range directly instead of logging a pacing mode -1; the screen this host switched takes its rate
from its mode, which maximumFramesPerSecond may not show yet. The link now also sets its range again when the
window moves to another screen, which kept the first screen's range before.

Checked on this Mac's built-in 120 Hz screen (a multiple of every link rate) with a harness that drives
front_mac.m asked for 50 fps: no switch in the window, 120 → 50 Hz in fullscreen, 120 Hz again on leaving it, and
120 Hz after quitting in fullscreen; at 60 fps and with DM_SCREEN_MATCH=0 nothing switched. Hades windowed and
fullscreen (DM_FULLSCREEN=1) paced at 60 as before, the unchanged host alike. nvn_mtl.m passes an iOS SDK
syntax check. The link's new range after a switch has not run here (it needs a 144 or 165 Hz screen); the PR's
author measured the windowed switch on a 144 Hz XB273U at 2560x1440: 120 Hz, the HUD's frame interval steady at
16.7 ms, 144 Hz again after quitting.

Co-authored-by: v0lumehi <34629096+v0lumehi@users.noreply.github.com>
2026-10-24 00:39:05 +08:00

253 lines
20 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Mac front end: with DM_WINDOW=1 open a window (CAMetalLayer, for the graphics backend to present frames) + keyboard and controllers → dm_input; the guest runs on a large-stack thread instead and the main thread belongs to AppKit.
// Without DM_WINDOW: the guest runs directly on the current thread, behaving the same as with no front end (windowless tests use this).
// The window is fullscreen-capable (the green button and ⌃⌘F); DM_FULLSCREEN starts it that way. ⌘Q quits; the left mouse button is the touchscreen.
// Key map (Nintendo layout): WASD left stick, arrow keys D-pad, J=A K=B U=X I=Y, Q=L E=R, Z=ZL C=ZR, F=press left stick G=press right stick, Return=+ Tab=−, Space=shake the controller.
// Controllers: buttons by position as on the iPhone (right A, bottom B, top X, left Y), shoulders L R, triggers ZL ZR, Menu + Options −, both sticks and their presses.
#import <Cocoa/Cocoa.h>
#import <QuartzCore/CAMetalLayer.h>
#import <Metal/Metal.h>
#import <GameController/GameController.h>
#include "dm.h"
#include "dm_input.h"
#include "front.h"
#include "hle.h"
#include <signal.h>
#include <math.h>
static CAMetalLayer *g_layer;
static uint64_t g_btn;
static int g_w, g_a, g_s, g_d;
void *front_metal_layer(void) { return (__bridge void *)g_layer; }
static _Atomic int g_screen_fps; // the window's screen's maximumFramesPerSecond, kept up to date when the window moves to another screen
int front_screen_max_fps(void) { return g_screen_fps; }
// The screen's refresh rate matched to the game's frame rate in fullscreen (front_screen_match, from PR #79; DM_SCREEN_MATCH=0 turns it off). 60 fps on a 144 Hz screen cannot be paced
// evenly: the display link counts a tick on the refresh nearest each 1/60 s slot (nvn_mtl.m link_range), so frames stay up 2 and 3 refreshes in turn (13.9 / 20.8 ms) and Apple's HUD shows
// the frame interval jumping between them. In fullscreen, a screen whose rate is not a multiple of the game's switches to the mode of the same size with the highest rate that is (144 → 120 Hz
// for 60 and 30 fps), for this app only: macOS switches it back when the host quits, crashes included, and leaving fullscreen or that screen switches it back at once. A window keeps the
// screen's rate, because the switch changes the whole screen for every app and most external screens go black for a second or more. A screen with no such mode keeps the virtual ticks
static NSWindow *g_win; static _Atomic int g_match_want; // the game's window; the frame rate to match (0 = not known yet), written by the present thread only
static CGDirectDisplayID g_match_id; static CGDisplayModeRef g_match_orig; // main thread: the screen this host switched and its own mode (NULL = none switched)
static double mode_hz(CGDisplayModeRef m) { double hz = CGDisplayModeGetRefreshRate(m); return hz > 0 ? hz : 0; } // 0 = not known (older built-in screens report none)
static int hz_multiple(double hz, int fps) { double k = round(hz / fps); return k >= 1 && fabs(hz / fps - k) < 0.01; } // 59.94 Hz counts as 60
static CGError screen_set_mode(CGDirectDisplayID id, CGDisplayModeRef m) { // for this app only (kCGConfigureForAppOnly)
CGDisplayConfigRef cfg; CGError r = CGBeginDisplayConfiguration(&cfg);
if (r != kCGErrorSuccess) return r;
r = CGConfigureDisplayWithDisplayMode(cfg, id, m, NULL);
if (r != kCGErrorSuccess) { CGCancelDisplayConfiguration(cfg); return r; }
return CGCompleteDisplayConfiguration(cfg, kCGConfigureForAppOnly);
}
static void screen_match_restore(int on_it) { // main thread: the screen this host switched gets its own mode again; on_it: the window is on that screen
double hz = mode_hz(g_match_orig); CGError r = screen_set_mode(g_match_id, g_match_orig);
LOG("front end: screen %s %.0f Hz", r == kCGErrorSuccess ? "back at" : "could not be switched back to", hz);
if (r == kCGErrorSuccess && on_it && hz > 0) g_screen_fps = (int)round(hz);
CGDisplayModeRelease(g_match_orig); g_match_orig = NULL;
}
static void screen_match_apply(void) { // main thread: on a new frame rate, on entering and leaving fullscreen and on a move to another screen
static int off = -1; if (off < 0) { const char *e = getenv("DM_SCREEN_MATCH"); off = e && !strcmp(e, "0"); if (off) LOG("front end: the screen keeps its refresh rate in fullscreen too (DM_SCREEN_MATCH=0)"); }
NSScreen *scr = g_win.screen; int want = g_match_want, full = (g_win.styleMask & NSWindowStyleMaskFullScreen) != 0;
CGDirectDisplayID id = scr ? [scr.deviceDescription[@"NSScreenNumber"] unsignedIntValue] : kCGNullDirectDisplay;
if (g_match_orig && (!full || id != g_match_id)) screen_match_restore(id == g_match_id);
if (off || !full || want <= 0 || !scr) return;
CGDisplayModeRef cur = CGDisplayCopyDisplayMode(id);
if (!cur) return;
double hz = mode_hz(cur);
if (hz <= 0 || hz_multiple(hz, want)) { // already a multiple, or not known
if (g_match_orig && hz > 0) g_screen_fps = (int)round(hz); // the screen this host switched: its rate from the mode, which maximumFramesPerSecond (the screen observer) may not show yet
CGDisplayModeRelease(cur); return;
}
size_t w = CGDisplayModeGetWidth(cur), h = CGDisplayModeGetHeight(cur), pw = CGDisplayModeGetPixelWidth(cur), ph = CGDisplayModeGetPixelHeight(cur);
CFArrayRef modes = CGDisplayCopyAllDisplayModes(id, (__bridge CFDictionaryRef)@{(__bridge NSString *)kCGDisplayShowDuplicateLowResolutionModes: @YES});
CGDisplayModeRef pick = NULL; double pick_hz = 0;
for (CFIndex i = 0; modes && i < CFArrayGetCount(modes); i++) {
CGDisplayModeRef m = (CGDisplayModeRef)CFArrayGetValueAtIndex(modes, i); double r = mode_hz(m);
if (CGDisplayModeGetWidth(m) != w || CGDisplayModeGetHeight(m) != h || CGDisplayModeGetPixelWidth(m) != pw || CGDisplayModeGetPixelHeight(m) != ph || !CGDisplayModeIsUsableForDesktopGUI(m)) continue;
if (hz_multiple(r, want) && r > pick_hz) { pick = m; pick_hz = r; }
}
if (!pick) LOG("front end: screen at %.0f Hz has no mode of this size at a multiple of %d Hz: frames are paced on the nearest refreshes", hz, want);
else {
CGError r = screen_set_mode(id, pick);
LOG("front end: screen at %.0f Hz %s %.0f Hz for %d fps in fullscreen (DM_SCREEN_MATCH=0 keeps it)", hz, r == kCGErrorSuccess ? "switched to" : "could not be switched to", pick_hz, want);
if (r == kCGErrorSuccess) { if (!g_match_orig) { g_match_orig = CGDisplayModeRetain(cur); g_match_id = id; } g_screen_fps = (int)round(pick_hz); }
}
if (modes) CFRelease(modes);
CGDisplayModeRelease(cur);
}
void front_screen_match(int fps) {
if (!g_layer || g_match_want == fps) return; // no window (a windowless run's main thread runs the guest), or the rate as before
g_match_want = fps; dispatch_async(dispatch_get_main_queue(), ^{ screen_match_apply(); });
}
static void push(void) {
int32_t lx = (g_d - g_a) * 32767, ly = (g_w - g_s) * 32767;
dm_input_set(g_btn, lx, ly, 0, 0);
}
static int key_bit(unsigned short code) {
switch (code) {
case 38: return 0; case 40: return 1; case 32: return 2; case 34: return 3; // J K U I → A B X Y
case 3: return 4; case 5: return 5; // F G → press stick
case 12: return 6; case 14: return 7; case 6: return 8; case 8: return 9; // Q E Z C → L R ZL ZR
case 36: return 10; case 48: return 11; // Return Tab → + −
case 123: return 12; case 126: return 13; case 124: return 14; case 125: return 15; // ← ↑ → ↓
}
return -1;
}
static void on_key(NSEvent *e, int down) {
unsigned short c = e.keyCode;
switch (c) { case 13: g_w = down; break; case 0: g_a = down; break; case 1: g_s = down; break; case 2: g_d = down; break; }
int b = key_bit(c);
uint64_t m = b >= 0 ? 1ull << b : c == 49 ? DM_BTN_SHAKE : 0; // Space: shaken while held
g_btn = down ? (g_btn | m) : (g_btn & ~m);
push();
}
// Physical controllers (GameController) go to dm_pad_set, as the iPhone's do (DmGamepad.m read_pad, same layout): ORed with the keyboard, a non-zero stick wins.
// DM_KEYS uses the same path, so a controller touched during a script takes it over
static int32_t pad_axis(float v) { return (int32_t)(fmaxf(-1.f, fminf(1.f, v)) * DM_STICK_MAX); }
static void pad_read(GCExtendedGamepad *g) {
uint64_t m = 0;
#define K(btn, bit) if (btn.isPressed) m |= bit
K(g.buttonB, DM_BTN_A); K(g.buttonA, DM_BTN_B); K(g.buttonY, DM_BTN_X); K(g.buttonX, DM_BTN_Y); // by position: right A, bottom B, top X, left Y
K(g.leftShoulder, DM_BTN_L); K(g.rightShoulder, DM_BTN_R); K(g.leftTrigger, DM_BTN_ZL); K(g.rightTrigger, DM_BTN_ZR);
K(g.buttonMenu, DM_BTN_PLUS); K(g.buttonOptions, DM_BTN_MINUS); K(g.leftThumbstickButton, DM_BTN_LS); K(g.rightThumbstickButton, DM_BTN_RS); // nil on pads without them: not pressed
K(g.dpad.left, DM_BTN_LEFT); K(g.dpad.up, DM_BTN_UP); K(g.dpad.right, DM_BTN_RIGHT); K(g.dpad.down, DM_BTN_DOWN);
#undef K
dm_pad_set(m, pad_axis(g.leftThumbstick.xAxis.value), pad_axis(g.leftThumbstick.yAxis.value), pad_axis(g.rightThumbstick.xAxis.value), pad_axis(g.rightThumbstick.yAxis.value));
}
static void pads_changed(void) { // every connected pad reports (the last one moved wins); when the last one goes away its buttons and sticks are released
int any = 0;
for (GCController *c in GCController.controllers) if (c.extendedGamepad) {
any = 1; c.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *g, GCControllerElement *el) { (void)el; pad_read(g); };
pad_read(c.extendedGamepad);
}
if (!any) dm_pad_set(0, 0, 0, 0, 0);
}
// Mouse as a single-point touchscreen (front.h dm_touch_set): left button down / drag = finger 0, release = lifted. The content view takes the clicks,
// not a window-wide event monitor, so clicks on the title bar, the window buttons and the resize edges still reach AppKit.
@interface DmGameView : NSView @end
@implementation DmGameView
- (void)mouseDown:(NSEvent *)e { [self mouseDragged:e]; }
- (void)mouseDragged:(NSEvent *)e {
NSPoint p = [self convertPoint:e.locationInWindow fromView:nil]; NSSize b = self.bounds.size;
double w = b.width, h = b.height;
if (w <= 0 || h <= 0) return;
if (w / h > 16.0 / 9.0) w = h * 16.0 / 9.0; else h = w * 9.0 / 16.0; // the centered 16:9 picture the present fits into the view, as on the iPhone (DmGamepad.m)
double x0 = (b.width - w) / 2, y0 = (b.height - h) / 2;
dm_touch t = {0, (float)fmin(fmax((p.x - x0) / w, 0), 0.999), (float)fmin(fmax(1 - (p.y - y0) / h, 0), 0.999)}; // The AppKit view origin is at bottom left, the touchscreen origin is at top left
dm_touch_set(&t, 1);
}
- (void)mouseUp:(NSEvent *)e { (void)e; dm_touch_set(NULL, 0); }
// The drawable follows the view in pixels: a layer assigned to a layer-backed view kept the drawable at the first window's 1280x720 in fullscreen too, so the game picture was
// scaled down to it and up again by the compositor
- (void)fitDrawable {
CAMetalLayer *l = (CAMetalLayer *)self.layer; if (![l isKindOfClass:CAMetalLayer.class] || !self.window) return;
l.contentsScale = self.window.backingScaleFactor;
NSSize px = [self convertSizeToBacking:self.bounds.size]; if (px.width < 1 || px.height < 1 || CGSizeEqualToSize(l.drawableSize, px)) return;
l.drawableSize = px; LOG("front end: drawable %.0fx%.0f", px.width, px.height);
}
- (void)setFrameSize:(NSSize)s { [super setFrameSize:s]; [self fitDrawable]; }
- (void)viewDidChangeBackingProperties { [super viewDidChangeBackingProperties]; [self fitDrawable]; }
@end
typedef struct { void (*fn)(void); } guest_arg;
static void *guest_tramp(void *p) {
static char alt[65536]; // The signal alternate stack is per thread; crash printing must also work on the guest thread
stack_t ss = {.ss_sp = alt, .ss_size = sizeof alt}; sigaltstack(&ss, 0);
((guest_arg *)p)->fn(); LOG("guest thread finished"); exit(0);
}
// Metal reads MTL_HUD_ENABLED when the process makes its first device, which can be before the window: set from DM_METAL_HUD before main (set in front_run, the HUD did not show)
__attribute__((constructor)) static void hud_env(void) { if (getenv("DM_METAL_HUD") && atoi(getenv("DM_METAL_HUD"))) setenv("MTL_HUD_ENABLED", "1", 0); }
void front_run(void (*guest)(void)) {
// A game: tell macOS its work is latency-critical, so the system does not trade its frame time for power while the frame rate is capped
static id activity; activity = [[NSProcessInfo processInfo] beginActivityWithOptions:NSActivityUserInitiated | NSActivityLatencyCritical reason:@"game"];
if (!getenv("DM_WINDOW")) { guest(); return; }
@autoreleasepool {
[NSApplication sharedApplication];
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
NSRect r = NSMakeRect(0, 0, 1280, 720);
NSWindow *win = [[NSWindow alloc] initWithContentRect:r
styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable
backing:NSBackingStoreBuffered defer:NO];
win.title = @DM_GAME_NAME; // the game profile's name (game name label)
// Fullscreen: without FullScreenPrimary the green button only zooms the window. The present path already scales
// the game picture to fit the drawable with black bars around it, so fullscreen keeps the game's aspect ratio.
win.collectionBehavior = NSWindowCollectionBehaviorFullScreenPrimary;
NSView *v = win.contentView = [[DmGameView alloc] initWithFrame:r];
v.wantsLayer = YES;
g_layer = [CAMetalLayer layer];
g_layer.device = MTLCreateSystemDefaultDevice();
g_layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
g_layer.contentsScale = win.backingScaleFactor;
if (getenv("DM_METAL_HUD") && atoi(getenv("DM_METAL_HUD"))) { g_layer.developerHUDProperties = @{@"mode": @"default"}; LOG("front end: Metal performance HUD on (DM_METAL_HUD)"); } // Apple's HUD over the game: frame rate, frame and GPU time, memory
v.layer = g_layer; [(DmGameView *)v fitDrawable];
[win center];
[win makeKeyAndOrderFront:nil];
g_screen_fps = (int)(win.screen ?: NSScreen.mainScreen).maximumFramesPerSecond;
g_win = win;
[NSApp activateIgnoringOtherApps:YES];
// Main menu: AppKit runs ⌘ shortcuts only through menu items, so ⌘Q quits and ⌃⌘F toggles fullscreen from here (AppKit shows the item as Globe-F and keeps ⌃⌘F as a hidden twin)
NSMenu *bar = [NSMenu new], *app = [NSMenu new], *wm = [[NSMenu alloc] initWithTitle:@"Window"];
[[bar addItemWithTitle:@"" action:nil keyEquivalent:@""] setSubmenu:app];
[app addItemWithTitle:@"Quit " DM_GAME_NAME action:@selector(terminate:) keyEquivalent:@"q"];
[[bar addItemWithTitle:@"Window" action:nil keyEquivalent:@""] setSubmenu:wm];
[wm addItemWithTitle:@"Enter Full Screen" action:@selector(toggleFullScreen:) keyEquivalent:@"f"].keyEquivalentModifierMask = NSEventModifierFlagControl | NSEventModifierFlagCommand;
NSApp.mainMenu = bar;
if (getenv("DM_FULLSCREEN")) dispatch_async(dispatch_get_main_queue(), ^{ [win toggleFullScreen:nil]; }); // start in fullscreen
for (NSNotificationName nm in @[GCControllerDidConnectNotification, GCControllerDidDisconnectNotification])
[NSNotificationCenter.defaultCenter addObserverForName:nm object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *n) {
GCController *c = n.object; LOG("controller %s: %s%s", n.name == GCControllerDidConnectNotification ? "connected" : "disconnected",
c.vendorName.UTF8String ?: "?", c.extendedGamepad ? "" : " (no extended gamepad profile: not used)");
pads_changed(); }];
pads_changed();
[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown | NSEventMaskKeyUp handler:^NSEvent *(NSEvent *e) {
if (e.window != win) return e; // Only swallow keys for the game window; other windows such as the software keyboard input box receive typing as usual
if (e.type == NSEventTypeKeyDown && (e.modifierFlags & NSEventModifierFlagCommand)) return e; // ⌘ shortcuts go to the menu; a key released while ⌘ is held still reaches the game, or it would stay pressed
if (!e.isARepeat) on_key(e, e.type == NSEventTypeKeyDown);
return nil; // Swallow it to avoid the system beep
}];
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification object:win queue:nil
usingBlock:^(NSNotification *n) { (void)n; exit(0); }];
// Lifecycle (hle.h / dm.h): focus lost → 2, focus regained → 1; minimized → 3 + audio paused + GPU suspended, restored → resume (followed by focus regained → 1)
NSNotificationCenter *nc = NSNotificationCenter.defaultCenter;
[nc addObserverForName:NSWindowDidChangeScreenNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; if (win.screen) { g_screen_fps = (int)win.screen.maximumFramesPerSecond; g_layer.contentsScale = win.backingScaleFactor; } screen_match_apply(); }];
// Entering and leaving fullscreen: the layer is layer-backed, so its frame (and with it the drawable) follows the
// content view by itself; only the scale factor has to be carried over to the screen the window ended up on. The screen's rate is matched in fullscreen only (screen_match_apply)
[nc addObserverForName:NSWindowDidEnterFullScreenNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; g_layer.contentsScale = win.backingScaleFactor; screen_match_apply(); }];
[nc addObserverForName:NSWindowDidExitFullScreenNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; g_layer.contentsScale = win.backingScaleFactor; screen_match_apply(); }];
[nc addObserverForName:NSWindowDidResignKeyNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; if (!win.miniaturized) dm_focus_set(2); }];
[nc addObserverForName:NSWindowDidBecomeKeyNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; dm_focus_set(1); }];
[nc addObserverForName:NSWindowDidMiniaturizeNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; dm_focus_set(3); dm_audio_suspend(1); dm_gfx_suspend(1); }];
[nc addObserverForName:NSWindowDidDeminiaturizeNotification object:win queue:nil usingBlock:^(NSNotification *n) { (void)n; dm_gfx_suspend(0); dm_audio_suspend(0); dm_focus_set(2); }];
// Guest thread: 64 MiB stack (the Switch main thread stack is specified by npdm; the host gives plenty)
static guest_arg ga; ga.fn = guest;
pthread_attr_t a; pthread_attr_init(&a); pthread_attr_setstacksize(&a, 64u << 20);
pthread_t th; if (pthread_create(&th, &a, guest_tramp, &ga)) die("pthread_create guest");
[NSApp run];
}
}
// Software keyboard input box (front.h): the main thread shows an NSAlert + text field and the guest thread waits synchronously for the result. Not shown in windowless mode (returns -1, the caller treats it as cancel)
int front_text_input(const char *title, const char *sub, const char *guide, const char *initial, int max_chars, char *out, int out_cap) {
if (!g_layer) return -1;
__block int r = 0;
__block NSString *text = nil;
dispatch_sync(dispatch_get_main_queue(), ^{
NSAlert *a = [NSAlert new];
a.messageText = title && *title ? @(title) : @"Input";
NSString *info = [NSString stringWithFormat:@"%s%s%s", sub ? sub : "", sub && *sub && guide && *guide ? "\n" : "", guide ? guide : ""];
a.informativeText = info;
[a addButtonWithTitle:@"OK"]; [a addButtonWithTitle:@"Cancel"];
NSTextField *f = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 300, 24)];
f.stringValue = initial ? @(initial) : @"";
if (guide && *guide) f.placeholderString = @(guide);
a.accessoryView = f; a.window.initialFirstResponder = f;
r = [a runModal] == NSAlertFirstButtonReturn;
text = f.stringValue;
});
if (!r) return 0;
(void)max_chars; // Truncation is done uniformly in hle_swkbd.c in UTF-16 units
snprintf(out, out_cap, "%s", text.UTF8String);
return 1;
}