First OSX keyboard stuff...works, with limitations:\nNo Mouse support yet (Add gamepad via IOKit as well?)\nCould poll less for less cpu time\nDon't know why rumble events don't seem to be sent to the class until right before it's deleted (Billiard?)

git-svn-id: https://dolphin-emu.googlecode.com/svn/trunk@5407 8ced0084-cf51-0410-be5f-012b33b47a6e
This commit is contained in:
Shawn Hoffman 2010-04-25 18:04:55 +00:00
parent a443ec7d8d
commit 452c2e711a
9 changed files with 5137 additions and 3960 deletions

View File

@ -43,7 +43,7 @@ void ControllerInterface::Init()
ciface::Xlib::Init( m_devices, m_hwnd ); ciface::Xlib::Init( m_devices, m_hwnd );
#endif #endif
#ifdef CIFACE_USE_OSX #ifdef CIFACE_USE_OSX
ciface::OSX::Init( m_devices, m_hwnd ); ciface::OSX::Init( m_devices );
#endif #endif
#ifdef CIFACE_USE_SDL #ifdef CIFACE_USE_SDL
ciface::SDL::Init( m_devices ); ciface::SDL::Init( m_devices );
@ -89,7 +89,7 @@ void ControllerInterface::DeInit()
// nothing needed // nothing needed
#endif #endif
#ifdef CIFACE_USE_OSX #ifdef CIFACE_USE_OSX
// nothing needed ciface::OSX::DeInit();
#endif #endif
#ifdef CIFACE_USE_SDL #ifdef CIFACE_USE_SDL
// there seems to be some sort of memory leak with SDL, quit isn't freeing everything up // there seems to be some sort of memory leak with SDL, quit isn't freeing everything up

View File

@ -1,101 +0,0 @@
#include "../ControllerInterface.h"
#ifdef CIFACE_USE_OSX
#include "OSX.h"
#include "OSXPrivate.h"
namespace ciface
{
namespace OSX
{
void Init( std::vector<ControllerInterface::Device*>& devices, void* hwnd )
{
// mouse will be added to this, Keyboard class will be turned into KeyboardMouse
// single device for combined keyboard/mouse, this will allow combinations like shift+click more easily
//devices.push_back( new Keyboard( hwnd ) );
}
Keyboard::Keyboard( void *View )
{
memset( &m_state, 0, sizeof(m_state) );
OSX_Init(View);
// This is REALLY dumb right here
// Should REALLY cover the entire UTF-8 Range
for (int a = 0; a < 256; ++a)
inputs.push_back( new Key( (char)a ) );
}
Keyboard::~Keyboard()
{
// might not need this func
}
ControlState Keyboard::GetInputState( const ControllerInterface::Device::Input* const input )
{
return ((Input*)input)->GetState( &m_state );
}
void Keyboard::SetOutputState( const ControllerInterface::Device::Output* const output, const ControlState state )
{
}
bool Keyboard::UpdateInput()
{
memset(m_state.keyboard, 0, 256);
OSX_UpdateKeys(256, m_state.keyboard );
// mouse stuff in here too
return true;
}
bool Keyboard::UpdateOutput()
{
return true;
}
std::string Keyboard::GetName() const
{
return "Keyboard";
//return "Keyboard Mouse"; // change to this later
}
std::string Keyboard::GetSource() const
{
return "OSX";
}
int Keyboard::GetId() const
{
return 0;
}
ControlState Keyboard::Key::GetState( State* const state )
{
for(unsigned int a = 0; a < 256; ++a)
if(state->keyboard[a] == m_index)
return true;
return false;
}
std::string Keyboard::Key::GetName() const
{
char temp[16];
sprintf(temp, "Key:%c", m_index);
return std::string(temp);
}
}
}
#endif

View File

@ -1,68 +1,114 @@
#ifndef _CIFACE_OSX_H_ #pragma once
#define _CIFACE_OSX_H_
#include "../ControllerInterface.h" #include "../ControllerInterface.h"
#include <IOKit/hid/IOHIDLib.h>
namespace ciface namespace ciface
{ {
namespace OSX namespace OSX
{
void Init( std::vector<ControllerInterface::Device*>& devices );
void DeInit();
class KeyboardMouse : public ControllerInterface::Device
{
friend class ControllerInterface;
friend class ControllerInterface::ControlReference;
protected:
struct State
{ {
unsigned char buttons[32];
};
void Init( std::vector<ControllerInterface::Device*>& devices, void* hwnd ); class Input : public ControllerInterface::Device::Input
{
friend class KeyboardMouse;
protected:
virtual ControlState GetState( const State* const state ) = 0;
};
class Keyboard : public ControllerInterface::Device class Output : public ControllerInterface::Device::Output
{ {
friend class ControllerInterface; friend class KeyboardMouse;
friend class ControllerInterface::ControlReference; protected:
virtual void SetState( const ControlState state, unsigned char* const state_out ) = 0;
};
protected: class Key : public Input
{
friend class KeyboardMouse;
public:
std::string GetName() const;
protected:
Key( IOHIDElementRef key_element );
ControlState GetState( const State* const state );
private:
IOHIDElementRef m_key_element;
std::string m_key_name;
};
struct State class Button : public Input
{ {
char keyboard[256]; // really dumb friend class KeyboardMouse;
// mouse crap will go here public:
}; std::string GetName() const;
protected:
Button( IOHIDElementRef button_element );
ControlState GetState( const State* const state );
private:
IOHIDElementRef m_button_element;
std::string m_button_name;
};
class Input : public ControllerInterface::Device::Input class Axis : public Input
{ {
friend class Keyboard; friend class KeyboardMouse;
protected: public:
Input( const unsigned char index ) : m_index(index) {} std::string GetName() const;
virtual ControlState GetState( State* const js ) = 0; protected:
Axis( IOHIDElementRef button_element );
ControlState GetState( const State* const state );
private:
IOHIDElementRef m_axis_element;
std::string m_axis_name;
};
const unsigned char m_index; class Light : public Output
}; {
friend class KeyboardMouse;
public:
std::string GetName() const;
protected:
Light( IOHIDElementRef light_element );
void SetState( const ControlState state, unsigned char* const state_out );
private:
IOHIDElementRef m_light_element;
std::string m_light_name;
};
class Key : public Input bool UpdateInput();
{ bool UpdateOutput();
friend class Keyboard;
public:
std::string GetName() const;
protected:
Key( const unsigned char key ) : Input(key) {}
ControlState GetState( State* const js );
}; ControlState GetInputState( const ControllerInterface::Device::Input* const input );
void SetOutputState( const ControllerInterface::Device::Output* const output, const ControlState state );
bool UpdateInput(); public:
bool UpdateOutput(); KeyboardMouse(IOHIDDeviceRef device);
~KeyboardMouse();
ControlState GetInputState( const ControllerInterface::Device::Input* const input ); std::string GetName() const;
void SetOutputState( const ControllerInterface::Device::Output* const output, const ControlState state ); std::string GetSource() const;
int GetId() const;
public: private:
Keyboard( void* view ); State m_state_in;
~Keyboard(); unsigned char m_state_out[6]; // ugly
IOHIDDeviceRef m_device;
std::string m_device_name;
};
std::string GetName() const;
std::string GetSource() const;
int GetId() const;
private:
State m_state;
};
}
} }
}
#endif

View File

@ -0,0 +1,354 @@
#include "../ControllerInterface.h"
#ifdef CIFACE_USE_OSX
#include "OSX.h"
#include <Foundation/Foundation.h>
#include <IOKit/hid/IOHIDLib.h>
namespace ciface
{
namespace OSX
{
struct PrettyLights
{
const uint32_t code;
const char* const name;
} named_lights[] =
{
{ kHIDUsage_LED_NumLock, "Num Lock" },
{ kHIDUsage_LED_CapsLock, "Caps Lock" },
{ kHIDUsage_LED_ScrollLock, "Scroll Lock" },
{ kHIDUsage_LED_Compose, "Compose" },
{ kHIDUsage_LED_Kana, "Kana" },
{ kHIDUsage_LED_Power, "Power" }
};
static IOHIDManagerRef HIDManager = NULL;
static CFStringRef OurRunLoop = CFSTR("DolphinOSXInput");
static void DeviceMatching_callback(void* inContext,
IOReturn inResult,
void* inSender,
IOHIDDeviceRef inIOHIDDeviceRef)
{
NSLog(@"Got Device: %@", IOHIDDeviceGetProperty(inIOHIDDeviceRef, CFSTR(kIOHIDProductKey)));
// Add to the devices vector if it's of a type we want
if (IOHIDDeviceConformsTo(inIOHIDDeviceRef, kHIDPage_GenericDesktop, kHIDUsage_GD_Keyboard) ||
IOHIDDeviceConformsTo(inIOHIDDeviceRef, kHIDPage_GenericDesktop, kHIDUsage_GD_Keypad)/* ||
IOHIDDeviceConformsTo(inIOHIDDeviceRef, kHIDPage_GenericDesktop, kHIDUsage_GD_Mouse)*/)
{
std::vector<ControllerInterface::Device*> *devices = (std::vector<ControllerInterface::Device*> *)inContext;
devices->push_back(new KeyboardMouse(inIOHIDDeviceRef));
}
/* else if (IOHIDDeviceConformsTo(inIOHIDDeviceRef, kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad))
{
}*/
else {
// Actually, we don't want it
NSLog(@"Throwing away...");
#define shortlog(x) NSLog(@"%s: %@", x, IOHIDDeviceGetProperty(inIOHIDDeviceRef, CFSTR(x)));
shortlog(kIOHIDTransportKey)
shortlog(kIOHIDVendorIDKey)
shortlog(kIOHIDVendorIDSourceKey)
shortlog(kIOHIDProductIDKey)
shortlog(kIOHIDVersionNumberKey)
shortlog(kIOHIDManufacturerKey)
shortlog(kIOHIDProductKey)
shortlog(kIOHIDSerialNumberKey)
shortlog(kIOHIDCountryCodeKey)
shortlog(kIOHIDLocationIDKey)
shortlog(kIOHIDDeviceUsageKey)
shortlog(kIOHIDDeviceUsagePageKey)
shortlog(kIOHIDDeviceUsagePairsKey)
shortlog(kIOHIDPrimaryUsageKey)
shortlog(kIOHIDPrimaryUsagePageKey)
shortlog(kIOHIDMaxInputReportSizeKey)
shortlog(kIOHIDMaxOutputReportSizeKey)
shortlog(kIOHIDMaxFeatureReportSizeKey)
shortlog(kIOHIDReportIntervalKey)
shortlog(kIOHIDReportDescriptorKey)
#undef shortlog
NSLog(@"\n\n");
}
}
// Will come in handy if we support hotplugging
static void DeviceRemoval_callback(void *inContext,
IOReturn inResult,
void *inSender,
IOHIDDeviceRef inIOHIDDeviceRef)
{
NSLog(@"%s( context: %p, result: %p, sender: %p, device: %p )",
__PRETTY_FUNCTION__, inContext, (void *)inResult, inSender, (void *)inIOHIDDeviceRef);
}
void Init( std::vector<ControllerInterface::Device*>& devices )
{
HIDManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
if (!HIDManager)
NSLog(@"Failed to create HID Manager reference");
// HID Manager will give us the following devices:
// Keyboard, Keypad, Mouse, GamePad
NSArray *matchingDevices = [NSArray arrayWithObjects:
[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInteger:kHIDPage_GenericDesktop], @ kIOHIDDeviceUsagePageKey,
[NSNumber numberWithInteger:kHIDUsage_GD_Keyboard], @ kIOHIDDeviceUsageKey, nil],
[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInteger:kHIDPage_GenericDesktop], @ kIOHIDDeviceUsagePageKey,
[NSNumber numberWithInteger:kHIDUsage_GD_Keypad], @ kIOHIDDeviceUsageKey, nil],
[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInteger:kHIDPage_GenericDesktop], @ kIOHIDDeviceUsagePageKey,
[NSNumber numberWithInteger:kHIDUsage_GD_Mouse], @ kIOHIDDeviceUsageKey, nil],
[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInteger:kHIDPage_GenericDesktop], @ kIOHIDDeviceUsagePageKey,
[NSNumber numberWithInteger:kHIDUsage_GD_GamePad], @ kIOHIDDeviceUsageKey, nil],
nil];
// Pass NULL to get all devices
IOHIDManagerSetDeviceMatchingMultiple(HIDManager, (CFArrayRef)matchingDevices);
// Callbacks for acquisition or loss of a matching device
IOHIDManagerRegisterDeviceMatchingCallback(HIDManager, DeviceMatching_callback, (void *)&devices);
IOHIDManagerRegisterDeviceRemovalCallback(HIDManager, DeviceRemoval_callback, (void *)&devices);
// Match devices that are plugged right now.
IOHIDManagerScheduleWithRunLoop(HIDManager, CFRunLoopGetCurrent(), OurRunLoop);
if (IOHIDManagerOpen(HIDManager, kIOHIDOptionsTypeNone) != kIOReturnSuccess)
NSLog(@"Failed to open HID Manager");
// Wait while current devices are initialized
while (CFRunLoopRunInMode(OurRunLoop,0,TRUE) == kCFRunLoopRunHandledSource);
// Things should be configured now. Disable hotplugging and other scheduling
// TODO: support hotplugging, get rid of the following:
IOHIDManagerRegisterDeviceMatchingCallback(HIDManager, NULL, NULL);
IOHIDManagerRegisterDeviceRemovalCallback(HIDManager, NULL, NULL);
IOHIDManagerUnscheduleFromRunLoop(HIDManager, CFRunLoopGetCurrent(), OurRunLoop);
}
void DeInit()
{
// This closes all devices as well
IOHIDManagerClose(HIDManager, kIOHIDOptionsTypeNone);
CFRelease(HIDManager);
}
void DeviceElementDebugPrint(const void *value, void *context)
{
IOHIDElementRef e = (IOHIDElementRef)value;
std::string type = "";
switch (IOHIDElementGetType(e))
{
case kIOHIDElementTypeInput_Axis: type = "axis"; break;
case kIOHIDElementTypeInput_Button: type = "button"; break;
case kIOHIDElementTypeInput_Misc: type = "misc"; break;
case kIOHIDElementTypeInput_ScanCodes: type = "scancodes"; break;
case kIOHIDElementTypeOutput: type = "output"; break;
case kIOHIDElementTypeFeature: type = "feature"; break;
case kIOHIDElementTypeCollection: type = "collection"; break;
}
std::string c_type = "";
if (type == "collection")
{
switch (IOHIDElementGetCollectionType(e))
{
case kIOHIDElementCollectionTypePhysical: c_type = "physical"; break;
case kIOHIDElementCollectionTypeApplication: c_type = "application"; break;
case kIOHIDElementCollectionTypeLogical: c_type = "logical"; break;
case kIOHIDElementCollectionTypeReport: c_type = "report"; break;
case kIOHIDElementCollectionTypeNamedArray: c_type = "namedArray"; break;
case kIOHIDElementCollectionTypeUsageSwitch: c_type = "usageSwitch"; break;
case kIOHIDElementCollectionTypeUsageModifier: c_type = "usageModifier"; break;
}
}
c_type.append(" ");
NSLog(@"%s%s%spage: 0x%x usage: 0x%x name: %s lmin: %u lmax: %u",
type.c_str(),
type == "collection" ? ":" : "",
type == "collection" ? c_type.c_str() : " ",
IOHIDElementGetUsagePage(e),
IOHIDElementGetUsage(e),
IOHIDElementGetName(e), // TOO BAD IT"S FUCKING USELESS
IOHIDElementGetLogicalMin(e),
IOHIDElementGetLogicalMax(e));
if (type == "collection")
{
CFArrayRef elements = IOHIDElementGetChildren(e);
CFRange range = {0, CFArrayGetCount(elements)};
// this leaks...but it's just debug code, right? :D
CFArrayApplyFunction(elements, range, DeviceElementDebugPrint, NULL);
}
}
KeyboardMouse::KeyboardMouse(IOHIDDeviceRef device)
: m_device(device)
{
m_device_name = [(NSString *)IOHIDDeviceGetProperty(m_device, CFSTR(kIOHIDProductKey)) UTF8String];
// Go through all the elements of the device we've been given and try to make something out of them
CFArrayRef elements = IOHIDDeviceCopyMatchingElements(m_device, NULL, kIOHIDOptionsTypeNone);
CFIndex idx = 0;
for (IOHIDElementRef e = (IOHIDElementRef)CFArrayGetValueAtIndex(elements, idx);
e;
e = (IOHIDElementRef)CFArrayGetValueAtIndex(elements, ++idx))
{
if ((IOHIDElementGetType(e) == kIOHIDElementTypeInput_Button) &&
(IOHIDElementGetUsagePage(e) == kHIDPage_KeyboardOrKeypad) &&
(IOHIDElementGetLogicalMin(e) == 0) &&
(IOHIDElementGetLogicalMax(e) == 1))
{
inputs.push_back(new Key(e));
}
else if((IOHIDElementGetType(e) == kIOHIDElementTypeOutput) &&
(IOHIDElementGetUsagePage(e) == kHIDPage_LEDs) &&
(IOHIDElementGetLogicalMin(e) == 0) &&
(IOHIDElementGetLogicalMax(e) == 1))
{
outputs.push_back(new Light(e));
}
else {
DeviceElementDebugPrint((void *)e, NULL);
}
}
CFRelease(elements);
}
ControlState KeyboardMouse::GetInputState( const ControllerInterface::Device::Input* const input )
{
return ((Input*)input)->GetState( &m_state_in );
}
void KeyboardMouse::SetOutputState( const ControllerInterface::Device::Output* const output, const ControlState state )
{
((Output*)output)->SetState(state, m_state_out);
}
bool KeyboardMouse::UpdateInput()
{
return true;
}
bool KeyboardMouse::UpdateOutput()
{
return true;
}
std::string KeyboardMouse::GetName() const
{
return m_device_name;
}
std::string KeyboardMouse::GetSource() const
{
return "OSX";
}
int KeyboardMouse::GetId() const
{
return 0;
}
KeyboardMouse::Key::Key(IOHIDElementRef key_element)
: m_key_element(key_element)
{
std::ostringstream s;
s << IOHIDElementGetUsage(m_key_element);
m_key_name = s.str();
}
ControlState KeyboardMouse::Key::GetState( const State* const state )
{
IOHIDValueRef value;
if (IOHIDDeviceGetValue(IOHIDElementGetDevice(m_key_element), m_key_element, &value) == kIOReturnSuccess)
{
double scaled_value = IOHIDValueGetScaledValue(value, kIOHIDValueScaleTypePhysical);
//NSLog(@"element %x value %x scaled %f", IOHIDElementGetUsage(m_key_element), value, scaled_value);
return scaled_value > 0;
}
return false;
}
std::string KeyboardMouse::Key::GetName() const
{
return m_key_name;
}
KeyboardMouse::Button::Button(IOHIDElementRef button_element)
: m_button_element(button_element)
{
std::ostringstream s;
s << IOHIDElementGetUsage(m_button_element);
m_button_name = s.str();
}
ControlState KeyboardMouse::Button::GetState( const State* const state )
{
return false;
}
std::string KeyboardMouse::Button::GetName() const
{
return m_button_name;
}
KeyboardMouse::Axis::Axis(IOHIDElementRef axis_element)
: m_axis_element(axis_element)
{
std::ostringstream s;
s << IOHIDElementGetUsage(m_axis_element);
m_axis_name = s.str();
}
ControlState KeyboardMouse::Axis::GetState( const State* const state )
{
return false;
}
std::string KeyboardMouse::Axis::GetName() const
{
return m_axis_name;
}
KeyboardMouse::Light::Light(IOHIDElementRef light_element)
: m_light_element(light_element)
{
int idx = IOHIDElementGetUsage(m_light_element);
m_light_name = (idx <= 5) ? named_lights[idx].name : "UNKNOWN";
}
void KeyboardMouse::Light::SetState( const ControlState state, unsigned char* const state_out )
{
uint64_t timestamp = 0;
IOHIDValueRef value = IOHIDValueCreateWithIntegerValue(kCFAllocatorDefault, m_light_element, timestamp, (int)state);
if (IOHIDDeviceSetValue(IOHIDElementGetDevice(m_light_element), m_light_element, value) == kIOReturnSuccess)
{
NSLog(@"element %x value %x", IOHIDElementGetUsage(m_light_element), value);
}
}
std::string KeyboardMouse::Light::GetName() const
{
return m_light_name;
}
}
}
#endif

View File

@ -1,3 +0,0 @@
void OSX_Init(void *_View);
void OSX_UpdateKeys( int max, char* keys );

View File

@ -1,38 +0,0 @@
#import <AppKit/NSWindow.h>
#import <AppKit/NSView.h>
#import <Cocoa/Cocoa.h>
#include "OSXPrivate.h"
NSWindow* m_Window;
void OSX_Init(void *_View)
{
// _View is really a wxNSView
//m_Window is really a wxNSWindow
NSView *View = (NSView*)_View;
m_Window = [View window];
}
void OSX_UpdateKeys( int max, char* keys )
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSEvent *event = [[NSEvent alloc] init];
/*event = [m_Window nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES ];
if ( event != nil ) {
switch ([event type]) {
case NSKeyDown:
//case NSKeyUp:
//case NSFlagsChanged: // For Command
memcpy(keys, [[event characters] UTF8String], max);
break;
default:
[m_Window sendEvent:event];
break;
}
}*/
[event release];
[pool release];
}

View File

@ -17,7 +17,7 @@ class Keyboard : public ControllerInterface::Device
friend class ControllerInterface; friend class ControllerInterface;
friend class ControllerInterface::ControlReference; friend class ControllerInterface::ControlReference;
protected: protected:
struct State struct State
{ {
@ -29,7 +29,7 @@ class Keyboard : public ControllerInterface::Device
{ {
friend class Keyboard; friend class Keyboard;
protected: protected:
virtual ControlState GetState(const State* const state) = 0; virtual ControlState GetState(const State* const state) = 0;
}; };
@ -37,14 +37,14 @@ class Keyboard : public ControllerInterface::Device
{ {
friend class Keyboard; friend class Keyboard;
public: public:
std::string GetName() const; std::string GetName() const;
protected: protected:
Key(Display* const display, KeyCode keycode); Key(Display* const display, KeyCode keycode);
ControlState GetState(const State* const state); ControlState GetState(const State* const state);
private: private:
Display* const m_display; Display* const m_display;
const KeyCode m_keycode; const KeyCode m_keycode;
std::string m_keyname; std::string m_keyname;
@ -55,14 +55,14 @@ class Keyboard : public ControllerInterface::Device
{ {
friend class Keyboard; friend class Keyboard;
public: public:
std::string GetName() const; std::string GetName() const;
protected: protected:
Button(const unsigned int index) : m_index(index) {} Button(const unsigned int index) : m_index(index) {}
ControlState GetState(const State* const state); ControlState GetState(const State* const state);
private: private:
const unsigned int m_index; const unsigned int m_index;
}; };
@ -72,7 +72,7 @@ class Keyboard : public ControllerInterface::Device
ControlState GetInputState(const ControllerInterface::Device::Input* const input); ControlState GetInputState(const ControllerInterface::Device::Input* const input);
void SetOutputState(const ControllerInterface::Device::Output* const output, const ControlState state); void SetOutputState(const ControllerInterface::Device::Output* const output, const ControlState state);
public: public:
Keyboard(Display* display); Keyboard(Display* display);
~Keyboard(); ~Keyboard();
@ -80,7 +80,7 @@ class Keyboard : public ControllerInterface::Device
std::string GetSource() const; std::string GetSource() const;
int GetId() const; int GetId() const;
private: private:
Window m_window; Window m_window;
Display* m_display; Display* m_display;
State m_state; State m_state;

View File

@ -28,8 +28,7 @@ if icenv['HAVE_SDL']:
if sys.platform == 'darwin': if sys.platform == 'darwin':
files += [ files += [
'ControllerInterface/OSX/OSX.cpp', 'ControllerInterface/OSX/OSX.mm'
'ControllerInterface/OSX/OSXPrivate.mm'
] ]
icenv['FRAMEWORKS'] = ['IOKit'] icenv['FRAMEWORKS'] = ['IOKit']

File diff suppressed because it is too large Load Diff